From fa6849245ca55ca407dc031afbad456f2925a8e9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 12:26:24 -0800 Subject: [PATCH] fix(scope-manager): fix assertion assignments not being marked as write references (#2809) Fixes #2804 --- .../tests/eslint-rules/prefer-const.test.ts | 24 +++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 21 ++++++- .../src/referencer/Referencer.ts | 22 +++++-- .../assignment/angle-bracket-assignment.ts | 2 + .../angle-bracket-assignment.ts.shot | 62 +++++++++++++++++++ .../assignment/as-assignment.ts | 2 + .../assignment/as-assignment.ts.shot | 62 +++++++++++++++++++ .../assignment/non-null-assignment.ts | 2 + .../assignment/non-null-assignment.ts.shot | 62 +++++++++++++++++++ 9 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot diff --git a/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts b/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts new file mode 100644 index 00000000000..a051ebb6861 --- /dev/null +++ b/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts @@ -0,0 +1,24 @@ +import rule from 'eslint/lib/rules/prefer-const'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('prefer-const', rule, { + valid: [ + ` +let x: number | undefined = 1; +x! += 1; + `, + ` +let x: number | undefined = 1; +(x) += 1; + `, + ` +let x: number | undefined = 1; +(x as number) += 1; + `, + ], + invalid: [], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 93b75e3a956..e25ce02f88b 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -797,7 +797,7 @@ declare module 'eslint/lib/rules/space-infix-ops' { 'missingSpace', [ { - int32Hint: boolean; + int32Hint?: boolean; }, ], { @@ -812,6 +812,25 @@ declare module 'eslint/lib/rules/space-infix-ops' { export = rule; } +declare module 'eslint/lib/rules/prefer-const' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'useConst', + [ + { + destructuring?: 'any' | 'all'; + ignoreReadBeforeAssign?: boolean; + }, + ], + { + 'Program:exit'(node: TSESTree.Program): void; + VariableDeclaration(node: TSESTree.VariableDeclaration): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/utils/ast-utils' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; diff --git a/packages/scope-manager/src/referencer/Referencer.ts b/packages/scope-manager/src/referencer/Referencer.ts index 56c974055bc..a36dcfc9c1a 100644 --- a/packages/scope-manager/src/referencer/Referencer.ts +++ b/packages/scope-manager/src/referencer/Referencer.ts @@ -386,10 +386,22 @@ class Referencer extends Visitor { } protected AssignmentExpression(node: TSESTree.AssignmentExpression): void { - if (PatternVisitor.isPattern(node.left)) { + let left = node.left; + switch (left.type) { + case AST_NODE_TYPES.TSAsExpression: + case AST_NODE_TYPES.TSTypeAssertion: + // explicitly visit the type annotation + this.visit(left.typeAnnotation); + // intentional fallthrough + case AST_NODE_TYPES.TSNonNullExpression: + // unwrap the expression + left = left.expression; + } + + if (PatternVisitor.isPattern(left)) { if (node.operator === '=') { this.visitPattern( - node.left, + left, (pattern, info) => { const maybeImplicitGlobal = !this.currentScope().isStrict ? { @@ -413,15 +425,15 @@ class Referencer extends Visitor { }, { processRightHandNodes: true }, ); - } else if (node.left.type === AST_NODE_TYPES.Identifier) { + } else if (left.type === AST_NODE_TYPES.Identifier) { this.currentScope().referenceValue( - node.left, + left, ReferenceFlag.ReadWrite, node.right, ); } } else { - this.visit(node.left); + this.visit(left); } this.visit(node.right); } diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts new file mode 100644 index 00000000000..af751891afe --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts @@ -0,0 +1,2 @@ +let x: number | undefined = 1; +(x) += 1; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot new file mode 100644 index 00000000000..b5ee6ad87b0 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-assertion assignment angle-bracket-assignment 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$2, + }, + Reference$2 { + identifier: Identifier<"x">, + init: false, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$3, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "x" => Variable$2, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts new file mode 100644 index 00000000000..ed2cc7ef0f5 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts @@ -0,0 +1,2 @@ +let x: number | undefined = 1; +(x as number) += 1; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot new file mode 100644 index 00000000000..3baa6079293 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-assertion assignment as-assignment 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$2, + }, + Reference$2 { + identifier: Identifier<"x">, + init: false, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$3, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "x" => Variable$2, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts new file mode 100644 index 00000000000..a64362f7b62 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts @@ -0,0 +1,2 @@ +let x: number | undefined = 1; +x! += 1; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot new file mode 100644 index 00000000000..6aa2a23aecb --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-assertion assignment non-null-assignment 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$2, + }, + Reference$2 { + identifier: Identifier<"x">, + init: false, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$3, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "x" => Variable$2, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + ], + }, + ], +} +`;