From 0a10459d33e34a8ee6447346350d50f3d8d00c07 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sat, 16 Feb 2019 16:32:35 +0900 Subject: [PATCH 01/15] feat(eslint-plugin): add prefer-string-starts-ends-with rule (fixes #285) --- .../rules/prefer-string-starts-ends-with.md | 54 + packages/eslint-plugin/package.json | 2 + .../rules/prefer-string-starts-ends-with.ts | 636 ++++++++++++ packages/eslint-plugin/tests/RuleTester.ts | 2 +- .../prefer-string-starts-ends-with.test.ts | 948 ++++++++++++++++++ .../eslint-plugin/typings/eslint-utils.d.ts | 178 ++++ packages/eslint-plugin/typings/ts-eslint.d.ts | 8 +- 7 files changed, 1825 insertions(+), 3 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md create mode 100644 packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts create mode 100644 packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts create mode 100644 packages/eslint-plugin/typings/eslint-utils.d.ts diff --git a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md new file mode 100644 index 00000000000..0dbe4e6c7ba --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md @@ -0,0 +1,54 @@ +# Enforce to use `String#startsWith` and `String#endsWith` over other options (prefer-string-starts-ends-with) + +There are multiple ways to verify if a string starts or ends with a specific string, auch as `foo.indexOf('bar') === 0`. + +Since ES2015 has added `String#startsWith` and `String#endsWith`, this rule reports other ways to be consistent. + +## Rule Details + +This rule is aimed at enforcing the consistent way that checks a string starts or ends with a specific string. + +Examples of **incorrect** code for this rule: + +```ts +let foo: string + +// starts with +foo[0] === 'b' +foo.charAt(0) === 'b' +foo.indexOf('bar') === 0 +foo.slice(0, 3) === 'bar' +foo.substring(0, 3) === 'bar' +foo.match(/^bar/) +/^bar/.test(foo) + +// ends with +foo[foo.length - 1] === 'b' +foo.charAt(foo.length - 1) === 'b' +foo.lastIndexOf('bar') === foo.length - 3 +foo.slice(-3) === 'bar' +foo.substring(foo.length - 3) === 'bar' +foo.match(/bar$/) +/bar$/.test(foo) +``` + +Examples of **correct** code for this rule: + +```ts +foo.startsWith('bar'); +foo.endsWith('bar'); +``` + +## Options + +There is no option. + +```JSON +{ + "@typescript-eslint/prefer-string-starts-ends-with": "error" +} +``` + +## When Not To Use It + +If you don't mind that style, you can turn this rule off safely. diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 5f3b1647ec2..3bd73ba1c59 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -37,6 +37,8 @@ "dependencies": { "@typescript-eslint/parser": "1.3.0", "@typescript-eslint/typescript-estree": "1.3.0", + "eslint-utils": "^1.3.1", + "regexpp": "^2.0.1", "requireindex": "^1.2.0", "tsutils": "^3.7.0" }, 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 new file mode 100644 index 00000000000..d1e4a072453 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -0,0 +1,636 @@ +/** + * @fileoverview Enforce to use `String#startsWith` and `String#endsWith` over other options. + * @author Toru Nagashima + */ + +import { TSESTree } from '@typescript-eslint/typescript-estree'; +import { + isNotClosingParenToken, + getPropertyName, + getStaticValue +} 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'; + +const EQ_OPERATORS = /^[=!]=/; +const regexpp = new RegExpParser(); + +export default createRule({ + name: 'prefer-string-starts-ends-with', + defaultOptions: [], + + meta: { + type: 'suggestion', + docs: { + description: + 'enforce to use `String#startsWith` and `String#endsWith` over other options', + category: 'Best Practices', + recommended: false + }, + messages: { + preferStartsWith: "Use 'String#startsWith' method instead.", + preferEndsWith: "Use 'String#endsWith' method instead." + }, + schema: [], + fixable: 'code' + }, + + create(context) { + 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); + } + + /** + * Check if a given node is a string. + * @param node The node to check. + */ + function isStringType(node: TSESTree.Node): boolean { + const objectType = types.getTypeAtLocation( + service.esTreeNodeToTSNodeMap.get(node) + ); + const typeName = getTypeName(objectType); + + return typeName === 'string'; + } + + /** + * Check if a given node is a `Literal` node that is a given value. + * @param node The node to check. + * @param value The expected value of the `Literal` node. + */ + function isNumber( + node: TSESTree.Node, + value: number + ): node is TSESTree.Literal { + const evaluated = getStaticValue(node, globalScope); + return evaluated != null && evaluated.value === value; + } + + /** + * Check if a given node is a `Literal` node that is a character. + * @param node The node to check. + * @param kind The method name to get a character. + */ + function isCharacter(node: TSESTree.Node): node is TSESTree.Literal { + const evaluated = getStaticValue(node, globalScope); + return ( + evaluated != null && + typeof evaluated.value === 'string' && + evaluated.value[0] === evaluated.value + ); + } + + /** + * Check if a given node is `==`, `===`, `!=`, or `!==`. + * @param node The node to check. + */ + function isEqualityComparison( + node: TSESTree.Node + ): node is TSESTree.BinaryExpression { + return ( + node.type === 'BinaryExpression' && EQ_OPERATORS.test(node.operator) + ); + } + + /** + * Check if two given nodes are the same meaning. + * @param node1 A node to compare. + * @param node2 Another node to compare. + */ + function isSameTokens(node1: TSESTree.Node, node2: TSESTree.Node): boolean { + const tokens1 = sourceCode.getTokens(node1); + const tokens2 = sourceCode.getTokens(node2); + + if (tokens1.length !== tokens2.length) { + return false; + } + + for (let i = 0; i < tokens1.length; ++i) { + const token1 = tokens1[i]; + const token2 = tokens2[i]; + + if (token1.type !== token2.type || token1.value !== token2.value) { + return false; + } + } + + return true; + } + + /** + * Check if a given node is the expression of the length of a string. + * + * - If `length` property access of `expectedObjectNode`, it's `true`. + * E.g., `foo` → `foo.length` / `"foo"` → `"foo".length` + * - If `expectedObjectNode` is a string literal, `node` can be a number. + * E.g., `"foo"` → `3` + * + * @param node The node to check. + * @param expectedObjectNode The node which is expected as the receiver of `length` property. + */ + function isLengthExpression( + node: TSESTree.Node, + expectedObjectNode: TSESTree.Node + ): boolean { + if (node.type === 'MemberExpression') { + return ( + getPropertyName(node, globalScope) === 'length' && + isSameTokens(node.object, expectedObjectNode) + ); + } + + const evaluatedLength = getStaticValue(node, globalScope); + const evaluatedString = getStaticValue(expectedObjectNode, globalScope); + return ( + evaluatedLength != null && + evaluatedString != null && + typeof evaluatedLength.value === 'number' && + typeof evaluatedString.value === 'string' && + evaluatedLength.value === evaluatedString.value.length + ); + } + + /** + * Check if a given node is the expression of the last index. + * + * E.g. `foo.length - 1` + * + * @param node The node to check. + * @param expectedObjectNode The node which is expected as the receiver of `length` property. + */ + function isLastIndexExpression( + node: TSESTree.Node, + expectedObjectNode: TSESTree.Node + ): boolean { + return ( + node.type === 'BinaryExpression' && + node.operator === '-' && + isLengthExpression(node.left, expectedObjectNode) && + isNumber(node.right, 1) + ); + } + + /** + * Get the range of the property of a given `MemberExpression` node. + * + * - `obj[foo]` → the range of `[foo]` + * - `obf.foo` → the range of `.foo` + * - `(obj).foo` → the range of `.foo` + * + * @param node The member expression node to get. + */ + function getPropertyRange( + node: TSESTree.MemberExpression + ): [number, number] { + const dotOrOpenBracket = sourceCode.getTokenAfter( + node.object, + isNotClosingParenToken + )!; + return [dotOrOpenBracket.range[0], node.range[1]]; + } + + /** + * Parse a given `RegExp` pattern to that string if it's a static string. + * @param pattern The RegExp pattern text to parse. + * @param uFlag The Unicode flag of the RegExp. + */ + function parseRegExpText(pattern: string, uFlag: boolean): string | null { + // Parse it. + const ast = regexpp.parsePattern(pattern, undefined, undefined, uFlag); + if (ast.alternatives.length !== 1) { + return null; + } + + // Drop `^`/`$` assertion. + const chars = ast.alternatives[0].elements; + const first = chars[0]; + if (first.type === 'Assertion' && first.kind === 'start') { + chars.shift(); + } else { + chars.pop(); + } + + // Check if it can determine a unique string. + if (!chars.every(c => c.type === 'Character')) { + return null; + } + + // To string. + return String.fromCodePoint( + ...chars.map(c => (c as RegExpAST.Character).value) + ); + } + + /** + * Parse a given node if it's a `RegExp` instance. + * @param node The node to parse. + */ + function parseRegExp( + node: TSESTree.Node + ): { isStartsWith: boolean; isEndsWith: boolean; text: string } | null { + const evaluated = getStaticValue(node, globalScope); + if (evaluated == null || !(evaluated.value instanceof RegExp)) { + return null; + } + + const { source, flags } = evaluated.value; + const isStartsWith = source.startsWith('^'); + const isEndsWith = source.endsWith('$'); + if ( + isStartsWith === isEndsWith || + flags.includes('i') || + flags.includes('m') + ) { + return null; + } + + const text = parseRegExpText(source, flags.includes('u')); + if (text == null) { + return null; + } + + return { isEndsWith, isStartsWith, text }; + } + + /** + * Fix code with using the right operand as the search string. + * For example: `foo.slice(0, 3) === 'bar'` → `foo.startsWith('bar')` + * @param fixer The rule fixer. + * @param node The node which was reported. + * @param kind The kind of the report. + * @param negative The flag to fix to negative condition. + */ + function* fixWithRightOperand( + fixer: RuleFixer, + node: TSESTree.BinaryExpression, + kind: 'start' | 'end', + negative: boolean + ): IterableIterator { + // left is CallExpression or MemberExpression. + const leftNode = (node.left.type === 'CallExpression' + ? node.left.callee + : node.left) as TSESTree.MemberExpression; + const propertyRange = getPropertyRange(leftNode); + + if (negative) { + yield fixer.insertTextBefore(node, '!'); + } + yield fixer.replaceTextRange( + [propertyRange[0], node.right.range[0]], + `.${kind}sWith(` + ); + yield fixer.replaceTextRange([node.right.range[1], node.range[1]], ')'); + } + + /** + * Fix code with using the first argument as the search string. + * For example: `foo.indexOf('bar') === 0` → `foo.startsWith('bar')` + * @param fixer The rule fixer. + * @param node The node which was reported. + * @param kind The kind of the report. + * @param negative The flag to fix to negative condition. + */ + function* fixWithArgument( + fixer: RuleFixer, + node: TSESTree.BinaryExpression, + kind: 'start' | 'end', + negative: boolean + ): IterableIterator { + const callNode = node.left as TSESTree.CallExpression; + const calleeNode = callNode.callee as TSESTree.MemberExpression; + + if (negative) { + yield fixer.insertTextBefore(node, '!'); + } + yield fixer.replaceTextRange( + getPropertyRange(calleeNode), + `.${kind}sWith` + ); + yield fixer.removeRange([callNode.range[1], node.range[1]]); + } + + return { + // foo[0] === "a" + // foo.charAt(0) === "a" + // foo[foo.length - 1] === "a" + // foo.charAt(foo.length - 1) === "a" + // @ts-ignore + [[ + 'BinaryExpression > MemberExpression.left[computed=true]', + 'CallExpression > MemberExpression.callee[property.name="charAt"][computed=false]' + ]](node: TSESTree.MemberExpression): void { + let parentNode = node.parent!; + let leftNode: TSESTree.Node = node; + let indexNode: TSESTree.Node | null = null; + if (parentNode.type === 'CallExpression') { + if (parentNode.arguments.length === 1) { + indexNode = parentNode.arguments[0]; + } + leftNode = parentNode; + parentNode = parentNode.parent!; + } else { + indexNode = node.property; + } + + if ( + indexNode == null || + !isEqualityComparison(parentNode) || + parentNode.left !== leftNode || + !isStringType(node.object) + ) { + return; + } + + const isEndsWith = isLastIndexExpression(indexNode, node.object); + const isStartsWith = !isEndsWith && isNumber(indexNode, 0); + if (!isStartsWith && !isEndsWith) { + return; + } + + const eqNode = parentNode; + context.report({ + node: parentNode, + messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', + fix(fixer) { + // Don't fix if it can change the behavior. + if (!isCharacter(eqNode.right)) { + return null; + } + return fixWithRightOperand( + fixer, + eqNode, + isStartsWith ? 'start' : 'end', + eqNode.operator.startsWith('!') + ); + } + }); + }, + + // foo.indexOf('bar') === 0 + 'CallExpression > MemberExpression.callee[property.name="indexOf"][computed=false]'( + node: TSESTree.MemberExpression + ): void { + const callNode = node.parent! as TSESTree.CallExpression; + const parentNode = callNode.parent!; + + if ( + callNode.arguments.length !== 1 || + !isEqualityComparison(parentNode) || + parentNode.left !== callNode || + !isNumber(parentNode.right, 0) || + !isStringType(node.object) + ) { + return; + } + + context.report({ + node: parentNode, + messageId: 'preferStartsWith', + fix(fixer) { + return fixWithArgument( + fixer, + parentNode, + 'start', + parentNode.operator.startsWith('!') + ); + } + }); + }, + + // foo.lastIndexOf('bar') === foo.length - 3 + // foo.lastIndexOf(bar) === foo.length - bar.length + 'CallExpression > MemberExpression.callee[property.name="lastIndexOf"][computed=false]'( + node: TSESTree.MemberExpression + ): void { + const callNode = node.parent! as TSESTree.CallExpression; + const parentNode = callNode.parent!; + + if ( + callNode.arguments.length !== 1 || + !isEqualityComparison(parentNode) || + parentNode.left !== callNode || + parentNode.right.type !== 'BinaryExpression' || + parentNode.right.operator !== '-' || + !isLengthExpression(parentNode.right.left, node.object) || + !isLengthExpression(parentNode.right.right, callNode.arguments[0]) || + !isStringType(node.object) + ) { + return; + } + + context.report({ + node: parentNode, + messageId: 'preferEndsWith', + fix(fixer) { + return fixWithArgument( + fixer, + parentNode, + 'end', + parentNode.operator.startsWith('!') + ); + } + }); + }, + + // foo.match(/^bar/) + // foo.match(/bar$/) + 'CallExpression > MemberExpression.callee[property.name="match"][computed=false]'( + node: TSESTree.MemberExpression + ): void { + if (!isStringType(node.object)) { + return; + } + + const callNode = node.parent as TSESTree.CallExpression; + const parsed = + callNode.arguments.length === 1 + ? parseRegExp(callNode.arguments[0]) + : null; + if (parsed == null) { + return; + } + + const { isStartsWith, text } = parsed; + context.report({ + node: callNode, + messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', + fix(fixer) { + return [ + fixer.replaceTextRange( + getPropertyRange(node), + `.${isStartsWith ? 'start' : 'end'}sWith` + ), + fixer.replaceText(callNode.arguments[0], JSON.stringify(text)) + ]; + } + }); + }, + + // foo.slice(0, 3) === 'bar' + // foo.slice(-3) === 'bar' + // foo.slice(-3, foo.length) === 'bar' + // foo.substring(0, 3) === 'bar' + // foo.substring(foo.length - 3) === 'bar' + // foo.substring(foo.length - 3, foo.length) === 'bar' + // @ts-ignore + [[ + 'CallExpression > MemberExpression.callee[property.name=slice][computed=false]', + 'CallExpression > MemberExpression.callee[property.name=substring][computed=false]' + ]](node: TSESTree.MemberExpression): void { + const callNode = node.parent! as TSESTree.CallExpression; + const parentNode = callNode.parent!; + if ( + !isEqualityComparison(parentNode) || + parentNode.left !== callNode || + !isStringType(node.object) + ) { + return; + } + + const isEndsWith = + callNode.arguments.length === 1 || + (callNode.arguments.length === 2 && + isLengthExpression(callNode.arguments[1], node.object)); + const isStartsWith = + !isEndsWith && + callNode.arguments.length === 2 && + isNumber(callNode.arguments[0], 0); + if (!isStartsWith && !isEndsWith) { + return; + } + + const eqNode = parentNode; + const negativeIndexSupported = (node.property as any).name === 'slice'; + context.report({ + node: parentNode, + messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', + fix(fixer) { + // Don't fix if it can change the behavior. + if ( + eqNode.operator.length === 2 && + (eqNode.right.type !== 'Literal' || + typeof eqNode.right.value !== 'string') + ) { + return null; + } + if (isStartsWith) { + if (!isLengthExpression(callNode.arguments[1], eqNode.right)) { + return null; + } + } else { + const posNode = callNode.arguments[0]; + const posNodeIsAbsolutelyValid = + (posNode.type === 'BinaryExpression' && + posNode.operator === '-' && + isLengthExpression(posNode.left, node.object) && + isLengthExpression(posNode.right, eqNode.right)) || + (negativeIndexSupported && + posNode.type === 'UnaryExpression' && + posNode.operator === '-' && + isLengthExpression(posNode.argument, eqNode.right)); + if (!posNodeIsAbsolutelyValid) { + return null; + } + } + + return fixWithRightOperand( + fixer, + parentNode, + isStartsWith ? 'start' : 'end', + parentNode.operator.startsWith('!') + ); + } + }); + }, + + // /^bar/.test(foo) + // /bar$/.test(foo) + 'CallExpression > MemberExpression.callee[property.name="test"][computed=false]'( + node: TSESTree.MemberExpression + ): void { + const callNode = node.parent as TSESTree.CallExpression; + const parsed = + callNode.arguments.length === 1 ? parseRegExp(node.object) : null; + if (parsed == null) { + return; + } + + const { isStartsWith, text } = parsed; + const messageId = isStartsWith ? 'preferStartsWith' : 'preferEndsWith'; + const methodName = isStartsWith ? 'startsWith' : 'endsWith'; + context.report({ + node: callNode, + messageId, + *fix(fixer) { + const argNode = callNode.arguments[0]; + const needsParen = + argNode.type !== 'Literal' && + argNode.type !== 'TemplateLiteral' && + argNode.type !== 'Identifier' && + argNode.type !== 'MemberExpression' && + argNode.type !== 'CallExpression'; + + yield fixer.removeRange([callNode.range[0], argNode.range[0]]); + if (needsParen) { + yield fixer.insertTextBefore(argNode, '('); + yield fixer.insertTextAfter(argNode, ')'); + } + yield fixer.insertTextAfter( + argNode, + `.${methodName}(${JSON.stringify(text)}` + ); + } + }); + } + }; + } +}); diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index 3a7a01b8fd7..beadc109eb4 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -22,7 +22,7 @@ interface InvalidTestCase< TOptions extends Readonly > extends ValidTestCase { errors: TestCaseError[]; - output?: string; + output?: string | null; } interface TestCaseError { diff --git a/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts new file mode 100644 index 00000000000..b17cbe070e2 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts @@ -0,0 +1,948 @@ +import path from 'path'; +import rule from '../../src/rules/prefer-string-starts-ends-with'; +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-string-starts-ends-with', rule, { + valid: [ + ` + function f(s: string[]) { + s[0] === "a" + } + `, + ` + function f(s: string) { + s[0] + "a" + } + `, + ` + function f(s: string) { + s[1] === "a" + } + `, + ` + function f(s: string | string[]) { + s[0] === "a" + } + `, + ` + function f(s: any) { + s[0] === "a" + } + `, + ` + function f(s: T) { + s[0] === "a" + } + `, + ` + function f(s: string[]) { + s[s.length - 1] === "a" + } + `, + ` + function f(s: string) { + s[s.length - 2] === "a" + } + `, + ` + function f(s: string[]) { + s.charAt(0) === "a" + } + `, + ` + function f(s: string) { + s.charAt(0) + "a" + } + `, + ` + function f(s: string) { + s.charAt(1) === "a" + } + `, + ` + function f(s: string) { + s.charAt() === "a" + } + `, + ` + function f(s: string[]) { + s.charAt(s.length - 1) === "a" + } + `, + ` + function f(a: string, b: string, c: string) { + (a + b).charAt((a + c).length - 1) === "a" + } + `, + ` + function f(a: string, b: string, c: string) { + (a + b).charAt(c.length - 1) === "a" + } + `, + ` + function f(s: string[]) { + s.indexOf(needle) === 0 + } + `, + ` + function f(s: string | string[]) { + s.indexOf(needle) === 0 + } + `, + ` + function f(s: string) { + s.indexOf(needle) === s.length - needle.length + } + `, + ` + function f(s: string[]) { + s.lastIndexOf(needle) === s.length - needle.length + } + `, + ` + function f(s: string) { + s.lastIndexOf(needle) === 0 + } + `, + ` + function f(s: { match(x: any): boolean }) { + s.match(/^foo/) + } + `, + ` + function f(s: { match(x: any): boolean }) { + s.match(/foo$/) + } + `, + ` + function f(s: string) { + s.match(/foo/) + } + `, + ` + function f(s: string) { + s.match(/^foo$/) + } + `, + ` + function f(s: string) { + s.match(/^foo./) + } + `, + ` + function f(s: string) { + s.match(/^foo|bar/) + } + `, + ` + function f(s: string) { + s.match(new RegExp("")) + } + `, + ` + function f(s: string) { + s.match(pattern) // cannot check '^'/'$' + } + `, + ` + function f(s: string) { + s.match(new RegExp("^/!{[", "u")) // has syntax error + } + `, + ` + function f(s: string) { + s.match() + } + `, + ` + function f(s: string) { + s.match(777) + } + `, + ` + function f(s: string[]) { + s.slice(0, needle.length) === needle + } + `, + ` + function f(s: string[]) { + s.slice(-needle.length) === needle + } + `, + ` + function f(s: string) { + s.slice(1, 4) === "bar" + } + `, + ` + function f(s: string) { + s.slice(-4, -1) === "bar" + } + `, + ` + function f(s: string) { + pattern.test(s) + } + `, + ` + function f(s: string) { + /^bar/.test() + } + `, + ` + function f(x: { test(): void }, s: string) { + x.test(s) + } + ` + ], + invalid: [ + // String indexing. + { + code: ` + function f(s: string) { + s[0] === "a" + } + `, + output: ` + function f(s: string) { + s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s[0] !== "a" + } + `, + output: ` + function f(s: string) { + !s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s[0] == "a" + } + `, + output: ` + function f(s: string) { + s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s[0] != "a" + } + `, + output: ` + function f(s: string) { + !s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s[0] === "あ" + } + `, + output: ` + function f(s: string) { + s.startsWith("あ") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s[0] === "👍" // the length is 2. + } + `, + output: null, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string, t: string) { + s[0] === t // the length of t is unknown. + } + `, + output: null, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s[s.length - 1] === "a" + } + `, + output: ` + function f(s: string) { + s.endsWith("a") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + (s)[0] === ("a") + } + `, + output: ` + function f(s: string) { + (s).startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + + // String#charAt + { + code: ` + function f(s: string) { + s.charAt(0) === "a" + } + `, + output: ` + function f(s: string) { + s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.charAt(0) !== "a" + } + `, + output: ` + function f(s: string) { + !s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.charAt(0) == "a" + } + `, + output: ` + function f(s: string) { + s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.charAt(0) != "a" + } + `, + output: ` + function f(s: string) { + !s.startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.charAt(0) === "あ" + } + `, + output: ` + function f(s: string) { + s.startsWith("あ") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.charAt(0) === "👍" // the length is 2. + } + `, + output: null, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string, t: string) { + s.charAt(0) === t // the length of t is unknown. + } + `, + output: null, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.charAt(s.length - 1) === "a" + } + `, + output: ` + function f(s: string) { + s.endsWith("a") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + (s).charAt(0) === "a" + } + `, + output: ` + function f(s: string) { + (s).startsWith("a") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + + // String#indexOf + { + code: ` + function f(s: string) { + s.indexOf(needle) === 0 + } + `, + output: ` + function f(s: string) { + s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.indexOf(needle) !== 0 + } + `, + output: ` + function f(s: string) { + !s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.indexOf(needle) == 0 + } + `, + output: ` + function f(s: string) { + s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.indexOf(needle) != 0 + } + `, + output: ` + function f(s: string) { + !s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + + // String#lastIndexOf + { + code: ` + function f(s: string) { + s.lastIndexOf("bar") === s.length - 3 + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.lastIndexOf("bar") !== s.length - 3 + } + `, + output: ` + function f(s: string) { + !s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.lastIndexOf("bar") == s.length - 3 + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.lastIndexOf("bar") != s.length - 3 + } + `, + output: ` + function f(s: string) { + !s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.lastIndexOf("bar") === s.length - "bar".length + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.lastIndexOf(needle) === s.length - needle.length + } + `, + output: ` + function f(s: string) { + s.endsWith(needle) + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + + // String#match + { + code: ` + function f(s: string) { + s.match(/^bar/) + } + `, + output: ` + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.match(/bar$/) + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + const pattern = /^bar/ + function f(s: string) { + s.match(pattern) + } + `, + output: ` + const pattern = /^bar/ + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + const pattern = new RegExp("^bar") + function f(s: string) { + s.match(pattern) + } + `, + output: ` + const pattern = new RegExp("^bar") + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + const pattern = /^"quoted"/ + function f(s: string) { + s.match(pattern) + } + `, + output: ` + const pattern = /^"quoted"/ + function f(s: string) { + s.startsWith("\\"quoted\\"") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + + // String#slice + { + code: ` + function f(s: string) { + s.slice(0, 3) === "bar" + } + `, + output: ` + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(0, 3) !== "bar" + } + `, + output: ` + function f(s: string) { + !s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(0, 3) == "bar" + } + `, + output: ` + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(0, 3) != "bar" + } + `, + output: ` + function f(s: string) { + !s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(0, needle.length) === needle + } + `, + output: ` + function f(s: string) { + s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(0, length) === needle // the 'length' can be different to 'needle.length' + } + `, + output: null, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(0, needle.length) == needle // hating implicit type conversion + } + `, + output: null, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(-3) === "bar" + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(-3) !== "bar" + } + `, + output: ` + function f(s: string) { + !s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(-needle.length) === needle + } + `, + output: ` + function f(s: string) { + s.endsWith(needle) + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(s.length - needle.length) === needle + } + `, + output: ` + function f(s: string) { + s.endsWith(needle) + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.slice(startIndex) === needle // 'startIndex' can be different + } + `, + output: null, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.substring(0, 3) === "bar" + } + `, + output: ` + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.substring(-3) === "bar" // the code is probably mistake. + } + `, + output: null, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.substring(s.length - 3, s.length) === "bar" + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + + // RegExp#test + { + code: ` + function f(s: string) { + /^bar/.test(s) + } + `, + output: ` + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + /bar$/.test(s) + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + const pattern = /^bar/ + function f(s: string) { + pattern.test(s) + } + `, + output: ` + const pattern = /^bar/ + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + const pattern = new RegExp("^bar") + function f(s: string) { + pattern.test(s) + } + `, + output: ` + const pattern = new RegExp("^bar") + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + const pattern = /^"quoted"/ + function f(s: string) { + pattern.test(s) + } + `, + output: ` + const pattern = /^"quoted"/ + function f(s: string) { + s.startsWith("\\"quoted\\"") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + /^bar/.test(a + b) + } + `, + output: ` + function f(s: string) { + (a + b).startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + + // Test for variation of string types. + { + code: ` + function f(s: "a" | "b") { + s.indexOf(needle) === 0 + } + `, + output: ` + function f(s: "a" | "b") { + s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: T) { + s.indexOf(needle) === 0 + } + `, + output: ` + function f(s: T) { + s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + type SafeString = string & {__HTML_ESCAPED__: void} + function f(s: SafeString) { + s.indexOf(needle) === 0 + } + `, + output: ` + type SafeString = string & {__HTML_ESCAPED__: void} + function f(s: SafeString) { + s.startsWith(needle) + } + `, + errors: [{ messageId: 'preferStartsWith' }] + } + ] +}); 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..f246c2470bc --- /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 9ab6ff35e21..eb1500bf425 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 { /** @@ -526,11 +528,13 @@ declare module 'ts-eslint' { ReportFixFunction, RuleContext, RuleFix, + RuleFixer, RuleFunction, RuleListener, RuleMetaData, RuleMetaDataDocs, - Scope + Scope, + SourceCode }; export default RuleModule; } From ee7c1df8c8a3378d598f53c1b6ae32e6c8194d85 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Tue, 19 Feb 2019 15:37:32 +0900 Subject: [PATCH 02/15] fix s.match behavior The return value of `s.match` is an `Array` object or `null`. --- .../rules/prefer-string-starts-ends-with.ts | 47 +++++---- .../prefer-string-starts-ends-with.test.ts | 98 ++++++++++++++++++- 2 files changed, 121 insertions(+), 24 deletions(-) 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 d1e4a072453..2c98695a13f 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 @@ -102,6 +102,15 @@ export default createRule({ return typeName === 'string'; } + /** + * Check if a given node is a `Literal` node that is null. + * @param node The node to check. + */ + function isNull(node: TSESTree.Node): node is TSESTree.Literal { + const evaluated = getStaticValue(node, globalScope); + return evaluated != null && evaluated.value === null; + } + /** * Check if a given node is a `Literal` node that is a given value. * @param node The node to check. @@ -366,16 +375,14 @@ export default createRule({ // @ts-ignore [[ 'BinaryExpression > MemberExpression.left[computed=true]', - 'CallExpression > MemberExpression.callee[property.name="charAt"][computed=false]' + 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="charAt"][computed=false]' ]](node: TSESTree.MemberExpression): void { let parentNode = node.parent!; - let leftNode: TSESTree.Node = node; let indexNode: TSESTree.Node | null = null; if (parentNode.type === 'CallExpression') { if (parentNode.arguments.length === 1) { indexNode = parentNode.arguments[0]; } - leftNode = parentNode; parentNode = parentNode.parent!; } else { indexNode = node.property; @@ -384,7 +391,6 @@ export default createRule({ if ( indexNode == null || !isEqualityComparison(parentNode) || - parentNode.left !== leftNode || !isStringType(node.object) ) { return; @@ -416,7 +422,7 @@ export default createRule({ }, // foo.indexOf('bar') === 0 - 'CallExpression > MemberExpression.callee[property.name="indexOf"][computed=false]'( + 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="indexOf"][computed=false]'( node: TSESTree.MemberExpression ): void { const callNode = node.parent! as TSESTree.CallExpression; @@ -448,7 +454,7 @@ export default createRule({ // foo.lastIndexOf('bar') === foo.length - 3 // foo.lastIndexOf(bar) === foo.length - bar.length - 'CallExpression > MemberExpression.callee[property.name="lastIndexOf"][computed=false]'( + 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="lastIndexOf"][computed=false]'( node: TSESTree.MemberExpression ): void { const callNode = node.parent! as TSESTree.CallExpression; @@ -481,16 +487,17 @@ export default createRule({ }); }, - // foo.match(/^bar/) - // foo.match(/bar$/) - 'CallExpression > MemberExpression.callee[property.name="match"][computed=false]'( + // foo.match(/^bar/) === null + // foo.match(/bar$/) === null + 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="match"][computed=false]'( node: TSESTree.MemberExpression ): void { - if (!isStringType(node.object)) { + const callNode = node.parent as TSESTree.CallExpression; + const parentNode = callNode.parent as TSESTree.BinaryExpression; + if (!isEqualityComparison(parentNode) || !isNull(parentNode.right) || !isStringType(node.object)) { return; } - const callNode = node.parent as TSESTree.CallExpression; const parsed = callNode.arguments.length === 1 ? parseRegExp(callNode.arguments[0]) @@ -503,14 +510,16 @@ export default createRule({ context.report({ node: callNode, messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', - fix(fixer) { - return [ - fixer.replaceTextRange( - getPropertyRange(node), - `.${isStartsWith ? 'start' : 'end'}sWith` - ), - fixer.replaceText(callNode.arguments[0], JSON.stringify(text)) - ]; + *fix(fixer) { + if (!parentNode.operator.startsWith("!")) { + yield fixer.insertTextBefore(parentNode, "!") + } + yield fixer.replaceTextRange( + getPropertyRange(node), + `.${isStartsWith ? 'start' : 'end'}sWith` + ) + yield fixer.replaceText(callNode.arguments[0], JSON.stringify(text)) + yield fixer.removeRange([callNode.range[1], parentNode.range[1]]) } }); }, diff --git a/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts index b17cbe070e2..1a3241677f2 100644 --- a/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts @@ -114,6 +114,16 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.lastIndexOf(needle) === 0 } `, + ` + function f(s: string) { + s.match(/^foo/) + } + `, + ` + function f(s: string) { + s.match(/foo$/) + } + `, ` function f(s: { match(x: any): boolean }) { s.match(/^foo/) @@ -566,7 +576,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { { code: ` function f(s: string) { - s.match(/^bar/) + s.match(/^bar/) !== null } `, output: ` @@ -579,7 +589,33 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { { code: ` function f(s: string) { - s.match(/bar$/) + s.match(/^bar/) != null + } + `, + output: ` + function f(s: string) { + s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.match(/bar$/) !== null + } + `, + output: ` + function f(s: string) { + s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.match(/bar$/) != null } `, output: ` @@ -589,11 +625,63 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { `, errors: [{ messageId: 'preferEndsWith' }] }, + { + code: ` + function f(s: string) { + s.match(/^bar/) === null + } + `, + output: ` + function f(s: string) { + !s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.match(/^bar/) == null + } + `, + output: ` + function f(s: string) { + !s.startsWith("bar") + } + `, + errors: [{ messageId: 'preferStartsWith' }] + }, + { + code: ` + function f(s: string) { + s.match(/bar$/) === null + } + `, + output: ` + function f(s: string) { + !s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, + { + code: ` + function f(s: string) { + s.match(/bar$/) == null + } + `, + output: ` + function f(s: string) { + !s.endsWith("bar") + } + `, + errors: [{ messageId: 'preferEndsWith' }] + }, { code: ` const pattern = /^bar/ function f(s: string) { - s.match(pattern) + s.match(pattern) != null } `, output: ` @@ -608,7 +696,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { code: ` const pattern = new RegExp("^bar") function f(s: string) { - s.match(pattern) + s.match(pattern) != null } `, output: ` @@ -623,7 +711,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { code: ` const pattern = /^"quoted"/ function f(s: string) { - s.match(pattern) + s.match(pattern) != null } `, output: ` From 34e9fe1885c4dc0490783da06b267f0f99f4f20c Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Tue, 19 Feb 2019 15:40:35 +0900 Subject: [PATCH 03/15] fix formating errors --- .../rules/prefer-string-starts-ends-with.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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 2c98695a13f..77b4b77a05a 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 @@ -494,7 +494,11 @@ export default createRule({ ): void { const callNode = node.parent as TSESTree.CallExpression; const parentNode = callNode.parent as TSESTree.BinaryExpression; - if (!isEqualityComparison(parentNode) || !isNull(parentNode.right) || !isStringType(node.object)) { + if ( + !isEqualityComparison(parentNode) || + !isNull(parentNode.right) || + !isStringType(node.object) + ) { return; } @@ -511,15 +515,18 @@ export default createRule({ node: callNode, messageId: isStartsWith ? 'preferStartsWith' : 'preferEndsWith', *fix(fixer) { - if (!parentNode.operator.startsWith("!")) { - yield fixer.insertTextBefore(parentNode, "!") + if (!parentNode.operator.startsWith('!')) { + yield fixer.insertTextBefore(parentNode, '!'); } yield fixer.replaceTextRange( getPropertyRange(node), `.${isStartsWith ? 'start' : 'end'}sWith` - ) - yield fixer.replaceText(callNode.arguments[0], JSON.stringify(text)) - yield fixer.removeRange([callNode.range[1], parentNode.range[1]]) + ); + yield fixer.replaceText( + callNode.arguments[0], + JSON.stringify(text) + ); + yield fixer.removeRange([callNode.range[1], parentNode.range[1]]); } }); }, From c39407afda5527efb45e36f10d78850afb43fb45 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 19 Feb 2019 16:17:48 +0900 Subject: [PATCH 04/15] Update packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md Co-Authored-By: mysticatea --- .../eslint-plugin/docs/rules/prefer-string-starts-ends-with.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md index 0dbe4e6c7ba..e412d767554 100644 --- a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md +++ b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md @@ -41,7 +41,7 @@ foo.endsWith('bar'); ## Options -There is no option. +There are no options. ```JSON { From 4cd6097d669ea3751eced74bb3047a1f344b3eff Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Tue, 19 Feb 2019 16:29:01 +0900 Subject: [PATCH 05/15] fix for coverage 100 some test cases were rejected by node selectors unintentionally. --- .../prefer-string-starts-ends-with.test.ts | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts index 1a3241677f2..22550359837 100644 --- a/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts @@ -124,59 +124,69 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.match(/foo$/) } `, + ` + function f(s: string) { + s.match(/^foo/) + 1 + } + `, + ` + function f(s: string) { + s.match(/foo$/) + 1 + } + `, ` function f(s: { match(x: any): boolean }) { - s.match(/^foo/) + s.match(/^foo/) !== null } `, ` function f(s: { match(x: any): boolean }) { - s.match(/foo$/) + s.match(/foo$/) !== null } `, ` function f(s: string) { - s.match(/foo/) + s.match(/foo/) !== null } `, ` function f(s: string) { - s.match(/^foo$/) + s.match(/^foo$/) !== null } `, ` function f(s: string) { - s.match(/^foo./) + s.match(/^foo./) !== null } `, ` function f(s: string) { - s.match(/^foo|bar/) + s.match(/^foo|bar/) !== null } `, ` function f(s: string) { - s.match(new RegExp("")) + s.match(new RegExp("")) !== null } `, ` function f(s: string) { - s.match(pattern) // cannot check '^'/'$' + s.match(pattern) !== null // cannot check '^'/'$' } `, ` function f(s: string) { - s.match(new RegExp("^/!{[", "u")) // has syntax error + s.match(new RegExp("^/!{[", "u")) !== null // has syntax error } `, ` function f(s: string) { - s.match() + s.match() !== null } `, ` function f(s: string) { - s.match(777) + s.match(777) !== null } `, ` From 881a3e4ac0de879c71dd70b4f8a5575a323821b1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 20 Feb 2019 15:15:20 +0900 Subject: [PATCH 06/15] fix format as following master branch --- .../rules/prefer-string-starts-ends-with.ts | 76 +++++----- .../prefer-string-starts-ends-with.test.ts | 136 +++++++++--------- .../eslint-plugin/typings/eslint-utils.d.ts | 70 ++++----- packages/eslint-plugin/typings/ts-eslint.d.ts | 2 +- 4 files changed, 142 insertions(+), 142 deletions(-) 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 77b4b77a05a..7456766a305 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 @@ -7,7 +7,7 @@ import { TSESTree } from '@typescript-eslint/typescript-estree'; import { isNotClosingParenToken, getPropertyName, - getStaticValue + getStaticValue, } from 'eslint-utils'; import { RegExpParser, AST as RegExpAST } from 'regexpp'; import { RuleFixer, RuleFix } from 'ts-eslint'; @@ -27,14 +27,14 @@ export default createRule({ description: 'enforce to use `String#startsWith` and `String#endsWith` over other options', category: 'Best Practices', - recommended: false + recommended: false, }, messages: { preferStartsWith: "Use 'String#startsWith' method instead.", - preferEndsWith: "Use 'String#endsWith' method instead." + preferEndsWith: "Use 'String#endsWith' method instead.", }, schema: [], - fixable: 'code' + fixable: 'code', }, create(context) { @@ -95,7 +95,7 @@ export default createRule({ */ function isStringType(node: TSESTree.Node): boolean { const objectType = types.getTypeAtLocation( - service.esTreeNodeToTSNodeMap.get(node) + service.esTreeNodeToTSNodeMap.get(node), ); const typeName = getTypeName(objectType); @@ -118,7 +118,7 @@ export default createRule({ */ function isNumber( node: TSESTree.Node, - value: number + value: number, ): node is TSESTree.Literal { const evaluated = getStaticValue(node, globalScope); return evaluated != null && evaluated.value === value; @@ -143,7 +143,7 @@ export default createRule({ * @param node The node to check. */ function isEqualityComparison( - node: TSESTree.Node + node: TSESTree.Node, ): node is TSESTree.BinaryExpression { return ( node.type === 'BinaryExpression' && EQ_OPERATORS.test(node.operator) @@ -188,7 +188,7 @@ export default createRule({ */ function isLengthExpression( node: TSESTree.Node, - expectedObjectNode: TSESTree.Node + expectedObjectNode: TSESTree.Node, ): boolean { if (node.type === 'MemberExpression') { return ( @@ -218,7 +218,7 @@ export default createRule({ */ function isLastIndexExpression( node: TSESTree.Node, - expectedObjectNode: TSESTree.Node + expectedObjectNode: TSESTree.Node, ): boolean { return ( node.type === 'BinaryExpression' && @@ -238,11 +238,11 @@ export default createRule({ * @param node The member expression node to get. */ function getPropertyRange( - node: TSESTree.MemberExpression + node: TSESTree.MemberExpression, ): [number, number] { const dotOrOpenBracket = sourceCode.getTokenAfter( node.object, - isNotClosingParenToken + isNotClosingParenToken, )!; return [dotOrOpenBracket.range[0], node.range[1]]; } @@ -275,7 +275,7 @@ export default createRule({ // To string. return String.fromCodePoint( - ...chars.map(c => (c as RegExpAST.Character).value) + ...chars.map(c => (c as RegExpAST.Character).value), ); } @@ -284,7 +284,7 @@ export default createRule({ * @param node The node to parse. */ function parseRegExp( - node: TSESTree.Node + node: TSESTree.Node, ): { isStartsWith: boolean; isEndsWith: boolean; text: string } | null { const evaluated = getStaticValue(node, globalScope); if (evaluated == null || !(evaluated.value instanceof RegExp)) { @@ -322,7 +322,7 @@ export default createRule({ fixer: RuleFixer, node: TSESTree.BinaryExpression, kind: 'start' | 'end', - negative: boolean + negative: boolean, ): IterableIterator { // left is CallExpression or MemberExpression. const leftNode = (node.left.type === 'CallExpression' @@ -335,7 +335,7 @@ export default createRule({ } yield fixer.replaceTextRange( [propertyRange[0], node.right.range[0]], - `.${kind}sWith(` + `.${kind}sWith(`, ); yield fixer.replaceTextRange([node.right.range[1], node.range[1]], ')'); } @@ -352,7 +352,7 @@ export default createRule({ fixer: RuleFixer, node: TSESTree.BinaryExpression, kind: 'start' | 'end', - negative: boolean + negative: boolean, ): IterableIterator { const callNode = node.left as TSESTree.CallExpression; const calleeNode = callNode.callee as TSESTree.MemberExpression; @@ -362,7 +362,7 @@ export default createRule({ } yield fixer.replaceTextRange( getPropertyRange(calleeNode), - `.${kind}sWith` + `.${kind}sWith`, ); yield fixer.removeRange([callNode.range[1], node.range[1]]); } @@ -375,7 +375,7 @@ export default createRule({ // @ts-ignore [[ 'BinaryExpression > MemberExpression.left[computed=true]', - 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="charAt"][computed=false]' + 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="charAt"][computed=false]', ]](node: TSESTree.MemberExpression): void { let parentNode = node.parent!; let indexNode: TSESTree.Node | null = null; @@ -415,15 +415,15 @@ export default createRule({ fixer, eqNode, isStartsWith ? 'start' : 'end', - eqNode.operator.startsWith('!') + eqNode.operator.startsWith('!'), ); - } + }, }); }, // foo.indexOf('bar') === 0 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="indexOf"][computed=false]'( - node: TSESTree.MemberExpression + node: TSESTree.MemberExpression, ): void { const callNode = node.parent! as TSESTree.CallExpression; const parentNode = callNode.parent!; @@ -446,16 +446,16 @@ export default createRule({ fixer, parentNode, 'start', - parentNode.operator.startsWith('!') + parentNode.operator.startsWith('!'), ); - } + }, }); }, // foo.lastIndexOf('bar') === foo.length - 3 // foo.lastIndexOf(bar) === foo.length - bar.length 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="lastIndexOf"][computed=false]'( - node: TSESTree.MemberExpression + node: TSESTree.MemberExpression, ): void { const callNode = node.parent! as TSESTree.CallExpression; const parentNode = callNode.parent!; @@ -481,16 +481,16 @@ export default createRule({ fixer, parentNode, 'end', - parentNode.operator.startsWith('!') + parentNode.operator.startsWith('!'), ); - } + }, }); }, // foo.match(/^bar/) === null // foo.match(/bar$/) === null 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="match"][computed=false]'( - node: TSESTree.MemberExpression + node: TSESTree.MemberExpression, ): void { const callNode = node.parent as TSESTree.CallExpression; const parentNode = callNode.parent as TSESTree.BinaryExpression; @@ -520,14 +520,14 @@ export default createRule({ } yield fixer.replaceTextRange( getPropertyRange(node), - `.${isStartsWith ? 'start' : 'end'}sWith` + `.${isStartsWith ? 'start' : 'end'}sWith`, ); yield fixer.replaceText( callNode.arguments[0], - JSON.stringify(text) + JSON.stringify(text), ); yield fixer.removeRange([callNode.range[1], parentNode.range[1]]); - } + }, }); }, @@ -540,7 +540,7 @@ export default createRule({ // @ts-ignore [[ 'CallExpression > MemberExpression.callee[property.name=slice][computed=false]', - 'CallExpression > MemberExpression.callee[property.name=substring][computed=false]' + 'CallExpression > MemberExpression.callee[property.name=substring][computed=false]', ]](node: TSESTree.MemberExpression): void { const callNode = node.parent! as TSESTree.CallExpression; const parentNode = callNode.parent!; @@ -602,16 +602,16 @@ export default createRule({ fixer, parentNode, isStartsWith ? 'start' : 'end', - parentNode.operator.startsWith('!') + parentNode.operator.startsWith('!'), ); - } + }, }); }, // /^bar/.test(foo) // /bar$/.test(foo) 'CallExpression > MemberExpression.callee[property.name="test"][computed=false]'( - node: TSESTree.MemberExpression + node: TSESTree.MemberExpression, ): void { const callNode = node.parent as TSESTree.CallExpression; const parsed = @@ -642,11 +642,11 @@ export default createRule({ } yield fixer.insertTextAfter( argNode, - `.${methodName}(${JSON.stringify(text)}` + `.${methodName}(${JSON.stringify(text)}`, ); - } + }, }); - } + }, }; - } + }, }); diff --git a/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts index 22550359837..cc1e0063bd2 100644 --- a/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-string-starts-ends-with.test.ts @@ -8,8 +8,8 @@ const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { tsconfigRootDir: rootPath, - project: './tsconfig.json' - } + project: './tsconfig.json', + }, }); ruleTester.run('prefer-string-starts-ends-with', rule, { @@ -223,7 +223,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { function f(x: { test(): void }, s: string) { x.test(s) } - ` + `, ], invalid: [ // String indexing. @@ -238,7 +238,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -251,7 +251,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -264,7 +264,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -277,7 +277,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -290,7 +290,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("あ") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -299,7 +299,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -308,7 +308,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -321,7 +321,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("a") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -334,7 +334,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { (s).startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, // String#charAt @@ -349,7 +349,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -362,7 +362,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -375,7 +375,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -388,7 +388,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -401,7 +401,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("あ") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -410,7 +410,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -419,7 +419,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -432,7 +432,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("a") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -445,7 +445,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { (s).startsWith("a") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, // String#indexOf @@ -460,7 +460,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -473,7 +473,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -486,7 +486,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -499,7 +499,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, // String#lastIndexOf @@ -514,7 +514,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -527,7 +527,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -540,7 +540,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -553,7 +553,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -566,7 +566,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -579,7 +579,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith(needle) } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, // String#match @@ -594,7 +594,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -607,7 +607,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -620,7 +620,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -633,7 +633,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -646,7 +646,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -659,7 +659,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -672,7 +672,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -685,7 +685,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -700,7 +700,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -715,7 +715,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -730,7 +730,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("\\"quoted\\"") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, // String#slice @@ -745,7 +745,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -758,7 +758,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -771,7 +771,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -784,7 +784,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -797,7 +797,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -806,7 +806,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -815,7 +815,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -828,7 +828,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -841,7 +841,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { !s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -854,7 +854,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith(needle) } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -867,7 +867,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith(needle) } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -876,7 +876,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -889,7 +889,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -898,7 +898,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { } `, output: null, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -911,7 +911,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, // RegExp#test @@ -926,7 +926,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -939,7 +939,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.endsWith("bar") } `, - errors: [{ messageId: 'preferEndsWith' }] + errors: [{ messageId: 'preferEndsWith' }], }, { code: ` @@ -954,7 +954,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -969,7 +969,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -984,7 +984,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith("\\"quoted\\"") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -997,7 +997,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { (a + b).startsWith("bar") } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, // Test for variation of string types. @@ -1012,7 +1012,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -1025,7 +1025,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] + errors: [{ messageId: 'preferStartsWith' }], }, { code: ` @@ -1040,7 +1040,7 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { s.startsWith(needle) } `, - errors: [{ messageId: 'preferStartsWith' }] - } - ] + errors: [{ messageId: 'preferStartsWith' }], + }, + ], }); diff --git a/packages/eslint-plugin/typings/eslint-utils.d.ts b/packages/eslint-plugin/typings/eslint-utils.d.ts index f246c2470bc..d926229b00e 100644 --- a/packages/eslint-plugin/typings/eslint-utils.d.ts +++ b/packages/eslint-plugin/typings/eslint-utils.d.ts @@ -7,14 +7,14 @@ declare module 'eslint-utils' { | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression, - sourceCode: SourceCode + sourceCode: SourceCode, ): TSESTree.SourceLocation; export function getFunctionNameWithKind( node: | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression - | TSESTree.ArrowFunctionExpression + | TSESTree.ArrowFunctionExpression, ): string; export function getPropertyName( @@ -22,17 +22,17 @@ declare module 'eslint-utils' { | TSESTree.MemberExpression | TSESTree.Property | TSESTree.MethodDefinition, - initialScope?: Scope.Scope + initialScope?: Scope.Scope, ): string | null; export function getStaticValue( node: TSESTree.Node, - initialScope?: Scope.Scope + initialScope?: Scope.Scope, ): { value: any } | null; export function getStringIfConstant( node: TSESTree.Node, - initialScope?: Scope.Scope + initialScope?: Scope.Scope, ): string | null; export function hasSideEffect( @@ -41,12 +41,12 @@ declare module 'eslint-utils' { options?: { considerGetters?: boolean; considerImplicitTypeConversion?: boolean; - } + }, ): boolean; export function isParenthesized( node: TSESTree.Node, - sourceCode: SourceCode + sourceCode: SourceCode, ): boolean; export class PatternMatcher { @@ -57,12 +57,12 @@ declare module 'eslint-utils' { export function findVariable( initialScope: Scope.Scope, - name: string + name: string, ): Scope.Variable | null; export function getInnermostScope( initialScope: Scope.Scope, - node: TSESTree.Node + node: TSESTree.Node, ): Scope.Scope; export class ReferenceTracker { @@ -75,17 +75,17 @@ declare module 'eslint-utils' { options?: { mode: 'strict' | 'legacy'; globalObjectNames: ReadonlyArray; - } + }, ); iterateGlobalReferences( - traceMap: ReferenceTracker.TraceMap + traceMap: ReferenceTracker.TraceMap, ): IterableIterator>; iterateCjsReferences( - traceMap: ReferenceTracker.TraceMap + traceMap: ReferenceTracker.TraceMap, ): IterableIterator>; iterateEsmReferences( - traceMap: ReferenceTracker.TraceMap + traceMap: ReferenceTracker.TraceMap, ): IterableIterator>; } @@ -110,69 +110,69 @@ declare module 'eslint-utils' { } export function isArrowToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotArrowToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isClosingBraceToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotClosingBraceToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isClosingBracketToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotClosingBracketToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isClosingParenToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotClosingParenToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isColonToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotColonToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isCommaToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotCommaToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isCommentToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotCommentToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isOpeningBraceToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotOpeningBraceToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isOpeningBracketToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotOpeningBracketToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isOpeningParenToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotOpeningParenToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isSemicolonToken( - token: TSESTree.Token | TSESTree.Comment + token: TSESTree.Token | TSESTree.Comment, ): boolean; export function isNotSemicolonToken( - token: TSESTree.Token | TSESTree.Comment + 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 e33aa40280c..85f8c8b63e0 100644 --- a/packages/eslint-plugin/typings/ts-eslint.d.ts +++ b/packages/eslint-plugin/typings/ts-eslint.d.ts @@ -298,7 +298,7 @@ declare module 'ts-eslint' { } type ReportFixFunction = ( - fixer: RuleFixer + fixer: RuleFixer, ) => null | RuleFix | RuleFix[] | IterableIterator; interface ReportDescriptor { From 08ab0ad71dc48b29820892ee8a113c8b84416406 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 22 Feb 2019 13:48:45 +0900 Subject: [PATCH 07/15] Update packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts Co-Authored-By: mysticatea --- .../eslint-plugin/src/rules/prefer-string-starts-ends-with.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7456766a305..65c0fb02e6f 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 @@ -25,7 +25,7 @@ export default createRule({ type: 'suggestion', docs: { description: - 'enforce to use `String#startsWith` and `String#endsWith` over other options', + 'enforce the use of `String#startsWith` and `String#endsWith` over other options', category: 'Best Practices', recommended: false, }, From 86018fda2275f0e5c011818de8a9c0b6a2df66c9 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Fri, 22 Feb 2019 13:48:54 +0900 Subject: [PATCH 08/15] Update packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts Co-Authored-By: mysticatea --- .../eslint-plugin/src/rules/prefer-string-starts-ends-with.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 65c0fb02e6f..886dc65526d 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 @@ -31,7 +31,7 @@ export default createRule({ }, messages: { preferStartsWith: "Use 'String#startsWith' method instead.", - preferEndsWith: "Use 'String#endsWith' method instead.", + preferEndsWith: "Use the 'String#endsWith' method instead.", }, schema: [], fixable: 'code', From 3c1def56357c999934989627b82f516d28645df0 Mon Sep 17 00:00:00 2001 From: Brian Ng Date: Fri, 22 Feb 2019 13:49:09 +0900 Subject: [PATCH 09/15] Update packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md Co-Authored-By: mysticatea --- .../eslint-plugin/docs/rules/prefer-string-starts-ends-with.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md index e412d767554..19b71183488 100644 --- a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md +++ b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md @@ -1,6 +1,6 @@ # Enforce to use `String#startsWith` and `String#endsWith` over other options (prefer-string-starts-ends-with) -There are multiple ways to verify if a string starts or ends with a specific string, auch as `foo.indexOf('bar') === 0`. +There are multiple ways to verify if a string starts or ends with a specific string, such as `foo.indexOf('bar') === 0`. Since ES2015 has added `String#startsWith` and `String#endsWith`, this rule reports other ways to be consistent. From 38005cdc37ec6d9def73f2c5b5f2a7ab9d4fd8fd Mon Sep 17 00:00:00 2001 From: Brian Ng Date: Fri, 22 Feb 2019 13:49:23 +0900 Subject: [PATCH 10/15] Update packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md Co-Authored-By: mysticatea --- .../eslint-plugin/docs/rules/prefer-string-starts-ends-with.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md index 19b71183488..c65b1a79bd2 100644 --- a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md +++ b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md @@ -6,7 +6,7 @@ Since ES2015 has added `String#startsWith` and `String#endsWith`, this rule repo ## Rule Details -This rule is aimed at enforcing the consistent way that checks a string starts or ends with a specific string. +This rule is aimed at enforcing a consistent way to check whether a string starts or ends with a specific string. Examples of **incorrect** code for this rule: From ef3e7d638d48befce01dff9b5389d0c2cee965b6 Mon Sep 17 00:00:00 2001 From: James Henry Date: Sun, 24 Feb 2019 02:16:50 +0900 Subject: [PATCH 11/15] Update packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md Co-Authored-By: mysticatea --- .../eslint-plugin/docs/rules/prefer-string-starts-ends-with.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md index c65b1a79bd2..18d19fbf504 100644 --- a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md +++ b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md @@ -1,4 +1,4 @@ -# Enforce to use `String#startsWith` and `String#endsWith` over other options (prefer-string-starts-ends-with) +# Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings (prefer-string-starts-ends-with) There are multiple ways to verify if a string starts or ends with a specific string, such as `foo.indexOf('bar') === 0`. From ab1dd858f707c93a132ae323a2903765ca4fc4df Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 24 Feb 2019 02:21:42 +0900 Subject: [PATCH 12/15] remove comment --- .../src/rules/prefer-string-starts-ends-with.ts | 5 ----- 1 file changed, 5 deletions(-) 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 886dc65526d..872a4a06da5 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 @@ -1,8 +1,3 @@ -/** - * @fileoverview Enforce to use `String#startsWith` and `String#endsWith` over other options. - * @author Toru Nagashima - */ - import { TSESTree } from '@typescript-eslint/typescript-estree'; import { isNotClosingParenToken, From 671edd6422b41a7df0bff1f2dfae67a7a28e7c92 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 24 Feb 2019 02:22:11 +0900 Subject: [PATCH 13/15] update description --- .../eslint-plugin/src/rules/prefer-string-starts-ends-with.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 872a4a06da5..5c625e0e1f5 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 @@ -20,7 +20,7 @@ export default createRule({ type: 'suggestion', docs: { description: - 'enforce the use of `String#startsWith` and `String#endsWith` over other options', + 'enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings', category: 'Best Practices', recommended: false, }, From fecf943c438868665767aa25caf80e5a105b7982 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 24 Feb 2019 02:22:30 +0900 Subject: [PATCH 14/15] update example --- .../rules/prefer-string-starts-ends-with.md | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md index 18d19fbf504..07ad4e1148f 100644 --- a/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md +++ b/packages/eslint-plugin/docs/rules/prefer-string-starts-ends-with.md @@ -11,25 +11,25 @@ This rule is aimed at enforcing a consistent way to check whether a string start Examples of **incorrect** code for this rule: ```ts -let foo: string +let foo: string; // starts with -foo[0] === 'b' -foo.charAt(0) === 'b' -foo.indexOf('bar') === 0 -foo.slice(0, 3) === 'bar' -foo.substring(0, 3) === 'bar' -foo.match(/^bar/) -/^bar/.test(foo) +foo[0] === 'b'; +foo.charAt(0) === 'b'; +foo.indexOf('bar') === 0; +foo.slice(0, 3) === 'bar'; +foo.substring(0, 3) === 'bar'; +foo.match(/^bar/) != null; +/^bar/.test(foo); // ends with -foo[foo.length - 1] === 'b' -foo.charAt(foo.length - 1) === 'b' -foo.lastIndexOf('bar') === foo.length - 3 -foo.slice(-3) === 'bar' -foo.substring(foo.length - 3) === 'bar' -foo.match(/bar$/) -/bar$/.test(foo) +foo[foo.length - 1] === 'b'; +foo.charAt(foo.length - 1) === 'b'; +foo.lastIndexOf('bar') === foo.length - 3; +foo.slice(-3) === 'bar'; +foo.substring(foo.length - 3) === 'bar'; +foo.match(/bar$/) != null; +/bar$/.test(foo); ``` Examples of **correct** code for this rule: From 872ed4996378d2b387e58ffd808f225b74a529b8 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Tue, 9 Apr 2019 19:22:17 +0900 Subject: [PATCH 15/15] fix for review --- .../src/rules/prefer-string-starts-ends-with.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 5c625e0e1f5..6b3079cdfdf 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 @@ -367,11 +367,10 @@ export default createRule({ // foo.charAt(0) === "a" // foo[foo.length - 1] === "a" // foo.charAt(foo.length - 1) === "a" - // @ts-ignore - [[ + [String([ 'BinaryExpression > MemberExpression.left[computed=true]', 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="charAt"][computed=false]', - ]](node: TSESTree.MemberExpression): void { + ])](node: TSESTree.MemberExpression): void { let parentNode = node.parent!; let indexNode: TSESTree.Node | null = null; if (parentNode.type === 'CallExpression') { @@ -532,11 +531,10 @@ export default createRule({ // foo.substring(0, 3) === 'bar' // foo.substring(foo.length - 3) === 'bar' // foo.substring(foo.length - 3, foo.length) === 'bar' - // @ts-ignore - [[ + [String([ 'CallExpression > MemberExpression.callee[property.name=slice][computed=false]', 'CallExpression > MemberExpression.callee[property.name=substring][computed=false]', - ]](node: TSESTree.MemberExpression): void { + ])](node: TSESTree.MemberExpression): void { const callNode = node.parent! as TSESTree.CallExpression; const parentNode = callNode.parent!; if (