Skip to content

Commit 4f8407d

Browse files
committed
New: hasSideEffect function
1 parent 8d7fbaf commit 4f8407d

File tree

5 files changed

+488
-1
lines changed

5 files changed

+488
-1
lines changed

docs/api/ast-utils.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,67 @@ function getStringIfConstant(node, initialScope) {
350350

351351
----
352352

353+
## hasSideEffect
354+
355+
```js
356+
const ret = utils.hasSideEffect(node, sourceCode, options)
357+
```
358+
359+
Check whether a given node has any side effect or not.
360+
361+
The side effect means that it *may* modify a certain variable or object member. This function considers the node which contains the following types as the node which has side effects:
362+
363+
- `AssignmentExpression`
364+
- `AwaitExpression`
365+
- `CallExpression`
366+
- `NewExpression`
367+
- `UnaryExpression` (`[operator = "delete"]`)
368+
- `UpdateExpression`
369+
- `YieldExpression`
370+
- When `options.considerGetters` is `true`:
371+
- `MemberExpression`
372+
- When `options.considerImplicitTypeConversion` is `true`:
373+
- `BinaryExpression` (`[operator = "==" | "!=" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in"]`)
374+
- `MemberExpression` (`[computed = true]`)
375+
- `MethodDefinition` (`[computed = true]`)
376+
- `Property` (`[computed = true]`)
377+
- `UnaryExpression` (`[operator = "-" | "+" | "!" | "~"]`)
378+
379+
### Parameters
380+
381+
Name | Type | Description
382+
:-----|:-----|:------------
383+
node | Node | The node to check.
384+
sourceCode | SourceCode | The source code object to get visitor keys.
385+
options.considerGetters | boolean | Default is `false`. If `true` then it considers member accesses as the node which has side effects.
386+
options.considerImplicitTypeConversion | boolean | Default is `false`. If `true` then it considers implicit type conversion as the node which has side effects.
387+
388+
### Return value
389+
390+
`true` if the node has a certain side effect.
391+
392+
### Example
393+
394+
```js{9}
395+
const { hasSideEffect } = require("eslint-utils")
396+
397+
module.exports = {
398+
meta: {},
399+
create(context) {
400+
const sourceCode = context.getSourceCode()
401+
return {
402+
":expression"(node) {
403+
if (hasSideEffect(node, sourceCode)) {
404+
// ...
405+
}
406+
},
407+
}
408+
},
409+
}
410+
```
411+
412+
----
413+
353414
## isParenthesized
354415

355416
```js

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"files": [
1010
"index.*"
1111
],
12-
"dependencies": {},
12+
"dependencies": {
13+
"eslint-visitor-keys": "^1.0.0"
14+
},
1315
"devDependencies": {
1416
"@mysticatea/eslint-plugin": "^5.0.1",
1517
"codecov": "^3.0.2",

src/has-side-effect.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import evk from "eslint-visitor-keys"
2+
3+
const typeConversionBinaryOps = Object.freeze(
4+
new Set([
5+
"==",
6+
"!=",
7+
"<",
8+
"<=",
9+
">",
10+
">=",
11+
"<<",
12+
">>",
13+
">>>",
14+
"+",
15+
"-",
16+
"*",
17+
"/",
18+
"%",
19+
"|",
20+
"^",
21+
"&",
22+
"in",
23+
])
24+
)
25+
const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"]))
26+
const visitor = Object.freeze(
27+
Object.assign(Object.create(null), {
28+
$visit(node, options, visitorKeys) {
29+
const { type } = node
30+
31+
if (typeof this[type] === "function") {
32+
return this[type](node, options, visitorKeys)
33+
}
34+
35+
return this.$visitChildren(node, options, visitorKeys)
36+
},
37+
38+
$visitChildren(node, options, visitorKeys) {
39+
const { type } = node
40+
41+
for (const key of visitorKeys[type] || evk.getKeys(node)) {
42+
const value = node[key]
43+
44+
if (Array.isArray(value)) {
45+
for (const element of value) {
46+
if (
47+
element &&
48+
this.$visit(element, options, visitorKeys)
49+
) {
50+
return true
51+
}
52+
}
53+
} else if (value && this.$visit(value, options, visitorKeys)) {
54+
return true
55+
}
56+
}
57+
58+
return false
59+
},
60+
61+
ArrowFunctionExpression() {
62+
return false
63+
},
64+
AssignmentExpression() {
65+
return true
66+
},
67+
AwaitExpression() {
68+
return true
69+
},
70+
BinaryExpression(node, options, visitorKeys) {
71+
if (
72+
options.considerImplicitTypeConversion &&
73+
typeConversionBinaryOps.has(node.operator) &&
74+
(node.left.type !== "Literal" || node.right.type !== "Literal")
75+
) {
76+
return true
77+
}
78+
return this.$visitChildren(node, options, visitorKeys)
79+
},
80+
CallExpression() {
81+
return true
82+
},
83+
FunctionExpression() {
84+
return false
85+
},
86+
MemberExpression(node, options, visitorKeys) {
87+
if (options.considerGetters) {
88+
return true
89+
}
90+
if (
91+
options.considerImplicitTypeConversion &&
92+
node.computed &&
93+
node.property.type !== "Literal"
94+
) {
95+
return true
96+
}
97+
return this.$visitChildren(node, options, visitorKeys)
98+
},
99+
MethodDefinition(node, options, visitorKeys) {
100+
if (
101+
options.considerImplicitTypeConversion &&
102+
node.computed &&
103+
node.key.type !== "Literal"
104+
) {
105+
return true
106+
}
107+
return this.$visitChildren(node, options, visitorKeys)
108+
},
109+
NewExpression() {
110+
return true
111+
},
112+
Property(node, options, visitorKeys) {
113+
if (
114+
options.considerImplicitTypeConversion &&
115+
node.computed &&
116+
node.key.type !== "Literal"
117+
) {
118+
return true
119+
}
120+
return this.$visitChildren(node, options, visitorKeys)
121+
},
122+
UnaryExpression(node, options, visitorKeys) {
123+
if (node.operator === "delete") {
124+
return true
125+
}
126+
if (
127+
options.considerImplicitTypeConversion &&
128+
typeConversionUnaryOps.has(node.operator) &&
129+
node.argument.type !== "Literal"
130+
) {
131+
return true
132+
}
133+
return this.$visitChildren(node, options, visitorKeys)
134+
},
135+
UpdateExpression() {
136+
return true
137+
},
138+
YieldExpression() {
139+
return true
140+
},
141+
})
142+
)
143+
144+
/**
145+
* Check whether a given node has any side effect or not.
146+
* @param {Node} node The node to get.
147+
* @param {SourceCode} sourceCode The source code object.
148+
* @param {object} [options] The option object.
149+
* @param {boolean} [options.considerGetters=false] If `true` then it considers member accesses as the node which has side effects.
150+
* @param {boolean} [options.considerImplicitTypeConversion=false] If `true` then it considers implicit type conversion as the node which has side effects.
151+
* @param {object} [options.visitorKeys=evk.KEYS] The keys to traverse nodes. Use `context.getSourceCode().visitorKeys`.
152+
* @returns {boolean} `true` if the node has a certain side effect.
153+
*/
154+
export function hasSideEffect(
155+
node,
156+
sourceCode,
157+
{ considerGetters = false, considerImplicitTypeConversion = false } = {}
158+
) {
159+
return visitor.$visit(
160+
node,
161+
{ considerGetters, considerImplicitTypeConversion },
162+
sourceCode.visitorKeys || evk.KEYS
163+
)
164+
}

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getInnermostScope } from "./get-innermost-scope"
55
import { getPropertyName } from "./get-property-name"
66
import { getStaticValue } from "./get-static-value"
77
import { getStringIfConstant } from "./get-string-if-constant"
8+
import { hasSideEffect } from "./has-side-effect"
89
import { isParenthesized } from "./is-parenthesized"
910
import { PatternMatcher } from "./pattern-matcher"
1011
import {
@@ -50,6 +51,7 @@ export default {
5051
getPropertyName,
5152
getStaticValue,
5253
getStringIfConstant,
54+
hasSideEffect,
5355
isArrowToken,
5456
isClosingBraceToken,
5557
isClosingBracketToken,
@@ -88,6 +90,7 @@ export {
8890
getPropertyName,
8991
getStaticValue,
9092
getStringIfConstant,
93+
hasSideEffect,
9194
isArrowToken,
9295
isClosingBraceToken,
9396
isClosingBracketToken,

0 commit comments

Comments
 (0)