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 926c49e82a5b..82451e181320 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 @@ -161,23 +161,22 @@ export default createRule({ } /** - * Check if a given node is a negative index expression - * - * E.g. `s.slice(- )`, `s.substring(s.length - )` - * - * @param node The node to check. - * @param expectedIndexedNode The node which is expected as the receiver of index expression. + * Returns true if `node` is `-substring.length` or + * `parentString.length - substring.length` */ - function isNegativeIndexExpression( + function isLengthAheadOfEnd( node: TSESTree.Node, - expectedIndexedNode: TSESTree.Node, + substring: TSESTree.Node, + parentString: TSESTree.Node, ): boolean { return ( (node.type === AST_NODE_TYPES.UnaryExpression && - node.operator === '-') || + node.operator === '-' && + isLengthExpression(node.argument, substring)) || (node.type === AST_NODE_TYPES.BinaryExpression && node.operator === '-' && - isLengthExpression(node.left, expectedIndexedNode)) + isLengthExpression(node.left, parentString) && + isLengthExpression(node.right, substring)) ); } @@ -567,16 +566,44 @@ export default createRule({ return; } - const isEndsWith = - (callNode.arguments.length === 1 || - (callNode.arguments.length === 2 && - isLengthExpression(callNode.arguments[1], node.object))) && - isNegativeIndexExpression(callNode.arguments[0], node.object); - const isStartsWith = - !isEndsWith && - callNode.arguments.length === 2 && - isNumber(callNode.arguments[0], 0) && - !isNegativeIndexExpression(callNode.arguments[1], node.object); + let isEndsWith = false; + let isStartsWith = false; + if (callNode.arguments.length === 1) { + if ( + // foo.slice(-bar.length) === bar + // foo.slice(foo.length - bar.length) === bar + isLengthAheadOfEnd( + callNode.arguments[0], + parentNode.right, + node.object, + ) + ) { + isEndsWith = true; + } + } else if (callNode.arguments.length === 2) { + if ( + // foo.slice(0, bar.length) === bar + isNumber(callNode.arguments[0], 0) && + isLengthExpression(callNode.arguments[1], parentNode.right) + ) { + isStartsWith = true; + } else if ( + // foo.slice(foo.length - bar.length, foo.length) === bar + // foo.slice(foo.length - bar.length, 0) === bar + // foo.slice(-bar.length, foo.length) === bar + // foo.slice(-bar.length, 0) === bar + (isLengthExpression(callNode.arguments[1], node.object) || + isNumber(callNode.arguments[1], 0)) && + isLengthAheadOfEnd( + callNode.arguments[0], + parentNode.right, + node.object, + ) + ) { + isEndsWith = true; + } + } + if (!isStartsWith && !isEndsWith) { return; } 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 aaea8e8b36c7..e51cbe3a8d87 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 @@ -242,6 +242,21 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { x.endsWith('foo') && x.slice(0, -4) === 'bar' } `, + ` + function f(s: string) { + s.slice(0, length) === needle // the 'length' can be different to 'needle.length' + } + `, + ` + function f(s: string) { + s.slice(-length) === needle // 'length' can be different + } + `, + ` + function f(s: string) { + s.slice(0, 3) === needle + } + `, ]), invalid: addOptional([ // String indexing. @@ -817,15 +832,6 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { `, 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) { @@ -887,15 +893,6 @@ ruleTester.run('prefer-string-starts-ends-with', rule, { `, errors: [{ messageId: 'preferEndsWith' }], }, - { - code: ` - function f(s: string) { - s.slice(-length) === needle // 'length' can be different - } - `, - output: null, - errors: [{ messageId: 'preferEndsWith' }], - }, { code: ` function f(s: string) {