diff --git a/docs/src/rules/no-extra-parens.md b/docs/src/rules/no-extra-parens.md index 8e5bb714cbd..9b1b8df0725 100644 --- a/docs/src/rules/no-extra-parens.md +++ b/docs/src/rules/no-extra-parens.md @@ -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 @@ -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: diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 246a5a0d5e1..75ac606ea74 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -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 } @@ -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" }); @@ -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; + } + } } /** diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index 96ae9f5bddf..3f57e5b2958 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -738,8 +738,39 @@ ruleTester.run("no-extra-parens", rule, { }, { code: "(Object.prototype.toString.call())", - options: ["functions"], - parserOptions: { ecmaVersion: 2020 } + options: ["functions"] + }, + + // "allowParensAfterCommentPattern" option + { + code: "const span = /**@type {HTMLSpanElement}*/(event.currentTarget);", + options: ["all", { allowParensAfterCommentPattern: "@type" }] + }, + { + code: "if (/** @type {Compiler | MultiCompiler} */(options).hooks) console.log('good');", + options: ["all", { allowParensAfterCommentPattern: "@type" }] + }, + { + code: ` + validate(/** @type {Schema} */ (schema), options, { + name: "Dev Server", + baseDataPath: "options", + }); + `, + options: ["all", { allowParensAfterCommentPattern: "@type" }] + }, + { + code: ` + if (condition) { + /** @type {ServerOptions} */ + (options.server.options).requestCert = false; + } + `, + options: ["all", { allowParensAfterCommentPattern: "@type" }] + }, + { + code: "const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));", + options: ["all", { allowParensAfterCommentPattern: "@type" }] } ], @@ -3192,6 +3223,160 @@ 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"], + 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"], + 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"], + 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"], + errors: [{ messageId: "unexpected" }] + }, + { + code: "const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));", + output: "const net = ipaddr.parseCIDR(/** @type {string} */ cidr);", + options: ["all"], + errors: [{ messageId: "unexpected" }] + }, + { + code: "const span = /**@type {HTMLSpanElement}*/(event.currentTarget);", + output: "const span = /**@type {HTMLSpanElement}*/event.currentTarget;", + options: ["all", { allowParensAfterCommentPattern: "invalid" }], + 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" }], + 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" }], + 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" }], + errors: [{ messageId: "unexpected" }] + }, + { + code: ` + if (condition) { + /** @type {ServerOptions} */ + /** extra coment */ + (options.server.options).requestCert = false; + } + `, + output: ` + if (condition) { + /** @type {ServerOptions} */ + /** extra coment */ + options.server.options.requestCert = false; + } + `, + options: ["all", { allowParensAfterCommentPattern: "@type" }], + 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: "@type" }], + errors: [{ messageId: "unexpected" }] + }, + { + code: ` + if (condition) { + /** @type {ServerOptions} */ + let foo = "bar"; + (options.server.options).requestCert = false; + } + `, + output: ` + if (condition) { + /** @type {ServerOptions} */ + let foo = "bar"; + options.server.options.requestCert = false; + } + `, + options: ["all", { allowParensAfterCommentPattern: "@type" }], + errors: [{ messageId: "unexpected" }] + }, + { + code: "const net = ipaddr.parseCIDR(/** @type {string} */ (cidr));", + output: "const net = ipaddr.parseCIDR(/** @type {string} */ cidr);", + options: ["all", { allowParensAfterCommentPattern: "invalid" }], + errors: [{ messageId: "unexpected" }] + }, + // Optional chaining { code: "var v = (obj?.aaa)?.aaa",