diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..db24720
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1 @@
+comment: off
diff --git a/.eslintignore b/.eslintignore
index 17fa7eb..d23f4fb 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,8 @@
-/docs/.vuepress/dist
+/.nyc_output
+/coverage
/node_modules
/index.*
+/test.*
+
!.vuepress
+/docs/.vuepress/dist
diff --git a/.eslintrc.yml b/.eslintrc.yml
index be94abe..abcafd7 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -1,12 +1,15 @@
root: true
extends:
- - plugin:@mysticatea/es2015
- - plugin:@mysticatea/+modules
- - plugin:@mysticatea/+node
+- plugin:@mysticatea/es2015
-rules:
- "@mysticatea/node/no-unsupported-features":
+overrides:
+- files:
+ - src/**/*.js
+ - test/**/*.js
+ extends: plugin:@mysticatea/+modules
+ rules:
+ init-declarations: "off"
+ "@mysticatea/node/no-unsupported-features/es-syntax":
- error
- - ignores:
- - modules
+ - ignores: [modules]
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..ce763b1
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github:
+- mysticatea
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..95a6301
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,68 @@
+name: CI
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+ schedule:
+ - cron: 0 0 * * 0
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ - name: Install Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: 12
+ - name: Install Packages
+ run: npm install
+ - name: Lint
+ run: npm run -s lint
+
+ test:
+ name: Test
+ strategy:
+ matrix:
+ node: [12, 10, 8, 6]
+ eslint: [6, 5]
+ exclude:
+ # ESLint 6 doesn't support Node 6.
+ - node: 6
+ eslint: 6
+ # Run ESLint 5 on only the newest and oldest Node.
+ - node: 10
+ eslint: 5
+ - node: 8
+ eslint: 5
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v1
+ with:
+ fetch-depth: 1
+ - name: Install Node.js v${{ matrix.node }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node }}
+ - name: Install Packages
+ run: |
+ if [ ${{ matrix.node }} -eq 6 ]; then
+ npm install --global npm@^6.0.0
+ fi
+ npm install
+ - name: Install ESLint v${{ matrix.eslint }}
+ run: npm install --no-save eslint@^${{ matrix.eslint }}.0.0
+ - name: Build
+ run: npm run -s build
+ - name: Test
+ run: npm run -s test:mocha
+ - name: Send Coverage
+ run: npm run -s codecov
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.nycrc b/.nycrc
index 9992db5..9ca13b9 100644
--- a/.nycrc
+++ b/.nycrc
@@ -1,5 +1,5 @@
{
"include": ["src/**/*.js"],
- "require": ["esm"],
- "cache": true
+ "reporter": ["lcov", "text-summary"],
+ "require": ["esm"]
}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2ad59ed..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-sudo: false
-
-language: node_js
-node_js:
- - 6.5.0
- - 8
- - 10
-
-script:
- - npm test
-
-after_success:
- - npm run -s codecov
-
-before_deploy:
- - npm run -s docs:build
-
-deploy:
- provider: pages
- skip-cleanup: true
- github-token: $ATOKEN
- keep-history: true
- local-dir: docs/.vuepress/dist
- on:
- branch: master
- node: 10
diff --git a/README.md b/README.md
index 7069f04..0358380 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://www.npmjs.com/package/eslint-utils)
[](http://www.npmtrends.com/eslint-utils)
-[](https://travis-ci.org/mysticatea/eslint-utils)
+[](https://github.com/mysticatea/eslint-utils/actions)
[](https://codecov.io/gh/mysticatea/eslint-utils)
[](https://david-dm.org/mysticatea/eslint-utils)
@@ -12,13 +12,13 @@ This package provides utility functions and classes for make ESLint custom rules
For examples:
-- [getStaticValue](https://mysticatea.github.io/eslint-utils/api/ast-utils.html#getstaticvalue) evaluates static value on AST.
-- [PatternMatcher](https://mysticatea.github.io/eslint-utils/api/ast-utils.html#patternmatcher-class) finds a regular expression pattern as handling escape sequences.
-- [ReferenceTracker](https://mysticatea.github.io/eslint-utils/api/scope-utils.html#referencetracker-class) checks the members of modules/globals as handling assignments and destructuring.
+- [getStaticValue](https://eslint-utils.mysticatea.dev/api/ast-utils.html#getstaticvalue) evaluates static value on AST.
+- [PatternMatcher](https://eslint-utils.mysticatea.dev/api/ast-utils.html#patternmatcher-class) finds a regular expression pattern as handling escape sequences.
+- [ReferenceTracker](https://eslint-utils.mysticatea.dev/api/scope-utils.html#referencetracker-class) checks the members of modules/globals as handling assignments and destructuring.
## 📖 Usage
-See [documentation](https://mysticatea.github.io/eslint-utils/).
+See [documentation](https://eslint-utils.mysticatea.dev/).
## 📰 Changelog
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 01cf0a4..edc74c0 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -1,11 +1,9 @@
"use strict"
module.exports = {
- base: "/eslint-utils/",
title: "eslint-utils",
description: "Utilities for ESLint plugins and custom rules.",
serviceWorker: true,
- ga: "UA-12936571-6",
themeConfig: {
repo: "mysticatea/eslint-utils",
diff --git a/docs/.vuepress/override.styl b/docs/.vuepress/styles/palette.styl
similarity index 100%
rename from docs/.vuepress/override.styl
rename to docs/.vuepress/styles/palette.styl
diff --git a/docs/README.md b/docs/README.md
index c94c231..3a20ec3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2,6 +2,13 @@
home: true
actionText: Get Started →
actionLink: /guide/getting-started
+features:
+- title: Scope Utilities
+ details: Finding the specific global variables and their members with tracking assignments, finding variables, etc...
+- title: AST Utilities
+ details: Computing the runtime value of a node, Getting the property name of a Property|MemberExpression|MemberExpression node, etc...
+- title: Token Utilities
+ details: Distinguishing the token types of a given token, etc...
---
diff --git a/docs/api/ast-utils.md b/docs/api/ast-utils.md
index a12a62f..4004355 100644
--- a/docs/api/ast-utils.md
+++ b/docs/api/ast-utils.md
@@ -350,6 +350,135 @@ function getStringIfConstant(node, initialScope) {
----
+## hasSideEffect
+
+```js
+const ret = utils.hasSideEffect(node, sourceCode, options)
+```
+
+Check whether a given node has any side effect or not.
+
+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:
+
+- `AssignmentExpression`
+- `AwaitExpression`
+- `CallExpression`
+- `ImportExpression`
+- `NewExpression`
+- `UnaryExpression` (`[operator = "delete"]`)
+- `UpdateExpression`
+- `YieldExpression`
+- When `options.considerGetters` is `true`:
+ - `MemberExpression`
+- When `options.considerImplicitTypeConversion` is `true`:
+ - `BinaryExpression` (`[operator = "==" | "!=" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in"]`)
+ - `MemberExpression` (`[computed = true]`)
+ - `MethodDefinition` (`[computed = true]`)
+ - `Property` (`[computed = true]`)
+ - `UnaryExpression` (`[operator = "-" | "+" | "!" | "~"]`)
+
+### Parameters
+
+ Name | Type | Description
+:-----|:-----|:------------
+node | Node | The node to check.
+sourceCode | SourceCode | The source code object to get visitor keys.
+options.considerGetters | boolean | Default is `false`. If `true` then it considers member accesses as the node which has side effects.
+options.considerImplicitTypeConversion | boolean | Default is `false`. If `true` then it considers implicit type conversion as the node which has side effects.
+
+### Return value
+
+`true` if the node has a certain side effect.
+
+### Example
+
+```js{9}
+const { hasSideEffect } = require("eslint-utils")
+
+module.exports = {
+ meta: {},
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ return {
+ ":expression"(node) {
+ if (hasSideEffect(node, sourceCode)) {
+ // ...
+ }
+ },
+ }
+ },
+}
+```
+
+----
+
+## isParenthesized
+
+```js
+const ret1 = utils.isParenthesized(times, node, sourceCode)
+const ret2 = utils.isParenthesized(node, sourceCode)
+```
+
+Check whether a given node is parenthesized or not.
+
+This function detects it correctly even if it's parenthesized by specific syntax.
+
+```js
+// those `a` are not parenthesized.
+f(a);
+new C(a);
+if (a) {}
+switch (a) {}
+while (a) {}
+do {} while (a);
+with (a) {}
+
+// those `b` are parenthesized.
+f((b));
+new C((b));
+if ((b)) {}
+switch ((b)) {}
+while ((b)) {}
+do {} while ((b));
+with ((b)) {}
+```
+
+### Parameters
+
+ Name | Type | Description
+:-----|:-----|:------------
+times | number | Optional. The number of redundant parenthesized. Default is `1`.
+node | Node | The node to check.
+sourceCode | SourceCode | The source code object to get tokens.
+
+### Return value
+
+`true` if the node is parenthesized.
+
+If `times` was given, it returns `true` only if the node is parenthesized the `times` times. For example, `isParenthesized(2, node, sourceCode)` returns `true` for `((foo))`, but not for `(foo)`.
+
+### Example
+
+```js{9}
+const { isParenthesized } = require("eslint-utils")
+
+module.exports = {
+ meta: {},
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ return {
+ ":expression"(node) {
+ if (isParenthesized(node, sourceCode)) {
+ // ...
+ }
+ },
+ }
+ },
+}
+```
+
+----
+
## PatternMatcher class
```js
diff --git a/package.json b/package.json
index ede9bb0..8af7d4f 100644
--- a/package.json
+++ b/package.json
@@ -1,44 +1,51 @@
{
"name": "eslint-utils",
- "version": "1.3.1",
+ "version": "1.4.3",
"description": "Utilities for ESLint plugins.",
"engines": {
"node": ">=6"
},
+ "sideEffects": false,
"main": "index",
+ "module": "index.mjs",
"files": [
"index.*"
],
- "dependencies": {},
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
"devDependencies": {
- "@mysticatea/eslint-plugin": "^5.0.1",
- "codecov": "^3.0.2",
- "eslint": "^5.0.1",
- "esm": "^3.0.55",
- "espree": "^4.0.0",
- "mocha": "^5.2.0",
- "nyc": "^12.0.2",
- "opener": "^1.4.3",
- "rimraf": "^2.6.2",
- "rollup": "^0.62.0",
+ "@mysticatea/eslint-plugin": "^12.0.0",
+ "codecov": "^3.6.1",
+ "dot-prop": "^4.2.0",
+ "eslint": "^6.5.1",
+ "esm": "^3.2.25",
+ "espree": "^6.1.1",
+ "mocha": "^6.2.2",
+ "npm-run-all": "^4.1.5",
+ "nyc": "^14.1.1",
+ "opener": "^1.5.1",
+ "rimraf": "^3.0.0",
+ "rollup": "^1.25.0",
"rollup-plugin-sourcemaps": "^0.4.2",
- "vuepress": "github:mysticatea/vuepress#skip-waiting"
+ "vuepress": "^1.2.0",
+ "warun": "^1.0.0"
},
"scripts": {
"prebuild": "npm run -s clean",
"build": "rollup -c",
"clean": "rimraf .nyc_output coverage index.*",
"codecov": "nyc report -r lcovonly && codecov",
- "coverage": "nyc report -r lcov && opener ./coverage/lcov-report/index.html",
+ "coverage": "opener ./coverage/lcov-report/index.html",
"docs:build": "vuepress build docs",
"docs:watch": "vuepress dev docs",
"lint": "eslint src test",
- "pretest": "npm run -s lint && npm run -s build",
- "test": "nyc mocha --reporter dot \"test/*.js\"",
+ "test": "run-s lint build test:mocha",
+ "test:mocha": "nyc mocha --reporter dot \"test/*.js\"",
"preversion": "npm test && npm run -s build",
"postversion": "git push && git push --tags",
"prewatch": "npm run -s clean",
- "watch": "mocha --require esm --reporter dot --watch --growl \"test/*.js\""
+ "watch": "warun \"{src,test}/**/*.js\" -- npm run -s test:mocha"
},
"repository": {
"type": "git",
diff --git a/rollup.config.js b/rollup.config.js
index 89718b9..069f8e1 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -16,11 +16,11 @@ function config(ext) {
file: `index${ext}`,
format: ext === ".mjs" ? "es" : "cjs",
sourcemap: true,
- sourcemapFile: `index${ext}.map`,
- strict: true,
- banner: `/*! @author Toru Nagashima */`,
+ banner:
+ "/*! @author Toru Nagashima */",
},
plugins: [sourcemaps()],
+ external: Object.keys(require("./package.json").dependencies),
}
}
diff --git a/src/get-static-value.js b/src/get-static-value.js
index 8f4a782..173c01a 100644
--- a/src/get-static-value.js
+++ b/src/get-static-value.js
@@ -1,9 +1,25 @@
+/* globals BigInt, globalThis, global, self, window */
+
import { findVariable } from "./find-variable"
+const globalObject =
+ typeof globalThis !== "undefined"
+ ? globalThis
+ : typeof self !== "undefined"
+ ? self
+ : typeof window !== "undefined"
+ ? window
+ : typeof global !== "undefined"
+ ? global
+ : {}
+
const builtinNames = Object.freeze(
new Set([
"Array",
"ArrayBuffer",
+ "BigInt",
+ "BigInt64Array",
+ "BigUint64Array",
"Boolean",
"DataView",
"Date",
@@ -11,9 +27,7 @@ const builtinNames = Object.freeze(
"decodeURIComponent",
"encodeURI",
"encodeURIComponent",
- "Error",
"escape",
- "EvalError",
"Float32Array",
"Float64Array",
"Function",
@@ -34,26 +48,97 @@ const builtinNames = Object.freeze(
"parseInt",
"Promise",
"Proxy",
- "RangeError",
- "ReferenceError",
"Reflect",
"RegExp",
"Set",
"String",
"Symbol",
- "SyntaxError",
- "TypeError",
"Uint16Array",
"Uint32Array",
"Uint8Array",
"Uint8ClampedArray",
"undefined",
"unescape",
- "URIError",
"WeakMap",
"WeakSet",
])
)
+const callAllowed = new Set(
+ [
+ Array.isArray,
+ typeof BigInt === "function" ? BigInt : undefined,
+ Boolean,
+ Date,
+ Date.parse,
+ decodeURI,
+ decodeURIComponent,
+ encodeURI,
+ encodeURIComponent,
+ escape,
+ isFinite,
+ isNaN,
+ isPrototypeOf,
+ ...Object.getOwnPropertyNames(Math)
+ .map(k => Math[k])
+ .filter(f => typeof f === "function"),
+ Number,
+ Number.isFinite,
+ Number.isNaN,
+ Number.parseFloat,
+ Number.parseInt,
+ Object,
+ Object.entries,
+ Object.is,
+ Object.isExtensible,
+ Object.isFrozen,
+ Object.isSealed,
+ Object.keys,
+ Object.values,
+ parseFloat,
+ parseInt,
+ RegExp,
+ String,
+ String.fromCharCode,
+ String.fromCodePoint,
+ String.raw,
+ Symbol,
+ Symbol.for,
+ Symbol.keyFor,
+ unescape,
+ ].filter(f => typeof f === "function")
+)
+const callPassThrough = new Set([
+ Object.freeze,
+ Object.preventExtensions,
+ Object.seal,
+])
+
+/**
+ * Get the property descriptor.
+ * @param {object} object The object to get.
+ * @param {string|number|symbol} name The property name to get.
+ */
+function getPropertyDescriptor(object, name) {
+ let x = object
+ while ((typeof x === "object" || typeof x === "function") && x !== null) {
+ const d = Object.getOwnPropertyDescriptor(x, name)
+ if (d) {
+ return d
+ }
+ x = Object.getPrototypeOf(x)
+ }
+ return null
+}
+
+/**
+ * Check if a property is getter or not.
+ * @param {object} object The object to check.
+ * @param {string|number|symbol} name The property name to check.
+ */
+function isGetter(object, name) {
+ const d = getPropertyDescriptor(object, name)
+ return d != null && d.get != null
+}
/**
* Get the element values of a given node list.
@@ -173,13 +258,23 @@ const operations = Object.freeze({
if (object != null && property != null) {
const receiver = object.value
const methodName = property.value
- return { value: receiver[methodName](...args) }
+ 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) {
const func = callee.value
- return { value: func(...args) }
+ if (callAllowed.has(func)) {
+ return { value: func(...args) }
+ }
+ if (callPassThrough.has(func)) {
+ return { value: args[0] }
+ }
}
}
}
@@ -210,9 +305,9 @@ const operations = Object.freeze({
variable != null &&
variable.defs.length === 0 &&
builtinNames.has(variable.name) &&
- variable.name in global
+ variable.name in globalObject
) {
- return { value: global[variable.name] }
+ return { value: globalObject[variable.name] }
}
// Constants.
@@ -233,11 +328,11 @@ const operations = Object.freeze({
Literal(node) {
//istanbul ignore if : this is implementation-specific behavior.
- if (node.regex != null && node.value == null) {
- // It was a RegExp literal, but Node.js didn't support it.
+ if ((node.regex != null || node.bigint != null) && node.value == null) {
+ // It was a RegExp/BigInt literal, but Node.js didn't support it.
return null
}
- return node
+ return { value: node.value }
},
LogicalExpression(node, initialScope) {
@@ -265,7 +360,11 @@ const operations = Object.freeze({
? getStaticValueR(node.property, initialScope)
: { value: node.property.name }
- if (object != null && property != null) {
+ if (
+ object != null &&
+ property != null &&
+ !isGetter(object.value, property.value)
+ ) {
return { value: object.value[property.value] }
}
return null
@@ -277,7 +376,9 @@ const operations = Object.freeze({
if (callee != null && args != null) {
const Func = callee.value
- return { value: new Func(...args) }
+ if (callAllowed.has(Func)) {
+ return { value: new Func(...args) }
+ }
}
return null
@@ -336,7 +437,9 @@ const operations = Object.freeze({
const strings = node.quasi.quasis.map(q => q.value.cooked)
strings.raw = node.quasi.quasis.map(q => q.value.raw)
- return { value: func(strings, ...expressions) }
+ if (func === String.raw) {
+ return { value: func(strings, ...expressions) }
+ }
}
return null
diff --git a/src/get-string-if-constant.js b/src/get-string-if-constant.js
index 61c5370..751018a 100644
--- a/src/get-string-if-constant.js
+++ b/src/get-string-if-constant.js
@@ -7,6 +7,16 @@ import { getStaticValue } from "./get-static-value"
* @returns {string|null} The value of the node, or `null`.
*/
export function getStringIfConstant(node, initialScope = null) {
+ // Handle the literals that the platform doesn't support natively.
+ if (node && node.type === "Literal" && node.value === null) {
+ if (node.regex) {
+ return `/${node.regex.pattern}/${node.regex.flags}`
+ }
+ if (node.bigint) {
+ return node.bigint
+ }
+ }
+
const evaluated = getStaticValue(node, initialScope)
return evaluated && String(evaluated.value)
}
diff --git a/src/has-side-effect.js b/src/has-side-effect.js
new file mode 100644
index 0000000..5ae28ee
--- /dev/null
+++ b/src/has-side-effect.js
@@ -0,0 +1,167 @@
+import evk from "eslint-visitor-keys"
+
+const typeConversionBinaryOps = Object.freeze(
+ new Set([
+ "==",
+ "!=",
+ "<",
+ "<=",
+ ">",
+ ">=",
+ "<<",
+ ">>",
+ ">>>",
+ "+",
+ "-",
+ "*",
+ "/",
+ "%",
+ "|",
+ "^",
+ "&",
+ "in",
+ ])
+)
+const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"]))
+const visitor = Object.freeze(
+ Object.assign(Object.create(null), {
+ $visit(node, options, visitorKeys) {
+ const { type } = node
+
+ if (typeof this[type] === "function") {
+ return this[type](node, options, visitorKeys)
+ }
+
+ return this.$visitChildren(node, options, visitorKeys)
+ },
+
+ $visitChildren(node, options, visitorKeys) {
+ const { type } = node
+
+ for (const key of visitorKeys[type] || evk.getKeys(node)) {
+ const value = node[key]
+
+ if (Array.isArray(value)) {
+ for (const element of value) {
+ if (
+ element &&
+ this.$visit(element, options, visitorKeys)
+ ) {
+ return true
+ }
+ }
+ } else if (value && this.$visit(value, options, visitorKeys)) {
+ return true
+ }
+ }
+
+ return false
+ },
+
+ ArrowFunctionExpression() {
+ return false
+ },
+ AssignmentExpression() {
+ return true
+ },
+ AwaitExpression() {
+ return true
+ },
+ BinaryExpression(node, options, visitorKeys) {
+ if (
+ options.considerImplicitTypeConversion &&
+ typeConversionBinaryOps.has(node.operator) &&
+ (node.left.type !== "Literal" || node.right.type !== "Literal")
+ ) {
+ return true
+ }
+ return this.$visitChildren(node, options, visitorKeys)
+ },
+ CallExpression() {
+ return true
+ },
+ FunctionExpression() {
+ return false
+ },
+ ImportExpression() {
+ return true
+ },
+ MemberExpression(node, options, visitorKeys) {
+ if (options.considerGetters) {
+ return true
+ }
+ if (
+ options.considerImplicitTypeConversion &&
+ node.computed &&
+ node.property.type !== "Literal"
+ ) {
+ return true
+ }
+ return this.$visitChildren(node, options, visitorKeys)
+ },
+ MethodDefinition(node, options, visitorKeys) {
+ if (
+ options.considerImplicitTypeConversion &&
+ node.computed &&
+ node.key.type !== "Literal"
+ ) {
+ return true
+ }
+ return this.$visitChildren(node, options, visitorKeys)
+ },
+ NewExpression() {
+ return true
+ },
+ Property(node, options, visitorKeys) {
+ if (
+ options.considerImplicitTypeConversion &&
+ node.computed &&
+ node.key.type !== "Literal"
+ ) {
+ return true
+ }
+ return this.$visitChildren(node, options, visitorKeys)
+ },
+ UnaryExpression(node, options, visitorKeys) {
+ if (node.operator === "delete") {
+ return true
+ }
+ if (
+ options.considerImplicitTypeConversion &&
+ typeConversionUnaryOps.has(node.operator) &&
+ node.argument.type !== "Literal"
+ ) {
+ return true
+ }
+ return this.$visitChildren(node, options, visitorKeys)
+ },
+ UpdateExpression() {
+ return true
+ },
+ YieldExpression() {
+ return true
+ },
+ })
+)
+
+/**
+ * Check whether a given node has any side effect or not.
+ * @param {Node} node The node to get.
+ * @param {SourceCode} sourceCode The source code object.
+ * @param {object} [options] The option object.
+ * @param {boolean} [options.considerGetters=false] If `true` then it considers member accesses as the node which has side effects.
+ * @param {boolean} [options.considerImplicitTypeConversion=false] If `true` then it considers implicit type conversion as the node which has side effects.
+ * @param {object} [options.visitorKeys=evk.KEYS] The keys to traverse nodes. Use `context.getSourceCode().visitorKeys`.
+ * @returns {boolean} `true` if the node has a certain side effect.
+ */
+export function hasSideEffect(
+ node,
+ sourceCode,
+ { considerGetters = false, considerImplicitTypeConversion = false } = {}
+) {
+ return visitor.$visit(
+ node,
+ { considerGetters, considerImplicitTypeConversion },
+ sourceCode.visitorKeys || evk.KEYS
+ )
+}
diff --git a/src/index.js b/src/index.js
index 02e6a1b..4641431 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,6 +5,8 @@ import { getInnermostScope } from "./get-innermost-scope"
import { getPropertyName } from "./get-property-name"
import { getStaticValue } from "./get-static-value"
import { getStringIfConstant } from "./get-string-if-constant"
+import { hasSideEffect } from "./has-side-effect"
+import { isParenthesized } from "./is-parenthesized"
import { PatternMatcher } from "./pattern-matcher"
import {
CALL,
@@ -49,6 +51,7 @@ export default {
getPropertyName,
getStaticValue,
getStringIfConstant,
+ hasSideEffect,
isArrowToken,
isClosingBraceToken,
isClosingBracketToken,
@@ -70,6 +73,7 @@ export default {
isOpeningBraceToken,
isOpeningBracketToken,
isOpeningParenToken,
+ isParenthesized,
isSemicolonToken,
PatternMatcher,
READ,
@@ -86,6 +90,7 @@ export {
getPropertyName,
getStaticValue,
getStringIfConstant,
+ hasSideEffect,
isArrowToken,
isClosingBraceToken,
isClosingBracketToken,
@@ -107,6 +112,7 @@ export {
isOpeningBraceToken,
isOpeningBracketToken,
isOpeningParenToken,
+ isParenthesized,
isSemicolonToken,
PatternMatcher,
READ,
diff --git a/src/is-parenthesized.js b/src/is-parenthesized.js
new file mode 100644
index 0000000..1ec81a4
--- /dev/null
+++ b/src/is-parenthesized.js
@@ -0,0 +1,114 @@
+import { isClosingParenToken, isOpeningParenToken } from "./token-predicate"
+
+/**
+ * Get the left parenthesis of the parent node syntax if it exists.
+ * E.g., `if (a) {}` then the `(`.
+ * @param {Node} node The AST node to check.
+ * @param {SourceCode} sourceCode The source code object to get tokens.
+ * @returns {Token|null} The left parenthesis of the parent node syntax
+ */
+function getParentSyntaxParen(node, sourceCode) {
+ const parent = node.parent
+
+ switch (parent.type) {
+ case "CallExpression":
+ case "NewExpression":
+ if (parent.arguments.length === 1 && parent.arguments[0] === node) {
+ return sourceCode.getTokenAfter(
+ parent.callee,
+ isOpeningParenToken
+ )
+ }
+ return null
+
+ case "DoWhileStatement":
+ if (parent.test === node) {
+ return sourceCode.getTokenAfter(
+ parent.body,
+ isOpeningParenToken
+ )
+ }
+ return null
+
+ case "IfStatement":
+ case "WhileStatement":
+ if (parent.test === node) {
+ return sourceCode.getFirstToken(parent, 1)
+ }
+ return null
+
+ case "ImportExpression":
+ if (parent.source === node) {
+ return sourceCode.getFirstToken(parent, 1)
+ }
+ return null
+
+ case "SwitchStatement":
+ if (parent.discriminant === node) {
+ return sourceCode.getFirstToken(parent, 1)
+ }
+ return null
+
+ case "WithStatement":
+ if (parent.object === node) {
+ return sourceCode.getFirstToken(parent, 1)
+ }
+ return null
+
+ default:
+ return null
+ }
+}
+
+/**
+ * Check whether a given node is parenthesized or not.
+ * @param {number} times The number of parantheses.
+ * @param {Node} node The AST node to check.
+ * @param {SourceCode} sourceCode The source code object to get tokens.
+ * @returns {boolean} `true` if the node is parenthesized the given times.
+ */
+/**
+ * Check whether a given node is parenthesized or not.
+ * @param {Node} node The AST node to check.
+ * @param {SourceCode} sourceCode The source code object to get tokens.
+ * @returns {boolean} `true` if the node is parenthesized.
+ */
+export function isParenthesized(
+ timesOrNode,
+ nodeOrSourceCode,
+ optionalSourceCode
+) {
+ let times, node, sourceCode, maybeLeftParen, maybeRightParen
+ if (typeof timesOrNode === "number") {
+ times = timesOrNode | 0
+ node = nodeOrSourceCode
+ sourceCode = optionalSourceCode
+ if (!(times >= 1)) {
+ throw new TypeError("'times' should be a positive integer.")
+ }
+ } else {
+ times = 1
+ node = timesOrNode
+ sourceCode = nodeOrSourceCode
+ }
+
+ if (node == null) {
+ return false
+ }
+
+ maybeLeftParen = maybeRightParen = node
+ do {
+ maybeLeftParen = sourceCode.getTokenBefore(maybeLeftParen)
+ maybeRightParen = sourceCode.getTokenAfter(maybeRightParen)
+ } while (
+ maybeLeftParen != null &&
+ maybeRightParen != null &&
+ isOpeningParenToken(maybeLeftParen) &&
+ isClosingParenToken(maybeRightParen) &&
+ // Avoid false positive such as `if (a) {}`
+ maybeLeftParen !== getParentSyntaxParen(node, sourceCode) &&
+ --times > 0
+ )
+
+ return times === 0
+}
diff --git a/src/pattern-matcher.js b/src/pattern-matcher.js
index 8ab56f9..35f5a17 100644
--- a/src/pattern-matcher.js
+++ b/src/pattern-matcher.js
@@ -3,7 +3,7 @@
* See LICENSE file in root directory for full license.
*/
-const placeholder = /\$(?:[$&`']|[1-9][0-9]?)/g
+const placeholder = /\$(?:[$&`']|[1-9][0-9]?)/gu
/** @type {WeakMap} */
const internal = new WeakMap()
@@ -70,7 +70,6 @@ function replaceS(matcher, str, replacement) {
return chunks.join("")
}
-//eslint-disable-next-line valid-jsdoc
/**
* Replace a given string by a given matcher.
* @param {PatternMatcher} matcher The pattern matcher.
@@ -146,7 +145,6 @@ export class PatternMatcher {
return !ret.done
}
- //eslint-disable-next-line valid-jsdoc
/**
* Replace a given string.
* @param {string} str The string to be replaced.
diff --git a/src/reference-tracker.js b/src/reference-tracker.js
index f108b39..ec5a193 100644
--- a/src/reference-tracker.js
+++ b/src/reference-tracker.js
@@ -2,8 +2,7 @@ import { findVariable } from "./find-variable"
import { getPropertyName } from "./get-property-name"
import { getStringIfConstant } from "./get-string-if-constant"
-const SENTINEL_TYPE = /^(?:.+?Statement|.+?Declaration|(?:Array|ArrowFunction|Assignment|Call|Class|Function|Member|New|Object)Expression|AssignmentPattern|Program|VariableDeclarator)$/
-const IMPORT_TYPE = /^(?:Import|Export(?:All|Default|Named))Declaration$/
+const IMPORT_TYPE = /^(?:Import|Export(?:All|Default|Named))Declaration$/u
const has = Function.call.bind(Object.hasOwnProperty)
export const READ = Symbol("read")
@@ -26,6 +25,28 @@ function isModifiedGlobal(variable) {
)
}
+/**
+ * Check if the value of a given node is passed through to the parent syntax as-is.
+ * For example, `a` and `b` in (`a || b` and `c ? a : b`) are passed through.
+ * @param {Node} node A node to check.
+ * @returns {boolean} `true` if the node is passed through.
+ */
+function isPassThrough(node) {
+ const parent = node.parent
+
+ switch (parent && parent.type) {
+ case "ConditionalExpression":
+ return parent.consequent === node || parent.alternate === node
+ case "LogicalExpression":
+ return true
+ case "SequenceExpression":
+ return parent.expressions[parent.expressions.length - 1] === node
+
+ default:
+ return false
+ }
+}
+
/**
* The reference tracker.
*/
@@ -162,11 +183,11 @@ export class ReferenceTracker {
esm
? nextTraceMap
: this.mode === "legacy"
- ? Object.assign(
- { default: nextTraceMap },
- nextTraceMap
- )
- : { default: nextTraceMap }
+ ? Object.assign(
+ { default: nextTraceMap },
+ nextTraceMap
+ )
+ : { default: nextTraceMap }
)
if (esm) {
@@ -224,10 +245,10 @@ export class ReferenceTracker {
* @param {object} traceMap The trace map.
* @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references.
*/
- //eslint-disable-next-line complexity, require-jsdoc
+ //eslint-disable-next-line complexity
*_iteratePropertyReferences(rootNode, path, traceMap) {
let node = rootNode
- while (!SENTINEL_TYPE.test(node.parent.type)) {
+ while (isPassThrough(node)) {
node = node.parent
}
diff --git a/test/find-variable.js b/test/find-variable.js
index d0060a8..d198607 100644
--- a/test/find-variable.js
+++ b/test/find-variable.js
@@ -3,7 +3,6 @@ import eslint from "eslint"
import { findVariable } from "../src/"
describe("The 'findVariable' function", () => {
- //eslint-disable-next-line require-jsdoc
function getVariable(code, selector, withString = null) {
const linter = new eslint.Linter()
let variable = null
diff --git a/test/get-static-value.js b/test/get-static-value.js
index af34f91..a9e3c29 100644
--- a/test/get-static-value.js
+++ b/test/get-static-value.js
@@ -59,7 +59,7 @@ describe("The 'getStaticValue' function", () => {
{ code: "false", expected: { value: false } },
{ code: "1", expected: { value: 1 } },
{ code: "'hello'", expected: { value: "hello" } },
- { code: "/foo/g", expected: { value: /foo/g } },
+ { code: "/foo/gu", expected: { value: /foo/gu } },
{ code: "true && 1", expected: { value: 1 } },
{ code: "false && a", expected: { value: false } },
{ code: "true || a", expected: { value: true } },
@@ -75,7 +75,7 @@ describe("The 'getStaticValue' function", () => {
{ code: "Symbol[iterator]", expected: null },
{ code: "Object.freeze", expected: { value: Object.freeze } },
{ code: "Object.xxx", expected: { value: undefined } },
- { code: "new Array(2)", expected: { value: new Array(2) } },
+ { code: "new Array(2)", expected: null },
{ code: "new Array(len)", expected: null },
{ code: "({})", expected: { value: {} } },
{
@@ -116,6 +116,33 @@ const aMap = Object.freeze({
;\`on\${eventName} : \${aMap[eventName]}\``,
expected: { value: "onclick : 777" },
},
+ {
+ code: 'Function("return process.env.npm_name")()',
+ expected: null,
+ },
+ {
+ code: 'new Function("return process.env.npm_name")()',
+ expected: null,
+ },
+ {
+ code:
+ '({}.constructor.constructor("return process.env.npm_name")())',
+ expected: null,
+ },
+ {
+ code:
+ 'JSON.stringify({a:1}, new {}.constructor.constructor("console.log(\\"code injected\\"); process.exit(1)"), 2)',
+ expected: null,
+ },
+ {
+ code:
+ 'Object.create(null, {a:{get:new {}.constructor.constructor("console.log(\\"code injected\\"); process.exit(1)")}}).a',
+ expected: null,
+ },
+ {
+ code: "RegExp.$1",
+ expected: null,
+ },
]) {
it(`should return ${JSON.stringify(expected)} from ${code}`, () => {
const linter = new eslint.Linter()
@@ -138,7 +165,7 @@ const aMap = Object.freeze({
if (actual == null) {
assert.strictEqual(actual, expected)
} else {
- assert.deepStrictEqual(actual.value, expected.value)
+ assert.deepStrictEqual(actual, expected)
}
})
}
diff --git a/test/get-string-if-constant.js b/test/get-string-if-constant.js
index 2761d25..b100410 100644
--- a/test/get-string-if-constant.js
+++ b/test/get-string-if-constant.js
@@ -19,6 +19,7 @@ describe("The 'getStringIfConstant' function", () => {
{ code: "`aaa${id}bbb`", expected: null }, //eslint-disable-line no-template-curly-in-string
{ code: "1 + 2", expected: "3" },
{ code: "'a' + 'b'", expected: "ab" },
+ { code: "/(?\\w+)\\k/gu", expected: "/(?\\w+)\\k/gu" },
]) {
it(`should return ${JSON.stringify(expected)} from ${code}`, () => {
const linter = new eslint.Linter()
diff --git a/test/has-side-effect.js b/test/has-side-effect.js
new file mode 100644
index 0000000..d84c300
--- /dev/null
+++ b/test/has-side-effect.js
@@ -0,0 +1,257 @@
+import assert from "assert"
+import eslint from "eslint"
+import dp from "dot-prop"
+import { hasSideEffect } from "../src/"
+
+describe("The 'hasSideEffect' function", () => {
+ for (const { code, key = "body.0.expression", options, expected } of [
+ {
+ code: "777",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "foo",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "a = 0",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "async function f() { await g }",
+ key: "body.0.body.body.0.expression",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "a + b",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "a + b",
+ options: { considerImplicitTypeConversion: true },
+ expected: true,
+ },
+ {
+ code: "1 + 2",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "f()",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "a + f()",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "obj.a",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "obj.a",
+ options: { considerGetters: 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 },
+ expected: false,
+ },
+ {
+ code: "obj['@@abc']",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "(class { f() { a++ } })",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "(class { f() { a++ } })",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "(class { [f]() { a++ } })",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "(class { [f]() { a++ } })",
+ options: { considerImplicitTypeConversion: true },
+ expected: true,
+ },
+ {
+ code: "(class { [0]() { a++ } })",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "(class { ['@@']() { a++ } })",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "new C()",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "({ key: 1 })",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "({ key: 1 })",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "({ [key]: 1 })",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "({ [key]: 1 })",
+ options: { considerImplicitTypeConversion: true },
+ expected: true,
+ },
+ {
+ code: "({ [0]: 1 })",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "({ ['@@']: 1 })",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "+1",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "+1",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "-1",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "-1",
+ options: { considerImplicitTypeConversion: true },
+ expected: false,
+ },
+ {
+ code: "+a",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "+a",
+ options: { considerImplicitTypeConversion: true },
+ expected: true,
+ },
+ {
+ code: "delete obj.a",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "++a",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "function* g() { yield 1 }",
+ key: "body.0.body.body.0.expression",
+ options: undefined,
+ expected: true,
+ },
+ {
+ code: "(a, b, c)",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "(a, b, ++c)",
+ options: undefined,
+ expected: true,
+ },
+
+ // Skip the definition body.
+ {
+ code: "(function f(a) { a++ })",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "((a) => { a++ })",
+ options: undefined,
+ expected: false,
+ },
+ {
+ code: "((a) => a++)",
+ options: undefined,
+ expected: false,
+ },
+ ]) {
+ it(`should return ${expected} on the code \`${code}\` and the options \`${JSON.stringify(
+ options
+ )}\``, () => {
+ const linter = new eslint.Linter()
+
+ let actual = null
+ linter.defineRule("test", context => ({
+ Program(node) {
+ actual = hasSideEffect(
+ dp.get(node, key),
+ context.getSourceCode(),
+ options
+ )
+ },
+ }))
+ const messages = linter.verify(code, {
+ env: { es6: true },
+ parserOptions: { ecmaVersion: 2018 },
+ rules: { test: "error" },
+ })
+
+ assert.strictEqual(
+ messages.length,
+ 0,
+ messages[0] && messages[0].message
+ )
+ assert.strictEqual(actual, expected)
+ })
+ }
+})
diff --git a/test/is-parenthesized.js b/test/is-parenthesized.js
new file mode 100644
index 0000000..f27722a
--- /dev/null
+++ b/test/is-parenthesized.js
@@ -0,0 +1,313 @@
+import assert from "assert"
+import dotProp from "dot-prop"
+import eslint from "eslint"
+import { isParenthesized } from "../src/"
+
+describe("The 'isParenthesized' function", () => {
+ for (const { code, expected } of [
+ {
+ code: "777",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ },
+ },
+ {
+ code: "(777)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": true,
+ },
+ },
+ {
+ code: "(777 + 223)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": true,
+ "body.0.expression.left": false,
+ "body.0.expression.right": false,
+ },
+ },
+ {
+ code: "(777) + 223",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.left": true,
+ "body.0.expression.right": false,
+ },
+ },
+ {
+ code: "((777) + 223)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": true,
+ "body.0.expression.left": true,
+ "body.0.expression.right": false,
+ },
+ },
+ {
+ code: "f()",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": false,
+ },
+ },
+ {
+ code: "(f())",
+ expected: {
+ "body.0": false,
+ "body.0.expression": true,
+ "body.0.expression.arguments.0": false,
+ },
+ },
+ {
+ code: "f(a)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": false,
+ },
+ },
+ {
+ code: "f((a))",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": true,
+ },
+ },
+ {
+ code: "f(a,b)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": false,
+ "body.0.expression.arguments.1": false,
+ },
+ },
+ {
+ code: "f((a),b)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": true,
+ "body.0.expression.arguments.1": false,
+ },
+ },
+ {
+ code: "f(a,(b))",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": false,
+ "body.0.expression.arguments.1": true,
+ },
+ },
+ {
+ code: "new f(a)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": false,
+ },
+ },
+ {
+ code: "new f((a))",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ "body.0.expression.arguments.0": true,
+ },
+ },
+ {
+ code: "do f(); while (a)",
+ expected: {
+ "body.0": false,
+ "body.0.test": false,
+ "body.0.body": false,
+ "body.0.body.expression": false,
+ },
+ },
+ {
+ code: "do (f()); while ((a))",
+ expected: {
+ "body.0": false,
+ "body.0.test": true,
+ "body.0.body": false,
+ "body.0.body.expression": true,
+ },
+ },
+ {
+ code: "if (a) b()",
+ expected: {
+ "body.0": false,
+ "body.0.test": false,
+ "body.0.consequent": false,
+ "body.0.consequent.expression": false,
+ },
+ },
+ {
+ code: "if ((a)) (b())",
+ expected: {
+ "body.0": false,
+ "body.0.test": true,
+ "body.0.consequent": false,
+ "body.0.consequent.expression": true,
+ },
+ },
+ {
+ code: "while (a) b()",
+ expected: {
+ "body.0": false,
+ "body.0.test": false,
+ "body.0.body": false,
+ "body.0.body.expression": false,
+ },
+ },
+ {
+ code: "while ((a)) (b())",
+ expected: {
+ "body.0": false,
+ "body.0.test": true,
+ "body.0.body": false,
+ "body.0.body.expression": true,
+ },
+ },
+ {
+ code: "switch (a) {}",
+ expected: {
+ "body.0": false,
+ "body.0.discriminant": false,
+ },
+ },
+ {
+ code: "switch ((a)) {}",
+ expected: {
+ "body.0": false,
+ "body.0.discriminant": true,
+ },
+ },
+ {
+ code: "with (a) {}",
+ expected: {
+ "body.0": false,
+ "body.0.object": false,
+ },
+ },
+ {
+ code: "with ((a)) {}",
+ expected: {
+ "body.0": false,
+ "body.0.object": true,
+ },
+ },
+ ]) {
+ describe(`on the code \`${code}\``, () => {
+ for (const key of Object.keys(expected)) {
+ it(`should return ${expected[key]} at "${key}"`, () => {
+ const linter = new eslint.Linter()
+
+ let actual = null
+ linter.defineRule("test", context => ({
+ Program(node) {
+ actual = isParenthesized(
+ dotProp.get(node, key),
+ context.getSourceCode()
+ )
+ },
+ }))
+ const messages = linter.verify(code, {
+ env: { es6: true },
+ parserOptions: { ecmaVersion: 2018 },
+ rules: { test: "error" },
+ })
+
+ assert.strictEqual(
+ messages.length,
+ 0,
+ messages[0] && messages[0].message
+ )
+ assert.strictEqual(actual, expected[key])
+ })
+ }
+ })
+ }
+
+ for (const { code, expected } of [
+ {
+ code: "777",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ },
+ },
+ {
+ code: "(777)",
+ expected: {
+ "body.0": false,
+ "body.0.expression": false,
+ },
+ },
+ {
+ code: "((777))",
+ expected: {
+ "body.0": false,
+ "body.0.expression": true,
+ },
+ },
+ {
+ code: "if (a) ;",
+ expected: {
+ "body.0": false,
+ "body.0.test": false,
+ },
+ },
+ {
+ code: "if ((a)) ;",
+ expected: {
+ "body.0": false,
+ "body.0.test": false,
+ },
+ },
+ {
+ code: "if (((a))) ;",
+ expected: {
+ "body.0": false,
+ "body.0.test": true,
+ },
+ },
+ ]) {
+ describe(`on the code \`${code}\` and 2 times`, () => {
+ for (const key of Object.keys(expected)) {
+ it(`should return ${expected[key]} at "${key}"`, () => {
+ const linter = new eslint.Linter()
+
+ let actual = null
+ linter.defineRule("test", context => ({
+ Program(node) {
+ actual = isParenthesized(
+ 2,
+ dotProp.get(node, key),
+ context.getSourceCode()
+ )
+ },
+ }))
+ const messages = linter.verify(code, {
+ env: { es6: true },
+ parserOptions: { ecmaVersion: 2018 },
+ rules: { test: "error" },
+ })
+
+ assert.strictEqual(
+ messages.length,
+ 0,
+ messages[0] && messages[0].message
+ )
+ assert.strictEqual(actual, expected[key])
+ })
+ }
+ })
+ }
+})
diff --git a/test/pattern-matcher.js b/test/pattern-matcher.js
index 7e17a26..717867f 100644
--- a/test/pattern-matcher.js
+++ b/test/pattern-matcher.js
@@ -3,7 +3,7 @@ import { PatternMatcher } from "../src/"
const NAMED_CAPTURE_GROUP_SUPPORTED = (() => {
try {
- new RegExp("(?)", "u") //eslint-disable-line no-new, @mysticatea/node/no-unsupported-features
+ new RegExp("(?)", "u") //eslint-disable-line no-new, prefer-regex-literals, @mysticatea/node/no-unsupported-features/es-syntax
return true
} catch (_error) {
return false
@@ -44,16 +44,16 @@ describe("The 'PatternMatcher' class:", () => {
]) {
assert.throws(
() => new PatternMatcher(value),
- /^TypeError: 'pattern' should be a RegExp instance\.$/
+ /^TypeError: 'pattern' should be a RegExp instance\.$/u
)
}
})
it("should throw Error if the RegExp value does not have 'g' flag.", () => {
- for (const value of [/foo/, /bar/im]) {
+ for (const value of [/foo/u, /bar/imu]) {
assert.throws(
() => new PatternMatcher(value),
- /^Error: 'pattern' should contains 'g' flag\.$/
+ /^Error: 'pattern' should contains 'g' flag\.$/u
)
}
})
@@ -116,7 +116,7 @@ describe("The 'PatternMatcher' class:", () => {
it(`should return ${JSON.stringify(
expected
)} in ${JSON.stringify(str)}.`, () => {
- const matcher = new PatternMatcher(/foo/g)
+ const matcher = new PatternMatcher(/foo/gu)
const actual = Array.from(matcher.execAll(str))
assert.deepStrictEqual(actual, expected)
})
@@ -139,14 +139,14 @@ describe("The 'PatternMatcher' class:", () => {
it(`should return ${JSON.stringify(
expected
)} in ${JSON.stringify(str)}.`, () => {
- const matcher = new PatternMatcher(/(\w)(\d)/g)
+ const matcher = new PatternMatcher(/(\w)(\d)/gu)
const actual = Array.from(matcher.execAll(str))
assert.deepStrictEqual(actual, expected)
})
}
it("should iterate for two strings in parallel.", () => {
- const matcher = new PatternMatcher(/\w/g)
+ const matcher = new PatternMatcher(/\w/gu)
const expected1 = [
newRegExpExecArray(["a"], 0, "a--b-c"),
newRegExpExecArray(["b"], 3, "a--b-c"),
@@ -213,7 +213,7 @@ describe("The 'PatternMatcher' class:", () => {
it(`should return ${JSON.stringify(
expected
)} in ${JSON.stringify(str)}.`, () => {
- const matcher = new PatternMatcher(/foo/g, {
+ const matcher = new PatternMatcher(/foo/gu, {
escaped: true,
})
const actual = Array.from(matcher.execAll(str))
@@ -238,7 +238,7 @@ describe("The 'PatternMatcher' class:", () => {
{ str: String.raw`-foo\foofooabcfoo-`, expected: true },
]) {
it(`should return ${expected} in ${JSON.stringify(str)}.`, () => {
- const matcher = new PatternMatcher(/foo/g)
+ const matcher = new PatternMatcher(/foo/gu)
const actual = matcher.test(str)
assert.deepStrictEqual(actual, expected)
})
@@ -278,7 +278,7 @@ describe("The 'PatternMatcher' class:", () => {
{ str: "abc", replacer: "$0", expected: "$0$0$0" },
{ str: "abc", replacer: "$1", expected: "$1$1$1" },
{
- pattern: /a(b)/g,
+ pattern: /a(b)/gu,
str: "abc",
replacer: "$1",
expected: "bc",
@@ -287,14 +287,14 @@ describe("The 'PatternMatcher' class:", () => {
it(`should return ${expected} in ${JSON.stringify(
str
)} and ${JSON.stringify(replacer)}.`, () => {
- const matcher = new PatternMatcher(pattern || /[a-c]/g)
+ const matcher = new PatternMatcher(pattern || /[a-c]/gu)
const actual = str.replace(matcher, replacer)
assert.deepStrictEqual(actual, expected)
})
}
- it(`should pass the correct arguments to replacers.`, () => {
- const matcher = new PatternMatcher(/(\w)(\d)/g)
+ it("should pass the correct arguments to replacers.", () => {
+ const matcher = new PatternMatcher(/(\w)(\d)/gu)
const actualArgs = []
const actual = "abc1d2efg".replace(matcher, (...args) => {
actualArgs.push(args)
diff --git a/test/reference-tracker.js b/test/reference-tracker.js
index fc8b3ad..eb7166e 100644
--- a/test/reference-tracker.js
+++ b/test/reference-tracker.js
@@ -4,6 +4,7 @@ import { CALL, CONSTRUCT, ESM, READ, ReferenceTracker } from "../src/"
const config = {
parserOptions: { ecmaVersion: 2018, sourceType: "module" },
+ globals: { Reflect: false },
rules: { test: "error" },
}
@@ -369,6 +370,18 @@ describe("The 'ReferenceTracker' class:", () => {
},
expected: [],
},
+ {
+ description:
+ "should not iterate the references through unary/binary expressions.",
+ code: [
+ 'var construct = typeof Reflect !== "undefined" ? Reflect.construct : undefined',
+ "construct()",
+ ].join("\n"),
+ traceMap: {
+ Reflect: { [CALL]: 1 },
+ },
+ expected: [],
+ },
]) {
it(description, () => {
const linter = new eslint.Linter()