Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add allowParensAfterCommentPattern option to no-extra-parens #16561

Merged
merged 8 commits into from Nov 29, 2022
29 changes: 29 additions & 0 deletions docs/src/rules/no-extra-parens.md
Expand Up @@ -38,6 +38,7 @@ This rule has an object option for exceptions to the `"all"` option:
* `"enforceForSequenceExpressions": false` allows extra parentheses around sequence expressions
* `"enforceForNewInMemberExpressions": false` allows extra parentheses around `new` expressions in member expressions
* `"enforceForFunctionPrototypeMethods": false` allows extra parentheses around immediate `.call` and `.apply` method calls on function expressions and around function expressions in the same context.
* `"allowParensAfterCommentPattern": "any-string-pattern"` allows extra parentheses preceded by a comment that matches a regular expression.

### all

Expand Down Expand Up @@ -322,6 +323,34 @@ const quux = (function () {}.apply());

:::

### allowParensAfterCommentPattern

To make this rule allow extra parentheses preceded by specific comments, set this option to a string pattern that will be passed to the [`RegExp` constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp).

Examples of **correct** code for this rule with the `"all"` and `{ "allowParensAfterCommentPattern": "@type" }` options:

::: correct

```js
/* eslint no-extra-parens: ["error", "all", { "allowParensAfterCommentPattern": "@type" }] */

const span = /**@type {HTMLSpanElement}*/(event.currentTarget);

if (/** @type {Foo | Bar} */(options).baz) console.log('Lint free');

foo(/** @type {Bar} */ (bar), options, {
name: "name",
path: "path",
});

if (foo) {
/** @type {Bar} */
(bar).prop = false;
}
```

:::

### functions

