From a5a026bd813ac7d30d2619c69a0a58a3b422ebd1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 16 Nov 2017 21:02:50 +0900 Subject: [PATCH] New: eslint-visitor-keys (#1) * New: eslint-visitor-keys * fix license field of package.json * update for review - fix the doc of `getKeys` - clarify the order of keys for `unionWith` --- .eslintrc.json | 7 ++ .gitattributes | 1 + .gitignore | 5 + .npmrc | 1 + .travis.yml | 8 ++ README.md | 98 ++++++++++++++++ lib/index.js | 81 ++++++++++++++ lib/visitor-keys.json | 235 +++++++++++++++++++++++++++++++++++++++ package.json | 40 +++++++ tests/lib/.eslintrc.json | 5 + tests/lib/index.js | 70 ++++++++++++ 11 files changed, 551 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 lib/index.js create mode 100644 lib/visitor-keys.json create mode 100644 package.json create mode 100644 tests/lib/.eslintrc.json create mode 100644 tests/lib/index.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7c36f98 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "extends": "eslint", + "env": { + "es6": true, + "node": true + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..88b9c12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.nyc_output +/.vscode +/coverage +/node_modules +/test.* diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..030b4a4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +sudo: false + +language: node_js +node_js: + - "4.0.0" + - "4" + - "6" + - "8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..250f5fa --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# eslint-visitor-keys + +[![npm version](https://img.shields.io/npm/v/eslint-visitor-keys.svg)](https://www.npmjs.com/package/eslint-visitor-keys) +[![Downloads/month](https://img.shields.io/npm/dm/eslint-visitor-keys.svg)](http://www.npmtrends.com/eslint-visitor-keys) +[![Build Status](https://travis-ci.org/eslint/eslint-visitor-keys.svg?branch=master)](https://travis-ci.org/eslint/eslint-visitor-keys) +[![Dependency Status](https://david-dm.org/eslint/eslint-visitor-keys.svg)](https://david-dm.org/eslint/eslint-visitor-keys) + +Constants and utilities about visitor keys to traverse AST. + +## 💿 Installation + +Use [npm] to install. + +```bash +$ npm install eslint-visitor-keys +``` + +### Requirements + +- [Node.js] 4.0.0 or later. + +## 📖 Usage + +```js +const evk = require("eslint-visitor-keys") +``` + +### evk.KEYS + +> type: `{ [type: string]: string[] | undefined }` + +Visitor keys. This keys are frozen. + +This is an object. Keys are the type of [ESTree] nodes. Their values are an array of property names which have child nodes. + +For example: + +``` +console.log(evk.KEYS.AssignmentExpression) // → ["left", "right"] +``` + +### evk.getKeys(node) + +> type: `(node: object) => string[]` + +Get the visitor keys of a given AST node. + +This is similar to `Object.keys(node)` of ES Standard, but some keys are excluded: `parent`, `leadingComments`, `trailingComments`, and names which start with `_`. + +This will be used to traverse unknown nodes. + +For example: + +``` +const node = { + type: "AssignmentExpression", + left: { type: "Identifier", name: "foo" }, + right: { type: "Literal", value: 0 } +} +console.log(evk.getKeys(node)) // → ["type", "left", "right"] +``` + +### evk.unionWith(additionalKeys) + +> type: `(additionalKeys: object) => { [type: string]: string[] | undefined }` + +Make the union set with `evk.KEYS` and the given keys. + +- The order of keys is, `additionalKeys` is at first, then `evk.KEYS` is concatenated after that. +- It removes duplicated keys as keeping the first one. + +For example: + +``` +console.log(evk.unionWith({ + MethodDefinition: ["decorators"] +})) // → { ..., MethodDefinition: ["decorators", "key", "value"], ... } +``` + +## 📰 Change log + +See [GitHub releases](https://github.com/eslint/eslint-visitor-keys/releases). + +## 🍻 Contributing + +Welcome. See [ESLint contribution guidelines](https://eslint.org/docs/developer-guide/contributing/). + +### Development commands + +- `npm test` runs tests and measures code coverage. +- `npm run lint` checks source codes with ESLint. +- `npm run coverage` opens the code coverage report of the previous test with your default browser. +- `npm run release` publishes this package to [npm] registory. + + +[npm]: https://www.npmjs.com/ +[Node.js]: https://nodejs.org/en/ +[ESTree]: https://github.com/estree/estree diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..cd8a326 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,81 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict"; + +const KEYS = require("./visitor-keys.json"); + +// Types. +const NODE_TYPES = Object.freeze(Object.keys(KEYS)); + +// Freeze the keys. +for (const type of NODE_TYPES) { + Object.freeze(KEYS[type]); +} +Object.freeze(KEYS); + +// List to ignore keys. +const KEY_BLACKLIST = new Set([ + "parent", + "leadingComments", + "trailingComments" +]); + +/** + * Check whether a given key should be used or not. + * @param {string} key The key to check. + * @returns {boolean} `true` if the key should be used. + */ +function filterKey(key) { + return !KEY_BLACKLIST.has(key) && key[0] !== "_"; +} + +//------------------------------------------------------------------------------ +// Public interfaces +//------------------------------------------------------------------------------ + +module.exports = Object.freeze({ + + /** + * Visitor keys. + * @type {{ [type: string]: string[] | undefined }} + */ + KEYS, + + /** + * Get visitor keys of a given node. + * @param {Object} node The AST node to get keys. + * @returns {string[]} Visitor keys of the node. + */ + getKeys(node) { + return Object.keys(node).filter(filterKey); + }, + + // Disable valid-jsdoc rule because it reports syntax error on the type of @returns. + // eslint-disable-next-line valid-jsdoc + /** + * Make the union set with `KEYS` and given keys. + * @param {Object} additionalKeys The additional keys. + * @returns {{ [type: string]: string[] | undefined }} The union set. + */ + unionWith(additionalKeys) { + const retv = Object.assign({}, KEYS); + + for (const type of Object.keys(additionalKeys)) { + if (retv.hasOwnProperty(type)) { + const keys = new Set(additionalKeys[type]); + + for (const key of retv[type]) { + keys.add(key); + } + + retv[type] = Object.freeze(Array.from(keys)); + } else { + retv[type] = Object.freeze(Array.from(additionalKeys[type])); + } + } + + return Object.freeze(retv); + } +}); diff --git a/lib/visitor-keys.json b/lib/visitor-keys.json new file mode 100644 index 0000000..a8898be --- /dev/null +++ b/lib/visitor-keys.json @@ -0,0 +1,235 @@ +{ + "AssignmentExpression": [ + "left", + "right" + ], + "AssignmentPattern": [ + "left", + "right" + ], + "ArrayExpression": [ + "elements" + ], + "ArrayPattern": [ + "elements" + ], + "ArrowFunctionExpression": [ + "params", + "body" + ], + "AwaitExpression": [ + "argument" + ], + "BlockStatement": [ + "body" + ], + "BinaryExpression": [ + "left", + "right" + ], + "BreakStatement": [ + "label" + ], + "CallExpression": [ + "callee", + "arguments" + ], + "CatchClause": [ + "param", + "body" + ], + "ClassBody": [ + "body" + ], + "ClassDeclaration": [ + "id", + "superClass", + "body" + ], + "ClassExpression": [ + "id", + "superClass", + "body" + ], + "ConditionalExpression": [ + "test", + "consequent", + "alternate" + ], + "ContinueStatement": [ + "label" + ], + "DebuggerStatement": [], + "DirectiveStatement": [], + "DoWhileStatement": [ + "body", + "test" + ], + "EmptyStatement": [], + "ExportAllDeclaration": [ + "source" + ], + "ExportDefaultDeclaration": [ + "declaration" + ], + "ExportNamedDeclaration": [ + "declaration", + "specifiers", + "source" + ], + "ExportSpecifier": [ + "exported", + "local" + ], + "ExpressionStatement": [ + "expression" + ], + "ForStatement": [ + "init", + "test", + "update", + "body" + ], + "ForInStatement": [ + "left", + "right", + "body" + ], + "ForOfStatement": [ + "left", + "right", + "body" + ], + "FunctionDeclaration": [ + "id", + "params", + "body" + ], + "FunctionExpression": [ + "id", + "params", + "body" + ], + "Identifier": [], + "IfStatement": [ + "test", + "consequent", + "alternate" + ], + "ImportDeclaration": [ + "specifiers", + "source" + ], + "ImportDefaultSpecifier": [ + "local" + ], + "ImportNamespaceSpecifier": [ + "local" + ], + "ImportSpecifier": [ + "imported", + "local" + ], + "Literal": [], + "LabeledStatement": [ + "label", + "body" + ], + "LogicalExpression": [ + "left", + "right" + ], + "MemberExpression": [ + "object", + "property" + ], + "MetaProperty": [ + "meta", + "property" + ], + "MethodDefinition": [ + "key", + "value" + ], + "ModuleSpecifier": [], + "NewExpression": [ + "callee", + "arguments" + ], + "ObjectExpression": [ + "properties" + ], + "ObjectPattern": [ + "properties" + ], + "Program": [ + "body" + ], + "Property": [ + "key", + "value" + ], + "RestElement": [ + "argument" + ], + "ReturnStatement": [ + "argument" + ], + "SequenceExpression": [ + "expressions" + ], + "SpreadElement": [ + "argument" + ], + "Super": [], + "SwitchStatement": [ + "discriminant", + "cases" + ], + "SwitchCase": [ + "test", + "consequent" + ], + "TaggedTemplateExpression": [ + "tag", + "quasi" + ], + "TemplateElement": [], + "TemplateLiteral": [ + "quasis", + "expressions" + ], + "ThisExpression": [], + "ThrowStatement": [ + "argument" + ], + "TryStatement": [ + "block", + "handler", + "finalizer" + ], + "UnaryExpression": [ + "argument" + ], + "UpdateExpression": [ + "argument" + ], + "VariableDeclaration": [ + "declarations" + ], + "VariableDeclarator": [ + "id", + "init" + ], + "WhileStatement": [ + "test", + "body" + ], + "WithStatement": [ + "object", + "body" + ], + "YieldExpression": [ + "argument" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..32306da --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "eslint-visitor-keys", + "version": "0.0.0", + "description": "Constants and utilities about visitor keys to traverse AST.", + "main": "lib/index.js", + "files": [ + "lib" + ], + "engines": { + "node": ">=4" + }, + "dependencies": {}, + "devDependencies": { + "eslint": "^4.7.2", + "eslint-config-eslint": "^4.0.0", + "eslint-release": "^0.10.3", + "mocha": "^3.5.3", + "nyc": "^11.2.1", + "opener": "^1.4.3" + }, + "scripts": { + "lint": "eslint lib tests/lib", + "pretest": "npm run -s lint", + "test": "nyc mocha tests/lib", + "coverage": "nyc report --reporter lcov && opener coverage/lcov-report/index.html", + "release": "eslint-release", + "ci-release": "eslint-ci-release" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/eslint/eslint-visitor-keys.git" + }, + "keywords": [], + "author": "Toru Nagashima (https://github.com/mysticatea)", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/eslint/eslint-visitor-keys/issues" + }, + "homepage": "https://github.com/eslint/eslint-visitor-keys#readme" +} diff --git a/tests/lib/.eslintrc.json b/tests/lib/.eslintrc.json new file mode 100644 index 0000000..4668ae7 --- /dev/null +++ b/tests/lib/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} diff --git a/tests/lib/index.js b/tests/lib/index.js new file mode 100644 index 0000000..d13d8cc --- /dev/null +++ b/tests/lib/index.js @@ -0,0 +1,70 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict"; + +const assert = require("assert"); +const fs = require("fs"); +const evk = require("../.."); + +const keys = JSON.parse(fs.readFileSync("lib/visitor-keys.json", "utf8")); + +describe("eslint-visitor-keys", () => { + describe("KEYS", () => { + it("should be same as lib/visitor-keys.json", () => { + assert.deepStrictEqual(evk.KEYS, keys); + }); + }); + + describe("getKeys()", () => { + it("should return keys", () => { + assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2 }), ["a", "b"]); + }); + + it("should not include 'parent' in the result", () => { + assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, parent: 3 }), ["a", "b"]); + }); + + it("should not include 'leadingComments' in the result", () => { + assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, leadingComments: 3 }), ["a", "b"]); + }); + + it("should not include 'trailingComments' in the result", () => { + assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, trailingComments: 3 }), ["a", "b"]); + }); + + it("should not include '_foo' in the result", () => { + assert.deepStrictEqual(evk.getKeys({ a: 1, b: 2, _foo: 3 }), ["a", "b"]); + }); + }); + + describe("unionWith()", () => { + const additionalKeys = { Program: ["body", "a"], AssignmentExpression: ["b"], additional: ["c"], MethodDefinition: ["a", "key", "b"] }; + const unionKeys = evk.unionWith(additionalKeys); + + it("should include all keys of lib/visitor-keys.json", () => { + for (const type of Object.keys(keys)) { + for (const key of keys[type]) { + assert(unionKeys[type].indexOf(key) !== -1, `'${key}' should be included in '${type}'.`); + } + } + }); + + it("should include all additional keys", () => { + for (const type of Object.keys(additionalKeys)) { + for (const key of additionalKeys[type]) { + assert(unionKeys[type].indexOf(key) !== -1, `'${key}' should be included in '${type}'.`); + } + } + }); + + it("should not have duplicate", () => { + assert(unionKeys.Program.filter(key => key === "body").length === 1); + }); + + it("should add additional keys, then concatenate original keys", () => { + assert.deepStrictEqual(unionKeys.MethodDefinition, ["a", "key", "b", "value"]); + }); + }); +});