From caa7feca3faaff0160d3d76fc6e3ed162c79af34 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sun, 6 Feb 2022 18:58:48 +0800 Subject: [PATCH 01/16] feat: bundle JSDoc-built TypeScript declaration file --- lib/index.js | 8 ++++---- lib/visitor-keys.js | 3 +++ package.json | 8 ++++++-- tsconfig.json | 19 +++++++++++++++++++ 4 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 tsconfig.json diff --git a/lib/index.js b/lib/index.js index 44f73d6..747d024 100644 --- a/lib/index.js +++ b/lib/index.js @@ -23,7 +23,7 @@ function filterKey(key) { /** * Get visitor keys of a given node. * @param {Object} node The AST node to get keys. - * @returns {string[]} Visitor keys of the node. + * @returns {readonly string[]} Visitor keys of the node. */ export function getKeys(node) { return Object.keys(node).filter(filterKey); @@ -33,11 +33,11 @@ export function getKeys(node) { // 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. + * @param {{ readonly [type: string]: ReadonlyArray }} additionalKeys The additional keys. + * @returns {{ readonly [type: string]: ReadonlyArray }} The union set. */ export function unionWith(additionalKeys) { - const retv = Object.assign({}, KEYS); + const retv = /** @type {{ [type: string]: ReadonlyArray }} */ (Object.assign({}, KEYS)); for (const type of Object.keys(additionalKeys)) { if (Object.prototype.hasOwnProperty.call(retv, type)) { diff --git a/lib/visitor-keys.js b/lib/visitor-keys.js index 4c7be3d..c55d8f3 100644 --- a/lib/visitor-keys.js +++ b/lib/visitor-keys.js @@ -1,3 +1,6 @@ +/** + * @type {{ readonly [type: string]: ReadonlyArray }} + */ const KEYS = { AssignmentExpression: [ "left", diff --git a/package.json b/package.json index 10ff44a..b3e4e17 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Constants and utilities about visitor keys to traverse AST.", "type": "module", "main": "dist/eslint-visitor-keys.cjs", + "types": "./dist/eslint-visitor.keys.d.ts", "exports": { ".": [ { @@ -15,6 +16,7 @@ "./package.json": "./package.json" }, "files": [ + "dist/eslint-visitor.keys.d.ts", "dist/eslint-visitor-keys.cjs", "lib" ], @@ -30,12 +32,14 @@ "eslint-release": "^3.2.0", "mocha": "^9.0.1", "opener": "^1.5.2", - "rollup": "^2.52.1" + "rollup": "^2.52.1", + "typescript": "^4.5.5" }, "scripts": { - "prepare": "npm run build", + "prepare": "npm run build && npm run tsc", "build": "rollup -c", "lint": "eslint .", + "tsc": "tsc", "test": "mocha tests/lib/**/*.cjs && c8 mocha tests/lib/**/*.js", "coverage": "c8 report --reporter lcov && opener coverage/lcov-report/index.html", "generate-release": "eslint-generate-release", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2cd1724 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "moduleResolution": "node", + "module": "esnext", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "noEmit": false, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "strict": true, + "target": "es5", + "outFile": "dist/eslint-visitor-keys.d.ts" + }, + "include": ["lib/**/*.js"], + "exclude": ["node_modules"] +} From 354f7208f3f6a13b5bb1de5e4a70a3bed43479b0 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sun, 6 Feb 2022 19:07:32 +0800 Subject: [PATCH 02/16] refactor: allow undefined keys (but not for keys argument or return value of `unionWith`) --- .eslintrc.json | 9 +++++++++ lib/index.js | 2 +- lib/visitor-keys.js | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 7bea33e..851ab42 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,6 +7,15 @@ "sourceType": "module", "ecmaVersion": 2020 }, + "settings": { + "jsdoc": { + "mode": "typescript", + "preferredTypes": { + "Object": "object", + "object<>": "Object" + } + } + }, "overrides": [ { "files": ["*.cjs"], diff --git a/lib/index.js b/lib/index.js index 747d024..93d22dc 100644 --- a/lib/index.js +++ b/lib/index.js @@ -22,7 +22,7 @@ function filterKey(key) { /** * Get visitor keys of a given node. - * @param {Object} node The AST node to get keys. + * @param {object} node The AST node to get keys. * @returns {readonly string[]} Visitor keys of the node. */ export function getKeys(node) { diff --git a/lib/visitor-keys.js b/lib/visitor-keys.js index c55d8f3..9b626dd 100644 --- a/lib/visitor-keys.js +++ b/lib/visitor-keys.js @@ -1,5 +1,5 @@ /** - * @type {{ readonly [type: string]: ReadonlyArray }} + * @type {{ readonly [type: string]: ReadonlyArray | undefined }} */ const KEYS = { AssignmentExpression: [ From 0a1c5d00312cf68b00313c980aa9e7c98ea2f557 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sun, 6 Feb 2022 23:48:39 +0800 Subject: [PATCH 03/16] fix: `types` and `files` location --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b3e4e17..baa0b9b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Constants and utilities about visitor keys to traverse AST.", "type": "module", "main": "dist/eslint-visitor-keys.cjs", - "types": "./dist/eslint-visitor.keys.d.ts", + "types": "./dist/eslint-visitor-keys.d.ts", "exports": { ".": [ { @@ -16,7 +16,7 @@ "./package.json": "./package.json" }, "files": [ - "dist/eslint-visitor.keys.d.ts", + "dist/eslint-visitor-keys.d.ts", "dist/eslint-visitor-keys.cjs", "lib" ], From 0e06d8f1116c9b4ee6b46133431e4888af638fea Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 7 Feb 2022 00:36:04 +0800 Subject: [PATCH 04/16] fix: resolve "Cannot find module" error by using `outDir` --- package.json | 8 +++++--- tsconfig.json | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index baa0b9b..13b477b 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "description": "Constants and utilities about visitor keys to traverse AST.", "type": "module", "main": "dist/eslint-visitor-keys.cjs", - "types": "./dist/eslint-visitor-keys.d.ts", + "types": "./dist/index.d.ts", "exports": { ".": [ { + "types": "./dist/index.d.ts", "import": "./lib/index.js", "require": "./dist/eslint-visitor-keys.cjs" }, @@ -16,7 +17,8 @@ "./package.json": "./package.json" }, "files": [ - "dist/eslint-visitor-keys.d.ts", + "dist/index.d.ts", + "dist/visitor-keys.d.ts", "dist/eslint-visitor-keys.cjs", "lib" ], @@ -33,7 +35,7 @@ "mocha": "^9.0.1", "opener": "^1.5.2", "rollup": "^2.52.1", - "typescript": "^4.5.5" + "typescript": "^4.6.0-dev.20220206" }, "scripts": { "prepare": "npm run build && npm run tsc", diff --git a/tsconfig.json b/tsconfig.json index 2cd1724..2d79219 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "lib": ["es2020"], - "moduleResolution": "node", + "moduleResolution": "nodenext", "module": "esnext", "resolveJsonModule": true, "allowJs": true, @@ -12,7 +12,7 @@ "emitDeclarationOnly": true, "strict": true, "target": "es5", - "outFile": "dist/eslint-visitor-keys.d.ts" + "outDir": "dist" }, "include": ["lib/**/*.js"], "exclude": ["node_modules"] From e4d83bc780b07e2469dc13710d6d74e84687ba0f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 8 Feb 2022 13:22:41 +0800 Subject: [PATCH 05/16] refactor: Shorten types with typedefs --- lib/index.js | 11 ++++++++--- lib/visitor-keys.js | 6 +++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/index.js b/lib/index.js index 93d22dc..5222cf1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,11 @@ */ import KEYS from "./visitor-keys.js"; +/** + * @typedef {{ [type: string]: ReadonlyArray }} KeysStrict + * @typedef {{ readonly [type: string]: ReadonlyArray }} KeysStrictReadonly + */ + // List to ignore keys. const KEY_BLACKLIST = new Set([ "parent", @@ -33,11 +38,11 @@ export function getKeys(node) { // eslint-disable-next-line valid-jsdoc /** * Make the union set with `KEYS` and given keys. - * @param {{ readonly [type: string]: ReadonlyArray }} additionalKeys The additional keys. - * @returns {{ readonly [type: string]: ReadonlyArray }} The union set. + * @param {KeysStrictReadonly} additionalKeys The additional keys. + * @returns {KeysStrictReadonly} The union set. */ export function unionWith(additionalKeys) { - const retv = /** @type {{ [type: string]: ReadonlyArray }} */ (Object.assign({}, KEYS)); + const retv = /** @type {KeysStrict} */ (Object.assign({}, KEYS)); for (const type of Object.keys(additionalKeys)) { if (Object.prototype.hasOwnProperty.call(retv, type)) { diff --git a/lib/visitor-keys.js b/lib/visitor-keys.js index 9b626dd..3c92456 100644 --- a/lib/visitor-keys.js +++ b/lib/visitor-keys.js @@ -1,5 +1,9 @@ /** - * @type {{ readonly [type: string]: ReadonlyArray | undefined }} + * @typedef {{ readonly [type: string]: ReadonlyArray }} KeysLooseReadonly + */ + +/** + * @type {KeysLooseReadonly} */ const KEYS = { AssignmentExpression: [ From 4283795861fcc0f3036ac001b8bcd07154ddd095 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 8 Feb 2022 13:40:45 +0800 Subject: [PATCH 06/16] refactor: Remove extra `types` --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 13b477b..19f4dbf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "exports": { ".": [ { - "types": "./dist/index.d.ts", "import": "./lib/index.js", "require": "./dist/eslint-visitor-keys.cjs" }, From ef69df66d5e891b003ae08cba38646020fa95222 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 9 Feb 2022 07:29:51 +0800 Subject: [PATCH 07/16] refactor: revert switch to `nodenext` --- package.json | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 19f4dbf..1fcffca 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "mocha": "^9.0.1", "opener": "^1.5.2", "rollup": "^2.52.1", - "typescript": "^4.6.0-dev.20220206" + "typescript": "^4.5.5" }, "scripts": { "prepare": "npm run build && npm run tsc", diff --git a/tsconfig.json b/tsconfig.json index 2d79219..dc1ccbd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "lib": ["es2020"], - "moduleResolution": "nodenext", + "moduleResolution": "node", "module": "esnext", "resolveJsonModule": true, "allowJs": true, From 33621338ccce8cdc4daad32fdd03bafa1b5618f5 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 9 Feb 2022 07:56:39 +0800 Subject: [PATCH 08/16] refactor: Drop use of `KeysLooseReadonly` --- lib/visitor-keys.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/visitor-keys.js b/lib/visitor-keys.js index 3c92456..425604e 100644 --- a/lib/visitor-keys.js +++ b/lib/visitor-keys.js @@ -1,9 +1,9 @@ /** - * @typedef {{ readonly [type: string]: ReadonlyArray }} KeysLooseReadonly + * @typedef {import('./index.js').KeysStrictReadonly} KeysStrictReadonly */ /** - * @type {KeysLooseReadonly} + * @type {KeysStrictReadonly} */ const KEYS = { AssignmentExpression: [ From 03da79c92c366a5d2485c462db4a245f5e6a929a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 9 Feb 2022 09:33:48 +0800 Subject: [PATCH 09/16] refactor: set target to `es6` --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index dc1ccbd..5afc060 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "declarationMap": true, "emitDeclarationOnly": true, "strict": true, - "target": "es5", + "target": "es6", "outDir": "dist" }, "include": ["lib/**/*.js"], From 8413d10ef3bd9713de766f2cd8d86926ed3f3fbc Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Thu, 10 Feb 2022 19:05:46 +0800 Subject: [PATCH 10/16] chore: move `tsc` to build step --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1fcffca..d6d00f9 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "typescript": "^4.5.5" }, "scripts": { - "prepare": "npm run build && npm run tsc", - "build": "rollup -c", + "prepare": "npm run build", + "build": "rollup -c && npm run tsc", "lint": "eslint .", "tsc": "tsc", "test": "mocha tests/lib/**/*.cjs && c8 mocha tests/lib/**/*.js", From ab6d321f59a9770a80a4d93d8098b6fb56a4eee6 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 9 Feb 2022 18:18:42 +0800 Subject: [PATCH 11/16] test: tsd tests of declaration file --- package.json | 4 +++- test-d/index.test-d.ts | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 test-d/index.test-d.ts diff --git a/package.json b/package.json index d6d00f9..1bb9a42 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "mocha": "^9.0.1", "opener": "^1.5.2", "rollup": "^2.52.1", + "tsd": "^0.19.1", "typescript": "^4.5.5" }, "scripts": { @@ -41,7 +42,8 @@ "build": "rollup -c && npm run tsc", "lint": "eslint .", "tsc": "tsc", - "test": "mocha tests/lib/**/*.cjs && c8 mocha tests/lib/**/*.js", + "tsd": "tsd", + "test": "mocha tests/lib/**/*.cjs && c8 mocha tests/lib/**/*.js && npm run tsd", "coverage": "c8 report --reporter lcov && opener coverage/lcov-report/index.html", "generate-release": "eslint-generate-release", "generate-alpharelease": "eslint-generate-prerelease alpha", diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts new file mode 100644 index 0000000..821c59f --- /dev/null +++ b/test-d/index.test-d.ts @@ -0,0 +1,45 @@ +import {expectType, expectAssignable} from 'tsd'; + +import { KEYS, getKeys, unionWith, KeysStrict, KeysStrictReadonly } from "../lib/index.js"; + +const assignmentExpression = { + type: "AssignmentExpression", + operator: "=", + left: { + type: "Identifier", + name: "a", + range: [ + 0, + 1 + ] + }, + right: { + type: "Literal", + value: 5, + raw: "5", + range: [ + 4, + 5 + ] + }, + range: [ + 0, + 5 + ] +}; + +expectType<{readonly [type: string]: readonly string[]}>(KEYS); + +expectType(getKeys(assignmentExpression)); + +expectType<{readonly [type: string]: readonly string[]}>(unionWith({ + TestInterface1: ["left", "right"], + TestInterface2: ["expression"] +})); + +expectAssignable({ + TestInterface1: ["left", "right"] +}); +expectAssignable({ + TestInterface1: ["left", "right"] +}); From fe4725ee643e6c7c9db7cddd1238bf02dd7c3e21 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 9 Feb 2022 19:37:34 +0800 Subject: [PATCH 12/16] test: exported type checks --- test-d/index.test-d.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/test-d/index.test-d.ts b/test-d/index.test-d.ts index 821c59f..8ea4d8e 100644 --- a/test-d/index.test-d.ts +++ b/test-d/index.test-d.ts @@ -1,4 +1,4 @@ -import {expectType, expectAssignable} from 'tsd'; +import { expectType, expectAssignable, expectError } from 'tsd'; import { KEYS, getKeys, unionWith, KeysStrict, KeysStrictReadonly } from "../lib/index.js"; @@ -37,9 +37,33 @@ expectType<{readonly [type: string]: readonly string[]}>(unionWith({ TestInterface2: ["expression"] })); -expectAssignable({ +const keys: { + [type: string]: readonly string[] +} = { TestInterface1: ["left", "right"] -}); -expectAssignable({ +}; + +const readonlyKeys: { + readonly [type: string]: readonly string[] +} = { TestInterface1: ["left", "right"] +}; + +expectAssignable(keys); + +expectAssignable(readonlyKeys); + +expectError(() => { + const erring: KeysStrict = { + TestInterface1: ["left", "right"] + }; + erring.TestInterface1 = "badType"; }); + +// https://github.com/SamVerschueren/tsd/issues/143 +// expectError(() => { +// const erring: KeysStrictReadonly = { +// TestInterface1: ["left", "right"] +// }; +// erring.TestInterface1 = ["badAttemptOverwrite"]; +// }); From dbed685a15119b20645ec1285455b61326453108 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 9 Feb 2022 14:05:53 +0800 Subject: [PATCH 13/16] refactor: Alphabetize for easier comparisons --- lib/visitor-keys.js | 70 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/visitor-keys.js b/lib/visitor-keys.js index 425604e..2e130cc 100644 --- a/lib/visitor-keys.js +++ b/lib/visitor-keys.js @@ -6,14 +6,6 @@ * @type {KeysStrictReadonly} */ const KEYS = { - AssignmentExpression: [ - "left", - "right" - ], - AssignmentPattern: [ - "left", - "right" - ], ArrayExpression: [ "elements" ], @@ -24,16 +16,24 @@ const KEYS = { "params", "body" ], + AssignmentExpression: [ + "left", + "right" + ], + AssignmentPattern: [ + "left", + "right" + ], AwaitExpression: [ "argument" ], - BlockStatement: [ - "body" - ], BinaryExpression: [ "left", "right" ], + BlockStatement: [ + "body" + ], BreakStatement: [ "label" ], @@ -75,6 +75,12 @@ const KEYS = { "test" ], EmptyStatement: [], + ExperimentalRestProperty: [ + "argument" + ], + ExperimentalSpreadProperty: [ + "argument" + ], ExportAllDeclaration: [ "exported", "source" @@ -94,18 +100,6 @@ const KEYS = { ExpressionStatement: [ "expression" ], - ExperimentalRestProperty: [ - "argument" - ], - ExperimentalSpreadProperty: [ - "argument" - ], - ForStatement: [ - "init", - "test", - "update", - "body" - ], ForInStatement: [ "left", "right", @@ -116,6 +110,12 @@ const KEYS = { "right", "body" ], + ForStatement: [ + "init", + "test", + "update", + "body" + ], FunctionDeclaration: [ "id", "params", @@ -156,6 +156,7 @@ const KEYS = { JSXClosingElement: [ "name" ], + JSXClosingFragment: [], JSXElement: [ "openingElement", "children", @@ -165,6 +166,11 @@ const KEYS = { JSXExpressionContainer: [ "expression" ], + JSXFragment: [ + "openingFragment", + "children", + "closingFragment" + ], JSXIdentifier: [], JSXMemberExpression: [ "object", @@ -178,22 +184,16 @@ const KEYS = { "name", "attributes" ], + JSXOpeningFragment: [], JSXSpreadAttribute: [ "argument" ], JSXText: [], - JSXFragment: [ - "openingFragment", - "children", - "closingFragment" - ], - JSXClosingFragment: [], - JSXOpeningFragment: [], - Literal: [], LabeledStatement: [ "label", "body" ], + Literal: [], LogicalExpression: [ "left", "right" @@ -248,14 +248,14 @@ const KEYS = { "body" ], Super: [], - SwitchStatement: [ - "discriminant", - "cases" - ], SwitchCase: [ "test", "consequent" ], + SwitchStatement: [ + "discriminant", + "cases" + ], TaggedTemplateExpression: [ "tag", "quasi" From 007163138af0b5a28eb89efb50c832a43f5bf2f4 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 11 Feb 2022 06:58:38 +0800 Subject: [PATCH 14/16] refactor: alphabetize inner keys --- lib/visitor-keys.js | 92 ++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/visitor-keys.js b/lib/visitor-keys.js index 2e130cc..c46cba8 100644 --- a/lib/visitor-keys.js +++ b/lib/visitor-keys.js @@ -13,8 +13,8 @@ const KEYS = { "elements" ], ArrowFunctionExpression: [ - "params", - "body" + "body", + "params" ], AssignmentExpression: [ "left", @@ -42,8 +42,8 @@ const KEYS = { "arguments" ], CatchClause: [ - "param", - "body" + "body", + "param" ], ChainExpression: [ "expression" @@ -52,19 +52,19 @@ const KEYS = { "body" ], ClassDeclaration: [ + "body", "id", - "superClass", - "body" + "superClass" ], ClassExpression: [ + "body", "id", - "superClass", - "body" + "superClass" ], ConditionalExpression: [ - "test", + "alternate", "consequent", - "alternate" + "test" ], ContinueStatement: [ "label" @@ -101,36 +101,36 @@ const KEYS = { "expression" ], ForInStatement: [ + "body", "left", - "right", - "body" + "right" ], ForOfStatement: [ + "body", "left", - "right", - "body" + "right" ], ForStatement: [ + "body", "init", "test", - "update", - "body" + "update" ], FunctionDeclaration: [ + "body", "id", - "params", - "body" + "params" ], FunctionExpression: [ + "body", "id", - "params", - "body" + "params" ], Identifier: [], IfStatement: [ - "test", + "alternate", "consequent", - "alternate" + "test" ], ImportDeclaration: [ "specifiers", @@ -158,18 +158,18 @@ const KEYS = { ], JSXClosingFragment: [], JSXElement: [ - "openingElement", "children", - "closingElement" + "closingElement", + "openingElement" ], JSXEmptyExpression: [], JSXExpressionContainer: [ "expression" ], JSXFragment: [ - "openingFragment", "children", - "closingFragment" + "closingFragment", + "openingFragment" ], JSXIdentifier: [], JSXMemberExpression: [ @@ -181,8 +181,8 @@ const KEYS = { "name" ], JSXOpeningElement: [ - "name", - "attributes" + "attributes", + "name" ], JSXOpeningFragment: [], JSXSpreadAttribute: [ @@ -190,8 +190,8 @@ const KEYS = { ], JSXText: [], LabeledStatement: [ - "label", - "body" + "body", + "label" ], Literal: [], LogicalExpression: [ @@ -211,8 +211,8 @@ const KEYS = { "value" ], NewExpression: [ - "callee", - "arguments" + "arguments", + "callee" ], ObjectExpression: [ "properties" @@ -249,21 +249,21 @@ const KEYS = { ], Super: [], SwitchCase: [ - "test", - "consequent" + "consequent", + "test" ], SwitchStatement: [ - "discriminant", - "cases" + "cases", + "discriminant" ], TaggedTemplateExpression: [ - "tag", - "quasi" + "quasi", + "tag" ], TemplateElement: [], TemplateLiteral: [ - "quasis", - "expressions" + "expressions", + "quasis" ], ThisExpression: [], ThrowStatement: [ @@ -271,8 +271,8 @@ const KEYS = { ], TryStatement: [ "block", - "handler", - "finalizer" + "finalizer", + "handler" ], UnaryExpression: [ "argument" @@ -288,12 +288,12 @@ const KEYS = { "init" ], WhileStatement: [ - "test", - "body" + "body", + "test" ], WithStatement: [ - "object", - "body" + "body", + "object" ], YieldExpression: [ "argument" From 7b41cb609e39ddfb1d9b7bd54ab0d25544f972d9 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 9 Feb 2022 08:08:01 +0800 Subject: [PATCH 15/16] chore: tool to build keys out of AST definitions Also alphabetizes keys Still remaining: 1. Confirm adding of interfaces to ignore 2. Confirm `traversableTypes` interfaces (how to detect) 3. Confirm hiding of `innerComments`, etc. and adding these `propertiesToIgnore` to `getKeys` 4. Remove json-diff; settle on chai with or without plugin --- lib/visitor-keys.js | 31 +-- package.json | 8 + tests/lib/get-keys-from-ts.js | 43 ++++ tools/build-keys-from-ts.js | 14 ++ tools/get-keys-from-ts.js | 356 ++++++++++++++++++++++++++++++++++ 5 files changed, 441 insertions(+), 11 deletions(-) create mode 100644 tests/lib/get-keys-from-ts.js create mode 100644 tools/build-keys-from-ts.js create mode 100644 tools/get-keys-from-ts.js diff --git a/lib/visitor-keys.js b/lib/visitor-keys.js index c46cba8..286b903 100644 --- a/lib/visitor-keys.js +++ b/lib/visitor-keys.js @@ -24,9 +24,14 @@ const KEYS = { "left", "right" ], + AssignmentProperty: [ + "key", + "value" + ], AwaitExpression: [ "argument" ], + BigIntLiteral: [], BinaryExpression: [ "left", "right" @@ -37,10 +42,6 @@ const KEYS = { BreakStatement: [ "label" ], - CallExpression: [ - "callee", - "arguments" - ], CatchClause: [ "body", "param" @@ -70,17 +71,14 @@ const KEYS = { "label" ], DebuggerStatement: [], + Directive: [ + "expression" + ], DoWhileStatement: [ "body", "test" ], EmptyStatement: [], - ExperimentalRestProperty: [ - "argument" - ], - ExperimentalSpreadProperty: [ - "argument" - ], ExportAllDeclaration: [ "exported", "source" @@ -153,6 +151,9 @@ const KEYS = { "name", "value" ], + JSXBoundaryElement: [ + "name" + ], JSXClosingElement: [ "name" ], @@ -188,12 +189,14 @@ const KEYS = { JSXSpreadAttribute: [ "argument" ], + JSXSpreadChild: [ + "expression" + ], JSXText: [], LabeledStatement: [ "body", "label" ], - Literal: [], LogicalExpression: [ "left", "right" @@ -232,6 +235,7 @@ const KEYS = { "key", "value" ], + RegExpLiteral: [], RestElement: [ "argument" ], @@ -241,6 +245,11 @@ const KEYS = { SequenceExpression: [ "expressions" ], + SimpleCallExpression: [ + "arguments", + "callee" + ], + SimpleLiteral: [], SpreadElement: [ "argument" ], diff --git a/package.json b/package.json index 1bb9a42..184ad43 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "devDependencies": { + "@types/estree": "^0.0.51", + "@types/estree-jsx": "^0.0.1", + "@typescript-eslint/parser": "^5.11.0", "c8": "^7.7.3", + "chai": "^4.3.6", + "deep-equal-in-any-order": "^1.1.15", "eslint": "^7.29.0", "eslint-config-eslint": "^7.0.0", "eslint-plugin-jsdoc": "^35.4.0", "eslint-plugin-node": "^11.1.0", "eslint-release": "^3.2.0", + "esquery": "^1.4.0", + "json-diff": "^0.7.1", "mocha": "^9.0.1", "opener": "^1.5.2", "rollup": "^2.52.1", @@ -43,6 +50,7 @@ "lint": "eslint .", "tsc": "tsc", "tsd": "tsd", + "build-keys": "node tools/build-keys-from-ts", "test": "mocha tests/lib/**/*.cjs && c8 mocha tests/lib/**/*.js && npm run tsd", "coverage": "c8 report --reporter lcov && opener coverage/lcov-report/index.html", "generate-release": "eslint-generate-release", diff --git a/tests/lib/get-keys-from-ts.js b/tests/lib/get-keys-from-ts.js new file mode 100644 index 0000000..432919a --- /dev/null +++ b/tests/lib/get-keys-from-ts.js @@ -0,0 +1,43 @@ +import { diffString } from "json-diff"; +import chai, { expect } from "chai"; +import deepEqualInAnyOrder from "deep-equal-in-any-order"; +import { alphabetizeKeyInterfaces, getKeysFromTsFile } from "../../tools/get-keys-from-ts.js"; +import { KEYS } from "../../lib/index.js"; + +chai.use(deepEqualInAnyOrder); + +describe("getKeysFromTsFile", () => { + it("gets keys", async () => { + const { keys, tsInterfaceDeclarations } = await getKeysFromTsFile( + "./node_modules/@types/estree/index.d.ts" + ); + const { keys: jsxKeys } = await getKeysFromTsFile( + "./node_modules/@types/estree-jsx/index.d.ts", + { + supplementaryDeclarations: tsInterfaceDeclarations + } + ); + + /** + * Alphabetizes the keys of the interface-keys object + * @param {KeysStrict} keysObj The interface-keys object to sort + * @returns {KeysStrict} The alphabetized keys + */ + function alphabetizeKeys(keysObj) { + const retObj = {}; + + for (const [interfaceName, keysArr] of Object.entries(keysObj)) { + retObj[interfaceName] = [...keysArr].sort(); + } + return retObj; + } + + const actual = alphabetizeKeys(alphabetizeKeyInterfaces({ ...keys, ...jsxKeys })); + + const expected = alphabetizeKeys(KEYS); + + console.log(diffString(actual, expected)); + + expect(actual).to.deep.equal(expected); + }); +}); diff --git a/tools/build-keys-from-ts.js b/tools/build-keys-from-ts.js new file mode 100644 index 0000000..2764b97 --- /dev/null +++ b/tools/build-keys-from-ts.js @@ -0,0 +1,14 @@ +import { alphabetizeKeyInterfaces, getKeysFromTsFile } from "./get-keys-from-ts.js"; + +(async () => { + const { keys, tsInterfaceDeclarations } = await getKeysFromTsFile("./node_modules/@types/estree/index.d.ts"); + const { keys: jsxKeys } = await getKeysFromTsFile( + "./node_modules/@types/estree-jsx/index.d.ts", + { + supplementaryDeclarations: tsInterfaceDeclarations + } + ); + + // eslint-disable-next-line no-console -- CLI + console.log("keys", alphabetizeKeyInterfaces({ ...keys, ...jsxKeys })); +})(); diff --git a/tools/get-keys-from-ts.js b/tools/get-keys-from-ts.js new file mode 100644 index 0000000..31b4e6f --- /dev/null +++ b/tools/get-keys-from-ts.js @@ -0,0 +1,356 @@ +import { promises } from "fs"; + +import { parseForESLint } from "@typescript-eslint/parser"; + +import esquery from "esquery"; + +import { getKeys } from "../lib/index.js"; + +const { readFile } = promises; + +const propertiesToIgnore = new Set([ + "comments", + "innerComments", + "type", + "operator" +]); + +const exemptedTypes = new Set([ + "TSBooleanKeyword", + "TSNumberKeyword", + "TSStringKeyword", + "TSLiteralType", // E.g., `true` + + // Apparently used for primitives, so exempting + "TSTypeLiteral", // E.g., `{value: {cooked, raw}}` + + "TSUnionType", // I.e., `|` + "TSTypeReference" +]); + +// Also excluding interfaces starting with "Base" in some contexts +const interfacesToIgnore = new Set([ + "Comment", + "Position", + "RegExp", + "SourceLocation" +]); + +/** + * Whether to ignore interface + * @param {string} type Type to check. + * @returns {boolean} If to be ignored. + */ +function ignoreInterface(type) { + return type.startsWith("Base") || interfacesToIgnore.has(type); +} + +// All items ending in `Statement` are also traversable +const traversableTypes = new Set([ + "Array", + "CatchClause", + "ChainElement", + "ClassBody", + "Declaration", + "Expression", + "FunctionExpression", + "Identifier", + "JSXClosingFragment", + "JSXIdentifier", + "JSXMemberExpression", + "JSXOpeningElement", + "JSXOpeningFragment", + "JSXClosingElement", + "Literal", + "Pattern", + "TemplateLiteral", + "VariableDeclaration" +]); + +const notTraversableTypes = new Set([ + "TSUndefinedKeyword", + "TSNullKeyword", + "TSBooleanKeyword", + "TSNumberKeyword", + "TSStringKeyword", + "TSBigIntKeyword", + "TSLiteralType" +]); + +/** + * Checks if a name is traverseable + * @param {string} name The name to check + * @returns {boolean} Whether it is traversable. + */ +function isTraversable(name) { + return name && (name.endsWith("Statement") || traversableTypes.has(name)); +} + +/** + * Determine whether the Node is traversable + * @param {Node} annotationType The annotation type Node + * @returns {boolean} Whether the node is traversable + */ +function checkTraversability(annotationType) { + if ( + notTraversableTypes.has(annotationType.type) || + interfacesToIgnore.has(annotationType.typeName?.name) + ) { + return false; + } + + if (annotationType.type === "TSTupleType") { + return annotationType.elementTypes.some(annType => checkTraversability(annType)); + } + + if (!isTraversable(annotationType.typeName.name)) { + + // Todo? + /* + const innerInterfaceName = tsAnnotation.typeName.name; + const innerTsDeclarationNode = findTsInterfaceDeclaration(innerInterfaceName); + + if (!innerTsDeclarationNode) { + + const innerTsTypeNode = findTsTypeDeclaration(innerInterfaceName); + + // We might iterate types here to see if children are iterable and + // fail if not + + unrecognizedTSTypeReferences.add(`${tsAnnotation.typeName.name}`); + break; + } + + // We might iterate interfaces here to see if children are iterable + // (see `addNodeForInterface` for a pattern of iteration) + */ + + throw new Error(`Type unknown as to traversability: ${annotationType.typeName.name}`); + } + + return true; +} + +/** + * Get the literal names out of AST + * @param {Node} excludedItem Excluded node + * @returns {string[]} The literal names + */ +function findOmitTypes(excludedItem) { + if (excludedItem.type === "TSUnionType") { + return excludedItem.types.map(typeNode => findOmitTypes(typeNode)); + } + if (excludedItem.type !== "TSLiteralType") { + throw new Error("Processing of non-literals in `Omit` not currently supported"); + } + return excludedItem.literal.value; +} + +/** + * Checks whether property should be excluded + * @param {string} property Property to check + * @param {string[]} excludedProperties Properties not to allow + * @returns {boolean} Whether or not to be excluded + */ +function isPropertyExcluded(property, excludedProperties) { + return propertiesToIgnore.has(property) || + (excludedProperties && excludedProperties.includes(property)); +} + +/** + * Returns alphabetized keys + * @param {KeysStrict} initialNodes Initial node list to sort + * @returns {KeysStrict} The keys + */ +function alphabetizeKeyInterfaces(initialNodes) { + const sortedNodeEntries = Object.entries(initialNodes).sort( + ([typeA], [typeB]) => (typeA < typeB ? -1 : 1) + ); + + return Object.fromEntries(sortedNodeEntries); +} + +/** + * Builds visitor keys based on TypeScript declaration. + * @param {string} code TypeScript declaration file as code to parse. + * @param {{supplementaryDeclarations: Node[]}} [options] The options + * @returns {Promise<{tsInterfaceDeclarations: Node[], keys: KeysStrict}>} The built visitor keys + */ +async function getKeysFromTs(code, { + + // Todo: Ideally we'd just get these from the import + supplementaryDeclarations = [] +} = {}) { + const unrecognizedTSTypeReferences = new Set(); + const unrecognizedTSTypes = new Set(); + + const parsedTSDeclaration = parseForESLint(code); + const tsInterfaceDeclarations = [...esquery.query( + parsedTSDeclaration.ast, + "TSInterfaceDeclaration", + { + + // TypeScript keys here to find our *.d.ts nodes (not for the ESTree + // ones we want) + visitorKeys: parsedTSDeclaration.visitorKeys + } + ), ...supplementaryDeclarations]; + + // const tsTypeDeclarations = esquery.query( + // parsedTSDeclaration.ast, + // "TSTypeAliasDeclaration", + // { + // + // // TypeScript keys here to find our *.d.ts nodes (not for the ESTree + // // ones we want) + // visitorKeys: parsedTSDeclaration.visitorKeys + // } + // ); + const initialNodes = {}; + + /** + * Finds a TypeScript interfaction declaration. + * @param {string} interfaceName The interface name. + * @returns {Node} The interface declaration node + */ + function findTsInterfaceDeclaration(interfaceName) { + return tsInterfaceDeclarations.find( + innerTsDeclaration => innerTsDeclaration.id.name === interfaceName + ); + } + + /** + * Adds a node for a given interface. + * @param {string} interfaceName Name of the interface + * @param {Node} tsDeclarationNode TypeScript declaration node + * @param {Node} node The Node on which to build + * @param {string[]} excludedProperties Excluded properties + * @returns {void} + */ + function addNodeForInterface(interfaceName, tsDeclarationNode, node, excludedProperties) { + if (interfacesToIgnore.has(interfaceName)) { + return; + } + + const tsPropertySignatures = tsDeclarationNode.body.body; + + for (const tsPropertySignature of tsPropertySignatures) { + const property = tsPropertySignature.key.name; + + if (isPropertyExcluded(property, excludedProperties)) { + continue; + } + + const tsAnnotation = tsPropertySignature.typeAnnotation.typeAnnotation; + const tsPropertyType = tsAnnotation.type; + + // For sanity-checking + if (!exemptedTypes.has(tsPropertyType)) { + unrecognizedTSTypes.add(tsPropertyType); + continue; + } + + switch (tsPropertyType) { + case "TSUnionType": + if (tsAnnotation.types.some(checkTraversability)) { + break; + } + continue; + case "TSTypeReference": { + if (checkTraversability(tsAnnotation)) { + break; + } + + continue; + } default: + continue; + } + + node[property] = null; + } + + for (const extension of tsDeclarationNode.extends || []) { + const { typeParameters, expression } = extension; + const innerInterfaceName = expression.name; + + if (typeParameters) { + if (innerInterfaceName !== "Omit") { + throw new Error("Unknown extension type with parameters"); + } + + const [param, ...excludedAST] = typeParameters.params; + const paramInterfaceName = param.typeName.name; + const excluded = excludedAST.flatMap(findOmitTypes); + const innerTsDeclarationNode = findTsInterfaceDeclaration(paramInterfaceName); + + if (!innerTsDeclarationNode) { + unrecognizedTSTypeReferences.add(`${paramInterfaceName}`); + return; + } + + addNodeForInterface(paramInterfaceName, innerTsDeclarationNode, node, excluded); + } else { + const innerTsDeclarationNode = findTsInterfaceDeclaration(innerInterfaceName); + + if (!innerTsDeclarationNode) { + unrecognizedTSTypeReferences.add(`${innerInterfaceName}`); + return; + } + + addNodeForInterface(innerInterfaceName, innerTsDeclarationNode, node); + } + } + } + + for (const tsDeclarationNode of tsInterfaceDeclarations) { + const interfaceName = tsDeclarationNode.id.name; + + if (ignoreInterface(interfaceName)) { + continue; + } + + const node = {}; + + addNodeForInterface(interfaceName, tsDeclarationNode, node); + + initialNodes[interfaceName] = getKeys(node); + } + + const nodes = alphabetizeKeyInterfaces(initialNodes); + + if (unrecognizedTSTypes.size) { + throw new Error( + "Unhandled TypeScript type; please update the code to " + + "handle the type or if not relevant, add it to " + + "`unrecognizedTSTypes`; see\n\n " + + `${[...unrecognizedTSTypes].join(", ")}\n` + ); + } + if (unrecognizedTSTypeReferences.size) { + throw new Error( + "Unhandled TypeScript type reference; please update the code to " + + "handle the type reference or if not relevant, add it to " + + "`unrecognizedTSTypeReferences`; see\n\n " + + `${[...unrecognizedTSTypeReferences].join(", ")}\n` + ); + } + + return { + keys: nodes, + tsInterfaceDeclarations + }; +} + +/** + * Builds visitor keys based on TypeScript declaration. + * @param {string} file TypeScript declaration file to parse. + * @param {{supplementaryDeclarations: Node[]}} options The options + * @returns {Promise<{tsInterfaceDeclarations: Node[], keys: KeysStrict}} The built visitor keys + */ +async function getKeysFromTsFile(file, options) { + const code = await readFile(file); + + return await getKeysFromTs(code, options); +} + +export { alphabetizeKeyInterfaces, getKeysFromTs, getKeysFromTsFile }; From 4c62e51e5ae0da2318ac45dac802ade0c5ecec16 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 11 Feb 2022 06:58:48 +0800 Subject: [PATCH 16/16] refactor: write to file --- tools/build-keys-from-ts.js | 33 ++++++++++++++++++++++++++++++++- tools/get-keys-from-ts.js | 20 +++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/tools/build-keys-from-ts.js b/tools/build-keys-from-ts.js index 2764b97..aef13b7 100644 --- a/tools/build-keys-from-ts.js +++ b/tools/build-keys-from-ts.js @@ -1,5 +1,8 @@ +import fs from "fs"; import { alphabetizeKeyInterfaces, getKeysFromTsFile } from "./get-keys-from-ts.js"; +const { promises: { writeFile } } = fs; + (async () => { const { keys, tsInterfaceDeclarations } = await getKeysFromTsFile("./node_modules/@types/estree/index.d.ts"); const { keys: jsxKeys } = await getKeysFromTsFile( @@ -9,6 +12,34 @@ import { alphabetizeKeyInterfaces, getKeysFromTsFile } from "./get-keys-from-ts. } ); + const mergedKeys = alphabetizeKeyInterfaces({ ...keys, ...jsxKeys }); + // eslint-disable-next-line no-console -- CLI - console.log("keys", alphabetizeKeyInterfaces({ ...keys, ...jsxKeys })); + console.log("keys", mergedKeys); + + writeFile( + "./lib/visitor-keys.js", + // eslint-disable-next-line indent -- Readability +`/** + * @typedef {import('./index.js').KeysStrictReadonly} KeysStrictReadonly + */ + +/** + * @type {KeysStrictReadonly} + */ +const KEYS = ${JSON.stringify(mergedKeys, null, 4).replace(/"(.*?)":/gu, "$1:")}; + +// Types. +const NODE_TYPES = Object.keys(KEYS); + +// Freeze the keys. +for (const type of NODE_TYPES) { + Object.freeze(KEYS[type]); +} +Object.freeze(KEYS); + +export default KEYS; +` + ); + })(); diff --git a/tools/get-keys-from-ts.js b/tools/get-keys-from-ts.js index 31b4e6f..fad3e41 100644 --- a/tools/get-keys-from-ts.js +++ b/tools/get-keys-from-ts.js @@ -163,9 +163,23 @@ function isPropertyExcluded(property, excludedProperties) { * @returns {KeysStrict} The keys */ function alphabetizeKeyInterfaces(initialNodes) { - const sortedNodeEntries = Object.entries(initialNodes).sort( - ([typeA], [typeB]) => (typeA < typeB ? -1 : 1) - ); + + /** + * Alphabetize + * @param {string} typeA The first type to compare + * @param {string} typeB The second type to compare + * @returns {1|-1} The sorting index + */ + function alphabetize([typeA], [typeB]) { + return typeA < typeB ? -1 : 1; + } + const sortedNodeEntries = Object.entries(initialNodes).sort(alphabetize); + + for (const [, keys] of sortedNodeEntries) { + keys.sort(alphabetize); + } + + console.log('sortedNodeEntries', sortedNodeEntries); return Object.fromEntries(sortedNodeEntries); }