From 504c70198802e6a37451c8843c8e0702a7e892e1 Mon Sep 17 00:00:00 2001 From: Ricky Lippmann Date: Wed, 20 Feb 2019 21:08:50 +0100 Subject: [PATCH 1/9] feat(eslint-plugin): add rule prefer-regexp-exec --- packages/eslint-plugin/package.json | 1 + .../src/rules/prefer-regexp-exec.ts | 119 ++++++++++++ .../tests/rules/prefer-regexp-exec.test.ts | 51 +++++ .../eslint-plugin/typings/eslint-utils.d.ts | 178 ++++++++++++++++++ packages/eslint-plugin/typings/ts-eslint.d.ts | 6 +- 5 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin/src/rules/prefer-regexp-exec.ts create mode 100644 packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts create mode 100644 packages/eslint-plugin/typings/eslint-utils.d.ts diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 1c6bc6ff1f4..7f99df2b072 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -37,6 +37,7 @@ "dependencies": { "@typescript-eslint/parser": "1.4.0", "@typescript-eslint/typescript-estree": "1.4.0", + "eslint-utils": "^1.3.1", "requireindex": "^1.2.0", "tsutils": "^3.7.0" }, diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts new file mode 100644 index 00000000000..764e90b475d --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -0,0 +1,119 @@ +/** + * @fileoverview Prefer RegExp#exec() over String#match() + * @author Ricky Lippmann + */ + +import { TSESTree } from '@typescript-eslint/typescript-estree'; +import ts from 'typescript'; +import { createRule, getParserServices } from '../util'; +import { getStaticValue } from 'eslint-utils'; + +export default createRule({ + name: 'prefer-regexp-exec', + defaultOptions: [], + + meta: { + type: 'suggestion', + docs: { + description: + 'Prefer RegExp#exec() over String#match() if no global flag is provided.', + category: 'Best Practices', + recommended: false, + }, + messages: { + regExpExecOverStringMatch: 'Use RegExp#exec() method instead.', + }, + schema: [], + }, + + create(context) { + const globalScope = context.getScope(); + const service = getParserServices(context); + const typeChecker = service.program.getTypeChecker(); + + /** + * Get the type name of a given type. + * @param type The type to get. + */ + function getTypeName(type: ts.Type): string { + // It handles `string` and string literal types as string. + if ((type.flags & ts.TypeFlags.StringLike) !== 0) { + return 'string'; + } + + // If the type is a type parameter which extends primitive string types, + // but it was not recognized as a string like. So check the constraint + // type of the type parameter. + if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { + // `type.getConstraint()` method doesn't return the constraint type of + // the type parameter for some reason. So this gets the constraint type + // via AST. + const node = type.symbol.declarations[0] as ts.TypeParameterDeclaration; + if (node.constraint != null) { + return getTypeName(typeChecker.getTypeFromTypeNode(node.constraint)); + } + } + + // If the type is a union and all types in the union are string like, + // return `string`. For example: + // - `"a" | "b"` is string. + // - `string | string[]` is not string. + if ( + type.isUnion() && + type.types.map(getTypeName).every(t => t === 'string') + ) { + return 'string'; + } + + // If the type is an intersection and a type in the intersection is string + // like, return `string`. For example: `string & {__htmlEscaped: void}` + if ( + type.isIntersection() && + type.types.map(getTypeName).some(t => t === 'string') + ) { + return 'string'; + } + + return typeChecker.typeToString(type); + } + + /** + * Check if a given node is a string. + * @param node The node to check. + */ + function isStringType(node: TSESTree.Node): boolean { + const objectType = typeChecker.getTypeAtLocation( + service.esTreeNodeToTSNodeMap.get(node), + ); + const typeName = getTypeName(objectType); + + return typeName === 'string'; + } + + return { + "CallExpression[arguments.length=1] > MemberExpression.callee[property.name='match'][computed=false]"( + node: TSESTree.MemberExpression, + ) { + const callNode = node.parent as TSESTree.CallExpression; + const arg = callNode.arguments[0]; + const evaluated = getStaticValue(arg, globalScope); + + if ( + evaluated && + evaluated.value instanceof RegExp && + evaluated.value.flags.includes('g') + ) { + return; + } + + if (isStringType(node.object)) { + context.report({ + node: callNode, + messageId: 'regExpExecOverStringMatch', + }); + return; + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts new file mode 100644 index 00000000000..34debe293a0 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts @@ -0,0 +1,51 @@ +import path from 'path'; +import rule from '../../src/rules/prefer-regexp-exec'; +import { RuleTester } from '../RuleTester'; + +const rootPath = path.join(process.cwd(), 'tests/fixtures/'); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('prefer-regexp-exec', rule, { + valid: [ + '"something".match();', + '"something".match(/thing/g);', + ` +const text = "something"; +const search = /thing/g; +text.match(search); +`, + ], + invalid: [ + { + code: '"something".match(/thing/);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, + { + code: ` +const text = "something"; +const search = /thing/; +text.match(search); + `, + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 4, + column: 1, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-utils.d.ts b/packages/eslint-plugin/typings/eslint-utils.d.ts new file mode 100644 index 00000000000..d926229b00e --- /dev/null +++ b/packages/eslint-plugin/typings/eslint-utils.d.ts @@ -0,0 +1,178 @@ +declare module 'eslint-utils' { + import { TSESTree } from '@typescript-eslint/typescript-estree'; + import { Scope, SourceCode } from 'ts-eslint'; + + export function getFunctionHeadLocation( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression, + sourceCode: SourceCode, + ): TSESTree.SourceLocation; + + export function getFunctionNameWithKind( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression, + ): string; + + export function getPropertyName( + node: + | TSESTree.MemberExpression + | TSESTree.Property + | TSESTree.MethodDefinition, + initialScope?: Scope.Scope, + ): string | null; + + export function getStaticValue( + node: TSESTree.Node, + initialScope?: Scope.Scope, + ): { value: any } | null; + + export function getStringIfConstant( + node: TSESTree.Node, + initialScope?: Scope.Scope, + ): string | null; + + export function hasSideEffect( + node: TSESTree.Node, + sourceCode: SourceCode, + options?: { + considerGetters?: boolean; + considerImplicitTypeConversion?: boolean; + }, + ): boolean; + + export function isParenthesized( + node: TSESTree.Node, + sourceCode: SourceCode, + ): boolean; + + export class PatternMatcher { + constructor(pattern: RegExp, options?: { escaped?: boolean }); + execAll(str: string): IterableIterator; + test(str: string): boolean; + } + + export function findVariable( + initialScope: Scope.Scope, + name: string, + ): Scope.Variable | null; + + export function getInnermostScope( + initialScope: Scope.Scope, + node: TSESTree.Node, + ): Scope.Scope; + + export class ReferenceTracker { + static readonly READ: unique symbol; + static readonly CALL: unique symbol; + static readonly CONSTRUCT: unique symbol; + + constructor( + globalScope: Scope.Scope, + options?: { + mode: 'strict' | 'legacy'; + globalObjectNames: ReadonlyArray; + }, + ); + + iterateGlobalReferences( + traceMap: ReferenceTracker.TraceMap, + ): IterableIterator>; + iterateCjsReferences( + traceMap: ReferenceTracker.TraceMap, + ): IterableIterator>; + iterateEsmReferences( + traceMap: ReferenceTracker.TraceMap, + ): IterableIterator>; + } + + export namespace ReferenceTracker { + export type READ = typeof ReferenceTracker.READ; + export type CALL = typeof ReferenceTracker.READ; + export type CONSTRUCT = typeof ReferenceTracker.READ; + export type ReferenceType = READ | CALL | CONSTRUCT; + export type TraceMap = Record>; + export interface TraceMapElement { + [ReferenceTracker.READ]?: T; + [ReferenceTracker.CALL]?: T; + [ReferenceTracker.CONSTRUCT]?: T; + [key: string]: TraceMapElement; + } + export interface FoundReference { + node: TSESTree.Node; + path: ReadonlyArray; + type: ReferenceType; + entry: T; + } + } + + export function isArrowToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotArrowToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isClosingBraceToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotClosingBraceToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isClosingBracketToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotClosingBracketToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isClosingParenToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotClosingParenToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isColonToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotColonToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isCommaToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotCommaToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isCommentToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotCommentToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isOpeningBraceToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotOpeningBraceToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isOpeningBracketToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotOpeningBracketToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isOpeningParenToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotOpeningParenToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isSemicolonToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; + export function isNotSemicolonToken( + token: TSESTree.Token | TSESTree.Comment, + ): boolean; +} diff --git a/packages/eslint-plugin/typings/ts-eslint.d.ts b/packages/eslint-plugin/typings/ts-eslint.d.ts index 998c6958342..85f8c8b63e0 100644 --- a/packages/eslint-plugin/typings/ts-eslint.d.ts +++ b/packages/eslint-plugin/typings/ts-eslint.d.ts @@ -297,7 +297,9 @@ declare module 'ts-eslint' { replaceTextRange(range: AST.Range, text: string): RuleFix; } - type ReportFixFunction = (fixer: RuleFixer) => null | RuleFix | RuleFix[]; + type ReportFixFunction = ( + fixer: RuleFixer, + ) => null | RuleFix | RuleFix[] | IterableIterator; interface ReportDescriptor { /** @@ -678,11 +680,13 @@ declare module 'ts-eslint' { ReportFixFunction, RuleContext, RuleFix, + RuleFixer, RuleFunction, RuleListener, RuleMetaData, RuleMetaDataDocs, Scope, + SourceCode, }; export default RuleModule; } From 5ed44d23c3278c15139e8da49e539c4e0b226a03 Mon Sep 17 00:00:00 2001 From: Ricky Lippmann <3674067+ldrick@users.noreply.github.com> Date: Wed, 20 Feb 2019 21:47:39 +0100 Subject: [PATCH 2/9] test(eslint-plugin): add more tests --- .../src/rules/prefer-regexp-exec.ts | 1 + .../tests/rules/prefer-regexp-exec.test.ts | 81 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index 764e90b475d..7f07c1075c9 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -98,6 +98,7 @@ export default createRule({ const arg = callNode.arguments[0]; const evaluated = getStaticValue(arg, globalScope); + // Do not run for global flag. if ( evaluated && evaluated.value instanceof RegExp && diff --git a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts index 34debe293a0..7fbe74aed61 100644 --- a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts @@ -20,6 +20,14 @@ ruleTester.run('prefer-regexp-exec', rule, { const text = "something"; const search = /thing/g; text.match(search); +`, + ` +const match = (s: RegExp) => "something"; +match(/thing/); +`, + ` +const a = {match : (s: RegExp) => "something"}; +a.match(/thing/); `, ], invalid: [ @@ -47,5 +55,78 @@ text.match(search); }, ], }, + { + code: '"212".match(2);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, + { + code: '"212".match(+2);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, + { + code: '"oNaNo".match(NaN);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, + { + code: + '"Infinity contains -Infinity and +Infinity in JavaScript.".match(Infinity);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, + { + code: + '"Infinity contains -Infinity and +Infinity in JavaScript.".match(+Infinity);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, + { + code: + '"Infinity contains -Infinity and +Infinity in JavaScript.".match(-Infinity);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, + { + code: '"void and null".match(null);', + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 1, + column: 1, + }, + ], + }, ], }); From d7b614832b62558fba1f18256424cc50b7a0ba57 Mon Sep 17 00:00:00 2001 From: Ricky Lippmann Date: Thu, 21 Feb 2019 17:31:09 +0100 Subject: [PATCH 3/9] docs(eslint-plugin): add documentation for prefer-regexp-exec rule --- packages/eslint-plugin/README.md | 1 + .../docs/rules/prefer-regexp-exec.md | 38 +++++++++++++++++++ .../tests/rules/prefer-regexp-exec.test.ts | 21 +++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 packages/eslint-plugin/docs/rules/prefer-regexp-exec.md diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 10268b00635..ae611abbde1 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -152,5 +152,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | :heavy_check_mark: | | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string. (`restrict-plus-operands` from TSLint) | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | :heavy_check_mark: | :wrench: | +| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Prefer RegExp#exec() over String#match() if no global flag is provided. | | | diff --git a/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md b/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md new file mode 100644 index 00000000000..cc6ebab82fc --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md @@ -0,0 +1,38 @@ +# Enforce to use `RegExp#exec` over `String#match` (prefer-regexp-exec) + +`String#match` is outperformed by `RegExp#exec` and both work in the same way, except for /g flag. + +## Rule Details + +This rule is aimed at enforcing the more performant way for applying regular expressions on Strings. + +Examples of **incorrect** code for this rule: + +```ts +'something'.match(/thing/); + +const text = 'something'; +const search = /thing/; +text.match(search); +``` + +Examples of **correct** code for this rule: + +```ts +'some things are just things'.match(/thing/g); +/thing/.exec('something'); +``` + +## Options + +There are no options. + +```JSON +{ + "@typescript-eslint/prefer-regexp-exec": "error" +} +``` + +## When Not To Use It + +If you don't mind performance, you can turn this rule off safely. diff --git a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts index 7fbe74aed61..f0a99c6f666 100644 --- a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts @@ -28,6 +28,11 @@ match(/thing/); ` const a = {match : (s: RegExp) => "something"}; a.match(/thing/); +`, + ` +function f(s: string | string[]) { + s.match(/e/); +} `, ], invalid: [ @@ -46,7 +51,7 @@ a.match(/thing/); const text = "something"; const search = /thing/; text.match(search); - `, +`, errors: [ { messageId: 'regExpExecOverStringMatch', @@ -128,5 +133,19 @@ text.match(search); }, ], }, + { + code: ` +function f(s: 'a' | 'b') { + s.match('a'); +} +`, + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 3, + column: 3, + }, + ], + }, ], }); From 99e14d7b3f355743e5dc2d81054b4cf90a9fc323 Mon Sep 17 00:00:00 2001 From: Ricky Lippmann Date: Thu, 21 Feb 2019 18:00:44 +0100 Subject: [PATCH 4/9] test(eslint-plugin): improve coverage for prefer-regexp-exec rule --- .../tests/rules/prefer-regexp-exec.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts index f0a99c6f666..98381cda6a1 100644 --- a/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-regexp-exec.test.ts @@ -147,5 +147,34 @@ function f(s: 'a' | 'b') { }, ], }, + { + code: ` +type SafeString = string & {__HTML_ESCAPED__: void} +function f(s: SafeString) { + s.match(/thing/); +} +`, + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 4, + column: 3, + }, + ], + }, + { + code: ` +function f(s: T) { + s.match(/thing/); +} + `, + errors: [ + { + messageId: 'regExpExecOverStringMatch', + line: 3, + column: 3, + }, + ], + }, ], }); From 8f4a014d890630bb33104a7294691cca0a798f4e Mon Sep 17 00:00:00 2001 From: Ricky Lippmann Date: Fri, 22 Feb 2019 13:18:16 +0100 Subject: [PATCH 5/9] docs(eslint-plugin): improve docs for prefer-regexp-exec rule --- packages/eslint-plugin/README.md | 2 +- .../docs/rules/prefer-regexp-exec.md | 25 +++++++++++++++---- .../src/rules/prefer-regexp-exec.ts | 6 ++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index ae611abbde1..ef82e6c8349 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -152,6 +152,6 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | :heavy_check_mark: | | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string. (`restrict-plus-operands` from TSLint) | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | :heavy_check_mark: | :wrench: | -| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Prefer RegExp#exec() over String#match() if no global flag is provided. | | | +| [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Enforce to use `RegExp#exec` over `String#match` | | | diff --git a/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md b/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md index cc6ebab82fc..282907851b8 100644 --- a/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md +++ b/packages/eslint-plugin/docs/rules/prefer-regexp-exec.md @@ -1,16 +1,26 @@ # Enforce to use `RegExp#exec` over `String#match` (prefer-regexp-exec) -`String#match` is outperformed by `RegExp#exec` and both work in the same way, except for /g flag. +`RegExp#exec` is faster than `String#match` and both work the same when not using the `/g` flag. ## Rule Details -This rule is aimed at enforcing the more performant way for applying regular expressions on Strings. +This rule is aimed at enforcing the more performant way of applying regular expressions on strings. + +From [`String#match` on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match): + +> If the regular expression does not include the g flag, returns the same result as RegExp.exec(). + +From [Stack Overflow](https://stackoverflow.com/questions/9214754/what-is-the-difference-between-regexp-s-exec-function-and-string-s-match-fun) + +> `RegExp.prototype.exec` is a lot faster than `String.prototype.match`, but that’s because they are not exactly the same thing, they are different. Examples of **incorrect** code for this rule: ```ts 'something'.match(/thing/); +'some things are just things'.match(/thing/); + const text = 'something'; const search = /thing/; text.match(search); @@ -19,15 +29,20 @@ text.match(search); Examples of **correct** code for this rule: ```ts -'some things are just things'.match(/thing/g); /thing/.exec('something'); + +'some things are just things'.match(/thing/g); + +const text = 'something'; +const search = /thing/; +search.exec(text); ``` ## Options There are no options. -```JSON +```json { "@typescript-eslint/prefer-regexp-exec": "error" } @@ -35,4 +50,4 @@ There are no options. ## When Not To Use It -If you don't mind performance, you can turn this rule off safely. +If you prefer consistent use of `String#match` for both, with `g` flag and without it, you can turn this rule off. diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index 7f07c1075c9..e68b6a79d08 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -1,5 +1,5 @@ /** - * @fileoverview Prefer RegExp#exec() over String#match() + * @fileoverview Prefer RegExp#exec() over String#match() if no global flag is provided. * @author Ricky Lippmann */ @@ -21,7 +21,7 @@ export default createRule({ recommended: false, }, messages: { - regExpExecOverStringMatch: 'Use RegExp#exec() method instead.', + regExpExecOverStringMatch: 'Use the `RegExp#exec()` method instead.', }, schema: [], }, @@ -98,7 +98,7 @@ export default createRule({ const arg = callNode.arguments[0]; const evaluated = getStaticValue(arg, globalScope); - // Do not run for global flag. + // Don't report regular expressions with global flag. if ( evaluated && evaluated.value instanceof RegExp && From 7134535ab176e4de6a7f635c9f907dabbe7113a3 Mon Sep 17 00:00:00 2001 From: Ricky Lippmann <3674067+ldrick@users.noreply.github.com> Date: Sat, 23 Feb 2019 20:07:47 +0100 Subject: [PATCH 6/9] fix(eslint-plugin): remove author --- packages/eslint-plugin/src/rules/prefer-regexp-exec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index e68b6a79d08..b3ea2271b96 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -1,8 +1,3 @@ -/** - * @fileoverview Prefer RegExp#exec() over String#match() if no global flag is provided. - * @author Ricky Lippmann - */ - import { TSESTree } from '@typescript-eslint/typescript-estree'; import ts from 'typescript'; import { createRule, getParserServices } from '../util'; From b6d759082d5006d78b5c4df3082c2c7305cb8312 Mon Sep 17 00:00:00 2001 From: Ricky Lippmann <3674067+ldrick@users.noreply.github.com> Date: Sun, 24 Feb 2019 01:37:10 +0100 Subject: [PATCH 7/9] feat(eslint-plugin): add config all.json --- packages/eslint-plugin/package.json | 2 +- packages/eslint-plugin/src/configs/all.json | 57 ++++++++++ .../src/configs/recommended.json | 3 + packages/eslint-plugin/tools/create-config.ts | 102 ++++++++++++++++++ .../eslint-plugin/tools/update-recommended.ts | 68 ------------ 5 files changed, 163 insertions(+), 69 deletions(-) create mode 100644 packages/eslint-plugin/src/configs/all.json create mode 100644 packages/eslint-plugin/tools/create-config.ts delete mode 100644 packages/eslint-plugin/tools/update-recommended.ts diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index ca82d78f6fb..d41b375d97c 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -28,7 +28,7 @@ "docs": "eslint-docs", "docs:check": "eslint-docs check", "test": "jest --coverage", - "recommended:update": "ts-node tools/update-recommended.ts", + "create:config": "ts-node --files tools/create-config.ts", "prebuild": "npm run clean", "build": "tsc -p tsconfig.build.json", "clean": "rimraf dist/", diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json new file mode 100644 index 00000000000..0d7c524d6fd --- /dev/null +++ b/packages/eslint-plugin/src/configs/all.json @@ -0,0 +1,57 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/adjacent-overload-signatures": "error", + "@typescript-eslint/array-type": "error", + "@typescript-eslint/ban-ts-ignore": "error", + "@typescript-eslint/ban-types": "error", + "camelcase": "off", + "@typescript-eslint/camelcase": "error", + "@typescript-eslint/class-name-casing": "error", + "@typescript-eslint/explicit-function-return-type": "warn", + "@typescript-eslint/explicit-member-accessibility": "error", + "@typescript-eslint/generic-type-naming": "warn", + "indent": "off", + "@typescript-eslint/indent": "error", + "@typescript-eslint/interface-name-prefix": "error", + "@typescript-eslint/member-delimiter-style": "error", + "@typescript-eslint/member-naming": "warn", + "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/no-angle-bracket-type-assertion": "error", + "no-array-constructor": "off", + "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-extraneous-class": "warn", + "@typescript-eslint/no-for-in-array": "warn", + "@typescript-eslint/no-inferrable-types": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-namespace": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/no-object-literal-type-assertion": "error", + "@typescript-eslint/no-parameter-properties": "error", + "@typescript-eslint/no-require-imports": "error", + "@typescript-eslint/no-this-alias": "warn", + "@typescript-eslint/no-triple-slash-reference": "error", + "@typescript-eslint/no-type-alias": "warn", + "@typescript-eslint/no-unnecessary-qualifier": "warn", + "@typescript-eslint/no-unnecessary-type-assertion": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-use-before-define": "error", + "@typescript-eslint/no-useless-constructor": "warn", + "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/prefer-interface": "error", + "@typescript-eslint/prefer-namespace-keyword": "error", + "@typescript-eslint/prefer-regexp-exec": "warn", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "warn", + "@typescript-eslint/restrict-plus-operands": "warn", + "@typescript-eslint/type-annotation-spacing": "error" + } +} diff --git a/packages/eslint-plugin/src/configs/recommended.json b/packages/eslint-plugin/src/configs/recommended.json index e107a645724..1032727f576 100644 --- a/packages/eslint-plugin/src/configs/recommended.json +++ b/packages/eslint-plugin/src/configs/recommended.json @@ -7,6 +7,7 @@ "rules": { "@typescript-eslint/adjacent-overload-signatures": "error", "@typescript-eslint/array-type": "error", + "@typescript-eslint/ban-ts-ignore": "error", "@typescript-eslint/ban-types": "error", "camelcase": "off", "@typescript-eslint/camelcase": "error", @@ -28,6 +29,7 @@ "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-object-literal-type-assertion": "error", "@typescript-eslint/no-parameter-properties": "error", + "@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-triple-slash-reference": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "warn", @@ -35,6 +37,7 @@ "@typescript-eslint/no-var-requires": "error", "@typescript-eslint/prefer-interface": "error", "@typescript-eslint/prefer-namespace-keyword": "error", + "@typescript-eslint/promise-function-async": "error", "@typescript-eslint/type-annotation-spacing": "error" } } diff --git a/packages/eslint-plugin/tools/create-config.ts b/packages/eslint-plugin/tools/create-config.ts new file mode 100644 index 00000000000..4f9d0cb8384 --- /dev/null +++ b/packages/eslint-plugin/tools/create-config.ts @@ -0,0 +1,102 @@ +/* eslint-disable no-console */ + +import path from 'path'; +import fs from 'fs'; +import requireIndex from 'requireindex'; + +const MAX_RULE_NAME_LENGTH = 33 + 'typescript-eslint/'.length; +const RULE_NAME_PREFIX = '@typescript-eslint'; + +const all = Object.entries( + requireIndex(path.resolve(__dirname, '../dist/rules')), +); + +const baseConfig = { + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + }, + plugins: ['@typescript-eslint'], +}; + +const bannedRules = new Set([ + 'camelcase', + 'indent', + 'no-array-constructor', + 'no-unused-vars', +]); + +/** + * Helper function reduces records to key - value pairs. + * @param config + * @param entry + */ +const reducer = ( + config: Record, + entry: Record, +): Record => { + const key = entry[0]; + const value = entry[1]; + const ruleName = `${RULE_NAME_PREFIX}/${key}`; + const setting = value.default.meta.docs.recommended; + const usedSetting = !setting ? 'warn' : setting; + + // having this here is just for output niceness (the keys will be ordered) + if (bannedRules.has(key)) { + console.log(key.padEnd(MAX_RULE_NAME_LENGTH), '= off'); + config[key] = 'off'; + } + console.log(ruleName.padEnd(MAX_RULE_NAME_LENGTH), '=', usedSetting); + config[ruleName] = usedSetting; + + return config; +}; + +/** + * Helper function to check for invalid recommended setting. + */ +function checkValidSettings(): boolean { + const validSettings = ['error', 'warn', false]; + let result = true; + + all.forEach(entry => { + const key = entry[0]; + const value = entry[1]; + const setting = value.default.meta.docs.recommended; + + if (!validSettings.includes(setting)) { + console.error(`ERR! Invalid level for rule ${key}: "${setting}"`); + result = false; + } + }); + + return result; +} + +/** + * Helper function generates configuration. + */ +function generate(rules: Record, filePath: string): void { + const config = Object.assign({}, baseConfig, { rules }); + + fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`); +} + +if (checkValidSettings()) { + console.log('------------------------- all.json -------------------------'); + generate( + all.reduce(reducer, {}), + path.resolve(__dirname, '../src/configs/all.json'), + ); + + console.log('--------------------- recommended.json ---------------------'); + const recommendedSettings = ['error', 'warn']; + generate( + all + .filter(entry => + recommendedSettings.includes(entry[1].default.meta.docs.recommended), + ) + .reduce(reducer, {}), + path.resolve(__dirname, '../src/configs/recommended.json'), + ); +} diff --git a/packages/eslint-plugin/tools/update-recommended.ts b/packages/eslint-plugin/tools/update-recommended.ts deleted file mode 100644 index 39861d35ee7..00000000000 --- a/packages/eslint-plugin/tools/update-recommended.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable no-console */ - -import path from 'path'; -import fs from 'fs'; -import requireIndex from 'requireindex'; - -const bannedRecommendedRules = new Set([ - 'camelcase', - 'indent', - 'no-array-constructor', - 'no-unused-vars', -]); -const MAX_RULE_NAME_LENGTH = 32 + 'typescript/'.length; - -function padEnd(str: string, length: number): string { - while (str.length < length) { - str += ' '; - } - return str; -} - -/** - * Generate recommended configuration - */ -function generate(): void { - // replace this with Object.entries when node > 8 - const allRules = requireIndex(path.resolve(__dirname, '../dist/lib/rules')); - - const rules = Object.keys(allRules) - .filter(key => !!allRules[key].meta.docs.recommended) - .reduce>((config, key) => { - // having this here is just for output niceness (the keys will be ordered) - if (bannedRecommendedRules.has(key)) { - console.log(padEnd(key, MAX_RULE_NAME_LENGTH), '= off'); - config[key] = 'off'; - } - - const ruleName = `@typescript-eslint/${key}`; - const setting = allRules[key].meta.docs.recommended; - - if (!['error', 'warn'].includes(setting)) { - console.log(`ERR! Invalid level for rule ${key}: "${setting}"`); - // Don't want to throw an error since ^ explains what happened. - // eslint-disable-next-line no-process-exit - process.exit(1); - } - - console.log(padEnd(ruleName, MAX_RULE_NAME_LENGTH), '=', setting); - config[ruleName] = setting; - - return config; - }, {}); - - const filePath = path.resolve(__dirname, '../src/configs/recommended.json'); - - const recommendedConfig = { - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - }, - plugins: ['@typescript-eslint'], - rules, - }; - - fs.writeFileSync(filePath, `${JSON.stringify(recommendedConfig, null, 4)}\n`); -} - -generate(); From f8aba109c9098bbe821cb0f7d3d811e5c06f2a63 Mon Sep 17 00:00:00 2001 From: Ricky Lippmann <3674067+ldrick@users.noreply.github.com> Date: Wed, 24 Apr 2019 22:44:17 +0200 Subject: [PATCH 8/9] Revert "feat(eslint-plugin): add config all.json" This reverts commit b6d759082d5006d78b5c4df3082c2c7305cb8312. --- packages/eslint-plugin/package.json | 2 +- packages/eslint-plugin/src/configs/all.json | 57 ---------- .../src/configs/recommended.json | 3 - packages/eslint-plugin/tools/create-config.ts | 102 ------------------ .../eslint-plugin/tools/update-recommended.ts | 68 ++++++++++++ 5 files changed, 69 insertions(+), 163 deletions(-) delete mode 100644 packages/eslint-plugin/src/configs/all.json delete mode 100644 packages/eslint-plugin/tools/create-config.ts create mode 100644 packages/eslint-plugin/tools/update-recommended.ts diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 7b9317138bf..c3034469f73 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -28,7 +28,7 @@ "docs": "eslint-docs", "docs:check": "eslint-docs check", "test": "jest --coverage", - "create:config": "ts-node --files tools/create-config.ts", + "recommended:update": "ts-node tools/update-recommended.ts", "prebuild": "npm run clean", "build": "tsc -p tsconfig.build.json", "clean": "rimraf dist/", diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json deleted file mode 100644 index 0d7c524d6fd..00000000000 --- a/packages/eslint-plugin/src/configs/all.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": { - "@typescript-eslint/adjacent-overload-signatures": "error", - "@typescript-eslint/array-type": "error", - "@typescript-eslint/ban-ts-ignore": "error", - "@typescript-eslint/ban-types": "error", - "camelcase": "off", - "@typescript-eslint/camelcase": "error", - "@typescript-eslint/class-name-casing": "error", - "@typescript-eslint/explicit-function-return-type": "warn", - "@typescript-eslint/explicit-member-accessibility": "error", - "@typescript-eslint/generic-type-naming": "warn", - "indent": "off", - "@typescript-eslint/indent": "error", - "@typescript-eslint/interface-name-prefix": "error", - "@typescript-eslint/member-delimiter-style": "error", - "@typescript-eslint/member-naming": "warn", - "@typescript-eslint/member-ordering": "warn", - "@typescript-eslint/no-angle-bracket-type-assertion": "error", - "no-array-constructor": "off", - "@typescript-eslint/no-array-constructor": "error", - "@typescript-eslint/no-empty-interface": "error", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-extraneous-class": "warn", - "@typescript-eslint/no-for-in-array": "warn", - "@typescript-eslint/no-inferrable-types": "error", - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-namespace": "error", - "@typescript-eslint/no-non-null-assertion": "error", - "@typescript-eslint/no-object-literal-type-assertion": "error", - "@typescript-eslint/no-parameter-properties": "error", - "@typescript-eslint/no-require-imports": "error", - "@typescript-eslint/no-this-alias": "warn", - "@typescript-eslint/no-triple-slash-reference": "error", - "@typescript-eslint/no-type-alias": "warn", - "@typescript-eslint/no-unnecessary-qualifier": "warn", - "@typescript-eslint/no-unnecessary-type-assertion": "warn", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "warn", - "@typescript-eslint/no-use-before-define": "error", - "@typescript-eslint/no-useless-constructor": "warn", - "@typescript-eslint/no-var-requires": "error", - "@typescript-eslint/prefer-function-type": "warn", - "@typescript-eslint/prefer-interface": "error", - "@typescript-eslint/prefer-namespace-keyword": "error", - "@typescript-eslint/prefer-regexp-exec": "warn", - "@typescript-eslint/promise-function-async": "error", - "@typescript-eslint/require-array-sort-compare": "warn", - "@typescript-eslint/restrict-plus-operands": "warn", - "@typescript-eslint/type-annotation-spacing": "error" - } -} diff --git a/packages/eslint-plugin/src/configs/recommended.json b/packages/eslint-plugin/src/configs/recommended.json index 1032727f576..e107a645724 100644 --- a/packages/eslint-plugin/src/configs/recommended.json +++ b/packages/eslint-plugin/src/configs/recommended.json @@ -7,7 +7,6 @@ "rules": { "@typescript-eslint/adjacent-overload-signatures": "error", "@typescript-eslint/array-type": "error", - "@typescript-eslint/ban-ts-ignore": "error", "@typescript-eslint/ban-types": "error", "camelcase": "off", "@typescript-eslint/camelcase": "error", @@ -29,7 +28,6 @@ "@typescript-eslint/no-non-null-assertion": "error", "@typescript-eslint/no-object-literal-type-assertion": "error", "@typescript-eslint/no-parameter-properties": "error", - "@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-triple-slash-reference": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "warn", @@ -37,7 +35,6 @@ "@typescript-eslint/no-var-requires": "error", "@typescript-eslint/prefer-interface": "error", "@typescript-eslint/prefer-namespace-keyword": "error", - "@typescript-eslint/promise-function-async": "error", "@typescript-eslint/type-annotation-spacing": "error" } } diff --git a/packages/eslint-plugin/tools/create-config.ts b/packages/eslint-plugin/tools/create-config.ts deleted file mode 100644 index 4f9d0cb8384..00000000000 --- a/packages/eslint-plugin/tools/create-config.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* eslint-disable no-console */ - -import path from 'path'; -import fs from 'fs'; -import requireIndex from 'requireindex'; - -const MAX_RULE_NAME_LENGTH = 33 + 'typescript-eslint/'.length; -const RULE_NAME_PREFIX = '@typescript-eslint'; - -const all = Object.entries( - requireIndex(path.resolve(__dirname, '../dist/rules')), -); - -const baseConfig = { - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - }, - plugins: ['@typescript-eslint'], -}; - -const bannedRules = new Set([ - 'camelcase', - 'indent', - 'no-array-constructor', - 'no-unused-vars', -]); - -/** - * Helper function reduces records to key - value pairs. - * @param config - * @param entry - */ -const reducer = ( - config: Record, - entry: Record, -): Record => { - const key = entry[0]; - const value = entry[1]; - const ruleName = `${RULE_NAME_PREFIX}/${key}`; - const setting = value.default.meta.docs.recommended; - const usedSetting = !setting ? 'warn' : setting; - - // having this here is just for output niceness (the keys will be ordered) - if (bannedRules.has(key)) { - console.log(key.padEnd(MAX_RULE_NAME_LENGTH), '= off'); - config[key] = 'off'; - } - console.log(ruleName.padEnd(MAX_RULE_NAME_LENGTH), '=', usedSetting); - config[ruleName] = usedSetting; - - return config; -}; - -/** - * Helper function to check for invalid recommended setting. - */ -function checkValidSettings(): boolean { - const validSettings = ['error', 'warn', false]; - let result = true; - - all.forEach(entry => { - const key = entry[0]; - const value = entry[1]; - const setting = value.default.meta.docs.recommended; - - if (!validSettings.includes(setting)) { - console.error(`ERR! Invalid level for rule ${key}: "${setting}"`); - result = false; - } - }); - - return result; -} - -/** - * Helper function generates configuration. - */ -function generate(rules: Record, filePath: string): void { - const config = Object.assign({}, baseConfig, { rules }); - - fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`); -} - -if (checkValidSettings()) { - console.log('------------------------- all.json -------------------------'); - generate( - all.reduce(reducer, {}), - path.resolve(__dirname, '../src/configs/all.json'), - ); - - console.log('--------------------- recommended.json ---------------------'); - const recommendedSettings = ['error', 'warn']; - generate( - all - .filter(entry => - recommendedSettings.includes(entry[1].default.meta.docs.recommended), - ) - .reduce(reducer, {}), - path.resolve(__dirname, '../src/configs/recommended.json'), - ); -} diff --git a/packages/eslint-plugin/tools/update-recommended.ts b/packages/eslint-plugin/tools/update-recommended.ts new file mode 100644 index 00000000000..39861d35ee7 --- /dev/null +++ b/packages/eslint-plugin/tools/update-recommended.ts @@ -0,0 +1,68 @@ +/* eslint-disable no-console */ + +import path from 'path'; +import fs from 'fs'; +import requireIndex from 'requireindex'; + +const bannedRecommendedRules = new Set([ + 'camelcase', + 'indent', + 'no-array-constructor', + 'no-unused-vars', +]); +const MAX_RULE_NAME_LENGTH = 32 + 'typescript/'.length; + +function padEnd(str: string, length: number): string { + while (str.length < length) { + str += ' '; + } + return str; +} + +/** + * Generate recommended configuration + */ +function generate(): void { + // replace this with Object.entries when node > 8 + const allRules = requireIndex(path.resolve(__dirname, '../dist/lib/rules')); + + const rules = Object.keys(allRules) + .filter(key => !!allRules[key].meta.docs.recommended) + .reduce>((config, key) => { + // having this here is just for output niceness (the keys will be ordered) + if (bannedRecommendedRules.has(key)) { + console.log(padEnd(key, MAX_RULE_NAME_LENGTH), '= off'); + config[key] = 'off'; + } + + const ruleName = `@typescript-eslint/${key}`; + const setting = allRules[key].meta.docs.recommended; + + if (!['error', 'warn'].includes(setting)) { + console.log(`ERR! Invalid level for rule ${key}: "${setting}"`); + // Don't want to throw an error since ^ explains what happened. + // eslint-disable-next-line no-process-exit + process.exit(1); + } + + console.log(padEnd(ruleName, MAX_RULE_NAME_LENGTH), '=', setting); + config[ruleName] = setting; + + return config; + }, {}); + + const filePath = path.resolve(__dirname, '../src/configs/recommended.json'); + + const recommendedConfig = { + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + }, + plugins: ['@typescript-eslint'], + rules, + }; + + fs.writeFileSync(filePath, `${JSON.stringify(recommendedConfig, null, 4)}\n`); +} + +generate(); From 5babaa3346d242ca7bea8083f9dd3114fb3df1ed Mon Sep 17 00:00:00 2001 From: Ricky Lippmann Date: Thu, 25 Apr 2019 15:58:03 +0200 Subject: [PATCH 9/9] feat(eslint-plugin): move getTypeName to util --- .../src/rules/prefer-regexp-exec.ts | 53 +---------------- .../rules/prefer-string-starts-ends-with.ts | 57 ++----------------- packages/eslint-plugin/src/util/types.ts | 57 +++++++++++++++++++ 3 files changed, 63 insertions(+), 104 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index b3ea2271b96..ea4e6742d05 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -1,6 +1,5 @@ import { TSESTree } from '@typescript-eslint/typescript-estree'; -import ts from 'typescript'; -import { createRule, getParserServices } from '../util'; +import { createRule, getParserServices, getTypeName } from '../util'; import { getStaticValue } from 'eslint-utils'; export default createRule({ @@ -26,52 +25,6 @@ export default createRule({ const service = getParserServices(context); const typeChecker = service.program.getTypeChecker(); - /** - * Get the type name of a given type. - * @param type The type to get. - */ - function getTypeName(type: ts.Type): string { - // It handles `string` and string literal types as string. - if ((type.flags & ts.TypeFlags.StringLike) !== 0) { - return 'string'; - } - - // If the type is a type parameter which extends primitive string types, - // but it was not recognized as a string like. So check the constraint - // type of the type parameter. - if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { - // `type.getConstraint()` method doesn't return the constraint type of - // the type parameter for some reason. So this gets the constraint type - // via AST. - const node = type.symbol.declarations[0] as ts.TypeParameterDeclaration; - if (node.constraint != null) { - return getTypeName(typeChecker.getTypeFromTypeNode(node.constraint)); - } - } - - // If the type is a union and all types in the union are string like, - // return `string`. For example: - // - `"a" | "b"` is string. - // - `string | string[]` is not string. - if ( - type.isUnion() && - type.types.map(getTypeName).every(t => t === 'string') - ) { - return 'string'; - } - - // If the type is an intersection and a type in the intersection is string - // like, return `string`. For example: `string & {__htmlEscaped: void}` - if ( - type.isIntersection() && - type.types.map(getTypeName).some(t => t === 'string') - ) { - return 'string'; - } - - return typeChecker.typeToString(type); - } - /** * Check if a given node is a string. * @param node The node to check. @@ -80,9 +33,7 @@ export default createRule({ const objectType = typeChecker.getTypeAtLocation( service.esTreeNodeToTSNodeMap.get(node), ); - const typeName = getTypeName(objectType); - - return typeName === 'string'; + return getTypeName(typeChecker, objectType) === 'string'; } return { diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index 6b3079cdfdf..9ef02636418 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -6,8 +6,7 @@ import { } from 'eslint-utils'; import { RegExpParser, AST as RegExpAST } from 'regexpp'; import { RuleFixer, RuleFix } from 'ts-eslint'; -import ts from 'typescript'; -import { createRule, getParserServices } from '../util'; +import { createRule, getParserServices, getTypeName } from '../util'; const EQ_OPERATORS = /^[=!]=/; const regexpp = new RegExpParser(); @@ -36,65 +35,17 @@ export default createRule({ const globalScope = context.getScope(); const sourceCode = context.getSourceCode(); const service = getParserServices(context); - const types = service.program.getTypeChecker(); - - /** - * Get the type name of a given type. - * @param type The type to get. - */ - function getTypeName(type: ts.Type): string { - // It handles `string` and string literal types as string. - if ((type.flags & ts.TypeFlags.StringLike) !== 0) { - return 'string'; - } - - // If the type is a type parameter which extends primitive string types, - // but it was not recognized as a string like. So check the constraint - // type of the type parameter. - if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { - // `type.getConstraint()` method doesn't return the constraint type of - // the type parameter for some reason. So this gets the constraint type - // via AST. - const node = type.symbol.declarations[0] as ts.TypeParameterDeclaration; - if (node.constraint != null) { - return getTypeName(types.getTypeFromTypeNode(node.constraint)); - } - } - - // If the type is a union and all types in the union are string like, - // return `string`. For example: - // - `"a" | "b"` is string. - // - `string | string[]` is not string. - if ( - type.isUnion() && - type.types.map(getTypeName).every(t => t === 'string') - ) { - return 'string'; - } - - // If the type is an intersection and a type in the intersection is string - // like, return `string`. For example: `string & {__htmlEscaped: void}` - if ( - type.isIntersection() && - type.types.map(getTypeName).some(t => t === 'string') - ) { - return 'string'; - } - - return types.typeToString(type); - } + const typeChecker = service.program.getTypeChecker(); /** * Check if a given node is a string. * @param node The node to check. */ function isStringType(node: TSESTree.Node): boolean { - const objectType = types.getTypeAtLocation( + const objectType = typeChecker.getTypeAtLocation( service.esTreeNodeToTSNodeMap.get(node), ); - const typeName = getTypeName(objectType); - - return typeName === 'string'; + return getTypeName(typeChecker, objectType) === 'string'; } /** diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index 4e5d455926a..f09ac64079d 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -35,3 +35,60 @@ export function containsTypeByName( bases.some(t => containsTypeByName(t, allowedNames)) ); } + +/** + * Get the type name of a given type. + * @param typeChecker The context sensitive TypeScript TypeChecker. + * @param type The type to get the name of. + */ +export function getTypeName( + typeChecker: ts.TypeChecker, + type: ts.Type, +): string { + // It handles `string` and string literal types as string. + if ((type.flags & ts.TypeFlags.StringLike) !== 0) { + return 'string'; + } + + // If the type is a type parameter which extends primitive string types, + // but it was not recognized as a string like. So check the constraint + // type of the type parameter. + if ((type.flags & ts.TypeFlags.TypeParameter) !== 0) { + // `type.getConstraint()` method doesn't return the constraint type of + // the type parameter for some reason. So this gets the constraint type + // via AST. + const node = type.symbol.declarations[0] as ts.TypeParameterDeclaration; + if (node.constraint != null) { + return getTypeName( + typeChecker, + typeChecker.getTypeFromTypeNode(node.constraint), + ); + } + } + + // If the type is a union and all types in the union are string like, + // return `string`. For example: + // - `"a" | "b"` is string. + // - `string | string[]` is not string. + if ( + type.isUnion() && + type.types + .map(value => getTypeName(typeChecker, value)) + .every(t => t === 'string') + ) { + return 'string'; + } + + // If the type is an intersection and a type in the intersection is string + // like, return `string`. For example: `string & {__htmlEscaped: void}` + if ( + type.isIntersection() && + type.types + .map(value => getTypeName(typeChecker, value)) + .some(t => t === 'string') + ) { + return 'string'; + } + + return typeChecker.typeToString(type); +}