From ecdb016fa82b090b1c696ded805de683e4a5853b Mon Sep 17 00:00:00 2001 From: nizarius Date: Tue, 5 Nov 2019 00:43:09 +0300 Subject: [PATCH 1/9] fix(parser): optional member expression --- .../no-restricted-globals.test.ts | 5 + .../tests/eslint-rules/no-undef.test.ts | 17 + packages/parser/src/analyze-scope.ts | 7 + .../lib/__snapshots__/typescript.ts.snap | 1134 +++-------------- 4 files changed, 187 insertions(+), 976 deletions(-) diff --git a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts index 40f3398fb7df..1820f78754f9 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts @@ -30,6 +30,11 @@ type Handler = (event: string) => any `, options: ['event'], }, + { + code: ` + const a = foo?.bar?.name ?? "foobar" + `, + }, ], invalid: [], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts index 84626c38201f..1c207665ad60 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts @@ -68,6 +68,23 @@ function eachr(subject: Object | Array): typeof subject { ` function eachr(subject: Map): typeof subject; `, + ` + var a = { b: 3 }; + var c = a?.b; + `, + ` + var a = { b: { c: 3 } }; + var d = a?.["b"]?.c; + `, + ` + var a = { b: 3 }; + var c = { }; + var d = (a || c)?.b; + `, + ` + var a = { b: () => {} }; + a?.b(); + `, ], invalid: [], }); diff --git a/packages/parser/src/analyze-scope.ts b/packages/parser/src/analyze-scope.ts index 1b98c064db09..569b6a03f9c7 100644 --- a/packages/parser/src/analyze-scope.ts +++ b/packages/parser/src/analyze-scope.ts @@ -344,6 +344,13 @@ class Referencer extends TSESLintScope.Referencer { node.arguments.forEach(this.visit, this); } + OptionalMemberExpression(node: TSESTree.MemberExpression): void { + this.visit(node.object); + if (node.computed) { + this.visit(node.property); + } + } + /** * Define the variable of this function declaration only once. * Because to avoid confusion of `no-redeclare` rule by overloading. diff --git a/packages/parser/tests/lib/__snapshots__/typescript.ts.snap b/packages/parser/tests/lib/__snapshots__/typescript.ts.snap index 3d94d81e424b..39f3da70df9c 100644 --- a/packages/parser/tests/lib/__snapshots__/typescript.ts.snap +++ b/packages/parser/tests/lib/__snapshots__/typescript.ts.snap @@ -22070,7 +22070,7 @@ Object { exports[`typescript fixtures/basics/optional-chain.src 1`] = ` Object { - "$id": 18, + "$id": 10, "block": Object { "range": Array [ 0, @@ -22080,7 +22080,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 17, + "$id": 9, "block": Object { "range": Array [ 0, @@ -22090,7 +22090,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 16, + "$id": 8, "block": Object { "range": Array [ 0, @@ -22105,7 +22105,7 @@ Object { Object { "$id": 3, "from": Object { - "$ref": 16, + "$ref": 8, }, "identifier": Object { "name": "one", @@ -22124,24 +22124,7 @@ Object { Object { "$id": 4, "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "two", - "range": Array [ - 45, - 48, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 5, - "from": Object { - "$ref": 16, + "$ref": 8, }, "identifier": Object { "name": "one", @@ -22158,43 +22141,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 6, - "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "two", - "range": Array [ - 57, - 60, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 7, - "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 61, - 66, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 8, + "$id": 5, "from": Object { - "$ref": 16, + "$ref": 8, }, "identifier": Object { "name": "one", @@ -22211,26 +22160,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 9, - "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 79, - 84, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 10, + "$id": 6, "from": Object { - "$ref": 16, + "$ref": 8, }, "identifier": Object { "name": "one", @@ -22247,43 +22179,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 11, - "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 97, - 102, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 12, - "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "four", - "range": Array [ - 103, - 107, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 13, + "$id": 7, "from": Object { - "$ref": 16, + "$ref": 8, }, "identifier": Object { "name": "one", @@ -22299,70 +22197,11 @@ Object { }, "writeExpr": undefined, }, - Object { - "$id": 14, - "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 120, - 125, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 15, - "from": Object { - "$ref": 16, - }, - "identifier": Object { - "name": "four", - "range": Array [ - 127, - 131, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - ], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 7, - }, - Object { - "$ref": 9, - }, - Object { - "$ref": 11, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 14, - }, - Object { - "$ref": 15, - }, ], + "throughReferences": Array [], "type": "function", "upperScope": Object { - "$ref": 17, + "$ref": 9, }, "variableMap": Object { "arguments": Object { @@ -22373,7 +22212,7 @@ Object { }, }, "variableScope": Object { - "$ref": 16, + "$ref": 8, }, "variables": Array [ Object { @@ -22384,7 +22223,7 @@ Object { "name": "arguments", "references": Array [], "scope": Object { - "$ref": 16, + "$ref": 8, }, }, Object { @@ -22427,20 +22266,20 @@ Object { "$ref": 3, }, Object { - "$ref": 5, + "$ref": 4, }, Object { - "$ref": 8, + "$ref": 5, }, Object { - "$ref": 10, + "$ref": 6, }, Object { - "$ref": 13, + "$ref": 7, }, ], "scope": Object { - "$ref": 16, + "$ref": 8, }, }, ], @@ -22449,35 +22288,10 @@ Object { "functionExpressionScope": false, "isStrict": true, "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 7, - }, - Object { - "$ref": 9, - }, - Object { - "$ref": 11, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 14, - }, - Object { - "$ref": 15, - }, - ], + "throughReferences": Array [], "type": "module", "upperScope": Object { - "$ref": 18, + "$ref": 10, }, "variableMap": Object { "processOptional": Object { @@ -22485,7 +22299,7 @@ Object { }, }, "variableScope": Object { - "$ref": 17, + "$ref": 9, }, "variables": Array [ Object { @@ -22525,7 +22339,7 @@ Object { "name": "processOptional", "references": Array [], "scope": Object { - "$ref": 17, + "$ref": 9, }, }, ], @@ -22534,37 +22348,12 @@ Object { "functionExpressionScope": false, "isStrict": false, "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 7, - }, - Object { - "$ref": 9, - }, - Object { - "$ref": 11, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 14, - }, - Object { - "$ref": 15, - }, - ], + "throughReferences": Array [], "type": "global", "upperScope": null, "variableMap": Object {}, "variableScope": Object { - "$ref": 18, + "$ref": 10, }, "variables": Array [], } @@ -22572,7 +22361,7 @@ Object { exports[`typescript fixtures/basics/optional-chain-call.src 1`] = ` Object { - "$id": 23, + "$id": 14, "block": Object { "range": Array [ 0, @@ -22582,7 +22371,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 22, + "$id": 13, "block": Object { "range": Array [ 0, @@ -22592,7 +22381,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 21, + "$id": 12, "block": Object { "range": Array [ 0, @@ -22607,7 +22396,7 @@ Object { Object { "$id": 3, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -22626,30 +22415,32 @@ Object { Object { "$id": 4, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { - "name": "fn", + "name": "one", "range": Array [ - 49, - 51, + 57, + 60, ], "type": "Identifier", }, "kind": "r", - "resolved": null, + "resolved": Object { + "$ref": 2, + }, "writeExpr": undefined, }, Object { "$id": 5, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { "name": "one", "range": Array [ - 57, - 60, + 74, + 77, ], "type": "Identifier", }, @@ -22662,133 +22453,29 @@ Object { Object { "$id": 6, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { - "name": "two", + "name": "one", "range": Array [ - 62, - 65, + 91, + 94, ], "type": "Identifier", }, "kind": "r", - "resolved": null, + "resolved": Object { + "$ref": 2, + }, "writeExpr": undefined, }, Object { "$id": 7, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { - "name": "fn", - "range": Array [ - 66, - 68, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 8, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "one", - "range": Array [ - 74, - 77, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": Object { - "$ref": 2, - }, - "writeExpr": undefined, - }, - Object { - "$id": 9, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "fn", - "range": Array [ - 83, - 85, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 10, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "one", - "range": Array [ - 91, - 94, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": Object { - "$ref": 2, - }, - "writeExpr": undefined, - }, - Object { - "$id": 11, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 100, - 105, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 12, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "fn", - "range": Array [ - 106, - 108, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 13, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "one", + "name": "one", "range": Array [ 114, 117, @@ -22802,43 +22489,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 14, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 123, - 128, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 15, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "fn", - "range": Array [ - 130, - 132, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 16, + "$id": 8, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -22855,9 +22508,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 17, + "$id": 9, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -22874,9 +22527,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 18, + "$id": 10, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -22893,9 +22546,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 19, + "$id": 11, "from": Object { - "$ref": 21, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -22911,56 +22564,11 @@ Object { }, "writeExpr": undefined, }, - Object { - "$id": 20, - "from": Object { - "$ref": 21, - }, - "identifier": Object { - "name": "two", - "range": Array [ - 187, - 190, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - ], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 7, - }, - Object { - "$ref": 9, - }, - Object { - "$ref": 11, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 14, - }, - Object { - "$ref": 15, - }, - Object { - "$ref": 20, - }, ], + "throughReferences": Array [], "type": "function", "upperScope": Object { - "$ref": 22, + "$ref": 13, }, "variableMap": Object { "arguments": Object { @@ -22971,7 +22579,7 @@ Object { }, }, "variableScope": Object { - "$ref": 21, + "$ref": 12, }, "variables": Array [ Object { @@ -22982,7 +22590,7 @@ Object { "name": "arguments", "references": Array [], "scope": Object { - "$ref": 21, + "$ref": 12, }, }, Object { @@ -23025,32 +22633,32 @@ Object { "$ref": 3, }, Object { - "$ref": 5, + "$ref": 4, }, Object { - "$ref": 8, + "$ref": 5, }, Object { - "$ref": 10, + "$ref": 6, }, Object { - "$ref": 13, + "$ref": 7, }, Object { - "$ref": 16, + "$ref": 8, }, Object { - "$ref": 17, + "$ref": 9, }, Object { - "$ref": 18, + "$ref": 10, }, Object { - "$ref": 19, + "$ref": 11, }, ], "scope": Object { - "$ref": 21, + "$ref": 12, }, }, ], @@ -23059,38 +22667,10 @@ Object { "functionExpressionScope": false, "isStrict": true, "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 7, - }, - Object { - "$ref": 9, - }, - Object { - "$ref": 11, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 14, - }, - Object { - "$ref": 15, - }, - Object { - "$ref": 20, - }, - ], + "throughReferences": Array [], "type": "module", "upperScope": Object { - "$ref": 23, + "$ref": 14, }, "variableMap": Object { "processOptionalCall": Object { @@ -23098,7 +22678,7 @@ Object { }, }, "variableScope": Object { - "$ref": 22, + "$ref": 13, }, "variables": Array [ Object { @@ -23138,7 +22718,7 @@ Object { "name": "processOptionalCall", "references": Array [], "scope": Object { - "$ref": 22, + "$ref": 13, }, }, ], @@ -23147,40 +22727,12 @@ Object { "functionExpressionScope": false, "isStrict": false, "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 7, - }, - Object { - "$ref": 9, - }, - Object { - "$ref": 11, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 14, - }, - Object { - "$ref": 15, - }, - Object { - "$ref": 20, - }, - ], + "throughReferences": Array [], "type": "global", "upperScope": null, "variableMap": Object {}, "variableScope": Object { - "$ref": 23, + "$ref": 14, }, "variables": Array [], } @@ -23188,7 +22740,7 @@ Object { exports[`typescript fixtures/basics/optional-chain-call-with-parens.src 1`] = ` Object { - "$id": 20, + "$id": 14, "block": Object { "range": Array [ 0, @@ -23198,7 +22750,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 19, + "$id": 13, "block": Object { "range": Array [ 0, @@ -23208,7 +22760,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 18, + "$id": 12, "block": Object { "range": Array [ 0, @@ -23223,7 +22775,7 @@ Object { Object { "$id": 3, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23242,24 +22794,7 @@ Object { Object { "$id": 4, "from": Object { - "$ref": 18, - }, - "identifier": Object { - "name": "fn", - "range": Array [ - 56, - 58, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 5, - "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23276,26 +22811,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 6, - "from": Object { - "$ref": 18, - }, - "identifier": Object { - "name": "two", - "range": Array [ - 71, - 74, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 7, + "$id": 5, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23312,26 +22830,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 8, - "from": Object { - "$ref": 18, - }, - "identifier": Object { - "name": "fn", - "range": Array [ - 94, - 96, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 9, + "$id": 6, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23348,26 +22849,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 10, - "from": Object { - "$ref": 18, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 113, - 118, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 11, + "$id": 7, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23384,43 +22868,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 12, - "from": Object { - "$ref": 18, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 138, - 143, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 13, - "from": Object { - "$ref": 18, - }, - "identifier": Object { - "name": "fn", - "range": Array [ - 145, - 147, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 14, + "$id": 8, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23437,9 +22887,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 15, + "$id": 9, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23456,9 +22906,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 16, + "$id": 10, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23475,9 +22925,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 17, + "$id": 11, "from": Object { - "$ref": 18, + "$ref": 12, }, "identifier": Object { "name": "one", @@ -23494,29 +22944,10 @@ Object { "writeExpr": undefined, }, ], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 8, - }, - Object { - "$ref": 10, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 13, - }, - ], + "throughReferences": Array [], "type": "function", "upperScope": Object { - "$ref": 19, + "$ref": 13, }, "variableMap": Object { "arguments": Object { @@ -23527,7 +22958,7 @@ Object { }, }, "variableScope": Object { - "$ref": 18, + "$ref": 12, }, "variables": Array [ Object { @@ -23538,7 +22969,7 @@ Object { "name": "arguments", "references": Array [], "scope": Object { - "$ref": 18, + "$ref": 12, }, }, Object { @@ -23581,32 +23012,32 @@ Object { "$ref": 3, }, Object { - "$ref": 5, + "$ref": 4, }, Object { - "$ref": 7, + "$ref": 5, }, Object { - "$ref": 9, + "$ref": 6, }, Object { - "$ref": 11, + "$ref": 7, }, Object { - "$ref": 14, + "$ref": 8, }, Object { - "$ref": 15, + "$ref": 9, }, Object { - "$ref": 16, + "$ref": 10, }, Object { - "$ref": 17, + "$ref": 11, }, ], "scope": Object { - "$ref": 18, + "$ref": 12, }, }, ], @@ -23615,29 +23046,10 @@ Object { "functionExpressionScope": false, "isStrict": true, "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 8, - }, - Object { - "$ref": 10, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 13, - }, - ], + "throughReferences": Array [], "type": "module", "upperScope": Object { - "$ref": 20, + "$ref": 14, }, "variableMap": Object { "processOptionalCallParens": Object { @@ -23645,7 +23057,7 @@ Object { }, }, "variableScope": Object { - "$ref": 19, + "$ref": 13, }, "variables": Array [ Object { @@ -23676,49 +23088,30 @@ Object { Object { "name": "processOptionalCallParens", "range": Array [ - 9, - 34, - ], - "type": "Identifier", - }, - ], - "name": "processOptionalCallParens", - "references": Array [], - "scope": Object { - "$ref": 19, - }, - }, - ], - }, - ], - "functionExpressionScope": false, - "isStrict": false, - "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 8, - }, - Object { - "$ref": 10, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 13, + 9, + 34, + ], + "type": "Identifier", + }, + ], + "name": "processOptionalCallParens", + "references": Array [], + "scope": Object { + "$ref": 13, + }, + }, + ], }, ], + "functionExpressionScope": false, + "isStrict": false, + "references": Array [], + "throughReferences": Array [], "type": "global", "upperScope": null, "variableMap": Object {}, "variableScope": Object { - "$ref": 20, + "$ref": 14, }, "variables": Array [], } @@ -24352,7 +23745,7 @@ Object { exports[`typescript fixtures/basics/optional-chain-with-parens.src 1`] = ` Object { - "$id": 19, + "$id": 11, "block": Object { "range": Array [ 0, @@ -24362,7 +23755,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 18, + "$id": 10, "block": Object { "range": Array [ 0, @@ -24372,7 +23765,7 @@ Object { }, "childScopes": Array [ Object { - "$id": 17, + "$id": 9, "block": Object { "range": Array [ 0, @@ -24387,7 +23780,7 @@ Object { Object { "$id": 3, "from": Object { - "$ref": 17, + "$ref": 9, }, "identifier": Object { "name": "one", @@ -24406,24 +23799,7 @@ Object { Object { "$id": 4, "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "two", - "range": Array [ - 52, - 55, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 5, - "from": Object { - "$ref": 17, + "$ref": 9, }, "identifier": Object { "name": "one", @@ -24440,26 +23816,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 6, - "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "two", - "range": Array [ - 66, - 69, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 7, + "$id": 5, "from": Object { - "$ref": 17, + "$ref": 9, }, "identifier": Object { "name": "one", @@ -24476,26 +23835,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 8, - "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 90, - 95, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 9, + "$id": 6, "from": Object { - "$ref": 17, + "$ref": 9, }, "identifier": Object { "name": "one", @@ -24512,26 +23854,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 10, - "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 110, - 115, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 11, + "$id": 7, "from": Object { - "$ref": 17, + "$ref": 9, }, "identifier": Object { "name": "one", @@ -24548,43 +23873,9 @@ Object { "writeExpr": undefined, }, Object { - "$id": 12, - "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 135, - 140, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 13, - "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "four", - "range": Array [ - 142, - 146, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 14, + "$id": 8, "from": Object { - "$ref": 17, + "$ref": 9, }, "identifier": Object { "name": "one", @@ -24600,70 +23891,11 @@ Object { }, "writeExpr": undefined, }, - Object { - "$id": 15, - "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "three", - "range": Array [ - 161, - 166, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - Object { - "$id": 16, - "from": Object { - "$ref": 17, - }, - "identifier": Object { - "name": "four", - "range": Array [ - 168, - 172, - ], - "type": "Identifier", - }, - "kind": "r", - "resolved": null, - "writeExpr": undefined, - }, - ], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 8, - }, - Object { - "$ref": 10, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 13, - }, - Object { - "$ref": 15, - }, - Object { - "$ref": 16, - }, ], + "throughReferences": Array [], "type": "function", "upperScope": Object { - "$ref": 18, + "$ref": 10, }, "variableMap": Object { "arguments": Object { @@ -24674,7 +23906,7 @@ Object { }, }, "variableScope": Object { - "$ref": 17, + "$ref": 9, }, "variables": Array [ Object { @@ -24685,7 +23917,7 @@ Object { "name": "arguments", "references": Array [], "scope": Object { - "$ref": 17, + "$ref": 9, }, }, Object { @@ -24728,23 +23960,23 @@ Object { "$ref": 3, }, Object { - "$ref": 5, + "$ref": 4, }, Object { - "$ref": 7, + "$ref": 5, }, Object { - "$ref": 9, + "$ref": 6, }, Object { - "$ref": 11, + "$ref": 7, }, Object { - "$ref": 14, + "$ref": 8, }, ], "scope": Object { - "$ref": 17, + "$ref": 9, }, }, ], @@ -24753,35 +23985,10 @@ Object { "functionExpressionScope": false, "isStrict": true, "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 8, - }, - Object { - "$ref": 10, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 13, - }, - Object { - "$ref": 15, - }, - Object { - "$ref": 16, - }, - ], + "throughReferences": Array [], "type": "module", "upperScope": Object { - "$ref": 19, + "$ref": 11, }, "variableMap": Object { "processOptionalParens": Object { @@ -24789,7 +23996,7 @@ Object { }, }, "variableScope": Object { - "$ref": 18, + "$ref": 10, }, "variables": Array [ Object { @@ -24829,7 +24036,7 @@ Object { "name": "processOptionalParens", "references": Array [], "scope": Object { - "$ref": 18, + "$ref": 10, }, }, ], @@ -24838,37 +24045,12 @@ Object { "functionExpressionScope": false, "isStrict": false, "references": Array [], - "throughReferences": Array [ - Object { - "$ref": 4, - }, - Object { - "$ref": 6, - }, - Object { - "$ref": 8, - }, - Object { - "$ref": 10, - }, - Object { - "$ref": 12, - }, - Object { - "$ref": 13, - }, - Object { - "$ref": 15, - }, - Object { - "$ref": 16, - }, - ], + "throughReferences": Array [], "type": "global", "upperScope": null, "variableMap": Object {}, "variableScope": Object { - "$ref": 19, + "$ref": 11, }, "variables": Array [], } From 6209a43aa08f1769426141eda575aa3f7332ecd7 Mon Sep 17 00:00:00 2001 From: nizarius Date: Tue, 5 Nov 2019 00:50:10 +0300 Subject: [PATCH 2/9] test: added test for no-use-before-define --- .../eslint-rules/no-use-before-define.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts diff --git a/packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts new file mode 100644 index 000000000000..012437e6a00b --- /dev/null +++ b/packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts @@ -0,0 +1,20 @@ +import rule from 'eslint/lib/rules/no-use-before-define'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: {}, + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-use-before-define', rule, { + valid: [ + ` + const updatedAt = data?.updatedAt; + `, + ], + invalid: [], +}); From 27e84d9437af4f420983c30ee8ff3e5f822ff6be Mon Sep 17 00:00:00 2001 From: nizarius Date: Tue, 5 Nov 2019 01:05:44 +0300 Subject: [PATCH 3/9] style: added OptionalMemberExpression description --- packages/parser/src/analyze-scope.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/parser/src/analyze-scope.ts b/packages/parser/src/analyze-scope.ts index 569b6a03f9c7..ca2136beb2e1 100644 --- a/packages/parser/src/analyze-scope.ts +++ b/packages/parser/src/analyze-scope.ts @@ -344,7 +344,11 @@ class Referencer extends TSESLintScope.Referencer { node.arguments.forEach(this.visit, this); } - OptionalMemberExpression(node: TSESTree.MemberExpression): void { + /** + * Visit optional member expression. + * @param node The OptionalMemberExpression node to visit. + */ + OptionalMemberExpression(node: TSESTree.OptionalMemberExpression): void { this.visit(node.object); if (node.computed) { this.visit(node.property); From f51b4484b6d87a0ea6829ac8d83d8b30bfa75abe Mon Sep 17 00:00:00 2001 From: nizarius Date: Sat, 9 Nov 2019 16:52:37 +0300 Subject: [PATCH 4/9] feat: added tests and optional chaining expression to referencer --- .../no-restricted-globals.test.ts | 62 ++++++++++++++++- .../tests/eslint-rules/no-undef.test.ts | 36 +++++++++- .../eslint-rules/no-use-before-define.test.ts | 69 ++++++++++++++++++- packages/parser/src/analyze-scope.ts | 12 ++++ 4 files changed, 175 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts index 1820f78754f9..0cb6ec5d2919 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-restricted-globals.test.ts @@ -30,11 +30,71 @@ type Handler = (event: string) => any `, options: ['event'], }, + { + code: ` + const a = foo?.bar?.name + `, + }, { code: ` const a = foo?.bar?.name ?? "foobar" `, }, + { + code: ` + const a = foo()?.bar; + `, + }, + { + code: ` + const a = foo()?.bar ?? true; + `, + }, + ], + invalid: [ + { + code: ` +function onClick() { + console.log(event); +} + +fdescribe("foo", function() { +}); + `, + options: ['event'], + errors: [ + { + message: "Unexpected use of 'event'.", + // the base rule doesn't use messageId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + }, + { + code: ` +confirm("TEST"); + `, + options: ['confirm'], + errors: [ + { + message: "Unexpected use of 'confirm'.", + // the base rule doesn't use messageId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + }, + { + code: ` +var a = confirm("TEST")?.a; + `, + options: ['confirm'], + errors: [ + { + message: "Unexpected use of 'confirm'.", + // the base rule doesn't use messageId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + }, ], - invalid: [], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts index 1c207665ad60..38d58b482fd3 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-undef.test.ts @@ -86,5 +86,39 @@ function eachr(subject: Map): typeof subject; a?.b(); `, ], - invalid: [], + invalid: [ + { + code: 'a = 5;', + errors: [ + { + messageId: 'undef', + data: { + name: 'a', + }, + }, + ], + }, + { + code: 'a?.b = 5;', + errors: [ + { + messageId: 'undef', + data: { + name: 'a', + }, + }, + ], + }, + { + code: 'a()?.b = 5;', + errors: [ + { + messageId: 'undef', + data: { + name: 'a', + }, + }, + ], + }, + ], }); diff --git a/packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts b/packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts index 012437e6a00b..4571cd04afbb 100644 --- a/packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts +++ b/packages/eslint-plugin/tests/eslint-rules/no-use-before-define.test.ts @@ -13,8 +13,73 @@ const ruleTester = new RuleTester({ ruleTester.run('no-use-before-define', rule, { valid: [ ` - const updatedAt = data?.updatedAt; +const updatedAt = data?.updatedAt; `, + ` +function f() { + return function t() {}; +} +f()?.(); + `, + ` +var a = { b: 5 }; +alert(a?.b); + `, + ], + invalid: [ + { + code: ` +f(); +function f() {} + `, + errors: [ + { + message: "'f' was used before it was defined.", + // the base rule doesn't use messageId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + }, + { + code: ` +alert(a); +var a = 10; + `, + errors: [ + { + message: "'a' was used before it was defined.", + // the base rule doesn't use messageId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + }, + { + code: ` +f()?.(); +function f() { + return function t() {}; +} + `, + errors: [ + { + message: "'f' was used before it was defined.", + // the base rule doesn't use messageId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + }, + { + code: ` +alert(a?.b); +var a = { b: 5 }; + `, + errors: [ + { + message: "'a' was used before it was defined.", + // the base rule doesn't use messageId + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + }, ], - invalid: [], }); diff --git a/packages/parser/src/analyze-scope.ts b/packages/parser/src/analyze-scope.ts index ca2136beb2e1..27e90a46565d 100644 --- a/packages/parser/src/analyze-scope.ts +++ b/packages/parser/src/analyze-scope.ts @@ -355,6 +355,18 @@ class Referencer extends TSESLintScope.Referencer { } } + /** + * Visit optional call expression. + * @param node The OptionalMemberExpression node to visit. + */ + OptionalCallExpression(node: TSESTree.OptionalCallExpression): void { + this.visitTypeParameters(node); + + this.visit(node.callee); + + node.arguments.forEach(this.visit, this); + } + /** * Define the variable of this function declaration only once. * Because to avoid confusion of `no-redeclare` rule by overloading. From f5eedb44de6f0a00a407250c4f6720bbb65444d3 Mon Sep 17 00:00:00 2001 From: nizarius Date: Tue, 5 Nov 2019 22:24:32 +0300 Subject: [PATCH 5/9] fix: no-unused-expressions-rule for optional call expression --- packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-unused-expressions.ts | 129 ++++++++++++++++++ .../tests/rules/no-unused-expressions.test.ts | 92 +++++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 20 +++ packages/parser/src/analyze-scope.ts | 12 ++ 5 files changed, 255 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/no-unused-expressions.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 5302abd05de0..4aa8beea1f63 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -39,6 +39,7 @@ import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; import noUnusedVars from './no-unused-vars'; +import noUnusedExpressions from './no-unused-expressions'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; @@ -106,6 +107,7 @@ export default { 'no-unnecessary-type-arguments': useDefaultTypeParameter, 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-unused-vars': noUnusedVars, + 'no-unused-expressions': noUnusedExpressions, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts new file mode 100644 index 000000000000..3c7cd91795b0 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -0,0 +1,129 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/no-unused-expressions'; +import * as util from '../util'; + +export default util.createRule({ + name: 'no-unused-expressions', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow unused expressions', + category: 'Best Practices', + recommended: false, + }, + schema: baseRule.meta.schema, + messages: { + expected: + 'Expected an assignment or function call and instead saw an expression.', + }, + }, + defaultOptions: [], + create(context) { + const config: Record = context.options[0] || {}, + allowShortCircuit = config.allowShortCircuit || false, + allowTernary = config.allowTernary || false, + allowTaggedTemplates = config.allowTaggedTemplates || false; + + /** + * @param node - any node + * @returns whether the given node structurally represents a directive + */ + function looksLikeDirective(node: TSESTree.Node): boolean { + return ( + node.type === 'ExpressionStatement' && + node.expression.type === 'Literal' && + typeof node.expression.value === 'string' + ); + } + + /** + * @param predicate - ([a] -> Boolean) the function used to make the determination + * @param list - the input list + * @returns the leading sequence of members in the given list that pass the given predicate + */ + function takeWhile(predicate: (a: T) => boolean, list: T[]): T[] { + for (let i = 0; i < list.length; ++i) { + if (!predicate(list[i])) { + return list.slice(0, i); + } + } + return list.slice(); + } + + /** + * @param node - a Program or BlockStatement node + * @returns the leading sequence of directive nodes in the given node's body + */ + function directives( + node: TSESTree.Program | TSESTree.BlockStatement, + ): TSESTree.Node[] { + return takeWhile(looksLikeDirective, node.body); + } + + /** + * @param node - any node + * @param ancestors - the given node's ancestors + * @returns whether the given node is considered a directive in its current position + */ + function isDirective( + node: TSESTree.Node, + ancestors: TSESTree.Node[], + ): boolean { + const parent = ancestors[ancestors.length - 1], + grandparent = ancestors[ancestors.length - 2]; + + return ( + (parent.type === 'Program' || + (parent.type === 'BlockStatement' && + grandparent.type.includes('Function'))) && + directives(parent).includes(node) + ); + } + + /** + * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. + * @param node - any node + * @returns whether the given node is a valid expression + */ + function isValidExpression(node: TSESTree.Node): boolean { + if (allowTernary) { + // Recursive check for ternary and logical expressions + if (node.type === 'ConditionalExpression') { + return ( + isValidExpression(node.consequent) && + isValidExpression(node.alternate) + ); + } + } + + if (allowShortCircuit) { + if (node.type === 'LogicalExpression') { + return isValidExpression(node.right); + } + } + + if (allowTaggedTemplates && node.type === 'TaggedTemplateExpression') { + return true; + } + + return ( + /^(?:Assignment|(Optional)?Call|New|Update|Yield|Await)Expression$/u.test( + node.type, + ) || + (node.type === 'UnaryExpression' && + ['delete', 'void'].includes(node.operator)) + ); + } + + return { + ExpressionStatement(node): void { + if ( + !isValidExpression(node.expression) && + !isDirective(node, context.getAncestors()) + ) { + context.report({ node, messageId: 'expected' }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts new file mode 100644 index 000000000000..0cca9cd253ba --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts @@ -0,0 +1,92 @@ +import rule from '../../src/rules/no-unused-expressions'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + ecmaFeatures: {}, + }, + parser: '@typescript-eslint/parser', +}); + +// the base rule doesn't have messageIds +function error( + messages: { line: number; column: number }[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any[] { + return messages.map(message => ({ + ...message, + message: + 'Expected an assignment or function call and instead saw an expression.', + })); +} + +ruleTester.run('no-unused-expressions', rule, { + valid: [ + ` + test.age?.toLocaleString(); + `, + ], + invalid: [ + { + code: ` +if(0) 0 + `, + errors: error([ + { + line: 2, + column: 7, + }, + ]), + }, + { + code: ` +f(0), {} + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a, b() + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a() && function namedFunctionInExpressionContext () {f();} + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + // ` + // {0} + // `, + // ` + // f(0), {} + // `, + // ` + // a && b() + // `, + // ` + // a, b() + // `, + // ` + // c = a, b; + // ` + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 120b11433cb9..aa8c7ec50d95 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -304,6 +304,26 @@ declare module 'eslint/lib/rules/no-unused-vars' { export = rule; } +declare module 'eslint/lib/rules/no-unused-expressions' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'expected', + ( + | 'all' + | 'local' + | { + allowShortCircuit?: boolean; + allowTernary?: boolean; + allowTaggedTemplates?: boolean; + })[], + { + ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/no-use-before-define' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; diff --git a/packages/parser/src/analyze-scope.ts b/packages/parser/src/analyze-scope.ts index 27e90a46565d..1759ba02970b 100644 --- a/packages/parser/src/analyze-scope.ts +++ b/packages/parser/src/analyze-scope.ts @@ -344,6 +344,18 @@ class Referencer extends TSESLintScope.Referencer { node.arguments.forEach(this.visit, this); } + /** + * Visit optional call expression. + * @param node The OptionalMemberExpression node to visit. + */ + OptionalCallExpression(node: TSESTree.OptionalCallExpression): void { + this.visitTypeParameters(node); + + this.visit(node.callee); + + node.arguments.forEach(this.visit, this); + } + /** * Visit optional member expression. * @param node The OptionalMemberExpression node to visit. From d74017576ce98ca10adb2cb087d4ef44eeaaa22a Mon Sep 17 00:00:00 2001 From: nizarius Date: Fri, 8 Nov 2019 17:53:10 +0300 Subject: [PATCH 6/9] fix: make no-unused-expressions rule based on eslint rule --- .../src/rules/no-unused-expressions.ts | 106 +----------------- .../tests/rules/no-unused-expressions.test.ts | 15 --- .../eslint-plugin/typings/eslint-rules.d.ts | 2 +- 3 files changed, 6 insertions(+), 117 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-expressions.ts b/packages/eslint-plugin/src/rules/no-unused-expressions.ts index 3c7cd91795b0..f7d86f50c1c7 100644 --- a/packages/eslint-plugin/src/rules/no-unused-expressions.ts +++ b/packages/eslint-plugin/src/rules/no-unused-expressions.ts @@ -1,4 +1,4 @@ -import { TSESTree } from '@typescript-eslint/experimental-utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; import baseRule from 'eslint/lib/rules/no-unused-expressions'; import * as util from '../util'; @@ -19,110 +19,14 @@ export default util.createRule({ }, defaultOptions: [], create(context) { - const config: Record = context.options[0] || {}, - allowShortCircuit = config.allowShortCircuit || false, - allowTernary = config.allowTernary || false, - allowTaggedTemplates = config.allowTaggedTemplates || false; - - /** - * @param node - any node - * @returns whether the given node structurally represents a directive - */ - function looksLikeDirective(node: TSESTree.Node): boolean { - return ( - node.type === 'ExpressionStatement' && - node.expression.type === 'Literal' && - typeof node.expression.value === 'string' - ); - } - - /** - * @param predicate - ([a] -> Boolean) the function used to make the determination - * @param list - the input list - * @returns the leading sequence of members in the given list that pass the given predicate - */ - function takeWhile(predicate: (a: T) => boolean, list: T[]): T[] { - for (let i = 0; i < list.length; ++i) { - if (!predicate(list[i])) { - return list.slice(0, i); - } - } - return list.slice(); - } - - /** - * @param node - a Program or BlockStatement node - * @returns the leading sequence of directive nodes in the given node's body - */ - function directives( - node: TSESTree.Program | TSESTree.BlockStatement, - ): TSESTree.Node[] { - return takeWhile(looksLikeDirective, node.body); - } - - /** - * @param node - any node - * @param ancestors - the given node's ancestors - * @returns whether the given node is considered a directive in its current position - */ - function isDirective( - node: TSESTree.Node, - ancestors: TSESTree.Node[], - ): boolean { - const parent = ancestors[ancestors.length - 1], - grandparent = ancestors[ancestors.length - 2]; - - return ( - (parent.type === 'Program' || - (parent.type === 'BlockStatement' && - grandparent.type.includes('Function'))) && - directives(parent).includes(node) - ); - } - - /** - * Determines whether or not a given node is a valid expression. Recurses on short circuit eval and ternary nodes if enabled by flags. - * @param node - any node - * @returns whether the given node is a valid expression - */ - function isValidExpression(node: TSESTree.Node): boolean { - if (allowTernary) { - // Recursive check for ternary and logical expressions - if (node.type === 'ConditionalExpression') { - return ( - isValidExpression(node.consequent) && - isValidExpression(node.alternate) - ); - } - } - - if (allowShortCircuit) { - if (node.type === 'LogicalExpression') { - return isValidExpression(node.right); - } - } - - if (allowTaggedTemplates && node.type === 'TaggedTemplateExpression') { - return true; - } - - return ( - /^(?:Assignment|(Optional)?Call|New|Update|Yield|Await)Expression$/u.test( - node.type, - ) || - (node.type === 'UnaryExpression' && - ['delete', 'void'].includes(node.operator)) - ); - } + const rules = baseRule.create(context); return { ExpressionStatement(node): void { - if ( - !isValidExpression(node.expression) && - !isDirective(node, context.getAncestors()) - ) { - context.report({ node, messageId: 'expected' }); + if (node.expression.type === AST_NODE_TYPES.OptionalCallExpression) { + return; } + rules.ExpressionStatement(node); }, }; }, diff --git a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts index 0cca9cd253ba..676739f92417 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts @@ -73,20 +73,5 @@ a() && function namedFunctionInExpressionContext () {f();} }, ]), }, - // ` - // {0} - // `, - // ` - // f(0), {} - // `, - // ` - // a && b() - // `, - // ` - // a, b() - // `, - // ` - // c = a, b; - // ` ], }); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index aa8c7ec50d95..c21b235be448 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -318,7 +318,7 @@ declare module 'eslint/lib/rules/no-unused-expressions' { allowTaggedTemplates?: boolean; })[], { - ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void; + ExpressionStatement(node: TSESTree.ExpressionStatement): void; } >; export = rule; From daa466828a8005bd12fc704a60a176a667436f14 Mon Sep 17 00:00:00 2001 From: nizarius Date: Sat, 9 Nov 2019 17:21:25 +0300 Subject: [PATCH 7/9] docs: update tests and docs --- .../docs/rules/no-unused-expressions.md | 25 +++++ .../tests/rules/no-unused-expressions.test.ts | 103 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-unused-expressions.md diff --git a/packages/eslint-plugin/docs/rules/no-unused-expressions.md b/packages/eslint-plugin/docs/rules/no-unused-expressions.md new file mode 100644 index 000000000000..7da998ab2c6c --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unused-expressions.md @@ -0,0 +1,25 @@ +# require or disallow semicolons instead of ASI (semi) + +This rule aims to eliminate unused expressions which have no effect on the state of the program. + +## Rule Details + +This rule extends the base [eslint/no-unused-expressions](https://eslint.org/docs/rules/no-unused-expressions) rule. +It supports all options and features of the base rule. +This version adds support for numerous typescript features. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "no-unused-expressions": "off", + "@typescript-eslint/no-unused-expressions": ["error"] +} +``` + +## Options + +See [eslint/no-unused-expressions options](https://eslint.org/docs/rules/no-unused-expressions#options). + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-unused-expressions.md) diff --git a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts index 676739f92417..46653b30c663 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-expressions.test.ts @@ -27,6 +27,21 @@ ruleTester.run('no-unused-expressions', rule, { ` test.age?.toLocaleString(); `, + ` + let a = (a?.b).c; + `, + ` + let b = a?.['b']; + `, + ` + let c = one[2]?.[3][4]; + `, + ` + one[2]?.[3][4]?.(); + `, + ` + a?.['b']?.c(); + `, ], invalid: [ { @@ -73,5 +88,93 @@ a() && function namedFunctionInExpressionContext () {f();} }, ]), }, + { + code: ` +a?.b + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +(a?.b).c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a?.['b'] + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +(a?.['b']).c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +a?.b()?.c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +(a?.b()).c + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +one[2]?.[3][4]; + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, + { + code: ` +one.two?.three.four; + `, + errors: error([ + { + line: 2, + column: 1, + }, + ]), + }, ], }); From c876e7e135fb2c7af9bcd11dd1763ca359a9ca37 Mon Sep 17 00:00:00 2001 From: nizarius Date: Sat, 9 Nov 2019 17:25:07 +0300 Subject: [PATCH 8/9] fix: removed dublicating code --- packages/parser/src/analyze-scope.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/parser/src/analyze-scope.ts b/packages/parser/src/analyze-scope.ts index 1759ba02970b..27e90a46565d 100644 --- a/packages/parser/src/analyze-scope.ts +++ b/packages/parser/src/analyze-scope.ts @@ -344,18 +344,6 @@ class Referencer extends TSESLintScope.Referencer { node.arguments.forEach(this.visit, this); } - /** - * Visit optional call expression. - * @param node The OptionalMemberExpression node to visit. - */ - OptionalCallExpression(node: TSESTree.OptionalCallExpression): void { - this.visitTypeParameters(node); - - this.visit(node.callee); - - node.arguments.forEach(this.visit, this); - } - /** * Visit optional member expression. * @param node The OptionalMemberExpression node to visit. From b05c365ad12e998e66284aa9c27424674ecb3d01 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 11 Nov 2019 09:23:24 -0800 Subject: [PATCH 9/9] docs: add to readme --- packages/eslint-plugin/README.md | 5 +++-- packages/eslint-plugin/src/configs/all.json | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 6d874a9c2b1c..b616245454e6 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -145,7 +145,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/await-thenable`](./docs/rules/await-thenable.md) | Disallows awaiting a value that is not a Thenable | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/ban-ts-ignore`](./docs/rules/ban-ts-ignore.md) | Bans “// @ts-ignore” comments from being used | :heavy_check_mark: | | | | [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | +| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | | [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | | | | [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions. | :heavy_check_mark: | | | @@ -181,6 +181,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@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: | +| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | | [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | | [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | | [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | @@ -193,7 +194,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/prefer-regexp-exec`](./docs/rules/prefer-regexp-exec.md) | Prefer RegExp#exec() over String#match() if no global flag is provided | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | :heavy_check_mark: | :wrench: | :thought_balloon: | | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: | -| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | +| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | | [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Enforce giving `compare` argument to `Array#sort` | | | :thought_balloon: | | [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 377f4b58f0e4..395c1af592ed 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -50,6 +50,7 @@ "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-unused-expressions": "error", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error", "no-use-before-define": "off",