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 @@ [![npm version](https://img.shields.io/npm/v/eslint-utils.svg)](https://www.npmjs.com/package/eslint-utils) [![Downloads/month](https://img.shields.io/npm/dm/eslint-utils.svg)](http://www.npmtrends.com/eslint-utils) -[![Build Status](https://travis-ci.org/mysticatea/eslint-utils.svg?branch=master)](https://travis-ci.org/mysticatea/eslint-utils) +[![Build Status](https://github.com/mysticatea/eslint-utils/workflows/CI/badge.svg)](https://github.com/mysticatea/eslint-utils/actions) [![Coverage Status](https://codecov.io/gh/mysticatea/eslint-utils/branch/master/graph/badge.svg)](https://codecov.io/gh/mysticatea/eslint-utils) [![Dependency Status](https://david-dm.org/mysticatea/eslint-utils.svg)](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()