Skip to content

Commit 9b2885a

Browse files
authored
✨ supports for Optional chaining & Nullish coalescing (#8)
1 parent f811251 commit 9b2885a

File tree

7 files changed

+309
-32
lines changed

7 files changed

+309
-32
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"rimraf": "^3.0.0",
2929
"rollup": "^1.25.0",
3030
"rollup-plugin-sourcemaps": "^0.4.2",
31+
"semver": "^7.3.2",
3132
"vuepress": "^1.2.0",
3233
"warun": "^1.0.0"
3334
},

src/get-static-value.js

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -251,23 +251,34 @@ const operations = Object.freeze({
251251
if (args != null) {
252252
if (calleeNode.type === "MemberExpression") {
253253
const object = getStaticValueR(calleeNode.object, initialScope)
254-
const property = calleeNode.computed
255-
? getStaticValueR(calleeNode.property, initialScope)
256-
: { value: calleeNode.property.name }
257-
258-
if (object != null && property != null) {
259-
const receiver = object.value
260-
const methodName = property.value
261-
if (callAllowed.has(receiver[methodName])) {
262-
return { value: receiver[methodName](...args) }
254+
if (object != null) {
255+
if (
256+
object.value == null &&
257+
(object.optional || node.optional)
258+
) {
259+
return { value: undefined, optional: true }
263260
}
264-
if (callPassThrough.has(receiver[methodName])) {
265-
return { value: args[0] }
261+
const property = calleeNode.computed
262+
? getStaticValueR(calleeNode.property, initialScope)
263+
: { value: calleeNode.property.name }
264+
265+
if (property != null) {
266+
const receiver = object.value
267+
const methodName = property.value
268+
if (callAllowed.has(receiver[methodName])) {
269+
return { value: receiver[methodName](...args) }
270+
}
271+
if (callPassThrough.has(receiver[methodName])) {
272+
return { value: args[0] }
273+
}
266274
}
267275
}
268276
} else {
269277
const callee = getStaticValueR(calleeNode, initialScope)
270278
if (callee != null) {
279+
if (callee.value == null && node.optional) {
280+
return { value: undefined, optional: true }
281+
}
271282
const func = callee.value
272283
if (callAllowed.has(func)) {
273284
return { value: func(...args) }
@@ -340,7 +351,8 @@ const operations = Object.freeze({
340351
if (left != null) {
341352
if (
342353
(node.operator === "||" && Boolean(left.value) === true) ||
343-
(node.operator === "&&" && Boolean(left.value) === false)
354+
(node.operator === "&&" && Boolean(left.value) === false) ||
355+
(node.operator === "??" && left.value != null)
344356
) {
345357
return left
346358
}
@@ -356,16 +368,25 @@ const operations = Object.freeze({
356368

357369
MemberExpression(node, initialScope) {
358370
const object = getStaticValueR(node.object, initialScope)
359-
const property = node.computed
360-
? getStaticValueR(node.property, initialScope)
361-
: { value: node.property.name }
362-
363-
if (
364-
object != null &&
365-
property != null &&
366-
!isGetter(object.value, property.value)
367-
) {
368-
return { value: object.value[property.value] }
371+
if (object != null) {
372+
if (object.value == null && (object.optional || node.optional)) {
373+
return { value: undefined, optional: true }
374+
}
375+
const property = node.computed
376+
? getStaticValueR(node.property, initialScope)
377+
: { value: node.property.name }
378+
379+
if (property != null && !isGetter(object.value, property.value)) {
380+
return { value: object.value[property.value] }
381+
}
382+
}
383+
return null
384+
},
385+
386+
ChainExpression(node, initialScope) {
387+
const expression = getStaticValueR(node.expression, initialScope)
388+
if (expression != null) {
389+
return { value: expression.value }
369390
}
370391
return null
371392
},
@@ -493,7 +514,7 @@ const operations = Object.freeze({
493514
* Get the value of a given node if it's a static value.
494515
* @param {Node} node The node to get.
495516
* @param {Scope|undefined} initialScope The scope to start finding variable.
496-
* @returns {{value:any}|null} The static value of the node, or `null`.
517+
* @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`.
497518
*/
498519
function getStaticValueR(node, initialScope) {
499520
if (node != null && Object.hasOwnProperty.call(operations, node.type)) {
@@ -506,7 +527,7 @@ function getStaticValueR(node, initialScope) {
506527
* Get the value of a given node if it's a static value.
507528
* @param {Node} node The node to get.
508529
* @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.
509-
* @returns {{value:any}|null} The static value of the node, or `null`.
530+
* @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`.
510531
*/
511532
export function getStaticValue(node, initialScope = null) {
512533
try {

src/has-side-effect.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ const typeConversionBinaryOps = Object.freeze(
2323
])
2424
)
2525
const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"]))
26+
27+
/**
28+
* Check whether the given value is an ASTNode or not.
29+
* @param {any} x The value to check.
30+
* @returns {boolean} `true` if the value is an ASTNode.
31+
*/
32+
function isNode(x) {
33+
return x !== null && typeof x === "object" && typeof x.type === "string"
34+
}
35+
2636
const visitor = Object.freeze(
2737
Object.assign(Object.create(null), {
2838
$visit(node, options, visitorKeys) {
@@ -44,13 +54,16 @@ const visitor = Object.freeze(
4454
if (Array.isArray(value)) {
4555
for (const element of value) {
4656
if (
47-
element &&
57+
isNode(element) &&
4858
this.$visit(element, options, visitorKeys)
4959
) {
5060
return true
5161
}
5262
}
53-
} else if (value && this.$visit(value, options, visitorKeys)) {
63+
} else if (
64+
isNode(value) &&
65+
this.$visit(value, options, visitorKeys)
66+
) {
5467
return true
5568
}
5669
}

src/reference-tracker.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ function isPassThrough(node) {
4141
return true
4242
case "SequenceExpression":
4343
return parent.expressions[parent.expressions.length - 1] === node
44+
case "ChainExpression":
45+
return true
4446

4547
default:
4648
return false

test/get-static-value.js

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "assert"
22
import eslint from "eslint"
3+
import semver from "semver"
34
import { getStaticValue } from "../src/"
45

56
describe("The 'getStaticValue' function", () => {
@@ -143,6 +144,102 @@ const aMap = Object.freeze({
143144
code: "RegExp.$1",
144145
expected: null,
145146
},
147+
...(semver.gte(eslint.CLIEngine.version, "6.0.0")
148+
? [
149+
{
150+
code: "const a = null, b = 42; a ?? b",
151+
expected: { value: 42 },
152+
},
153+
{
154+
code: "const a = undefined, b = 42; a ?? b",
155+
expected: { value: 42 },
156+
},
157+
{
158+
code: "const a = false, b = 42; a ?? b",
159+
expected: { value: false },
160+
},
161+
{
162+
code: "const a = 42, b = null; a ?? b",
163+
expected: { value: 42 },
164+
},
165+
{
166+
code: "const a = 42, b = undefined; a ?? b",
167+
expected: { value: 42 },
168+
},
169+
{
170+
code: "const a = { b: { c: 42 } }; a?.b?.c",
171+
expected: { value: 42 },
172+
},
173+
{
174+
code: "const a = { b: { c: 42 } }; a?.b?.['c']",
175+
expected: { value: 42 },
176+
},
177+
{
178+
code: "const a = { b: null }; a?.b?.c",
179+
expected: { value: undefined },
180+
},
181+
{
182+
code: "const a = { b: undefined }; a?.b?.c",
183+
expected: { value: undefined },
184+
},
185+
{
186+
code: "const a = { b: null }; a?.b?.['c']",
187+
expected: { value: undefined },
188+
},
189+
{
190+
code: "const a = null; a?.b?.c",
191+
expected: { value: undefined },
192+
},
193+
{
194+
code: "const a = null; a?.b.c",
195+
expected: { value: undefined },
196+
},
197+
{
198+
code: "const a = void 0; a?.b.c",
199+
expected: { value: undefined },
200+
},
201+
{
202+
code: "const a = { b: { c: 42 } }; (a?.b).c",
203+
expected: { value: 42 },
204+
},
205+
{
206+
code: "const a = null; (a?.b).c",
207+
expected: null,
208+
},
209+
{
210+
code: "const a = { b: null }; (a?.b).c",
211+
expected: null,
212+
},
213+
{
214+
code: "const a = { b: { c: String } }; a?.b?.c?.(42)",
215+
expected: { value: "42" },
216+
},
217+
{
218+
code: "const a = null; a?.b?.c?.(42)",
219+
expected: { value: undefined },
220+
},
221+
{
222+
code: "const a = { b: { c: String } }; a?.b.c(42)",
223+
expected: { value: "42" },
224+
},
225+
{
226+
code: "const a = null; a?.b.c(42)",
227+
expected: { value: undefined },
228+
},
229+
{
230+
code: "null?.()",
231+
expected: { value: undefined },
232+
},
233+
{
234+
code: "const a = null; a?.()",
235+
expected: { value: undefined },
236+
},
237+
{
238+
code: "a?.()",
239+
expected: null,
240+
},
241+
]
242+
: []),
146243
]) {
147244
it(`should return ${JSON.stringify(expected)} from ${code}`, () => {
148245
const linter = new eslint.Linter()
@@ -158,7 +255,11 @@ const aMap = Object.freeze({
158255
}))
159256
linter.verify(code, {
160257
env: { es6: true },
161-
parserOptions: { ecmaVersion: 2018 },
258+
parserOptions: {
259+
ecmaVersion: semver.gte(eslint.CLIEngine.version, "6.0.0")
260+
? 2020
261+
: 2018,
262+
},
162263
rules: { test: "error" },
163264
})
164265

test/has-side-effect.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "assert"
22
import eslint from "eslint"
3+
import semver from "semver"
34
import dp from "dot-prop"
45
import { hasSideEffect } from "../src/"
56

@@ -46,11 +47,29 @@ describe("The 'hasSideEffect' function", () => {
4647
options: undefined,
4748
expected: true,
4849
},
50+
...(semver.gte(eslint.CLIEngine.version, "6.0.0")
51+
? [
52+
{
53+
code: "f?.()",
54+
options: undefined,
55+
expected: true,
56+
},
57+
]
58+
: []),
4959
{
5060
code: "a + f()",
5161
options: undefined,
5262
expected: true,
5363
},
64+
...(semver.gte(eslint.CLIEngine.version, "6.0.0")
65+
? [
66+
{
67+
code: "a + f?.()",
68+
options: undefined,
69+
expected: true,
70+
},
71+
]
72+
: []),
5473
{
5574
code: "obj.a",
5675
options: undefined,
@@ -61,6 +80,20 @@ describe("The 'hasSideEffect' function", () => {
6180
options: { considerGetters: true },
6281
expected: true,
6382
},
83+
...(semver.gte(eslint.CLIEngine.version, "6.0.0")
84+
? [
85+
{
86+
code: "obj?.a",
87+
options: undefined,
88+
expected: false,
89+
},
90+
{
91+
code: "obj?.a",
92+
options: { considerGetters: true },
93+
expected: true,
94+
},
95+
]
96+
: []),
6497
{
6598
code: "obj[a]",
6699
options: undefined,
@@ -76,6 +109,25 @@ describe("The 'hasSideEffect' function", () => {
76109
options: { considerImplicitTypeConversion: true },
77110
expected: true,
78111
},
112+
...(semver.gte(eslint.CLIEngine.version, "6.0.0")
113+
? [
114+
{
115+
code: "obj?.[a]",
116+
options: undefined,
117+
expected: false,
118+
},
119+
{
120+
code: "obj?.[a]",
121+
options: { considerGetters: true },
122+
expected: true,
123+
},
124+
{
125+
code: "obj?.[a]",
126+
options: { considerImplicitTypeConversion: true },
127+
expected: true,
128+
},
129+
]
130+
: []),
79131
{
80132
code: "obj[0]",
81133
options: { considerImplicitTypeConversion: true },
@@ -242,7 +294,11 @@ describe("The 'hasSideEffect' function", () => {
242294
}))
243295
const messages = linter.verify(code, {
244296
env: { es6: true },
245-
parserOptions: { ecmaVersion: 2018 },
297+
parserOptions: {
298+
ecmaVersion: semver.gte(eslint.CLIEngine.version, "6.0.0")
299+
? 2020
300+
: 2018,
301+
},
246302
rules: { test: "error" },
247303
})
248304

0 commit comments

Comments
 (0)