From 66456c5356310fc4309b4fe2756995f27b907747 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 5 Oct 2018 22:22:29 +0900 Subject: [PATCH] New: isParenthesized function --- docs/api/ast-utils.md | 66 +++++++++++ package.json | 1 + src/index.js | 3 + src/is-parenthesized.js | 79 +++++++++++++ test/is-parenthesized.js | 237 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 386 insertions(+) create mode 100644 src/is-parenthesized.js create mode 100644 test/is-parenthesized.js diff --git a/docs/api/ast-utils.md b/docs/api/ast-utils.md index a12a62f..7bc47b6 100644 --- a/docs/api/ast-utils.md +++ b/docs/api/ast-utils.md @@ -350,6 +350,72 @@ function getStringIfConstant(node, initialScope) { ---- +## isParenthesized + +```js +const ret = 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 +f(a); //→ this `a` is not parenthesized. +f((b)); //→ this `b` is parenthesized. + +new C(a); //→ this `a` is not parenthesized. +new C((b)); //→ this `b` is parenthesized. + +if (a) {} //→ this `a` is not parenthesized. +if ((b)) {} //→ this `b` is parenthesized. + +switch (a) {} //→ this `a` is not parenthesized. +switch ((b)) {} //→ this `b` is parenthesized. + +while (a) {} //→ this `a` is not parenthesized. +while ((b)) {} //→ this `b` is parenthesized. + +do {} while (a); //→ this `a` is not parenthesized. +do {} while ((b)); //→ this `b` is parenthesized. + +with (a) {} //→ this `a` is not parenthesized. +with ((b)) {} //→ this `b` is parenthesized. +``` + +### Parameters + + Name | Type | Description +:-----|:-----|:------------ +node | Node | The node to check. +sourceCode | SourceCode | The source code object to get tokens. + +### Return value + +`true` if the node is parenthesized. + +### 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..ddb9c94 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@mysticatea/eslint-plugin": "^5.0.1", "codecov": "^3.0.2", + "dot-prop": "^4.2.0", "eslint": "^5.0.1", "esm": "^3.0.55", "espree": "^4.0.0", diff --git a/src/index.js b/src/index.js index 02e6a1b..ddba760 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ 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 { isParenthesized } from "./is-parenthesized" import { PatternMatcher } from "./pattern-matcher" import { CALL, @@ -70,6 +71,7 @@ export default { isOpeningBraceToken, isOpeningBracketToken, isOpeningParenToken, + isParenthesized, isSemicolonToken, PatternMatcher, READ, @@ -107,6 +109,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..3fd3649 --- /dev/null +++ b/src/is-parenthesized.js @@ -0,0 +1,79 @@ +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 "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 {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(node, sourceCode) { + if (node == null) { + return false + } + + const maybeLeftParen = sourceCode.getTokenBefore(node) + const maybeRightParen = sourceCode.getTokenAfter(node) + + return ( + maybeLeftParen != null && + maybeRightParen != null && + isOpeningParenToken(maybeLeftParen) && + isClosingParenToken(maybeRightParen) && + // Avoid false positive such as `if (a) {}` + maybeLeftParen !== getParentSyntaxParen(node, sourceCode) + ) +} diff --git a/test/is-parenthesized.js b/test/is-parenthesized.js new file mode 100644 index 0000000..cdaa9e6 --- /dev/null +++ b/test/is-parenthesized.js @@ -0,0 +1,237 @@ +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]) + }) + } + }) + } +})