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/src/get-static-value.js b/src/get-static-value.js index 173c01a..2511a98 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) } @@ -340,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 } @@ -356,16 +368,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 +514,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 +527,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..fe4e947 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,6 +144,102 @@ const aMap = Object.freeze({ code: "RegExp.$1", expected: null, }, + ...(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 }, + }, + { + 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 }, + }, + { + code: "null?.()", + expected: { value: undefined }, + }, + { + code: "const a = null; a?.()", + expected: { value: undefined }, + }, + { + code: "a?.()", + expected: null, + }, + ] + : []), ]) { it(`should return ${JSON.stringify(expected)} from ${code}`, () => { const linter = new eslint.Linter() @@ -158,7 +255,11 @@ const aMap = Object.freeze({ })) linter.verify(code, { env: { es6: true }, - parserOptions: { ecmaVersion: 2018 }, + 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 d84c300..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,11 +47,29 @@ describe("The 'hasSideEffect' function", () => { 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, }, + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + { + code: "a + f?.()", + options: undefined, + expected: true, + }, + ] + : []), { code: "obj.a", options: undefined, @@ -61,6 +80,20 @@ describe("The 'hasSideEffect' function", () => { 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, @@ -76,6 +109,25 @@ describe("The 'hasSideEffect' function", () => { 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 }, @@ -242,7 +294,11 @@ describe("The 'hasSideEffect' function", () => { })) const messages = linter.verify(code, { env: { es6: true }, - parserOptions: { ecmaVersion: 2018 }, + 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 fe95ee0..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: 2018, sourceType: "module" }, + parserOptions: { + ecmaVersion: semver.gte(eslint.CLIEngine.version, "6.0.0") + ? 2020 + : 2018, + sourceType: "module", + }, globals: { Reflect: false }, rules: { test: "error" }, } @@ -504,7 +510,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 +539,15 @@ describe("The 'ReferenceTracker' class:", () => { "abc();", "new abc();", "abc.xyz;", + ...(semver.gte(eslint.CLIEngine.version, "6.0.0") + ? [ + "abc?.xyz;", + "abc?.();", + "abc?.xyz?.();", + "(abc.def).ghi;", + "(abc?.def)?.ghi;", + ] + : []), ].join("\n"), traceMap: { abc: { @@ -533,6 +555,7 @@ describe("The 'ReferenceTracker' class:", () => { [CALL]: 2, [CONSTRUCT]: 3, xyz: { [READ]: 4 }, + def: { ghi: { [READ]: 5 } }, }, }, expected: [ @@ -560,6 +583,52 @@ describe("The 'ReferenceTracker' class:", () => { type: READ, info: 4, }, + ...(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, + }, + ] + : []), ], }, { @@ -613,7 +682,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 +964,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 } + : {} + ), + }) ) }, }))