From 348d2f6b207acb9e18721cba53f0aa85908fa777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Thu, 12 Dec 2019 00:37:02 +0100 Subject: [PATCH 1/5] chore(parser): mark TS as optional peer dependency (#1327) --- packages/parser/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/parser/package.json b/packages/parser/package.json index ffe2ca6787bc..420e5504930f 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -51,5 +51,10 @@ "@types/glob": "^7.1.1", "@typescript-eslint/shared-fixtures": "2.11.0", "glob": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } } From 3923a090bba9a9b65d6318dc213220e7e7f3c832 Mon Sep 17 00:00:00 2001 From: Alexander T Date: Mon, 16 Dec 2019 13:18:01 +0200 Subject: [PATCH 2/5] fix(eslint-plugin): [quotes] ignore backticks for interface properties (#1311) --- packages/eslint-plugin/src/rules/quotes.ts | 20 +-- .../eslint-plugin/tests/rules/quotes.test.ts | 164 ++++++++++-------- 2 files changed, 100 insertions(+), 84 deletions(-) diff --git a/packages/eslint-plugin/src/rules/quotes.ts b/packages/eslint-plugin/src/rules/quotes.ts index 97b7a0a17ca2..25a9d3b4c9e1 100644 --- a/packages/eslint-plugin/src/rules/quotes.ts +++ b/packages/eslint-plugin/src/rules/quotes.ts @@ -1,7 +1,4 @@ -import { - AST_NODE_TYPES, - TSESTree, -} from '@typescript-eslint/experimental-utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; import baseRule from 'eslint/lib/rules/quotes'; import * as util from '../util'; @@ -32,21 +29,14 @@ export default util.createRule({ create(context, [option]) { const rules = baseRule.create(context); - const isModuleDeclaration = (node: TSESTree.Literal): boolean => { - return ( - !!node.parent && node.parent.type === AST_NODE_TYPES.TSModuleDeclaration - ); - }; - - const isTypeLiteral = (node: TSESTree.Literal): boolean => { - return !!node.parent && node.parent.type === AST_NODE_TYPES.TSLiteralType; - }; - return { Literal(node): void { + const parent = node.parent; if ( option === 'backtick' && - (isModuleDeclaration(node) || isTypeLiteral(node)) + (parent?.type === AST_NODE_TYPES.TSModuleDeclaration || + parent?.type === AST_NODE_TYPES.TSLiteralType || + parent?.type === AST_NODE_TYPES.TSPropertySignature) ) { return; } diff --git a/packages/eslint-plugin/tests/rules/quotes.test.ts b/packages/eslint-plugin/tests/rules/quotes.test.ts index bec1f4b7e98c..feb312deb99b 100644 --- a/packages/eslint-plugin/tests/rules/quotes.test.ts +++ b/packages/eslint-plugin/tests/rules/quotes.test.ts @@ -33,7 +33,6 @@ ruleTester.run('quotes', rule, { { code: `declare module '*.html' {}`, options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: ` @@ -42,7 +41,6 @@ ruleTester.run('quotes', rule, { } `, options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, /** ESLint */ @@ -85,7 +83,6 @@ ruleTester.run('quotes', rule, { code: `var foo = <>Hello world;`, options: ['single'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -95,7 +92,6 @@ ruleTester.run('quotes', rule, { code: `var foo = <>Hello world;`, options: ['double'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -110,7 +106,6 @@ ruleTester.run('quotes', rule, { }, ], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -120,7 +115,6 @@ ruleTester.run('quotes', rule, { code: `var foo = <>Hello world;`, options: ['backtick'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -130,7 +124,6 @@ ruleTester.run('quotes', rule, { code: `var foo =
Hello world
;`, options: ['single'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -140,7 +133,6 @@ ruleTester.run('quotes', rule, { code: `var foo =
;`, options: ['single'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -150,7 +142,6 @@ ruleTester.run('quotes', rule, { code: `var foo =
Hello world
;`, options: ['double'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -165,7 +156,6 @@ ruleTester.run('quotes', rule, { }, ], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -174,17 +164,14 @@ ruleTester.run('quotes', rule, { { code: 'var foo = `bar`;', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: "var foo = `bar 'baz'`;", options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `bar "baz"`;', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: `var foo = 1;`, @@ -203,7 +190,6 @@ ruleTester.run('quotes', rule, { code: `var foo =
;`, options: ['backtick'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -213,7 +199,6 @@ ruleTester.run('quotes', rule, { code: `var foo =
Hello world
;`, options: ['backtick'], parserOptions: { - ecmaVersion: 6, ecmaFeatures: { jsx: true, }, @@ -224,47 +209,38 @@ ruleTester.run('quotes', rule, { { code: 'var foo = `back\ntick`;', options: ['single'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `back\rtick`;', options: ['single'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `back\u2028tick`;', options: ['single'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `back\u2029tick`;', options: ['single'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `back\\\\\ntick`;', // 2 backslashes followed by a newline options: ['single'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `back\\\\\\\\\ntick`;', options: ['single'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `\n`;', options: ['single'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `back${x}tick`;', options: ['double'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = tag`backtick`;', options: ['double'], - parserOptions: { ecmaVersion: 6 }, }, // Backticks are also okay if allowTemplateLiterals @@ -276,7 +252,6 @@ ruleTester.run('quotes', rule, { allowTemplateLiterals: true, }, ], - parserOptions: { ecmaVersion: 6 }, }, { code: 'var foo = `bar \'foo\' baz` + "bar";', @@ -286,7 +261,6 @@ ruleTester.run('quotes', rule, { allowTemplateLiterals: true, }, ], - parserOptions: { ecmaVersion: 6 }, }, { code: "var foo = `bar 'foo' baz` + `bar`;", @@ -296,80 +270,92 @@ ruleTester.run('quotes', rule, { allowTemplateLiterals: true, }, ], - parserOptions: { ecmaVersion: 6 }, }, // `backtick` should not warn the directive prologues. { code: '"use strict"; var foo = `backtick`;', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: '"use strict"; \'use strong\'; "use asm"; var foo = `backtick`;', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: 'function foo() { "use strict"; "use strong"; "use asm"; var foo = `backtick`; }', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: "(function() { 'use strict'; 'use strong'; 'use asm'; var foo = `backtick`; })();", options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: '(() => { "use strict"; "use strong"; "use asm"; var foo = `backtick`; })();', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, // `backtick` should not warn import/export sources. { code: `import "a"; import 'b';`, options: ['backtick'], - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - }, }, { code: `import a from "a"; import b from 'b';`, options: ['backtick'], - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - }, }, { code: `export * from "a"; export * from 'b';`, options: ['backtick'], - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - }, }, // `backtick` should not warn property/method names (not computed). { code: `var obj = {"key0": 0, 'key1': 1};`, options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: `class Foo { 'bar'(){} }`, options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, }, { code: `class Foo { static ''(){} }`, options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, + }, + + { + code: ` +interface Foo { + a: number; + b: string; + "a-b": boolean; + "a-b-c": boolean; +} + `, + }, + { + code: ` +interface Foo { + a: number; + b: string; + 'a-b': boolean; + 'a-b-c': boolean; +} + `, + options: ['single'], + }, + { + code: ` +interface Foo { + a: number; + b: string; + 'a-b': boolean; + 'a-b-c': boolean; +} + `, + options: ['backtick'], }, ], @@ -389,7 +375,6 @@ ruleTester.run('quotes', rule, { code: 'var foo = `bar`;', output: `var foo = 'bar';`, options: ['single'], - parserOptions: { ecmaVersion: 6 }, errors: [useSingleQuote], }, { @@ -416,7 +401,6 @@ ruleTester.run('quotes', rule, { code: 'var foo = `bar`;', output: `var foo = "bar";`, options: ['double'], - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { @@ -478,21 +462,18 @@ ruleTester.run('quotes', rule, { code: `var foo = 'bar';`, output: 'var foo = `bar`;', options: ['backtick'], - parserOptions: { ecmaVersion: 2015 }, errors: [useBacktick], }, { code: "var foo = 'b${x}a$r';", output: 'var foo = `b\\${x}a$r`;', options: ['backtick'], - parserOptions: { ecmaVersion: 2015 }, errors: [useBacktick], }, { code: 'var foo = "bar";', output: 'var foo = `bar`;', options: ['backtick'], - parserOptions: { ecmaVersion: 2015 }, errors: [useBacktick], }, { @@ -504,7 +485,6 @@ ruleTester.run('quotes', rule, { avoidEscape: true, }, ], - parserOptions: { ecmaVersion: 2015 }, errors: [useBacktick], }, { @@ -516,7 +496,6 @@ ruleTester.run('quotes', rule, { avoidEscape: true, }, ], - parserOptions: { ecmaVersion: 2015 }, errors: [useBacktick], }, @@ -525,21 +504,18 @@ ruleTester.run('quotes', rule, { code: 'var foo = `backtick`; "use strict";', output: 'var foo = `backtick`; `use strict`;', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, errors: [useBacktick], }, { code: '{ "use strict"; var foo = `backtick`; }', output: '{ `use strict`; var foo = `backtick`; }', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, errors: [useBacktick], }, { code: 'if (1) { "use strict"; var foo = `backtick`; }', output: 'if (1) { `use strict`; var foo = `backtick`; }', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, errors: [useBacktick], }, @@ -555,7 +531,6 @@ ruleTester.run('quotes', rule, { code: `class Foo { ['a'](){} static ['b'](){} }`, output: 'class Foo { [`a`](){} static [`b`](){} }', options: ['backtick'], - parserOptions: { ecmaVersion: 6 }, errors: [useBacktick, useBacktick], }, @@ -590,7 +565,6 @@ ruleTester.run('quotes', rule, { ecmaFeatures: { jsx: true, }, - ecmaVersion: 2015, }, errors: [useBacktick], }, @@ -599,37 +573,31 @@ ruleTester.run('quotes', rule, { { code: '`use strict`;', output: null, - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: 'function foo() { `use strict`; foo(); }', output: null, - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: 'foo = function() { `use strict`; foo(); }', output: null, - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: '() => { `use strict`; foo(); }', output: null, - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: '() => { foo(); `use strict`; }', output: `() => { foo(); "use strict"; }`, - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: 'foo(); `use strict`;', output: 'foo(); "use strict";', - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, @@ -637,26 +605,84 @@ ruleTester.run('quotes', rule, { { code: 'var foo = `foo\\nbar`;', output: 'var foo = "foo\\nbar";', - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: 'var foo = `foo\\\nbar`;', // 1 backslash followed by a newline output: 'var foo = "foo\\\nbar";', - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: 'var foo = `foo\\\\\\\nbar`;', // 3 backslashes followed by a newline output: 'var foo = "foo\\\\\\\nbar";', - parserOptions: { ecmaVersion: 6 }, errors: [useDoubleQuote], }, { code: '````', output: '""``', - parserOptions: { ecmaVersion: 6 }, errors: [{ ...useDoubleQuote, line: 1, column: 1 }], }, + + { + code: ` +interface Foo { + a: number; + b: string; + 'a-b': boolean; + 'a-b-c': boolean; +} + `, + output: ` +interface Foo { + a: number; + b: string; + "a-b": boolean; + "a-b-c": boolean; +} + `, + errors: [ + { + ...useDoubleQuote, + line: 5, + column: 3, + }, + { + ...useDoubleQuote, + line: 6, + column: 3, + }, + ], + }, + { + code: ` +interface Foo { + a: number; + b: string; + "a-b": boolean; + "a-b-c": boolean; +} + `, + output: ` +interface Foo { + a: number; + b: string; + 'a-b': boolean; + 'a-b-c': boolean; +} + `, + errors: [ + { + ...useSingleQuote, + line: 5, + column: 3, + }, + { + ...useSingleQuote, + line: 6, + column: 3, + }, + ], + options: ['single'], + }, ], }); From f9a9fbfc83fea7c5677adc5b2b6d8004f23ed60a Mon Sep 17 00:00:00 2001 From: Dmytro Borysovskyi Date: Mon, 16 Dec 2019 13:20:18 +0200 Subject: [PATCH 3/5] fix(eslint-plugin): [prefer-null-coal] fixer w/ mixed logicals (#1326) --- .../src/rules/prefer-nullish-coalescing.ts | 28 +++++++++++++----- packages/eslint-plugin/src/util/astUtils.ts | 14 +++++++-- .../rules/prefer-nullish-coalescing.test.ts | 29 +++++++++++++++++-- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts index 649c8b3db21c..e0beb74c79c6 100644 --- a/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts +++ b/packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts @@ -103,6 +103,24 @@ export default util.createRule({ util.NullThrowsReasons.MissingToken('operator', node.type), ); + function* fix( + fixer: TSESLint.RuleFixer, + ): IterableIterator { + if (node.parent && util.isLogicalOrOperator(node.parent)) { + // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c) + if ( + node.left.type === AST_NODE_TYPES.LogicalExpression && + !util.isLogicalOrOperator(node.left.left) + ) { + yield fixer.insertTextBefore(node.left.right, '('); + } else { + yield fixer.insertTextBefore(node.left, '('); + } + yield fixer.insertTextAfter(node.right, ')'); + } + yield fixer.replaceText(barBarOperator, '??'); + } + const fixer = isMixedLogical || forceSuggestionFixer ? // suggestion instead for cases where we aren't sure if the fixer is completely safe @@ -110,17 +128,11 @@ export default util.createRule({ suggest: [ { messageId: 'preferNullish', - fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { - return fixer.replaceText(barBarOperator, '??'); - }, + fix, }, ], } as const) - : { - fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix { - return fixer.replaceText(barBarOperator, '??'); - }, - }; + : { fix }; context.report({ node: barBarOperator, diff --git a/packages/eslint-plugin/src/util/astUtils.ts b/packages/eslint-plugin/src/util/astUtils.ts index 9398f4eb64b3..328d25ae71c9 100644 --- a/packages/eslint-plugin/src/util/astUtils.ts +++ b/packages/eslint-plugin/src/util/astUtils.ts @@ -1,7 +1,7 @@ import { - TSESTree, - AST_TOKEN_TYPES, AST_NODE_TYPES, + AST_TOKEN_TYPES, + TSESTree, } from '@typescript-eslint/experimental-utils'; const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/; @@ -42,6 +42,15 @@ function isOptionalOptionalChain( ); } +/** + * Returns true if and only if the node represents logical OR + */ +function isLogicalOrOperator(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.LogicalExpression && node.operator === '||' + ); +} + /** * Determines whether two adjacent tokens are on the same line */ @@ -59,5 +68,6 @@ export { isOptionalChainPunctuator, isOptionalOptionalChain, isTokenOnSameLine, + isLogicalOrOperator, LINEBREAK_MATCHER, }; diff --git a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts index 7dd9212770c6..015f7988fe17 100644 --- a/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts +++ b/packages/eslint-plugin/tests/rules/prefer-nullish-coalescing.test.ts @@ -317,7 +317,7 @@ declare const a: ${type} | ${nullish}; declare const b: ${type} | ${nullish}; declare const c: ${type} | ${nullish}; declare const d: ${type} | ${nullish}; -a ?? b || c && d; +(a ?? b) || c && d; `.trimRight(), }, ], @@ -367,7 +367,7 @@ declare const a: ${type} | ${nullish}; declare const b: ${type} | ${nullish}; declare const c: ${type} | ${nullish}; declare const d: ${type} | ${nullish}; -a && b ?? c || d; +a && (b ?? c) || d; `.trimRight(), }, ], @@ -463,5 +463,30 @@ x ?? 'foo'; }, ], }, + + // https://github.com/typescript-eslint/typescript-eslint/issues/1290 + ...nullishTypeInvalidTest((nullish, type) => ({ + code: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type}; +declare const c: ${type}; +a || b || c; + `.trimRight(), + output: ` +declare const a: ${type} | ${nullish}; +declare const b: ${type}; +declare const c: ${type}; +(a ?? b) || c; + `.trimRight(), + errors: [ + { + messageId: 'preferNullish', + line: 5, + column: 3, + endLine: 5, + endColumn: 5, + }, + ], + })), ], }); From a2a8a0a90195a4cbfab7b225261ae9bdf65ad2de Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Mon, 16 Dec 2019 08:13:34 -0500 Subject: [PATCH 4/5] feat(eslint-plugin): [no-unnec-cond] check optional chaining (#1315) Co-authored-by: Brad Zacher --- packages/eslint-plugin/README.md | 2 +- .../docs/rules/no-unnecessary-condition.md | 13 +- .../src/rules/no-unnecessary-condition.ts | 63 +++++- .../rules/no-unnecessary-condition.test.ts | 206 ++++++++++++++++++ 4 files changed, 281 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 503c06e6894e..08030874d996 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -179,7 +179,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-require-imports`](./docs/rules/no-require-imports.md) | Disallows invocation of `require()` | | | | | [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` | :heavy_check_mark: | | | | [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases | | | | -| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Prevents conditionals where the type is always truthy or always falsy | | | :thought_balloon: | +| [`@typescript-eslint/no-unnecessary-condition`](./docs/rules/no-unnecessary-condition.md) | Prevents conditionals where the type is always truthy or always falsy | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-qualifier`](./docs/rules/no-unnecessary-qualifier.md) | Warns when a namespace qualifier is unnecessary | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-arguments`](./docs/rules/no-unnecessary-type-arguments.md) | Warns if an explicitly specified type argument is the default for that type parameter | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md index 4bc160e68ccd..cf6c277dfb5e 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-condition.md @@ -5,7 +5,8 @@ Any expression being used as a condition must be able to evaluate as truthy or f The following expressions are checked: - Arguments to the `&&`, `||` and `?:` (ternary) operators -- Conditions for `if`, `for`, `while`, and `do-while` statements. +- Conditions for `if`, `for`, `while`, and `do-while` statements +- Base values of optional chain expressions Examples of **incorrect** code for this rule: @@ -22,6 +23,11 @@ function foo(arg: 'bar' | 'baz') { if (arg) { } } + +function bar(arg: string) { + // arg can never be nullish, so ?. is unnecessary + return arg?.length; +} ``` Examples of **correct** code for this rule: @@ -39,6 +45,11 @@ function foo(arg: string) { if (arg) { } } + +function bar(arg?: string | null) { + // Necessary, since arg might be nullish + return arg?.length; +} ``` ## Options diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts index 1ee2c2629806..5c884f4506ca 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-condition.ts @@ -1,6 +1,7 @@ import { TSESTree, AST_NODE_TYPES, + AST_TOKEN_TYPES, } from '@typescript-eslint/experimental-utils'; import ts, { TypeFlags } from 'typescript'; import { @@ -14,6 +15,9 @@ import { createRule, getParserServices, getConstrainedTypeAtLocation, + isNullableType, + nullThrows, + NullThrowsReasons, } from '../util'; // Truthiness utilities @@ -65,7 +69,8 @@ export type MessageId = | 'neverNullish' | 'alwaysNullish' | 'literalBooleanExpression' - | 'never'; + | 'never' + | 'neverOptionalChain'; export default createRule({ name: 'no-unnecessary-conditionals', meta: { @@ -91,6 +96,7 @@ export default createRule({ additionalProperties: false, }, ], + fixable: 'code', messages: { alwaysTruthy: 'Unnecessary conditional, value is always truthy.', alwaysFalsy: 'Unnecessary conditional, value is always falsy.', @@ -101,6 +107,7 @@ export default createRule({ literalBooleanExpression: 'Unnecessary conditional, both sides of the expression are literal values', never: 'Unnecessary conditional, value is `never`', + neverOptionalChain: 'Unnecessary optional chain on a non-nullish value', }, }, defaultOptions: [ @@ -112,6 +119,7 @@ export default createRule({ create(context, [{ allowConstantLoopConditions, ignoreRhs }]) { const service = getParserServices(context); const checker = service.program.getTypeChecker(); + const sourceCode = context.getSourceCode(); function getNodeType(node: TSESTree.Node): ts.Type { const tsNode = service.esTreeNodeToTSNodeMap.get(node); @@ -260,6 +268,57 @@ export default createRule({ checkNode(node.test); } + function checkOptionalChain( + node: TSESTree.OptionalMemberExpression | TSESTree.OptionalCallExpression, + beforeOperator: TSESTree.Node, + fix: '' | '.', + ): void { + // We only care if this step in the chain is optional. If just descend + // from an optional chain, then that's fine. + if (!node.optional) { + return; + } + + const type = getNodeType(node); + if ( + isTypeFlagSet(type, ts.TypeFlags.Any) || + isTypeFlagSet(type, ts.TypeFlags.Unknown) || + isNullableType(type, { allowUndefined: true }) + ) { + return; + } + + const questionDotOperator = nullThrows( + sourceCode.getTokenAfter( + beforeOperator, + token => + token.type === AST_TOKEN_TYPES.Punctuator && token.value === '?.', + ), + NullThrowsReasons.MissingToken('operator', node.type), + ); + + context.report({ + node, + loc: questionDotOperator.loc, + messageId: 'neverOptionalChain', + fix(fixer) { + return fixer.replaceText(questionDotOperator, fix); + }, + }); + } + + function checkOptionalMemberExpression( + node: TSESTree.OptionalMemberExpression, + ): void { + checkOptionalChain(node, node.object, '.'); + } + + function checkOptionalCallExpression( + node: TSESTree.OptionalCallExpression, + ): void { + checkOptionalChain(node, node.callee, ''); + } + return { BinaryExpression: checkIfBinaryExpressionIsNecessaryConditional, ConditionalExpression: checkIfTestExpressionIsNecessaryConditional, @@ -268,6 +327,8 @@ export default createRule({ IfStatement: checkIfTestExpressionIsNecessaryConditional, LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals, WhileStatement: checkIfLoopIsNecessaryConditional, + OptionalMemberExpression: checkOptionalMemberExpression, + OptionalCallExpression: checkOptionalCallExpression, }; }, }); diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 0fe1326811c0..e2e466962249 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -121,6 +121,62 @@ do {} while(true) `, options: [{ allowConstantLoopConditions: true }], }, + ` +let foo: undefined | { bar: true }; +foo?.bar; +`, + ` +let foo: null | { bar: true }; +foo?.bar; +`, + ` +let foo: undefined; +foo?.bar; +`, + ` +let foo: undefined; +foo?.bar.baz; +`, + ` +let foo: null; +foo?.bar; +`, + ` +let anyValue: any; +anyValue?.foo; +`, + ` +let unknownValue: unknown; +unknownValue?.foo; +`, + ` +let foo: undefined | (() => {}); +foo?.(); +`, + ` +let foo: null | (() => {}); +foo?.(); +`, + ` +let foo: undefined; +foo?.(); +`, + ` +let foo: undefined; +foo?.().bar; +`, + ` +let foo: null; +foo?.(); +`, + ` +let anyValue: any; +anyValue?.(); +`, + ` +let unknownValue: unknown; +unknownValue?.(); +`, ], invalid: [ // Ensure that it's checking in all the right places @@ -267,5 +323,155 @@ do {} while(true) ruleError(4, 13, 'alwaysTruthy'), ], }, + { + code: ` +let foo = { bar: true }; +foo?.bar; +foo ?. bar; +foo ?. + bar; +foo + ?. bar; +`, + output: ` +let foo = { bar: true }; +foo.bar; +foo . bar; +foo . + bar; +foo + . bar; +`, + errors: [ + { + messageId: 'neverOptionalChain', + line: 3, + column: 4, + endLine: 3, + endColumn: 6, + }, + { + messageId: 'neverOptionalChain', + line: 4, + column: 5, + endLine: 4, + endColumn: 7, + }, + { + messageId: 'neverOptionalChain', + line: 5, + column: 5, + endLine: 5, + endColumn: 7, + }, + { + messageId: 'neverOptionalChain', + line: 8, + column: 3, + endLine: 8, + endColumn: 5, + }, + ], + }, + { + code: ` +let foo = () => {}; +foo?.(); +foo ?. (); +foo ?. + (); +foo + ?. (); +`, + output: ` +let foo = () => {}; +foo(); +foo (); +foo${' '} + (); +foo + (); +`, + errors: [ + { + messageId: 'neverOptionalChain', + line: 3, + column: 4, + endLine: 3, + endColumn: 6, + }, + { + messageId: 'neverOptionalChain', + line: 4, + column: 5, + endLine: 4, + endColumn: 7, + }, + { + messageId: 'neverOptionalChain', + line: 5, + column: 5, + endLine: 5, + endColumn: 7, + }, + { + messageId: 'neverOptionalChain', + line: 8, + column: 3, + endLine: 8, + endColumn: 5, + }, + ], + }, + { + code: ` +let foo = () => {}; +foo?.(bar); +foo ?. (bar); +foo ?. + (bar); +foo + ?. (bar); +`, + output: ` +let foo = () => {}; +foo(bar); +foo (bar); +foo${' '} + (bar); +foo + (bar); +`, + errors: [ + { + messageId: 'neverOptionalChain', + line: 3, + column: 4, + endLine: 3, + endColumn: 6, + }, + { + messageId: 'neverOptionalChain', + line: 4, + column: 5, + endLine: 4, + endColumn: 7, + }, + { + messageId: 'neverOptionalChain', + line: 5, + column: 5, + endLine: 5, + endColumn: 7, + }, + { + messageId: 'neverOptionalChain', + line: 8, + column: 3, + endLine: 8, + endColumn: 5, + }, + ], + }, ], }); From 6b7b88b628664309a8672d286614879dce479b4b Mon Sep 17 00:00:00 2001 From: James Henry Date: Mon, 16 Dec 2019 18:01:42 +0000 Subject: [PATCH 5/5] chore: publish v2.12.0 --- CHANGELOG.md | 17 +++++++++++++++++ lerna.json | 2 +- packages/eslint-plugin-tslint/CHANGELOG.md | 8 ++++++++ packages/eslint-plugin-tslint/package.json | 6 +++--- packages/eslint-plugin/CHANGELOG.md | 17 +++++++++++++++++ packages/eslint-plugin/package.json | 4 ++-- packages/experimental-utils/CHANGELOG.md | 8 ++++++++ packages/experimental-utils/package.json | 4 ++-- packages/parser/CHANGELOG.md | 8 ++++++++ packages/parser/package.json | 8 ++++---- packages/shared-fixtures/CHANGELOG.md | 8 ++++++++ packages/shared-fixtures/package.json | 2 +- packages/typescript-estree/CHANGELOG.md | 8 ++++++++ packages/typescript-estree/package.json | 4 ++-- 14 files changed, 89 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7607ea06f7..df0c5be866b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.12.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.11.0...v2.12.0) (2019-12-16) + + +### Bug Fixes + +* **eslint-plugin:** [prefer-null-coal] fixer w/ mixed logicals ([#1326](https://github.com/typescript-eslint/typescript-eslint/issues/1326)) ([f9a9fbf](https://github.com/typescript-eslint/typescript-eslint/commit/f9a9fbf)) +* **eslint-plugin:** [quotes] ignore backticks for interface properties ([#1311](https://github.com/typescript-eslint/typescript-eslint/issues/1311)) ([3923a09](https://github.com/typescript-eslint/typescript-eslint/commit/3923a09)) + + +### Features + +* **eslint-plugin:** [no-unnec-cond] check optional chaining ([#1315](https://github.com/typescript-eslint/typescript-eslint/issues/1315)) ([a2a8a0a](https://github.com/typescript-eslint/typescript-eslint/commit/a2a8a0a)) + + + + + # [2.11.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.10.0...v2.11.0) (2019-12-09) diff --git a/lerna.json b/lerna.json index dc773f1cb452..b97db751fcc3 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.11.0", + "version": "2.12.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index 8293ab343103..3ac0eebb630d 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.12.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.11.0...v2.12.0) (2019-12-16) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + + + + + # [2.11.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.10.0...v2.11.0) (2019-12-09) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index 559b761c1ae5..07bcb72f222d 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-tslint", - "version": "2.11.0", + "version": "2.12.0", "main": "dist/index.js", "typings": "src/index.ts", "description": "TSLint wrapper plugin for ESLint", @@ -31,7 +31,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.11.0", + "@typescript-eslint/experimental-utils": "2.12.0", "lodash.memoize": "^4.1.2" }, "peerDependencies": { @@ -41,6 +41,6 @@ }, "devDependencies": { "@types/lodash.memoize": "^4.1.4", - "@typescript-eslint/parser": "2.11.0" + "@typescript-eslint/parser": "2.12.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 13d3eff3377d..834ea49824c4 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.12.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.11.0...v2.12.0) (2019-12-16) + + +### Bug Fixes + +* **eslint-plugin:** [prefer-null-coal] fixer w/ mixed logicals ([#1326](https://github.com/typescript-eslint/typescript-eslint/issues/1326)) ([f9a9fbf](https://github.com/typescript-eslint/typescript-eslint/commit/f9a9fbf)) +* **eslint-plugin:** [quotes] ignore backticks for interface properties ([#1311](https://github.com/typescript-eslint/typescript-eslint/issues/1311)) ([3923a09](https://github.com/typescript-eslint/typescript-eslint/commit/3923a09)) + + +### Features + +* **eslint-plugin:** [no-unnec-cond] check optional chaining ([#1315](https://github.com/typescript-eslint/typescript-eslint/issues/1315)) ([a2a8a0a](https://github.com/typescript-eslint/typescript-eslint/commit/a2a8a0a)) + + + + + # [2.11.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.10.0...v2.11.0) (2019-12-09) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index b70e26301203..aec47666e8e5 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "2.11.0", + "version": "2.12.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -40,7 +40,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "2.11.0", + "@typescript-eslint/experimental-utils": "2.12.0", "eslint-utils": "^1.4.3", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index 6a3f92667947..ea18bc6404fb 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.12.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.11.0...v2.12.0) (2019-12-16) + +**Note:** Version bump only for package @typescript-eslint/experimental-utils + + + + + # [2.11.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.10.0...v2.11.0) (2019-12-09) **Note:** Version bump only for package @typescript-eslint/experimental-utils diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index fe0b531ad244..44e9c846a328 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "2.11.0", + "version": "2.12.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -37,7 +37,7 @@ }, "dependencies": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.11.0", + "@typescript-eslint/typescript-estree": "2.12.0", "eslint-scope": "^5.0.0" }, "peerDependencies": { diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index 4dce721a6b9a..62852162ae98 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.12.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.11.0...v2.12.0) (2019-12-16) + +**Note:** Version bump only for package @typescript-eslint/parser + + + + + # [2.11.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.10.0...v2.11.0) (2019-12-09) **Note:** Version bump only for package @typescript-eslint/parser diff --git a/packages/parser/package.json b/packages/parser/package.json index 420e5504930f..c06e68ed728d 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "2.11.0", + "version": "2.12.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -43,13 +43,13 @@ }, "dependencies": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.11.0", - "@typescript-eslint/typescript-estree": "2.11.0", + "@typescript-eslint/experimental-utils": "2.12.0", + "@typescript-eslint/typescript-estree": "2.12.0", "eslint-visitor-keys": "^1.1.0" }, "devDependencies": { "@types/glob": "^7.1.1", - "@typescript-eslint/shared-fixtures": "2.11.0", + "@typescript-eslint/shared-fixtures": "2.12.0", "glob": "*" }, "peerDependenciesMeta": { diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index de6b5d8158e8..00ca4225093c 100644 --- a/packages/shared-fixtures/CHANGELOG.md +++ b/packages/shared-fixtures/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.12.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.11.0...v2.12.0) (2019-12-16) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + # [2.11.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.10.0...v2.11.0) (2019-12-09) **Note:** Version bump only for package @typescript-eslint/shared-fixtures diff --git a/packages/shared-fixtures/package.json b/packages/shared-fixtures/package.json index 8f549941a756..0714237eac5a 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "2.11.0", + "version": "2.12.0", "private": true, "scripts": { "build": "tsc -b tsconfig.build.json", diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index cba63aa7c51d..16aec514aa5e 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [2.12.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.11.0...v2.12.0) (2019-12-16) + +**Note:** Version bump only for package @typescript-eslint/typescript-estree + + + + + # [2.11.0](https://github.com/typescript-eslint/typescript-eslint/compare/v2.10.0...v2.11.0) (2019-12-09) **Note:** Version bump only for package @typescript-eslint/typescript-estree diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 3426420f6f42..8226cbca00f1 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "2.11.0", + "version": "2.12.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/parser.js", "types": "dist/parser.d.ts", @@ -59,7 +59,7 @@ "@types/lodash.unescape": "^4.0.4", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", - "@typescript-eslint/shared-fixtures": "2.11.0", + "@typescript-eslint/shared-fixtures": "2.12.0", "babel-code-frame": "^6.26.0", "lodash.isplainobject": "4.0.6", "tmp": "^0.1.0",