Skip to content

Commit e2f94fe

Browse files
fiskersindresorhus
andauthored
prefer-string-starts-ends-with: Fix missing parentheses for some cases (#976)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 48390c1 commit e2f94fe

File tree

5 files changed

+383
-1
lines changed

5 files changed

+383
-1
lines changed

rules/prefer-string-starts-ends-with.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const {isParenthesized} = require('eslint-utils');
33
const getDocumentationUrl = require('./utils/get-documentation-url');
44
const methodSelector = require('./utils/method-selector');
55
const quoteString = require('./utils/quote-string');
6+
const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object');
67

78
const MESSAGE_STARTS_WITH = 'prefer-starts-with';
89
const MESSAGE_ENDS_WITH = 'prefer-ends-with';
@@ -79,7 +80,7 @@ const create = context => {
7980
if (
8081
// If regex is parenthesized, we can use it, so we don't need add again
8182
!isParenthesized(regexNode, sourceCode) &&
82-
(isParenthesized(target, sourceCode) || target.type === 'AwaitExpression')
83+
(isParenthesized(target, sourceCode) || shouldAddParenthesesToMemberExpressionObject(target, sourceCode))
8384
) {
8485
targetString = `(${targetString})`;
8586
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
const {isOpeningParenToken, isClosingParenToken} = require('eslint-utils');
4+
5+
// Determine whether this node is a decimal integer literal.
6+
// Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/utils/ast-utils.js#L1237
7+
const DECIMAL_INTEGER_PATTERN = /^(?:0|0[0-7]*[89]\d*|[1-9](?:_?\d)*)$/u;
8+
const isDecimalInteger = node =>
9+
node.type === 'Literal' &&
10+
typeof node.value === 'number' &&
11+
DECIMAL_INTEGER_PATTERN.test(node.raw);
12+
13+
/**
14+
Determine if a constructor function is newed-up with parens.
15+
16+
@param {Node} node - The `NewExpression` node to be checked.
17+
@param {SourceCode} sourceCode - The source code object.
18+
@returns {boolean} True if the constructor is called with parens.
19+
20+
Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/no-extra-parens.js#L252
21+
*/
22+
function isNewExpressionWithParentheses(node, sourceCode) {
23+
if (node.arguments.length > 0) {
24+
return true;
25+
}
26+
27+
const [penultimateToken, lastToken] = sourceCode.getLastTokens(node, 2);
28+
// The expression should end with its own parens, for example, `new new Foo()` is not a new expression with parens.
29+
return isOpeningParenToken(penultimateToken) &&
30+
isClosingParenToken(lastToken) &&
31+
node.callee.range[1] < node.range[1];
32+
}
33+
34+
/**
35+
Check if parentheses should to be added to a `node` when it's used as an `object` of `MemberExpression`.
36+
37+
@param {Node} node - The AST node to check.
38+
@param {SourceCode} sourceCode - The source code object.
39+
@returns {boolean}
40+
*/
41+
function shouldAddParenthesesToMemberExpressionObject(node, sourceCode) {
42+
switch (node.type) {
43+
// This is not a full list. Some other nodes like `FunctionDeclaration` don't need parentheses,
44+
// but it's not possible to be in the place we are checking at this point.
45+
case 'Identifier':
46+
case 'MemberExpression':
47+
case 'CallExpression':
48+
case 'ChainExpression':
49+
case 'TemplateLiteral':
50+
return false;
51+
case 'NewExpression':
52+
return !isNewExpressionWithParentheses(node, sourceCode);
53+
case 'Literal': {
54+
/* istanbul ignore next */
55+
if (isDecimalInteger(node)) {
56+
return true;
57+
}
58+
59+
return false;
60+
}
61+
62+
default:
63+
return true;
64+
}
65+
}
66+
67+
module.exports = shouldAddParenthesesToMemberExpressionObject;

test/prefer-string-starts-ends-with.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,24 @@ test({
133133
})
134134
]
135135
});
136+
137+
test.visualize([
138+
'/^a/.test("string")',
139+
'/^a/.test((0, "string"))',
140+
'async function a() {return /^a/.test(await foo())}',
141+
'/^a/.test(foo + bar)',
142+
'/^a/.test(foo || bar)',
143+
'/^a/.test(new SomeString)',
144+
'/^a/.test(new (SomeString))',
145+
'/^a/.test(new SomeString())',
146+
'/^a/.test(new new SomeClassReturnsAStringSubClass())',
147+
'/^a/.test(new SomeString(/* comment */))',
148+
'/^a/.test(new SomeString("string"))',
149+
'/^a/.test(foo.bar)',
150+
'/^a/.test(foo.bar())',
151+
'/^a/.test(foo?.bar)',
152+
'/^a/.test(foo?.bar())',
153+
'/^a/.test(`string`)',
154+
'/^a/.test(tagged`string`)',
155+
'(/^a/).test((0, "string"))'
156+
]);

0 commit comments

Comments
 (0)