diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index 5e5bab756a9..72eb598c64a 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -19,10 +19,11 @@ }, "license": "MIT", "scripts": { - "test": "jest --coverage", - "prebuild": "npm run clean", "build": "tsc -p tsconfig.build.json", "clean": "rimraf dist/", + "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", + "prebuild": "npm run clean", + "test": "jest --coverage", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index c3034469f73..e85f8489268 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -25,13 +25,14 @@ "license": "MIT", "main": "dist/index.js", "scripts": { + "build": "tsc -p tsconfig.build.json", + "clean": "rimraf dist/", "docs": "eslint-docs", "docs:check": "eslint-docs check", - "test": "jest --coverage", - "recommended:update": "ts-node tools/update-recommended.ts", + "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", "prebuild": "npm run clean", - "build": "tsc -p tsconfig.build.json", - "clean": "rimraf dist/", + "recommended:update": "ts-node tools/update-recommended.ts", + "test": "jest --coverage", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/eslint-plugin/src/rules/no-extra-parens.ts b/packages/eslint-plugin/src/rules/no-extra-parens.ts index 3ad963974c2..d14e1e01a5a 100644 --- a/packages/eslint-plugin/src/rules/no-extra-parens.ts +++ b/packages/eslint-plugin/src/rules/no-extra-parens.ts @@ -1,4 +1,4 @@ -import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/typescript-estree'; import baseRule from 'eslint/lib/rules/no-extra-parens'; import * as util from '../util'; @@ -22,10 +22,204 @@ export default util.createRule({ create(context) { const rules = baseRule.create(context); + function binaryExp( + node: TSESTree.BinaryExpression | TSESTree.LogicalExpression, + ) { + const rule = rules.BinaryExpression as (n: typeof node) => void; + + // makes the rule think it should skip the left or right + if (node.left.type === AST_NODE_TYPES.TSAsExpression) { + return rule({ + ...node, + left: { + ...node.left, + type: AST_NODE_TYPES.BinaryExpression as any, + }, + }); + } + if (node.right.type === AST_NODE_TYPES.TSAsExpression) { + return rule({ + ...node, + right: { + ...node.right, + type: AST_NODE_TYPES.BinaryExpression as any, + }, + }); + } + + return rule(node); + } + function callExp(node: TSESTree.CallExpression | TSESTree.NewExpression) { + const rule = rules.CallExpression as (n: typeof node) => void; + + if (node.callee.type === AST_NODE_TYPES.TSAsExpression) { + // reduces the precedence of the node so the rule thinks it needs to be wrapped + return rule({ + ...node, + callee: { + ...node.callee, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + + return rule(node); + } + function unaryUpdateExpression( + node: TSESTree.UnaryExpression | TSESTree.UpdateExpression, + ) { + const rule = rules.UnaryExpression as (n: typeof node) => void; + + if (node.argument.type === AST_NODE_TYPES.TSAsExpression) { + // reduces the precedence of the node so the rule thinks it needs to be wrapped + return rule({ + ...node, + argument: { + ...node.argument, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + + return rule(node); + } + return Object.assign({}, rules, { - MemberExpression(node: TSESTree.MemberExpression) { - if (node.object.type !== AST_NODE_TYPES.TSAsExpression) { - return rules.MemberExpression(node); + // ArrayExpression + ArrowFunctionExpression(node) { + if (node.body.type !== AST_NODE_TYPES.TSAsExpression) { + return rules.ArrowFunctionExpression(node); + } + }, + // AssignmentExpression + // AwaitExpression + BinaryExpression: binaryExp, + CallExpression: callExp, + // ClassDeclaration + // ClassExpression + ConditionalExpression(node) { + // reduces the precedence of the node so the rule thinks it needs to be wrapped + if (node.test.type === AST_NODE_TYPES.TSAsExpression) { + return rules.ConditionalExpression({ + ...node, + test: { + ...node.test, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + if (node.consequent.type === AST_NODE_TYPES.TSAsExpression) { + return rules.ConditionalExpression({ + ...node, + consequent: { + ...node.consequent, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + if (node.alternate.type === AST_NODE_TYPES.TSAsExpression) { + // reduces the precedence of the node so the rule thinks it needs to be rapped + return rules.ConditionalExpression({ + ...node, + alternate: { + ...node.alternate, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + return rules.ConditionalExpression(node); + }, + // DoWhileStatement + 'ForInStatement, ForOfStatement'( + node: TSESTree.ForInStatement | TSESTree.ForOfStatement, + ) { + if (node.right.type === AST_NODE_TYPES.TSAsExpression) { + // makes the rule skip checking of the right + return rules['ForInStatement, ForOfStatement']({ + ...node, + type: AST_NODE_TYPES.ForOfStatement as any, + right: { + ...node.right, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + + return rules['ForInStatement, ForOfStatement'](node); + }, + ForStatement(node) { + // make the rule skip the piece by removing it entirely + if (node.init && node.init.type === AST_NODE_TYPES.TSAsExpression) { + return rules.ForStatement({ + ...node, + init: null, + }); + } + if (node.test && node.test.type === AST_NODE_TYPES.TSAsExpression) { + return rules.ForStatement({ + ...node, + test: null, + }); + } + if (node.update && node.update.type === AST_NODE_TYPES.TSAsExpression) { + return rules.ForStatement({ + ...node, + update: null, + }); + } + + return rules.ForStatement(node); + }, + // IfStatement + LogicalExpression: binaryExp, + MemberExpression(node) { + if (node.object.type === AST_NODE_TYPES.TSAsExpression) { + // reduces the precedence of the node so the rule thinks it needs to be wrapped + return rules.MemberExpression({ + ...node, + object: { + ...node.object, + type: AST_NODE_TYPES.SequenceExpression as any, + }, + }); + } + + return rules.MemberExpression(node); + }, + NewExpression: callExp, + // ObjectExpression + // ReturnStatement + // SequenceExpression + SpreadElement(node) { + if (node.argument.type !== AST_NODE_TYPES.TSAsExpression) { + return rules.SpreadElement(node); + } + }, + SwitchCase(node) { + if (node.test.type !== AST_NODE_TYPES.TSAsExpression) { + return rules.SwitchCase(node); + } + }, + // SwitchStatement + ThrowStatement(node) { + if ( + node.argument && + node.argument.type !== AST_NODE_TYPES.TSAsExpression + ) { + return rules.ThrowStatement(node); + } + }, + UnaryExpression: unaryUpdateExpression, + UpdateExpression: unaryUpdateExpression, + // VariableDeclarator + // WhileStatement + // WithStatement - i'm not going to even bother implementing this terrible and never used feature + YieldExpression(node) { + if ( + node.argument && + node.argument.type !== AST_NODE_TYPES.TSAsExpression + ) { + return rules.YieldExpression(node); } }, }); diff --git a/packages/eslint-plugin/tests/RuleTester.ts b/packages/eslint-plugin/tests/RuleTester.ts index c51fa532a82..d708a53cb4a 100644 --- a/packages/eslint-plugin/tests/RuleTester.ts +++ b/packages/eslint-plugin/tests/RuleTester.ts @@ -61,11 +61,68 @@ function getFixturesRootDir() { return path.join(process.cwd(), 'tests/fixtures/'); } +/** + * Converts a batch of single line tests into a number of separate test cases. + * This makes it easier to write tests which use the same options. + * + * Why wouldn't you just leave them as one test? + * Because it makes the test error messages harder to decipher. + * This way each line will fail separately, instead of them all failing together. + */ +function batchedSingleLineTests>( + test: ValidTestCase, +): ValidTestCase[]; +/** + * Converts a batch of single line tests into a number of separate test cases. + * This makes it easier to write tests which use the same options. + * + * Why wouldn't you just leave them as one test? + * Because it makes the test error messages harder to decipher. + * This way each line will fail separately, instead of them all failing together. + * + * Make sure you have your line numbers correct for error reporting, as it will match + * the line numbers up with the split tests! + */ +function batchedSingleLineTests< + TMessageIds extends string, + TOptions extends Readonly +>( + test: InvalidTestCase, +): InvalidTestCase[]; +function batchedSingleLineTests< + TMessageIds extends string, + TOptions extends Readonly +>( + options: ValidTestCase | InvalidTestCase, +): (ValidTestCase | InvalidTestCase)[] { + // eslint counts lines from 1 + const lineOffset = options.code[0] === '\n' ? 2 : 1; + return options.code + .trim() + .split('\n') + .map((code, i) => { + const lineNum = i + lineOffset; + const errors = + 'errors' in options + ? options.errors.filter(e => e.line === lineNum) + : []; + return { + ...options, + code, + errors: errors.map(e => ({ + ...e, + line: 1, + })), + }; + }); +} + export { RuleTester, RunTests, TestCaseError, InvalidTestCase, ValidTestCase, + batchedSingleLineTests, getFixturesRootDir, }; diff --git a/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts b/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts index 154179c192c..64e93de21c3 100644 --- a/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts +++ b/packages/eslint-plugin/tests/rules/no-extra-parens.test.ts @@ -1,5 +1,5 @@ import rule from '../../src/rules/no-extra-parens'; -import { RuleTester } from '../RuleTester'; +import { RuleTester, batchedSingleLineTests } from '../RuleTester'; const ruleTester = new RuleTester({ parserOptions: { @@ -12,36 +12,27 @@ const ruleTester = new RuleTester({ ruleTester.run('no-extra-parens', rule, { valid: [ - { + ...batchedSingleLineTests({ 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); +(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); + `, + }), + `t.true((me.get as SinonStub).calledWithExactly('/foo', other));`, + ...batchedSingleLineTests({ + code: ` +while ((foo = bar())) {} +if ((foo = bar())) {} +do; while ((foo = bar())) +for (;(a = 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) { @@ -66,201 +57,213 @@ ruleTester.run('no-extra-parens', rule, { 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;`, + ...batchedSingleLineTests({ + code: ` +x = a || (b && c); +x = a + (b * c); +x = (a * b) / c; + `, options: ['all', { nestedBinaryExpressions: false }], - }, + }), { code: ` - const Component = (
) - const Component = ( -
- ) +const Component = (
) +const Component = ( +
+) `, options: ['all', { ignoreJSX: 'all' }], }, { code: ` - const Component = ( -
-

-

- ) - const Component = ( -
- ) +const Component = ( +
+

+

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

) +const Component = (
) +const Component = (

) `, options: ['all', { ignoreJSX: 'single-line' }], - }, - { + }), + ...batchedSingleLineTests({ code: ` - const b = a => 1 ? 2 : 3; - const d = c => (1 ? 2 : 3); +const b = a => 1 ? 2 : 3; +const d = c => (1 ? 2 : 3); `, options: ['all', { enforceForArrowConditionals: false }], - }, - { + }), + ...batchedSingleLineTests({ code: ` - (0).toString(); - (Object.prototype.toString.call()); - ({}.toString.call()); - (function(){} ? a() : b()); - (/^a$/).test(x); - a = (b * c); - (a * b) + c; - typeof (a); +(0).toString(); +(Object.prototype.toString.call()); +({}.toString.call()); +(function(){} ? a() : b()); +(/^a$/).test(x); +a = (b * c); +(a * b) + c; +typeof (a); `, options: ['functions'], - }, + }), + ...batchedSingleLineTests({ + code: ` +[a as b]; +() => (1 as 1); +x = a as b; +const x = (1 as 1) | 2; +const x = 1 | (2 as 2); +const x = await (foo as Promise); +const res2 = (fn as foo)(); +(x as boolean) ? 1 : 0; +x ? (1 as 1) : 2; +x ? 1 : (2 as 2); +while (foo as boolean) {}; +do {} while (foo as boolean); +for (let i of ([] as Foo)) {} +for (let i in ({} as Foo)) {} +for ((1 as 1);;) {} +for (;(1 as 1);) {} +for (;;(1 as 1)) {} +if (1 as 1) {} +const x = (1 as 1).toString(); +new (1 as 1)(); +const x = { ...(1 as 1), ...{} }; +throw (1 as 1); +throw 1; +const x = !(1 as 1); +const x = (1 as 1)++; +function *x() { yield (1 as 1); yield 1; } +switch (foo) { case 1: case (2 as 2): } + `, + options: [ + 'all', + { + nestedBinaryExpressions: false, + }, + ], + }), ], invalid: [ - { - code: `a = (b * c);`, + ...batchedSingleLineTests({ + code: ` +a = (b * c); +(a * b) + c; +for (a in (b, c)); +for (a in (b)); +for (a of (b)); +typeof (a); + `, errors: [ { messageId: 'unexpected', - line: 1, + line: 2, column: 5, }, - ], - }, - { - code: `(a * b) + c;`, - errors: [ { messageId: 'unexpected', - line: 1, + line: 3, column: 1, }, - ], - }, - { - code: `for (a in (b, c));`, - errors: [ { messageId: 'unexpected', - line: 1, + line: 4, column: 11, }, - ], - }, - { - code: `for (a in (b));`, - errors: [ { messageId: 'unexpected', - line: 1, + line: 5, column: 11, }, - ], - }, - { - code: `for (a of (b));`, - errors: [ { messageId: 'unexpected', - line: 1, + line: 6, column: 11, }, - ], - }, - { - code: `typeof (a);`, - errors: [ { messageId: 'unexpected', - line: 1, + line: 7, column: 8, }, ], - }, - { + }), + ...batchedSingleLineTests({ code: ` - const Component = (
) - const Component = (

) +const Component = (
) +const Component = (

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

-

- ) - const Component = ( -
- ) +const Component = ( +
+

+

+) +const Component = ( +
+) `, options: ['all', { ignoreJSX: 'single-line' }], errors: [ { messageId: 'unexpected', line: 2, - column: 27, + column: 19, }, { messageId: 'unexpected', line: 7, - column: 27, + column: 19, }, ], }, - { - code: `((function foo() {}))();`, + ...batchedSingleLineTests({ + code: ` +((function foo() {}))(); +var y = (function () {return 1;}); + `, options: ['functions'], errors: [ { messageId: 'unexpected', - line: 1, + line: 2, column: 2, }, - ], - }, - { - code: `var y = (function () {return 1;});`, - options: ['functions'], - errors: [ { messageId: 'unexpected', - line: 1, + line: 3, column: 9, }, ], - }, + }), ], }); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 28d05114e26..cf3693453f1 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -359,18 +359,48 @@ declare module 'eslint/lib/rules/no-extra-parens' { const rule: RuleModule< 'unexpected', - ( - | 'all' - | 'functions' - | { - conditionalAssign?: boolean; - returnAssign?: boolean; - nestedBinaryExpressions?: boolean; - ignoreJSX?: 'none' | 'all' | 'multi-line' | 'single-line'; - enforceForArrowConditionals?: boolean; - })[], + [ + 'all' | 'functions', + { + conditionalAssign?: boolean; + returnAssign?: boolean; + nestedBinaryExpressions?: boolean; + ignoreJSX?: 'none' | 'all' | 'multi-line' | 'single-line'; + enforceForArrowConditionals?: boolean; + }? + ], { + ArrayExpression(node: TSESTree.ArrayExpression): void; + ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void; + AssignmentExpression(node: TSESTree.AssignmentExpression): void; + AwaitExpression(node: TSESTree.AwaitExpression): void; + BinaryExpression(node: TSESTree.BinaryExpression): void; + CallExpression(node: TSESTree.CallExpression): void; + ClassDeclaration(node: TSESTree.ClassDeclaration): void; + ClassExpression(node: TSESTree.ClassExpression): void; + ConditionalExpression(node: TSESTree.ConditionalExpression): void; + DoWhileStatement(node: TSESTree.DoWhileStatement): void; + 'ForInStatement, ForOfStatement'( + node: TSESTree.ForInStatement | TSESTree.ForOfStatement, + ): void; + ForStatement(node: TSESTree.ForStatement): void; + IfStatement(node: TSESTree.IfStatement): void; + LogicalExpression(node: TSESTree.LogicalExpression): void; MemberExpression(node: TSESTree.MemberExpression): void; + NewExpression(node: TSESTree.NewExpression): void; + ObjectExpression(node: TSESTree.ObjectExpression): void; + ReturnStatement(node: TSESTree.ReturnStatement): void; + SequenceExpression(node: TSESTree.SequenceExpression): void; + SpreadElement(node: TSESTree.SpreadElement): void; + SwitchCase(node: TSESTree.SwitchCase): void; + SwitchStatement(node: TSESTree.SwitchStatement): void; + ThrowStatement(node: TSESTree.ThrowStatement): void; + UnaryExpression(node: TSESTree.UnaryExpression): void; + UpdateExpression(node: TSESTree.UpdateExpression): void; + VariableDeclarator(node: TSESTree.VariableDeclarator): void; + WhileStatement(node: TSESTree.WhileStatement): void; + WithStatement(node: TSESTree.WithStatement): void; + YieldExpression(node: TSESTree.YieldExpression): void; } >; export = rule; diff --git a/packages/eslint-plugin/typings/ts-eslint.d.ts b/packages/eslint-plugin/typings/ts-eslint.d.ts index 83c58d548a6..e32069feccf 100644 --- a/packages/eslint-plugin/typings/ts-eslint.d.ts +++ b/packages/eslint-plugin/typings/ts-eslint.d.ts @@ -562,6 +562,8 @@ declare module 'ts-eslint' { TSUnionType?: RuleFunction; TSUnknownKeyword?: RuleFunction; TSVoidKeyword?: RuleFunction; + UnaryExpression?: RuleFunction; + UpdateExpression?: RuleFunction; VariableDeclaration?: RuleFunction; VariableDeclarator?: RuleFunction; WhileStatement?: RuleFunction; diff --git a/packages/parser/package.json b/packages/parser/package.json index 8c0df30f2f5..5beae5aa527 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -26,9 +26,10 @@ "eslint" ], "scripts": { - "prebuild": "npm run clean", "build": "tsc -p tsconfig.build.json", "clean": "rimraf dist/", + "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", + "prebuild": "npm run clean", "test": "jest --coverage", "typecheck": "tsc --noEmit" }, diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index d4b7665e618..3a57c313925 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -27,13 +27,14 @@ "syntax" ], "scripts": { - "prebuild": "npm run clean", + "ast-alignment-tests": "jest spec.ts", "build": "tsc -p tsconfig.build.json", "clean": "rimraf dist/", + "format": "prettier --write \"./**/*.{ts,js,json,md}\" --ignore-path ../../.prettierignore", + "prebuild": "npm run clean", "test": "jest --coverage", - "unit-tests": "jest \"./tests/lib/.*\"", - "ast-alignment-tests": "jest spec.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "unit-tests": "jest \"./tests/lib/.*\"" }, "dependencies": { "lodash.unescape": "4.0.1", diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index 9de0fc257d1..e35384b6081 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -964,7 +964,7 @@ export interface ThisExpression extends BaseNode { export interface ThrowStatement extends BaseNode { type: AST_NODE_TYPES.ThrowStatement; - argument: Statement | null; + argument: Statement | TSAsExpression | null; } export interface TryStatement extends BaseNode {