From 4f025d35d91c322d6417f5f7f686299f7416078b Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 14 Jun 2020 15:57:36 +0900 Subject: [PATCH 1/5] Supports for Optional chaining - Updated ReferenceTracker to parse optional chaining. - Updated getStaticValue to parse optional chaining. - Fixed hasSideEffect infinite loop. It will continue to traverse the `type` string on nodes that do not exist in `"eslint-visitor-keys"`, resulting in an infinite loop. --- src/get-static-value.js | 66 +++++++++++++++++++++++++-------------- src/has-side-effect.js | 17 ++++++++-- src/reference-tracker.js | 2 ++ test/get-static-value.js | 62 +++++++++++++++++++++++++++++++++++- test/has-side-effect.js | 37 +++++++++++++++++++++- test/reference-tracker.js | 65 +++++++++++++++++++++++++++++++++++--- 6 files changed, 218 insertions(+), 31 deletions(-) diff --git a/src/get-static-value.js b/src/get-static-value.js index 173c01a..bce6598 100644 --- a/src/get-static-value.js +++ b/src/get-static-value.js @@ -251,23 +251,34 @@ const operations = Object.freeze({ if (args != null) { if (calleeNode.type === "MemberExpression") { const object = getStaticValueR(calleeNode.object, initialScope) - const property = calleeNode.computed - ? getStaticValueR(calleeNode.property, initialScope) - : { value: calleeNode.property.name } - - if (object != null && property != null) { - const receiver = object.value - const methodName = property.value - if (callAllowed.has(receiver[methodName])) { - return { value: receiver[methodName](...args) } + if (object != null) { + if ( + object.value == null && + (object.optional || node.optional) + ) { + return { value: undefined, optional: true } } - if (callPassThrough.has(receiver[methodName])) { - return { value: args[0] } + const property = calleeNode.computed + ? getStaticValueR(calleeNode.property, initialScope) + : { value: calleeNode.property.name } + + if (property != null) { + const receiver = object.value + const methodName = property.value + if (callAllowed.has(receiver[methodName])) { + return { value: receiver[methodName](...args) } + } + if (callPassThrough.has(receiver[methodName])) { + return { value: args[0] } + } } } } else { const callee = getStaticValueR(calleeNode, initialScope) if (callee != null) { + if (callee.value == null && node.optional) { + return { value: undefined, optional: true } + } const func = callee.value if (callAllowed.has(func)) { return { value: func(...args) } @@ -356,16 +367,25 @@ const operations = Object.freeze({ MemberExpression(node, initialScope) { const object = getStaticValueR(node.object, initialScope) - const property = node.computed - ? getStaticValueR(node.property, initialScope) - : { value: node.property.name } - - if ( - object != null && - property != null && - !isGetter(object.value, property.value) - ) { - return { value: object.value[property.value] } + if (object != null) { + if (object.value == null && (object.optional || node.optional)) { + return { value: undefined, optional: true } + } + const property = node.computed + ? getStaticValueR(node.property, initialScope) + : { value: node.property.name } + + if (property != null && !isGetter(object.value, property.value)) { + return { value: object.value[property.value] } + } + } + return null + }, + + ChainExpression(node, initialScope) { + const expression = getStaticValueR(node.expression, initialScope) + if (expression != null) { + return { value: expression.value } } return null }, @@ -493,7 +513,7 @@ const operations = Object.freeze({ * Get the value of a given node if it's a static value. * @param {Node} node The node to get. * @param {Scope|undefined} initialScope The scope to start finding variable. - * @returns {{value:any}|null} The static value of the node, or `null`. + * @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`. */ function getStaticValueR(node, initialScope) { if (node != null && Object.hasOwnProperty.call(operations, node.type)) { @@ -506,7 +526,7 @@ function getStaticValueR(node, initialScope) { * Get the value of a given node if it's a static value. * @param {Node} node The node to get. * @param {Scope} [initialScope] The scope to start finding variable. Optional. If this scope was given, this tries to resolve identifier references which are in the given node as much as possible. - * @returns {{value:any}|null} The static value of the node, or `null`. + * @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`. */ export function getStaticValue(node, initialScope = null) { try { diff --git a/src/has-side-effect.js b/src/has-side-effect.js index 5ae28ee..d5a248c 100644 --- a/src/has-side-effect.js +++ b/src/has-side-effect.js @@ -23,6 +23,16 @@ const typeConversionBinaryOps = Object.freeze( ]) ) const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"])) + +/** + * Check whether the given value is an ASTNode or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if the value is an ASTNode. + */ +function isNode(x) { + return x !== null && typeof x === "object" && typeof x.type === "string" +} + const visitor = Object.freeze( Object.assign(Object.create(null), { $visit(node, options, visitorKeys) { @@ -44,13 +54,16 @@ const visitor = Object.freeze( if (Array.isArray(value)) { for (const element of value) { if ( - element && + isNode(element) && this.$visit(element, options, visitorKeys) ) { return true } } - } else if (value && this.$visit(value, options, visitorKeys)) { + } else if ( + isNode(value) && + this.$visit(value, options, visitorKeys) + ) { return true } } diff --git a/src/reference-tracker.js b/src/reference-tracker.js index 548388b..f88e0e3 100644 --- a/src/reference-tracker.js +++ b/src/reference-tracker.js @@ -41,6 +41,8 @@ function isPassThrough(node) { return true case "SequenceExpression": return parent.expressions[parent.expressions.length - 1] === node + case "ChainExpression": + return true default: return false diff --git a/test/get-static-value.js b/test/get-static-value.js index a9e3c29..f459360 100644 --- a/test/get-static-value.js +++ b/test/get-static-value.js @@ -143,6 +143,66 @@ const aMap = Object.freeze({ code: "RegExp.$1", expected: null, }, + { + code: "const a = { b: { c: 42 } }; a?.b?.c", + expected: { value: 42 }, + }, + { + code: "const a = { b: { c: 42 } }; a?.b?.['c']", + expected: { value: 42 }, + }, + { + code: "const a = { b: null }; a?.b?.c", + expected: { value: undefined }, + }, + { + code: "const a = { b: undefined }; a?.b?.c", + expected: { value: undefined }, + }, + { + code: "const a = { b: null }; a?.b?.['c']", + expected: { value: undefined }, + }, + { + code: "const a = null; a?.b?.c", + expected: { value: undefined }, + }, + { + code: "const a = null; a?.b.c", + expected: { value: undefined }, + }, + { + code: "const a = void 0; a?.b.c", + expected: { value: undefined }, + }, + { + code: "const a = { b: { c: 42 } }; (a?.b).c", + expected: { value: 42 }, + }, + { + code: "const a = null; (a?.b).c", + expected: null, + }, + { + code: "const a = { b: null }; (a?.b).c", + expected: null, + }, + { + code: "const a = { b: { c: String } }; a?.b?.c?.(42)", + expected: { value: "42" }, + }, + { + code: "const a = null; a?.b?.c?.(42)", + expected: { value: undefined }, + }, + { + code: "const a = { b: { c: String } }; a?.b.c(42)", + expected: { value: "42" }, + }, + { + code: "const a = null; a?.b.c(42)", + expected: { value: undefined }, + }, ]) { it(`should return ${JSON.stringify(expected)} from ${code}`, () => { const linter = new eslint.Linter() @@ -158,7 +218,7 @@ const aMap = Object.freeze({ })) linter.verify(code, { env: { es6: true }, - parserOptions: { ecmaVersion: 2018 }, + parserOptions: { ecmaVersion: 2020 }, rules: { test: "error" }, }) diff --git a/test/has-side-effect.js b/test/has-side-effect.js index d84c300..2884f6b 100644 --- a/test/has-side-effect.js +++ b/test/has-side-effect.js @@ -46,11 +46,21 @@ describe("The 'hasSideEffect' function", () => { options: undefined, expected: true, }, + { + code: "f?.()", + options: undefined, + expected: true, + }, { code: "a + f()", options: undefined, expected: true, }, + { + code: "a + f?.()", + options: undefined, + expected: true, + }, { code: "obj.a", options: undefined, @@ -61,6 +71,16 @@ describe("The 'hasSideEffect' function", () => { options: { considerGetters: true }, expected: true, }, + { + code: "obj?.a", + options: undefined, + expected: false, + }, + { + code: "obj?.a", + options: { considerGetters: true }, + expected: true, + }, { code: "obj[a]", options: undefined, @@ -76,6 +96,21 @@ describe("The 'hasSideEffect' function", () => { options: { considerImplicitTypeConversion: true }, expected: true, }, + { + code: "obj?.[a]", + options: undefined, + expected: false, + }, + { + code: "obj?.[a]", + options: { considerGetters: true }, + expected: true, + }, + { + code: "obj?.[a]", + options: { considerImplicitTypeConversion: true }, + expected: true, + }, { code: "obj[0]", options: { considerImplicitTypeConversion: true }, @@ -242,7 +277,7 @@ describe("The 'hasSideEffect' function", () => { })) const messages = linter.verify(code, { env: { es6: true }, - parserOptions: { ecmaVersion: 2018 }, + parserOptions: { ecmaVersion: 2020 }, rules: { test: "error" }, }) diff --git a/test/reference-tracker.js b/test/reference-tracker.js index fe95ee0..8158be4 100644 --- a/test/reference-tracker.js +++ b/test/reference-tracker.js @@ -3,7 +3,7 @@ import eslint from "eslint" import { CALL, CONSTRUCT, ESM, READ, ReferenceTracker } from "../src/" const config = { - parserOptions: { ecmaVersion: 2018, sourceType: "module" }, + parserOptions: { ecmaVersion: 2020, sourceType: "module" }, globals: { Reflect: false }, rules: { test: "error" }, } @@ -504,7 +504,14 @@ describe("The 'ReferenceTracker' class:", () => { actual = Array.from( tracker.iterateGlobalReferences(traceMap) ).map(x => - Object.assign(x, { node: { type: x.node.type } }) + Object.assign(x, { + node: Object.assign( + { type: x.node.type }, + x.node.optional + ? { optional: x.node.optional } + : {} + ), + }) ) }, })) @@ -526,6 +533,11 @@ describe("The 'ReferenceTracker' class:", () => { "abc();", "new abc();", "abc.xyz;", + "abc?.xyz;", + "abc?.();", + "abc?.xyz?.();", + "(abc.def).ghi;", + "(abc?.def)?.ghi;", ].join("\n"), traceMap: { abc: { @@ -533,6 +545,7 @@ describe("The 'ReferenceTracker' class:", () => { [CALL]: 2, [CONSTRUCT]: 3, xyz: { [READ]: 4 }, + def: { ghi: { [READ]: 5 } }, }, }, expected: [ @@ -560,6 +573,36 @@ describe("The 'ReferenceTracker' class:", () => { type: READ, info: 4, }, + { + node: { type: "MemberExpression", optional: true }, + path: ["abc", "xyz"], + type: READ, + info: 4, + }, + { + node: { type: "CallExpression", optional: true }, + path: ["abc"], + type: CALL, + info: 2, + }, + { + node: { type: "MemberExpression", optional: true }, + path: ["abc", "xyz"], + type: READ, + info: 4, + }, + { + node: { type: "MemberExpression" }, + path: ["abc", "def", "ghi"], + type: READ, + info: 5, + }, + { + node: { type: "MemberExpression", optional: true }, + path: ["abc", "def", "ghi"], + type: READ, + info: 5, + }, ], }, { @@ -613,7 +656,14 @@ describe("The 'ReferenceTracker' class:", () => { actual = Array.from( tracker.iterateCjsReferences(traceMap) ).map(x => - Object.assign(x, { node: { type: x.node.type } }) + Object.assign(x, { + node: Object.assign( + { type: x.node.type }, + x.node.optional + ? { optional: x.node.optional } + : {} + ), + }) ) }, })) @@ -888,7 +938,14 @@ describe("The 'ReferenceTracker' class:", () => { actual = Array.from( tracker.iterateEsmReferences(traceMap) ).map(x => - Object.assign(x, { node: { type: x.node.type } }) + Object.assign(x, { + node: Object.assign( + { type: x.node.type }, + x.node.optional + ? { optional: x.node.optional } + : {} + ), + }) ) }, })) From a7d5167ddfce075cbf296010e82785e5d57607c1 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 14 Jun 2020 16:14:15 +0900 Subject: [PATCH 2/5] Update testcase for eslint5 --- package.json | 1 + test/get-static-value.js | 131 ++++++++++++++++++++------------------ test/has-side-effect.js | 93 ++++++++++++++++----------- test/reference-tracker.js | 98 +++++++++++++++++----------- 4 files changed, 190 insertions(+), 133 deletions(-) diff --git a/package.json b/package.json index a119886..f713175 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "rimraf": "^3.0.0", "rollup": "^1.25.0", "rollup-plugin-sourcemaps": "^0.4.2", + "semver": "^7.3.2", "vuepress": "^1.2.0", "warun": "^1.0.0" }, diff --git a/test/get-static-value.js b/test/get-static-value.js index f459360..f593273 100644 --- a/test/get-static-value.js +++ b/test/get-static-value.js @@ -1,5 +1,6 @@ import assert from "assert" import eslint from "eslint" +import semver from "semver" import { getStaticValue } from "../src/" describe("The 'getStaticValue' function", () => { @@ -143,66 +144,70 @@ const aMap = Object.freeze({ code: "RegExp.$1", expected: null, }, - { - code: "const a = { b: { c: 42 } }; a?.b?.c", - expected: { value: 42 }, - }, - { - code: "const a = { b: { c: 42 } }; a?.b?.['c']", - expected: { value: 42 }, - }, - { - code: "const a = { b: null }; a?.b?.c", - expected: { value: undefined }, - }, - { - code: "const a = { b: undefined }; a?.b?.c", - expected: { value: undefined }, - }, - { - code: "const a = { b: null }; a?.b?.['c']", - expected: { value: undefined }, - }, - { - code: "const a = null; a?.b?.c", - expected: { value: undefined }, - }, - { - code: "const a = null; a?.b.c", - expected: { value: undefined }, - }, - { - code: "const a = void 0; a?.b.c", - expected: { value: undefined }, - }, - { - code: "const a = { b: { c: 42 } }; (a?.b).c", - expected: { value: 42 }, - }, - { - code: "const a = null; (a?.b).c", - expected: null, - }, - { - code: "const a = { b: null }; (a?.b).c", - expected: null, - }, - { - code: "const a = { b: { c: String } }; a?.b?.c?.(42)", - expected: { value: "42" }, - }, - { - code: "const a = null; a?.b?.c?.(42)", - expected: { value: undefined }, - }, - { - code: "const a = { b: { c: String } }; a?.b.c(42)", - expected: { value: "42" }, - }, - { - code: "const a = null; a?.b.c(42)", - expected: { value: undefined }, - }, + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + { + code: "const a = { b: { c: 42 } }; a?.b?.c", + expected: { value: 42 }, + }, + { + code: "const a = { b: { c: 42 } }; a?.b?.['c']", + expected: { value: 42 }, + }, + { + code: "const a = { b: null }; a?.b?.c", + expected: { value: undefined }, + }, + { + code: "const a = { b: undefined }; a?.b?.c", + expected: { value: undefined }, + }, + { + code: "const a = { b: null }; a?.b?.['c']", + expected: { value: undefined }, + }, + { + code: "const a = null; a?.b?.c", + expected: { value: undefined }, + }, + { + code: "const a = null; a?.b.c", + expected: { value: undefined }, + }, + { + code: "const a = void 0; a?.b.c", + expected: { value: undefined }, + }, + { + code: "const a = { b: { c: 42 } }; (a?.b).c", + expected: { value: 42 }, + }, + { + code: "const a = null; (a?.b).c", + expected: null, + }, + { + code: "const a = { b: null }; (a?.b).c", + expected: null, + }, + { + code: "const a = { b: { c: String } }; a?.b?.c?.(42)", + expected: { value: "42" }, + }, + { + code: "const a = null; a?.b?.c?.(42)", + expected: { value: undefined }, + }, + { + code: "const a = { b: { c: String } }; a?.b.c(42)", + expected: { value: "42" }, + }, + { + code: "const a = null; a?.b.c(42)", + expected: { value: undefined }, + }, + ] + : []), ]) { it(`should return ${JSON.stringify(expected)} from ${code}`, () => { const linter = new eslint.Linter() @@ -218,7 +223,11 @@ const aMap = Object.freeze({ })) linter.verify(code, { env: { es6: true }, - parserOptions: { ecmaVersion: 2020 }, + parserOptions: { + ecmaVersion: semver.gte(eslint.CLIEngine.version, "6.0.0") + ? 2020 + : 2018, + }, rules: { test: "error" }, }) diff --git a/test/has-side-effect.js b/test/has-side-effect.js index 2884f6b..a951ace 100644 --- a/test/has-side-effect.js +++ b/test/has-side-effect.js @@ -1,5 +1,6 @@ import assert from "assert" import eslint from "eslint" +import semver from "semver" import dp from "dot-prop" import { hasSideEffect } from "../src/" @@ -46,21 +47,29 @@ describe("The 'hasSideEffect' function", () => { options: undefined, expected: true, }, - { - code: "f?.()", - options: undefined, - expected: true, - }, + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + { + code: "f?.()", + options: undefined, + expected: true, + }, + ] + : []), { code: "a + f()", options: undefined, expected: true, }, - { - code: "a + f?.()", - options: undefined, - expected: true, - }, + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + { + code: "a + f?.()", + options: undefined, + expected: true, + }, + ] + : []), { code: "obj.a", options: undefined, @@ -71,16 +80,20 @@ describe("The 'hasSideEffect' function", () => { options: { considerGetters: true }, expected: true, }, - { - code: "obj?.a", - options: undefined, - expected: false, - }, - { - code: "obj?.a", - options: { considerGetters: true }, - expected: true, - }, + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + { + code: "obj?.a", + options: undefined, + expected: false, + }, + { + code: "obj?.a", + options: { considerGetters: true }, + expected: true, + }, + ] + : []), { code: "obj[a]", options: undefined, @@ -96,21 +109,25 @@ describe("The 'hasSideEffect' function", () => { options: { considerImplicitTypeConversion: true }, expected: true, }, - { - code: "obj?.[a]", - options: undefined, - expected: false, - }, - { - code: "obj?.[a]", - options: { considerGetters: true }, - expected: true, - }, - { - code: "obj?.[a]", - options: { considerImplicitTypeConversion: true }, - expected: true, - }, + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + { + code: "obj?.[a]", + options: undefined, + expected: false, + }, + { + code: "obj?.[a]", + options: { considerGetters: true }, + expected: true, + }, + { + code: "obj?.[a]", + options: { considerImplicitTypeConversion: true }, + expected: true, + }, + ] + : []), { code: "obj[0]", options: { considerImplicitTypeConversion: true }, @@ -277,7 +294,11 @@ describe("The 'hasSideEffect' function", () => { })) const messages = linter.verify(code, { env: { es6: true }, - parserOptions: { ecmaVersion: 2020 }, + parserOptions: { + ecmaVersion: semver.gte(eslint.CLIEngine.version, "6.0.0") + ? 2020 + : 2018, + }, rules: { test: "error" }, }) diff --git a/test/reference-tracker.js b/test/reference-tracker.js index 8158be4..d5b67d9 100644 --- a/test/reference-tracker.js +++ b/test/reference-tracker.js @@ -1,9 +1,15 @@ import assert from "assert" import eslint from "eslint" +import semver from "semver" import { CALL, CONSTRUCT, ESM, READ, ReferenceTracker } from "../src/" const config = { - parserOptions: { ecmaVersion: 2020, sourceType: "module" }, + parserOptions: { + ecmaVersion: semver.gte(eslint.CLIEngine.version, "6.0.0") + ? 2020 + : 2018, + sourceType: "module", + }, globals: { Reflect: false }, rules: { test: "error" }, } @@ -533,11 +539,15 @@ describe("The 'ReferenceTracker' class:", () => { "abc();", "new abc();", "abc.xyz;", - "abc?.xyz;", - "abc?.();", - "abc?.xyz?.();", - "(abc.def).ghi;", - "(abc?.def)?.ghi;", + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + "abc?.xyz;", + "abc?.();", + "abc?.xyz?.();", + "(abc.def).ghi;", + "(abc?.def)?.ghi;", + ] + : []), ].join("\n"), traceMap: { abc: { @@ -573,36 +583,52 @@ describe("The 'ReferenceTracker' class:", () => { type: READ, info: 4, }, - { - node: { type: "MemberExpression", optional: true }, - path: ["abc", "xyz"], - type: READ, - info: 4, - }, - { - node: { type: "CallExpression", optional: true }, - path: ["abc"], - type: CALL, - info: 2, - }, - { - node: { type: "MemberExpression", optional: true }, - path: ["abc", "xyz"], - type: READ, - info: 4, - }, - { - node: { type: "MemberExpression" }, - path: ["abc", "def", "ghi"], - type: READ, - info: 5, - }, - { - node: { type: "MemberExpression", optional: true }, - path: ["abc", "def", "ghi"], - type: READ, - info: 5, - }, + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + { + node: { + type: "MemberExpression", + optional: true, + }, + path: ["abc", "xyz"], + type: READ, + info: 4, + }, + { + node: { + type: "CallExpression", + optional: true, + }, + path: ["abc"], + type: CALL, + info: 2, + }, + { + node: { + type: "MemberExpression", + optional: true, + }, + path: ["abc", "xyz"], + type: READ, + info: 4, + }, + { + node: { type: "MemberExpression" }, + path: ["abc", "def", "ghi"], + type: READ, + info: 5, + }, + { + node: { + type: "MemberExpression", + optional: true, + }, + path: ["abc", "def", "ghi"], + type: READ, + info: 5, + }, + ] + : []), ], }, { From ee966c1f1ffee17ba8a522d0a73adfad02777edf Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 14 Jun 2020 16:24:58 +0900 Subject: [PATCH 3/5] Add supports nullish coalescing --- src/get-static-value.js | 3 ++- test/get-static-value.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/get-static-value.js b/src/get-static-value.js index bce6598..2511a98 100644 --- a/src/get-static-value.js +++ b/src/get-static-value.js @@ -351,7 +351,8 @@ const operations = Object.freeze({ if (left != null) { if ( (node.operator === "||" && Boolean(left.value) === true) || - (node.operator === "&&" && Boolean(left.value) === false) + (node.operator === "&&" && Boolean(left.value) === false) || + (node.operator === "??" && left.value != null) ) { return left } diff --git a/test/get-static-value.js b/test/get-static-value.js index f593273..3088e57 100644 --- a/test/get-static-value.js +++ b/test/get-static-value.js @@ -146,6 +146,26 @@ const aMap = Object.freeze({ }, ...(semver.gte(eslint.CLIEngine.version, "6.0.0") ? [ + { + code: "const a = null, b = 42; a ?? b", + expected: { value: 42 }, + }, + { + code: "const a = undefined, b = 42; a ?? b", + expected: { value: 42 }, + }, + { + code: "const a = false, b = 42; a ?? b", + expected: { value: false }, + }, + { + code: "const a = 42, b = null; a ?? b", + expected: { value: 42 }, + }, + { + code: "const a = 42, b = undefined; a ?? b", + expected: { value: 42 }, + }, { code: "const a = { b: { c: 42 } }; a?.b?.c", expected: { value: 42 }, From f7561de8be858e15627038b7d0400446d2f9f12d Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 14 Jun 2020 16:44:04 +0900 Subject: [PATCH 4/5] Add testcase --- test/get-static-value.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/get-static-value.js b/test/get-static-value.js index 3088e57..ca60555 100644 --- a/test/get-static-value.js +++ b/test/get-static-value.js @@ -226,6 +226,14 @@ const aMap = Object.freeze({ code: "const a = null; a?.b.c(42)", expected: { value: undefined }, }, + { + code: "null?.()", + expected: { value: undefined }, + }, + { + code: "const a = null; a?.()", + expected: { value: undefined }, + }, ] : []), ]) { From f45eaca8c5240a7e85aca9be0a16e3a120fdf1c5 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sun, 14 Jun 2020 16:46:05 +0900 Subject: [PATCH 5/5] Add testcase --- test/get-static-value.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/get-static-value.js b/test/get-static-value.js index ca60555..fe4e947 100644 --- a/test/get-static-value.js +++ b/test/get-static-value.js @@ -234,6 +234,10 @@ const aMap = Object.freeze({ code: "const a = null; a?.()", expected: { value: undefined }, }, + { + code: "a?.()", + expected: null, + }, ] : []), ]) {