Examples of **incorrect** code for this rule with the `"functions"` option:
Expand Down
17 changes: 16 additions & 1 deletion lib/rules/no-extra-parens.js
Expand Up @@ -52,7 +52,8 @@ module.exports = {
enforceForArrowConditionals: { type: "boolean" },
enforceForSequenceExpressions: { type: "boolean" },
enforceForNewInMemberExpressions: { type: "boolean" },
enforceForFunctionPrototypeMethods: { type: "boolean" }
enforceForFunctionPrototypeMethods: { type: "boolean" },
allowParensAfterCommentPattern: { type: "string" }
},
additionalProperties: false
}
Expand Down Expand Up @@ -86,6 +87,7 @@ module.exports = {
context.options[1].enforceForNewInMemberExpressions === false;
const IGNORE_FUNCTION_PROTOTYPE_METHODS = ALL_NODES && context.options[1] &&
context.options[1].enforceForFunctionPrototypeMethods === false;
const ALLOW_PARENS_AFTER_COMMENT_PATTERN = ALL_NODES && context.options[1] && context.options[1].allowParensAfterCommentPattern;

const PRECEDENCE_OF_ASSIGNMENT_EXPR = precedence({ type: "AssignmentExpression" });
const PRECEDENCE_OF_UPDATE_EXPR = precedence({ type: "UpdateExpression" });
Expand Down Expand Up @@ -402,6 +404,19 @@ module.exports = {
if (isIIFE(node) && !isParenthesised(node.callee)) {
return;
}

if (ALLOW_PARENS_AFTER_COMMENT_PATTERN) {
const commentsBeforeLeftParenToken = sourceCode.getCommentsBefore(leftParenToken);
const totalCommentsBeforeLeftParenTokenCount = commentsBeforeLeftParenToken.length;
const ignorePattern = new RegExp(ALLOW_PARENS_AFTER_COMMENT_PATTERN, "u");

if (
totalCommentsBeforeLeftParenTokenCount > 0 &&
ignorePattern.test(commentsBeforeLeftParenToken[totalCommentsBeforeLeftParenTokenCount - 1].value)
) {
return;
}
}
}

/**
Expand Down
169 changes: 168 additions & 1 deletion tests/lib/rules/no-extra-parens.js
Expand Up @@ -738,7 +738,43 @@ ruleTester.run("no-extra-parens", rule, {
},
{
code: "(Object.prototype.toString.call())",
options: ["functions"],
options: ["functions"]
},

// "allowParensAfterCommentPattern" option
{
code: "const span = /**@type {HTMLSpanElement}*/(event.currentTarget);",
options: ["all", { allowParensAfterCommentPattern: "@type" }],
parserOptions: { ecmaVersion: 2020 }
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
},
{
code: "if (/** @type {Compiler | MultiCompiler} */(options).hooks) console.log('good');",
options: ["all", { allowParensAfterCommentPattern: "@type" }],
parserOptions: { ecmaVersion: 2020 }
},
{
code: `
validate(/** @type {Schema} */ (schema), options, {
name: "Dev Server",
baseDataPath: "options",
});
`,
options: ["all", { allowParensAfterCommentPattern: "@type" }],
parserOptions: { ecmaVersion: 2020 }
},
{
code: `
if (condition) {
/** @type {ServerOptions} */
(options.server.options).requestCert = false;
}
`,
options: ["all", { allowParensAfterCommentPattern: "@type" }],
parserOptions: { ecmaVersion: 2020 }
},
{
code: "const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));",
options: ["all", { allowParensAfterCommentPattern: "@type" }],
parserOptions: { ecmaVersion: 2020 }
}
],
Expand Down Expand Up @@ -3192,6 +3228,137 @@ ruleTester.run("no-extra-parens", rule, {
errors: [{ messageId: "unexpected" }]
},

// "allowParensAfterCommentPattern" option (off by default)
{
code: "const span = /**@type {HTMLSpanElement}*/(event.currentTarget);",
output: "const span = /**@type {HTMLSpanElement}*/event.currentTarget;",
options: ["all"],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: "if (/** @type {Compiler | MultiCompiler} */(options).hooks) console.log('good');",
output: "if (/** @type {Compiler | MultiCompiler} */options.hooks) console.log('good');",
options: ["all"],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: `
validate(/** @type {Schema} */ (schema), options, {
name: "Dev Server",
baseDataPath: "options",
});
`,
output: `
validate(/** @type {Schema} */ schema, options, {
name: "Dev Server",
baseDataPath: "options",
});
`,
options: ["all"],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: `
if (condition) {
/** @type {ServerOptions} */
(options.server.options).requestCert = false;
}
`,
output: `
if (condition) {
/** @type {ServerOptions} */
options.server.options.requestCert = false;
}
`,
options: ["all"],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: "const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));",
output: "const net = ipaddr.parseCIDR(/** @type {string} */ cidr);",
options: ["all"],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: "const span = /**@type {HTMLSpanElement}*/(event.currentTarget);",
output: "const span = /**@type {HTMLSpanElement}*/event.currentTarget;",
options: ["all", { allowParensAfterCommentPattern: "invalid" }],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: "if (/** @type {Compiler | MultiCompiler} */(options).hooks) console.log('good');",
output: "if (/** @type {Compiler | MultiCompiler} */options.hooks) console.log('good');",
options: ["all", { allowParensAfterCommentPattern: "invalid" }],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: `
validate(/** @type {Schema} */ (schema), options, {
name: "Dev Server",
baseDataPath: "options",
});
`,
output: `
validate(/** @type {Schema} */ schema, options, {
name: "Dev Server",
baseDataPath: "options",
});
`,
options: ["all", { allowParensAfterCommentPattern: "invalid" }],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: `
if (condition) {
/** @type {ServerOptions} */
(options.server.options).requestCert = false;
}
`,
output: `
if (condition) {
/** @type {ServerOptions} */
options.server.options.requestCert = false;
}
`,
options: ["all", { allowParensAfterCommentPattern: "invalid" }],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: `
if (condition) {
/** @type {ServerOptions} */
/** extra coment */
(options.server.options).requestCert = false;
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
}
`,
output: `
if (condition) {
/** @type {ServerOptions} */
/** extra coment */
options.server.options.requestCert = false;
}
`,
options: ["all", { allowParensAfterCommentPattern: "@type" }],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},
{
code: "const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));",
output: "const net = ipaddr.parseCIDR(/** @type {string} */ cidr);",
options: ["all", { allowParensAfterCommentPattern: "invalid" }],
parserOptions: { ecmaVersion: 2020 },
errors: [{ messageId: "unexpected" }]
},

// Optional chaining
{
code: "var v = (obj?.aaa)?.aaa",
Expand Down