Skip to content

Commit

Permalink
prefer-string-starts-ends-with: Fix missing parentheses for some ca…
Browse files Browse the repository at this point in the history
…ses (#976)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
fisker and sindresorhus committed Jan 6, 2021
1 parent 48390c1 commit e2f94fe
Show file tree
Hide file tree
Showing 5 changed files with 383 additions and 1 deletion.
3 changes: 2 additions & 1 deletion rules/prefer-string-starts-ends-with.js
Expand Up @@ -3,6 +3,7 @@ const {isParenthesized} = require('eslint-utils');
const getDocumentationUrl = require('./utils/get-documentation-url');
const methodSelector = require('./utils/method-selector');
const quoteString = require('./utils/quote-string');
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object');

const MESSAGE_STARTS_WITH = 'prefer-starts-with';
const MESSAGE_ENDS_WITH = 'prefer-ends-with';
Expand Down Expand Up @@ -79,7 +80,7 @@ const create = context => {
if (
// If regex is parenthesized, we can use it, so we don't need add again
!isParenthesized(regexNode, sourceCode) &&
(isParenthesized(target, sourceCode) || target.type === 'AwaitExpression')
(isParenthesized(target, sourceCode) || shouldAddParenthesesToMemberExpressionObject(target, sourceCode))
) {
targetString = `(${targetString})`;
}
Expand Down
67 changes: 67 additions & 0 deletions rules/utils/should-add-parentheses-to-member-expression-object.js
@@ -0,0 +1,67 @@
'use strict';

const {isOpeningParenToken, isClosingParenToken} = require('eslint-utils');

// Determine whether this node is a decimal integer literal.
// Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/utils/ast-utils.js#L1237
const DECIMAL_INTEGER_PATTERN = /^(?:0|0[0-7]*[89]\d*|[1-9](?:_?\d)*)$/u;
const isDecimalInteger = node =>
node.type === 'Literal' &&
typeof node.value === 'number' &&
DECIMAL_INTEGER_PATTERN.test(node.raw);

/**
Determine if a constructor function is newed-up with parens.
@param {Node} node - The `NewExpression` node to be checked.
@param {SourceCode} sourceCode - The source code object.
@returns {boolean} True if the constructor is called with parens.
Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/no-extra-parens.js#L252
*/
function isNewExpressionWithParentheses(node, sourceCode) {
if (node.arguments.length > 0) {
return true;
}

const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2);
// The expression should end with its own parens, for example, `new new Foo()` is not a new expression with parens.
return isOpeningParenToken(penultimateToken) &&
isClosingParenToken(lastToken) &&
node.callee.range[1] < node.range[1];
}

/**
Check if parentheses should to be added to a `node` when it's used as an `object` of `MemberExpression`.
@param {Node} node - The AST node to check.
@param {SourceCode} sourceCode - The source code object.
@returns {boolean}
*/
function shouldAddParenthesesToMemberExpressionObject(node, sourceCode) {
switch (node.type) {
// This is not a full list. Some other nodes like `FunctionDeclaration` don't need parentheses,
// but it's not possible to be in the place we are checking at this point.
case 'Identifier':
case 'MemberExpression':
case 'CallExpression':
case 'ChainExpression':
case 'TemplateLiteral':
return false;
case 'NewExpression':
return !isNewExpressionWithParentheses(node, sourceCode);
case 'Literal': {
/* istanbul ignore next */
if (isDecimalInteger(node)) {
return true;
}

return false;
}

default:
return true;
}
}

module.exports = shouldAddParenthesesToMemberExpressionObject;
21 changes: 21 additions & 0 deletions test/prefer-string-starts-ends-with.js
Expand Up @@ -133,3 +133,24 @@ test({
})
]
});

test.visualize([
'/^a/.test("string")',
'/^a/.test((0, "string"))',
'async function a() {return /^a/.test(await foo())}',
'/^a/.test(foo + bar)',
'/^a/.test(foo || bar)',
'/^a/.test(new SomeString)',
'/^a/.test(new (SomeString))',
'/^a/.test(new SomeString())',
'/^a/.test(new new SomeClassReturnsAStringSubClass())',
'/^a/.test(new SomeString(/* comment */))',
'/^a/.test(new SomeString("string"))',
'/^a/.test(foo.bar)',
'/^a/.test(foo.bar())',
'/^a/.test(foo?.bar)',
'/^a/.test(foo?.bar())',
'/^a/.test(`string`)',
'/^a/.test(tagged`string`)',
'(/^a/).test((0, "string"))'
]);

0 comments on commit e2f94fe

Please sign in to comment.