From 0e6d5054aa01cb58d37f15e63c3fe1f7aebf986f Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Fri, 20 Jul 2018 18:16:14 +0200 Subject: [PATCH] add tool to copy method signatures from base classes --- .prettierignore | 1 + package.json | 4 +- tooling/inherit-types.js | 144 ++++++++++++++++++++++++++++++++++ tooling/typescript-program.js | 17 ++++ tsconfig.json | 2 +- 5 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 tooling/inherit-types.js create mode 100644 tooling/typescript-program.js diff --git a/.prettierignore b/.prettierignore index 1f362540511..44cf7b81cdd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,6 +9,7 @@ !buildin/*.js !benchmark/**/*.js !test/*.js +!tooling/*.js !test/**/webpack.config.js !examples/**/webpack.config.js !schemas/**/*.js diff --git a/package.json b/package.json index dd1fe8b9e7f..3e82d902025 100644 --- a/package.json +++ b/package.json @@ -128,10 +128,10 @@ "pretest": "yarn lint", "prelint": "yarn setup", "lint": "yarn code-lint && yarn schema-lint && yarn type-lint", - "code-lint": "eslint --cache setup lib bin hot buildin benchmark \"test/*.js\" \"test/**/webpack.config.js\" \"examples/**/webpack.config.js\" \"schemas/**/*.js\"", + "code-lint": "eslint --cache setup lib bin hot buildin benchmark tooling \"test/*.js\" \"test/**/webpack.config.js\" \"examples/**/webpack.config.js\" \"schemas/**/*.js\"", "type-lint": "tsc --pretty", "fix": "yarn code-lint --fix", - "pretty": "prettier --write \"setup/**/*.js\" \"lib/**/*.js\" \"bin/*.js\" \"hot/*.js\" \"buildin/*.js\" \"benchmark/**/*.js\" \"test/*.js\" \"test/**/webpack.config.js\" \"examples/**/webpack.config.js\" \"schemas/**/*.js\" \"declarations.d.ts\" \"tsconfig.json\"", + "pretty": "prettier --write \"setup/**/*.js\" \"lib/**/*.js\" \"bin/*.js\" \"hot/*.js\" \"buildin/*.js\" \"benchmark/**/*.js\" \"tooling/*.js\" \"test/*.js\" \"test/**/webpack.config.js\" \"examples/**/webpack.config.js\" \"schemas/**/*.js\" \"declarations.d.ts\" \"tsconfig.json\"", "schema-lint": "node --max-old-space-size=4096 node_modules/jest-cli/bin/jest --testMatch \"/test/*.lint.js\" --no-verbose", "benchmark": "node --max-old-space-size=4096 --trace-deprecation node_modules/jest-cli/bin/jest --testMatch \"/test/*.benchmark.js\" --runInBand", "cover": "yarn cover:init && yarn cover:all && yarn cover:report", diff --git a/tooling/inherit-types.js b/tooling/inherit-types.js new file mode 100644 index 00000000000..79bcdcdd0f7 --- /dev/null +++ b/tooling/inherit-types.js @@ -0,0 +1,144 @@ +const path = require("path"); +const fs = require("fs"); +const ts = require("typescript"); +const program = require("./typescript-program"); + +// When --override is set, base jsdoc will override sub class jsdoc +// Elsewise on a conflict it will create a merge conflict in the file +const override = process.argv.includes("--override"); + +// When --write is set, files will be written in place +// Elsewise it only prints outdated files +const doWrite = process.argv.includes("--write"); + +const typeChecker = program.getTypeChecker(); + +/** + * @param {ts.ClassDeclaration} node the class declaration + * @returns {Set} the base class declarations + */ +const getBaseClasses = node => { + /** @type {Set} */ + const decls = new Set(); + if (node.heritageClauses) { + for (const clause of node.heritageClauses) { + for (const clauseType of clause.types) { + const type = typeChecker.getTypeAtLocation(clauseType); + if (ts.isClassDeclaration(type.symbol.valueDeclaration)) + decls.add(type.symbol.valueDeclaration); + } + } + } + return decls; +}; + +/** + * @param {ts.ClassDeclaration} classNode the class declaration + * @param {string} memberName name of the member + * @returns {ts.MethodDeclaration | null} base class member declaration when found + */ +const findDeclarationInBaseClass = (classNode, memberName) => { + for (const baseClass of getBaseClasses(classNode)) { + for (const node of baseClass.members) { + if (ts.isMethodDeclaration(node)) { + if (node.name.getText() === memberName) { + return node; + } + } + } + const result = findDeclarationInBaseClass(baseClass, memberName); + if (result) return result; + } + return null; +}; + +const libPath = path.resolve(__dirname, "../lib"); + +for (const sourceFile of program.getSourceFiles()) { + let file = sourceFile.fileName; + if ( + file.toLowerCase().startsWith(libPath.replace(/\\/g, "/").toLowerCase()) + ) { + const updates = []; + sourceFile.forEachChild(node => { + if (ts.isClassDeclaration(node)) { + for (const member of node.members) { + if (ts.isMethodDeclaration(member)) { + const baseDecl = findDeclarationInBaseClass( + node, + member.name.getText() + ); + if (baseDecl) { + const memberAsAny = /** @type {any} */ (member); + const baseDeclAsAny = /** @type {any} */ (baseDecl); + const currentJsDoc = memberAsAny.jsDoc && memberAsAny.jsDoc[0]; + const baseJsDoc = baseDeclAsAny.jsDoc && baseDeclAsAny.jsDoc[0]; + const currentJsDocText = currentJsDoc && currentJsDoc.getText(); + let baseJsDocText = baseJsDoc && baseJsDoc.getText(); + if (baseJsDocText) { + baseJsDocText = baseJsDocText.replace( + /\t \* @abstract\r?\n/g, + "" + ); + if (!currentJsDocText) { + // add js doc + updates.push({ + member: member.name.getText(), + start: member.getStart(), + end: member.getStart(), + content: baseJsDocText + "\n\t" + }); + } else if ( + baseJsDocText && + currentJsDocText !== baseJsDocText + ) { + // update js doc + if (override || !doWrite) { + updates.push({ + member: member.name.getText(), + start: currentJsDoc.getStart(), + end: currentJsDoc.getEnd(), + content: baseJsDocText + }); + } else { + updates.push({ + member: member.name.getText(), + start: currentJsDoc.getStart() - 1, + end: currentJsDoc.getEnd(), + content: `<<<<<<< original comment\n\t${currentJsDocText}\n=======\n\t${baseJsDocText}\n>>>>>>> comment from base class` + }); + } + } + } + } + } + } + } + }); + if (updates.length > 0) { + if (doWrite) { + let fileContent = fs.readFileSync(file, "utf-8"); + updates.sort((a, b) => { + return b.start - a.start; + }); + for (const update of updates) { + fileContent = + fileContent.substr(0, update.start) + + update.content + + fileContent.substr(update.end); + } + console.log(`${file} ${updates.length} JSDoc comments added/updated`); + fs.writeFileSync(file, fileContent, "utf-8"); + } else { + console.log(file); + for (const update of updates) { + console.log( + `* ${update.member} should have this JSDoc:\n\t${update.content}` + ); + } + console.log(); + } + process.exitCode = 1; + } + } +} diff --git a/tooling/typescript-program.js b/tooling/typescript-program.js new file mode 100644 index 00000000000..bd9413ab6ae --- /dev/null +++ b/tooling/typescript-program.js @@ -0,0 +1,17 @@ +const path = require("path"); +const fs = require("fs"); +const ts = require("typescript"); + +const rootPath = path.resolve(__dirname, ".."); +const configPath = path.resolve(__dirname, "../tsconfig.json"); +const configContent = fs.readFileSync(configPath, "utf-8"); +const configJsonFile = ts.parseJsonText(configPath, configContent); +const parsedConfig = ts.parseJsonSourceFileConfigFileContent( + configJsonFile, + ts.sys, + rootPath, + { noEmit: true } +); +const { fileNames, options } = parsedConfig; + +module.exports = ts.createProgram(fileNames, options); diff --git a/tsconfig.json b/tsconfig.json index b4a8a3be59c..d265178d6c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "types": ["node"], "esModuleInterop": true }, - "include": ["declarations.d.ts", "bin/*.js", "lib/**/*.js"] + "include": ["declarations.d.ts", "bin/*.js", "lib/**/*.js", "tooling/**/*.js"] }