diff --git a/packages/eslint-plugin/docs/rules/no-extra-parens.md b/packages/eslint-plugin/docs/rules/no-extra-parens.md new file mode 100644 index 00000000000..d7942ec1b16 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-extra-parens.md @@ -0,0 +1,22 @@ +# disallow unnecessary parentheses (no-extra-parens) + +This rule restricts the use of parentheses to only where they are necessary. + +## Rule Details + +This rule extends the base [eslint/no-extra-parens](https://eslint.org/docs/rules/no-extra-parens) rule. +It supports all options and features of the base rule plus TS type assertions. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "no-extra-parens": "off", + "@typescript-eslint/no-extra-parens": ["error"] +} +``` + +## Options + +See [eslint/no-extra-parens options](https://eslint.org/docs/rules/no-extra-parens#options). diff --git a/packages/eslint-plugin/src/rules/no-extra-parens.ts b/packages/eslint-plugin/src/rules/no-extra-parens.ts new file mode 100644 index 00000000000..3ad963974c2 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-extra-parens.ts @@ -0,0 +1,33 @@ +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; +import baseRule from 'eslint/lib/rules/no-extra-parens'; +import * as util from '../util'; + +type Options = util.InferOptionsTypeFromRule; +type MessageIds = util.InferMessageIdsTypeFromRule; + +export default util.createRule({ + name: 'no-extra-parens', + meta: { + type: 'layout', + docs: { + description: 'disallow unnecessary parentheses', + category: 'Possible Errors', + recommended: false, + }, + fixable: 'code', + schema: baseRule.meta.schema, + messages: baseRule.meta.messages, + }, + defaultOptions: ['all'], + create(context) { + const rules = baseRule.create(context); + + return Object.assign({}, rules, { + MemberExpression(node: TSESTree.MemberExpression) { + if (node.object.type !== AST_NODE_TYPES.TSAsExpression) { + return rules.MemberExpression(node); + } + }, + }); + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts b/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts new file mode 100644 index 00000000000..154179c192c --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts @@ -0,0 +1,266 @@ +import rule from '../../src/rules/no-extra-parens'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-extra-parens', rule, { + valid: [ + { + code: ` + (0).toString(); + (function(){}) ? a() : b(); + (/^a$/).test(x); + for (a of (b, c)); + for (a of b); + for (a in b, c); + for (a in b); + `, + }, + { + code: `t.true((me.get as SinonStub).calledWithExactly('/foo', other));`, + }, + { + code: `while ((foo = bar())) {}`, + options: ['all', { conditionalAssign: false }], + }, + { + code: `if ((foo = bar())) {}`, + options: ['all', { conditionalAssign: false }], + }, + { + code: `do; while ((foo = bar()))`, + options: ['all', { conditionalAssign: false }], + }, + { + code: `for (;(a = b););`, + options: ['all', { conditionalAssign: false }], + }, + { + code: ` + function a(b) { + return (b = 1); + } + `, + options: ['all', { returnAssign: false }], + }, + { + code: ` + function a(b) { + return b ? (c = d) : (c = e); + } + `, + options: ['all', { returnAssign: false }], + }, + { + code: `b => (b = 1);`, + options: ['all', { returnAssign: false }], + }, + { + code: `b => b ? (c = d) : (c = e);`, + options: ['all', { returnAssign: false }], + }, + { + code: `x = a || (b && c);`, + options: ['all', { nestedBinaryExpressions: false }], + }, + { + code: `x = a + (b * c);`, + options: ['all', { nestedBinaryExpressions: false }], + }, + { + code: `x = (a * b) / c;`, + options: ['all', { nestedBinaryExpressions: false }], + }, + { + code: ` + const Component = (
) + const Component = ( +
+ ) + `, + options: ['all', { ignoreJSX: 'all' }], + }, + { + code: ` + const Component = ( +
+

+

+ ) + const Component = ( +
+ ) + `, + options: ['all', { ignoreJSX: 'multi-line' }], + }, + { + code: ` + const Component = (
) + const Component = (

) + `, + options: ['all', { ignoreJSX: 'single-line' }], + }, + { + code: ` + const b = a => 1 ? 2 : 3; + const d = c => (1 ? 2 : 3); + `, + options: ['all', { enforceForArrowConditionals: false }], + }, + { + code: ` + (0).toString(); + (Object.prototype.toString.call()); + ({}.toString.call()); + (function(){} ? a() : b()); + (/^a$/).test(x); + a = (b * c); + (a * b) + c; + typeof (a); + `, + options: ['functions'], + }, + ], + + invalid: [ + { + code: `a = (b * c);`, + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 5, + }, + ], + }, + { + code: `(a * b) + c;`, + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 1, + }, + ], + }, + { + code: `for (a in (b, c));`, + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 11, + }, + ], + }, + { + code: `for (a in (b));`, + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 11, + }, + ], + }, + { + code: `for (a of (b));`, + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 11, + }, + ], + }, + { + code: `typeof (a);`, + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 8, + }, + ], + }, + { + code: ` + const Component = (
) + const Component = (

) + `, + options: ['all', { ignoreJSX: 'multi-line' }], + errors: [ + { + messageId: 'unexpected', + line: 2, + column: 27, + }, + { + messageId: 'unexpected', + line: 3, + column: 27, + }, + ], + }, + { + code: ` + const Component = ( +
+

+

+ ) + const Component = ( +
+ ) + `, + options: ['all', { ignoreJSX: 'single-line' }], + errors: [ + { + messageId: 'unexpected', + line: 2, + column: 27, + }, + { + messageId: 'unexpected', + line: 7, + column: 27, + }, + ], + }, + { + code: `((function foo() {}))();`, + options: ['functions'], + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 2, + }, + ], + }, + { + code: `var y = (function () {return 1;});`, + options: ['functions'], + errors: [ + { + messageId: 'unexpected', + line: 1, + column: 9, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index b4094c05474..aca4c8d8638 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -330,3 +330,26 @@ declare module 'eslint/lib/rules/no-useless-constructor' { >; export = rule; } + +declare module 'eslint/lib/rules/no-extra-parens' { + import { TSESTree } from '@typescript-eslint/typescript-estree'; + import RuleModule from 'ts-eslint'; + + const rule: RuleModule< + 'unexpected', + ( + | 'all' + | 'functions' + | { + conditionalAssign?: boolean; + returnAssign?: boolean; + nestedBinaryExpressions?: boolean; + ignoreJSX?: 'none' | 'all' | 'multi-line' | 'single-line'; + enforceForArrowConditionals?: boolean; + })[], + { + MemberExpression(node: TSESTree.MemberExpression): void; + } + >; + export = rule; +} diff --git a/packages/eslint-plugin/typings/ts-eslint.d.ts b/packages/eslint-plugin/typings/ts-eslint.d.ts index 34705d73e34..ce6e6c577c2 100644 --- a/packages/eslint-plugin/typings/ts-eslint.d.ts +++ b/packages/eslint-plugin/typings/ts-eslint.d.ts @@ -209,7 +209,11 @@ declare module 'ts-eslint' { /** * The general category the rule falls within */ - category: 'Best Practices' | 'Stylistic Issues' | 'Variables'; + category: + | 'Best Practices' + | 'Stylistic Issues' + | 'Variables' + | 'Possible Errors'; /** * Concise description of the rule */ diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index 6c0808b9562..52f1214f1d3 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -323,7 +323,8 @@ export type LeftHandSideExpression = | MemberExpression | PrimaryExpression | TaggedTemplateExpression - | TSNonNullExpression; + | TSNonNullExpression + | TSAsExpression; export type LiteralExpression = BigIntLiteral | Literal | TemplateLiteral; export type Modifier = | TSAbstractKeyword