From dc5679ffd3f596b44b14afef909128a6ce0beeee Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Mon, 12 Apr 2021 13:12:19 -0700 Subject: [PATCH] Switch to accessibility insights's version of these tools (#603) * Switch to accessibility insights's version of these tools * auto-add files meeting strict checks --- package.json | 4 +- strict-migration-tools/.gitignore | 1 - strict-migration-tools/README.md | 25 ----- strict-migration-tools/autoAdd.js | 55 ---------- strict-migration-tools/index.js | 53 ---------- strict-migration-tools/package-lock.json | 90 ---------------- strict-migration-tools/package.json | 17 --- strict-migration-tools/src/config.js | 3 - .../src/getStrictNullCheckEligibleFiles.js | 94 ---------------- strict-migration-tools/src/tsHelper.js | 44 -------- strict-null-checks/README.md | 8 ++ strict-null-checks/auto-add.js | 100 ++++++++++++++++++ .../collapse-completed-directories.js | 83 +++++++++++++++ strict-null-checks/config.js | 11 ++ strict-null-checks/eligible-file-finder.js | 70 ++++++++++++ strict-null-checks/find.js | 63 +++++++++++ strict-null-checks/import-finder.js | 34 ++++++ strict-null-checks/write-tsconfig.js | 12 +++ tsconfig.strict.json | 94 ++++++++-------- 19 files changed, 432 insertions(+), 429 deletions(-) delete mode 100644 strict-migration-tools/.gitignore delete mode 100644 strict-migration-tools/README.md delete mode 100644 strict-migration-tools/autoAdd.js delete mode 100644 strict-migration-tools/index.js delete mode 100644 strict-migration-tools/package-lock.json delete mode 100644 strict-migration-tools/package.json delete mode 100644 strict-migration-tools/src/config.js delete mode 100644 strict-migration-tools/src/getStrictNullCheckEligibleFiles.js delete mode 100644 strict-migration-tools/src/tsHelper.js create mode 100644 strict-null-checks/README.md create mode 100644 strict-null-checks/auto-add.js create mode 100644 strict-null-checks/collapse-completed-directories.js create mode 100644 strict-null-checks/config.js create mode 100644 strict-null-checks/eligible-file-finder.js create mode 100644 strict-null-checks/find.js create mode 100644 strict-null-checks/import-finder.js create mode 100644 strict-null-checks/write-tsconfig.js diff --git a/package.json b/package.json index e15deb826..cc1f0ea53 100644 --- a/package.json +++ b/package.json @@ -200,8 +200,8 @@ "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"", "lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"", "build:contracts": "npm run compile:contracts", - "strictEligibleFiles": "node ./strict-migration-tools/index.js", - "autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js", + "strict:find": "node ./strict-null-checks/find.js", + "strict:add": "node ./strict-null-checks/auto-add.js", "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks", "generateARMClients": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts" }, diff --git a/strict-migration-tools/.gitignore b/strict-migration-tools/.gitignore deleted file mode 100644 index b512c09d4..000000000 --- a/strict-migration-tools/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/strict-migration-tools/README.md b/strict-migration-tools/README.md deleted file mode 100644 index 654116bbe..000000000 --- a/strict-migration-tools/README.md +++ /dev/null @@ -1,25 +0,0 @@ -Borrowed from https://github.com/mjbvz/vscode-strict-null-check-migration-tools/tree/f1da7c12fe6e93618a310cb3662e2ade808be0c4 - -Scripts to help [migrate VS Code to use strict null checks](https://github.com/Microsoft/vscode/issues/60565) - -## Usage - -```bash -$ npm install -``` - -**index.js** - -The main script prints of list of files that are eligible for strict null checks. This includes all files that only import files thare are already strict null checked. - -```bash -$ node index.js /path/to/vscode -``` - -**autoAdd.js** - -Very simple script that tries to auto add any eligible file to the `tsconfig.strictNullChecks.json`. This iteratively compiles the `tsconfig` project with just that file added. If there are no errors, it is added to the `tsconfig` - -```bash -$ node autoAdd.js /path/to/vscode -``` diff --git a/strict-migration-tools/autoAdd.js b/strict-migration-tools/autoAdd.js deleted file mode 100644 index 230805c10..000000000 --- a/strict-migration-tools/autoAdd.js +++ /dev/null @@ -1,55 +0,0 @@ -// @ts-check -const path = require("path"); -const fs = require("fs"); -const child_process = require("child_process"); -const config = require("./src/config"); -const { forStrictNullCheckEligibleFiles } = require("./src/getStrictNullCheckEligibleFiles"); - -const vscodeRoot = path.join(process.cwd()); -const srcRoot = path.join(vscodeRoot, "src"); - -const buildCompletePattern = /Found (\d+) errors?\. Watching for file changes\./gi; - -forStrictNullCheckEligibleFiles(vscodeRoot, () => {}).then(async files => { - const tsconfigPath = path.join(srcRoot, config.targetTsconfig); - - const child = child_process.spawn("tsc", ["-p", tsconfigPath, "--watch"]); - for (const file of files) { - await tryAutoAddStrictNulls(child, tsconfigPath, file); - } - child.kill(); -}); - -function tryAutoAddStrictNulls(child, tsconfigPath, file) { - return new Promise(resolve => { - const relativeFilePath = path.relative(srcRoot, file); - console.log(`Trying to auto add ./src/${relativeFilePath}`); - - const originalConifg = JSON.parse(fs.readFileSync(tsconfigPath).toString()); - originalConifg.files = Array.from(new Set(originalConifg.files.sort())); - - // Config on accept - const newConfig = Object.assign({}, originalConifg); - newConfig.files = Array.from(new Set(originalConifg.files.concat("./src/" + relativeFilePath).sort())); - - fs.writeFileSync(tsconfigPath, JSON.stringify(newConfig, null, "\t")); - - const listener = data => { - const textOut = data.toString(); - const match = buildCompletePattern.exec(textOut); - if (match) { - const errorCount = +match[1]; - if (errorCount === 0) { - console.log(`👍`); - fs.writeFileSync(tsconfigPath, JSON.stringify(newConfig, null, "\t")); - } else { - console.log(`💥 - ${errorCount}`); - fs.writeFileSync(tsconfigPath, JSON.stringify(originalConifg, null, "\t")); - } - resolve(); - child.stdout.removeListener("data", listener); - } - }; - child.stdout.on("data", listener); - }); -} diff --git a/strict-migration-tools/index.js b/strict-migration-tools/index.js deleted file mode 100644 index cf9c7e574..000000000 --- a/strict-migration-tools/index.js +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-check -const path = require("path"); -const glob = require("glob"); -const { forStrictNullCheckEligibleFiles, forEachFileInSrc } = require("./src/getStrictNullCheckEligibleFiles"); -const { getImportsForFile } = require("./src/tsHelper"); - -const projectRoot = path.join(process.cwd()); -const srcRoot = path.join(projectRoot, "src"); - -let sort = true; -let filter; -let printDependedOnCount = true; -let includeTests = false; - -// if (false) { -// // Generate test files listing -// sort = false; -// filter = x => x.endsWith(".test.ts"); -// printDependedOnCount = false; -// includeTests = true; -// } - -forStrictNullCheckEligibleFiles(projectRoot, () => {}, { includeTests }).then(async eligibleFiles => { - console.log(eligibleFiles); - // const eligibleSet = new Set(eligibleFiles); - // const dependedOnCount = new Map(eligibleFiles.map(file => [file, 0])); - // for (const file of await forEachFileInSrc(srcRoot)) { - // if (eligibleSet.has(file)) { - // // Already added - // continue; - // } - // for (const imp of getImportsForFile(file, srcRoot)) { - // if (dependedOnCount.has(imp)) { - // dependedOnCount.set(imp, dependedOnCount.get(imp) + 1); - // } - // } - // } - // let out = Array.from(dependedOnCount.entries()); - // if (filter) { - // out = out.filter(x => filter(x[0])); - // } - // if (sort) { - // out = out.sort((a, b) => b[1] - a[1]); - // } - // for (const pair of out) { - // console.log(toFormattedFilePath(pair[0]) + (printDependedOnCount ? ` — Depended on by **${pair[1]}** files` : "")); - // } -}); - -// function toFormattedFilePath(file) { -// // return `"./${path.relative(srcRoot, file)}",`; -// return `- [ ] \`"./${path.relative(srcRoot, file)}"\``; -// } diff --git a/strict-migration-tools/package-lock.json b/strict-migration-tools/package-lock.json deleted file mode 100644 index 974049169..000000000 --- a/strict-migration-tools/package-lock.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "vscode-strict-null-tools", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "typescript": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.2.tgz", - "integrity": "sha512-gOoGJWbNnFAfP9FlrSV63LYD5DJqYJHG5ky1kOXSl3pCImn4rqWy/flyq1BRd4iChQsoCqjbQaqtmXO4yCVPCA==" - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } -} diff --git a/strict-migration-tools/package.json b/strict-migration-tools/package.json deleted file mode 100644 index 186da4e45..000000000 --- a/strict-migration-tools/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "vscode-strict-null-tools", - "private": true, - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "Matt Bierner", - "license": "MIT", - "dependencies": { - "typescript": "3.1.2", - "glob": "^7.1.3" - } -} diff --git a/strict-migration-tools/src/config.js b/strict-migration-tools/src/config.js deleted file mode 100644 index f65e25d9e..000000000 --- a/strict-migration-tools/src/config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports.targetTsconfig = "../tsconfig.strict.json"; - -module.exports.skippedFiles = new Set([]); diff --git a/strict-migration-tools/src/getStrictNullCheckEligibleFiles.js b/strict-migration-tools/src/getStrictNullCheckEligibleFiles.js deleted file mode 100644 index 74322673f..000000000 --- a/strict-migration-tools/src/getStrictNullCheckEligibleFiles.js +++ /dev/null @@ -1,94 +0,0 @@ -// @ts-check -const path = require("path"); -const fs = require("fs"); -const { getImportsForFile } = require("./tsHelper"); -const glob = require("glob"); -const config = require("./config"); - -/** - * @param {string} srcRoot - * @param {{ includeTests: boolean }} [options] - */ -const forEachFileInSrc = (srcRoot, options) => { - return new Promise((resolve, reject) => { - glob(`${srcRoot}/**/*.ts`, (err, files) => { - if (err) { - return reject(err); - } - - return resolve( - files.filter( - file => !file.endsWith(".d.ts") && (options && options.includeTests ? true : !file.endsWith(".test.ts")) - ) - ); - }); - }); -}; -module.exports.forEachFileInSrc = forEachFileInSrc; - -/** - * @param {string} vscodeRoot - * @param {(file: string) => void} forEach - * @param {{ includeTests: boolean }} [options] - */ -module.exports.forStrictNullCheckEligibleFiles = async (vscodeRoot, forEach, options) => { - const srcRoot = path.join(vscodeRoot, "src"); - - const tsconfig = JSON.parse(fs.readFileSync(path.join(srcRoot, config.targetTsconfig)).toString()); - const checkedFiles = await getCheckedFiles(tsconfig, vscodeRoot); - - const imports = new Map(); - const getMemoizedImportsForFile = (file, srcRoot) => { - if (imports.has(file)) { - return imports.get(file); - } - const importList = getImportsForFile(file, srcRoot); - imports.set(file, importList); - return importList; - }; - - const files = await forEachFileInSrc(srcRoot, options); - return files - .filter(file => !checkedFiles.has(file)) - .filter(file => !config.skippedFiles.has(path.relative(srcRoot, file))) - .filter(file => { - const allProjImports = getMemoizedImportsForFile(file, srcRoot); - - const nonCheckedImports = allProjImports - .filter(x => x !== file) - .filter(imp => { - if (checkedFiles.has(imp)) { - return false; - } - // Don't treat cycles as blocking - const impImports = getMemoizedImportsForFile(imp, srcRoot); - return impImports.filter(x => x !== file).filter(x => !checkedFiles.has(x)).length !== 0; - }); - - const isEdge = nonCheckedImports.length === 0; - if (isEdge) { - forEach(file); - } - return isEdge; - }); -}; - -async function getCheckedFiles(tsconfig, srcRoot) { - const set = new Set(tsconfig.files.map(include => path.join(srcRoot, include))); - const includes = tsconfig.include.map(include => { - return new Promise((resolve, reject) => { - glob(path.join(srcRoot, include), (err, files) => { - if (err) { - return reject(err); - } - - for (const file of files) { - set.add(file); - } - resolve(); - }); - }); - }); - await Promise.all(includes); - return set; -} diff --git a/strict-migration-tools/src/tsHelper.js b/strict-migration-tools/src/tsHelper.js deleted file mode 100644 index 3d79a6304..000000000 --- a/strict-migration-tools/src/tsHelper.js +++ /dev/null @@ -1,44 +0,0 @@ -// @ts-check -const path = require("path"); -const ts = require("typescript"); -const fs = require("fs"); - -module.exports.getImportsForFile = function getImportsForFile(file, srcRoot) { - const fileInfo = ts.preProcessFile(fs.readFileSync(file).toString()); - return fileInfo.importedFiles - .map(importedFile => importedFile.fileName) - .filter(fileName => !/svg|gif|png|html|less|json|externals|css|ico/.test(fileName)) // remove image imports - .filter(x => /\//.test(x)) // remove node modules (the import must contain '/') - .filter(x => !/\@/.test(x)) // remove @ scoped modules - .filter( - x => - !/url-polyfill|office-ui-fabric|rxjs|\@nteract|bootstrap|promise-polyfill|abort-controller|es6-object-assign|es6-symbol|webcrypto-liner|promise.prototype.finally|object.entries/.test( - x - ) - ) // remove other modules - .filter(x => !/worker-loader/.test(x)) // remove other modules - .map(fileName => { - if (/(^\.\/)|(^\.\.\/)/.test(fileName)) { - return path.join(path.dirname(file), fileName); - } - if (/^vs/.test(fileName)) { - return path.join(srcRoot, fileName); - } - return fileName; - }) - .map(fileName => { - if (fs.existsSync(`${fileName}.ts`)) { - return `${fileName}.ts`; - } - if (fs.existsSync(`${fileName}.js`)) { - return `${fileName}.js`; - } - if (fs.existsSync(`${fileName}.d.ts`)) { - return `${fileName}.d.ts`; - } - if (fs.existsSync(`${fileName}.tsx`)) { - return `${fileName}.tsx`; - } - throw new Error(`Unresolved import ${fileName} in ${file}`); - }); -}; diff --git a/strict-null-checks/README.md b/strict-null-checks/README.md new file mode 100644 index 000000000..d32e6a091 --- /dev/null +++ b/strict-null-checks/README.md @@ -0,0 +1,8 @@ + + +Scripts to help migrate to use strict null checks. + +Modified from [strict-null-checks](https://github.com/microsoft/accessibility-insights-web/tree/f2ec74cc5f09d41a4b617b0eb941b6da332a4343/tools/strict-null-checks) diff --git a/strict-null-checks/auto-add.js b/strict-null-checks/auto-add.js new file mode 100644 index 000000000..a124d0053 --- /dev/null +++ b/strict-null-checks/auto-add.js @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// @ts-check +const child_process = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const { collapseCompletedDirectories } = require("./collapse-completed-directories"); +const config = require("./config"); +const { getUncheckedLeafFiles } = require("./eligible-file-finder"); +const { writeTsconfigSync } = require("./write-tsconfig"); + +const repoRoot = config.repoRoot; +const tscPath = path.join(repoRoot, "node_modules", "typescript", "bin", "tsc"); +const tsconfigPath = path.join(repoRoot, config.targetTsconfig); + +async function main() { + console.log("## Initializing tsc --watch process..."); + const tscWatchProcess = child_process.spawn("node", [tscPath, "-p", tsconfigPath, "--watch"]); + await waitForBuildComplete(tscWatchProcess); + + const alreadyAttempted = new Set(); + + for (let pass = 1; ; pass += 1) { + let successesThisPass = 0; + const uncheckedLeafFiles = await getUncheckedLeafFiles(); + const candidateFiles = uncheckedLeafFiles.filter((f) => !alreadyAttempted.has(f)); + const candidateCount = candidateFiles.length; + console.log(`## Starting pass ${pass} with ${candidateCount} candidate files`); + + for (const file of candidateFiles) { + alreadyAttempted.add(file); + if (await tryAutoAddStrictNulls(tscWatchProcess, tsconfigPath, file)) { + successesThisPass += 1; + } + } + + console.log(`### Finished pass ${pass} (added ${successesThisPass}/${candidateCount})`); + if (successesThisPass === 0) { + break; + } + } + + console.log("## Stopping tsc --watch process..."); + tscWatchProcess.kill(); + + console.log('## Collapsing fully null-checked directories into "include" patterns...'); + collapseCompletedDirectories(tsconfigPath); +} + +async function tryAutoAddStrictNulls(child, tsconfigPath, file) { + const relativeFilePath = path.relative(repoRoot, file).replace(/\\/g, "/"); + const originalConfig = JSON.parse(fs.readFileSync(tsconfigPath).toString()); + originalConfig.files = Array.from(new Set(originalConfig.files.sort())); + + // Config on accept + const newConfig = Object.assign({}, originalConfig); + newConfig.files = Array.from(new Set(originalConfig.files.concat("./" + relativeFilePath).sort())); + + const buildCompetePromise = waitForBuildComplete(child); + + writeTsconfigSync(tsconfigPath, newConfig); + + const errorCount = await buildCompetePromise; + const success = errorCount === 0; + if (success) { + console.log(`${relativeFilePath}: added`); + } else { + console.log(`${relativeFilePath}: ${errorCount} error(s), skipped`); + writeTsconfigSync(tsconfigPath, originalConfig); + } + + return success; +} + +const buildCompletePattern = /Found (\d+) errors?\. Watching for file changes\./gi; +async function waitForBuildComplete(tscWatchProcess) { + const match = await waitForStdoutMatching(tscWatchProcess, buildCompletePattern); + const errorCount = +match[1]; + return errorCount; +} + +async function waitForStdoutMatching(child, pattern) { + return new Promise((resolve) => { + const listener = (data) => { + const textOut = data.toString(); + const match = pattern.exec(textOut); + if (match) { + child.stdout.removeListener("data", listener); + resolve(match); + } + }; + child.stdout.on("data", listener); + }); +} + +main().catch((error) => { + console.error(error.stack); + process.exit(1); +}); diff --git a/strict-null-checks/collapse-completed-directories.js b/strict-null-checks/collapse-completed-directories.js new file mode 100644 index 000000000..67b516e7c --- /dev/null +++ b/strict-null-checks/collapse-completed-directories.js @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// @ts-check +const fs = require("fs"); +const path = require("path"); +const config = require("./config"); +const { writeTsconfigSync } = require("./write-tsconfig"); + +const repoRoot = config.repoRoot; + +function collapseCompletedDirectories(tsconfigPath) { + const tsconfigContent = JSON.parse(fs.readFileSync(tsconfigPath).toString()); + const listedFiles = Array.from(new Set(tsconfigContent.files.sort())); + const listedIncludes = Array.from(new Set(tsconfigContent.include.sort())); + const listedDirectories = listedIncludes.map(includeToDirectory); + const completedSet = new Set([...listedFiles, ...listedDirectories]); + + reduceCompletedSet(completedSet, "./src"); + + const completedPaths = Array.from(completedSet).sort(); + tsconfigContent.files = completedPaths.filter(isTsFile); + tsconfigContent.include = completedPaths.filter(isSourceDirectory).map(directoryToInclude); + + writeTsconfigSync(tsconfigPath, tsconfigContent); +} + +// convert from src/common/styles/**/* to ./src/common/styles +function includeToDirectory(include) { + return "./" + include.replace("/**/*", ""); +} + +// convert from ./src/common/styles to src/common/styles/**/* +function directoryToInclude(directory) { + return directory.substring(2) + "/**/*"; +} + +function reduceCompletedSet(completedSet, root) { + if (completedSet.has(root)) { + return true; + } + if (!isSourceDirectory(root)) { + return false; + } + + const children = listRelevantChildren(root); + let allChildrenReduced = true; + for (const child of children) { + const childReduced = reduceCompletedSet(completedSet, child); + allChildrenReduced = allChildrenReduced && childReduced; + } + + if (allChildrenReduced) { + for (const child of children) { + completedSet.delete(child); + } + completedSet.add(root); + } + return allChildrenReduced; +} + +function isSourceDirectory(relativePath) { + // this assumes directories don't have .s in their names, which isn't robust generally + // but happens to be true in our repo + const isDirectory = -1 === relativePath.indexOf(".", 1); + return isDirectory && !relativePath.includes("__snapshots__"); +} + +const isTsFileRegex = /\.(ts|tsx)$/; +function isTsFile(relativePath) { + return isTsFileRegex.test(relativePath); +} + +function listRelevantChildren(relativePath) { + const rawReaddir = fs.readdirSync(path.join(repoRoot, relativePath)); + const directories = rawReaddir.filter(isSourceDirectory); + const tsFiles = rawReaddir.filter(isTsFile); + return [...directories, ...tsFiles].map((name) => relativePath + "/" + name); +} + +module.exports = { + collapseCompletedDirectories, +}; diff --git a/strict-null-checks/config.js b/strict-null-checks/config.js new file mode 100644 index 000000000..14dac77a6 --- /dev/null +++ b/strict-null-checks/config.js @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +const path = require("path"); +const repoRoot = path.join(__dirname, "../").replace(/\\/g, "/"); + +module.exports = { + repoRoot: repoRoot, + srcRoot: `${repoRoot}/src`, + targetTsconfig: "tsconfig.strict.json", + skippedFiles: new Set([]), +}; diff --git a/strict-null-checks/eligible-file-finder.js b/strict-null-checks/eligible-file-finder.js new file mode 100644 index 000000000..0a890c836 --- /dev/null +++ b/strict-null-checks/eligible-file-finder.js @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// @ts-check +const fs = require("fs"); +const path = require("path"); +const glob = require("glob"); +const config = require("./config"); +const { getMemoizedImportsForFile } = require("./import-finder"); + +// "Eligible" means "a file that we might want to list in tsconfig.strictNullChecks.json" +// "Checked" means "a file that is currently listed in tsconfig.strictNullChecks.json" +// It is possible for an ineligible file to be checked (eg, a png under /src/icons/**) + +const isEligibleFile = (file) => !config.skippedFiles.has(path.relative(config.srcRoot, file)); + +function globAsync(pattern) { + return new Promise((resolve, reject) => glob(pattern, (err, files) => (err ? reject(err) : resolve(files)))); +} + +// Includes both checked and unchecked files (ie, doesn't care about inclusion in tsconfig.strictNullChecks.json) +async function getAllEligibleFiles() { + const tsFiles = await globAsync(`${config.srcRoot}/**/*.@(ts|tsx)`); + return tsFiles.filter(isEligibleFile); +} + +// Includes ineligible files that are listed under glob patterns in tsconfig.strictNullChecks +async function getAllCheckedFiles() { + const tsconfigPath = path.join(config.repoRoot, config.targetTsconfig); + const tsconfigContent = JSON.parse(fs.readFileSync(tsconfigPath).toString()); + + const set = new Set(tsconfigContent.files.map((f) => path.join(config.repoRoot, f).replace(/\\/g, "/"))); + await Promise.all( + tsconfigContent.include.map(async (include) => { + const includePath = path.join(config.repoRoot, include); + const files = await globAsync(includePath); + for (const file of files) { + set.add(file); + } + }) + ); + return set; +} + +async function getUncheckedLeafFiles() { + const checkedFiles = await getAllCheckedFiles(); + const eligibleFiles = await getAllEligibleFiles(); + const eligibleFileSet = new Set(eligibleFiles); + const allUncheckedFiles = eligibleFiles.filter((file) => !checkedFiles.has(file)); + + const areAllImportsChecked = (file) => { + const allImports = getMemoizedImportsForFile(file, config.srcRoot); + const uncheckedImports = allImports.filter((imp) => !checkedFiles.has(imp)); + const ineligibleUncheckedImports = uncheckedImports.filter((imp) => !eligibleFileSet.has(imp)); + if (ineligibleUncheckedImports.length > 0) { + console.warn( + `Eligible file ${file} with unchecked ineligible imports [${ineligibleUncheckedImports.join(", ")}]` + ); + } + return uncheckedImports.length === 0; + }; + + return allUncheckedFiles.filter(areAllImportsChecked); +} + +module.exports = { + getAllEligibleFiles, + getUncheckedLeafFiles, + getAllCheckedFiles, +}; diff --git a/strict-null-checks/find.js b/strict-null-checks/find.js new file mode 100644 index 000000000..924556662 --- /dev/null +++ b/strict-null-checks/find.js @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// @ts-check +const path = require("path"); +const process = require("process"); +const { srcRoot } = require("./config"); +const { getUncheckedLeafFiles, getAllEligibleFiles } = require("./eligible-file-finder"); +const { getImportsForFile } = require("./import-finder"); + +if (process.argv.includes("--help")) { + console.log("yarn null:find [--sort=name|count] [--show-count] [--filter file_path_substring]"); + process.exit(0); +} +const sortBy = process.argv.includes("--sort=name") ? "name" : "count"; +const printDependedOnCount = process.argv.includes("--show-count"); +const filterArgIndex = process.argv.indexOf("--filter") + 1; +const filterArg = filterArgIndex === 0 ? null : process.argv[filterArgIndex]; +const filter = filterArg && ((file) => file.includes(filterArg)); + +async function main() { + const eligibleFiles = await getUncheckedLeafFiles(); + + const eligibleSet = new Set(eligibleFiles); + + const dependedOnCount = new Map(eligibleFiles.map((file) => [file, 0])); + + for (const file of await getAllEligibleFiles()) { + if (eligibleSet.has(file)) { + // Already added + continue; + } + + for (const imp of getImportsForFile(file, srcRoot)) { + if (dependedOnCount.has(imp)) { + dependedOnCount.set(imp, dependedOnCount.get(imp) + 1); + } + } + } + + let out = Array.from(dependedOnCount.entries()); + if (filter) { + out = out.filter((x) => filter(x[0])); + } + if (sortBy === "count") { + out = out.sort((a, b) => b[1] - a[1]); + } else if (sortBy === "name") { + out = out.sort((a, b) => a[0].localeCompare(b[0])); + } + for (const pair of out) { + console.log(toFormattedFilePath(pair[0]) + (printDependedOnCount ? ` — Depended on by **${pair[1]}** files` : "")); + } +} + +function toFormattedFilePath(file) { + const relativePath = path.relative(srcRoot, file).replace(/\\/g, "/"); + return `"./src/${relativePath}",`; +} + +main().catch((error) => { + console.error(error.stack); + process.exit(1); +}); diff --git a/strict-null-checks/import-finder.js b/strict-null-checks/import-finder.js new file mode 100644 index 000000000..f3b60a5b4 --- /dev/null +++ b/strict-null-checks/import-finder.js @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// @ts-check +const fs = require("fs"); +const path = require("path"); +const ts = require("typescript"); + +const imports = new Map(); +const getMemoizedImportsForFile = (file, srcRoot) => { + if (imports.has(file)) { + return imports.get(file); + } + const importList = getImportsForFile(file, srcRoot); + imports.set(file, importList); + return importList; +}; + +function getImportsForFile(parent, srcRoot) { + return ts + .preProcessFile(fs.readFileSync(parent).toString()) + .importedFiles.map(({ fileName }) => fileName) + .filter((base) => /\//.test(base)) // remove node modules (the import must contain '/') + .map((base) => (/(^\.\/)|(^\.\.\/)/.test(base) ? path.join(path.dirname(parent), base) : path.join(srcRoot, base))) + .map((base) => (fs.existsSync(base) ? path.join(base, "index") : base)) + .map((base) => base.replace(/\\/g, "/")) + .map((base) => ["ts", "tsx", "d.ts", "js", "jsx"].map((ext) => `${base}.${ext}`).find(fs.existsSync)) + .filter((base) => base && base !== parent); +} + +module.exports = { + getImportsForFile, + getMemoizedImportsForFile, +}; diff --git a/strict-null-checks/write-tsconfig.js b/strict-null-checks/write-tsconfig.js new file mode 100644 index 000000000..3bc81a7d6 --- /dev/null +++ b/strict-null-checks/write-tsconfig.js @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +const fs = require("fs"); + +module.exports = { + writeTsconfigSync: (tsconfigPath, content) => { + let serializedContent = JSON.stringify(content, null, " "); + serializedContent += "\n"; + + fs.writeFileSync(tsconfigPath, serializedContent); + }, +}; diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 536d7b4ab..c0972d852 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -15,107 +15,111 @@ "./src/Common/DocumentUtility.ts", "./src/Common/EnvironmentUtility.ts", "./src/Common/HashMap.ts", + "./src/Common/HeadersUtility.test.ts", "./src/Common/HeadersUtility.ts", "./src/Common/Logger.ts", + "./src/Common/MessageHandler.test.ts", "./src/Common/MessageHandler.ts", "./src/Common/MongoUtility.ts", + "./src/Common/ObjectCache.test.ts", "./src/Common/ObjectCache.ts", + "./src/Common/OfferUtility.test.ts", "./src/Common/OfferUtility.ts", + "./src/Common/Splitter.ts", "./src/Common/ThemeUtility.ts", "./src/Common/UrlUtility.ts", - "./src/Common/Splitter.ts", "./src/ConfigContext.ts", "./src/Contracts/ActionContracts.ts", "./src/Contracts/DataModels.ts", "./src/Contracts/Diagnostics.ts", "./src/Contracts/ExplorerContracts.ts", + "./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SubscriptionType.ts", "./src/Contracts/Versions.ts", - "./src/Controls/Heatmap/Heatmap.ts", - "./src/Controls/Heatmap/HeatmapDatatypes.ts", "./src/DefaultAccountExperienceType.ts", - "./src/Definitions/globals.d.ts", - "./src/Definitions/html.d.ts", - "./src/Definitions/jquery-ui.d.ts", - "./src/Definitions/jquery.d.ts", - "./src/Definitions/plotly.js-cartesian-dist.d-min.ts", - "./src/Definitions/svg.d.ts", - "./src/Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.ts", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/InputTypeahead/InputTypeahead.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts", - "./src/Explorer/Graph/GraphExplorerComponent/__mocks__/GremlinClient.ts", + "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts", "./src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts", "./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts", - "./src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts", "./src/Explorer/Notebook/FileSystemUtil.ts", "./src/Explorer/Notebook/NTeractUtil.ts", "./src/Explorer/Notebook/NotebookComponent/actions.ts", "./src/Explorer/Notebook/NotebookComponent/loadTransform.ts", "./src/Explorer/Notebook/NotebookComponent/reducers.ts", "./src/Explorer/Notebook/NotebookComponent/types.ts", + "./src/Explorer/Notebook/NotebookContentClient.ts", "./src/Explorer/Notebook/NotebookContentItem.ts", + "./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx", + "./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx", "./src/Explorer/Notebook/NotebookUtil.ts", - "./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/Explorer/Panes/PaneComponents.ts", + "./src/Explorer/Panes/PanelFooterComponent.tsx", + "./src/Explorer/Panes/PanelLoadingScreen.tsx", "./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts", "./src/Explorer/Tables/Constants.ts", "./src/Explorer/Tables/CqlUtilities.ts", - "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts", "./src/Explorer/Tables/DataTable/CacheBase.ts", "./src/Explorer/Tables/Entities.ts", - "./src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts", - "./src/Explorer/Notebook/NotebookContentClient.ts", + "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts", + "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts", + "./src/Explorer/Tree/AccessibleVerticalList.ts", "./src/GitHub/GitHubConnector.ts", + "./src/HostedExplorerChildFrame.ts", "./src/Index.ts", "./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts", + "./src/Platform/Hosted/Components/SignInButton.tsx", + "./src/Platform/Hosted/extractFeatures.test.ts", + "./src/Platform/Hosted/extractFeatures.ts", "./src/ReactDevTools.ts", "./src/ResourceProvider/IResourceProviderClient.ts", + "./src/SelfServe/SelfServeStyles.tsx", + "./src/SelfServe/SqlX/SqlxTypes.ts", "./src/Shared/Constants.ts", + "./src/Shared/DefaultExperienceUtility.ts", "./src/Shared/ExplorerSettings.ts", "./src/Shared/PriceEstimateCalculator.ts", + "./src/Shared/StorageUtility.test.ts", "./src/Shared/StorageUtility.ts", + "./src/Shared/StringUtility.test.ts", "./src/Shared/StringUtility.ts", - "./src/Shared/Telemetry/TelemetryConstants.ts", - "./src/Shared/Telemetry/TelemetryProcessor.ts", "./src/Shared/appInsights.ts", - "./src/Shared/DefaultExperienceUtility.ts", - "./src/Terminal/index.ts", - "./src/Terminal/JupyterLabAppFactory.ts", "./src/UserContext.ts", + "./src/Utils/AutoPilotUtils.ts", + "./src/Utils/Base64Utils.test.ts", "./src/Utils/Base64Utils.ts", "./src/Utils/BlobUtils.ts", + "./src/Utils/GitHubUtils.test.ts", "./src/Utils/GitHubUtils.ts", + "./src/Utils/MessageValidation.test.ts", "./src/Utils/MessageValidation.ts", - "./src/Utils/StringUtils.ts", - "./src/Utils/WindowUtils.ts", - "./src/Utils/arm/generatedClients/2020-04-01/types.ts", - "./src/Utils/AutoPilotUtils.ts", "./src/Utils/PricingUtils.ts", - "./src/Utils/arm/generatedClients/2020-04-01/cassandraResources.ts", - "./src/Utils/arm/generatedClients/2020-04-01/collection.ts", - "./src/Utils/arm/generatedClients/2020-04-01/collectionPartition.ts", - "./src/Utils/arm/generatedClients/2020-04-01/collectionPartitionRegion.ts", - "./src/Utils/arm/generatedClients/2020-04-01/collectionRegion.ts", - "./src/Utils/arm/generatedClients/2020-04-01/database.ts", - "./src/Utils/arm/generatedClients/2020-04-01/databaseAccountRegion.ts", - "./src/Utils/arm/generatedClients/2020-04-01/databaseAccounts.ts", - "./src/Utils/arm/generatedClients/2020-04-01/gremlinResources.ts", - "./src/Utils/arm/generatedClients/2020-04-01/mongoDBResources.ts", - "./src/Utils/arm/generatedClients/2020-04-01/operations.ts", - "./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeId.ts", - "./src/Utils/arm/generatedClients/2020-04-01/partitionKeyRangeIdRegion.ts", - "./src/Utils/arm/generatedClients/2020-04-01/percentile.ts", - "./src/Utils/arm/generatedClients/2020-04-01/percentileSourceTarget.ts", - "./src/Utils/arm/generatedClients/2020-04-01/percentileTarget.ts", - "./src/Utils/arm/generatedClients/2020-04-01/sqlResources.ts", - "./src/Utils/arm/generatedClients/2020-04-01/tableResources.ts", - "./src/Utils/arm/request.ts", + "./src/Utils/StringUtils.ts", + "./src/Utils/WindowUtils.test.ts", + "./src/Utils/WindowUtils.ts", + "./src/hooks/useDirectories.tsx", + "./src/i18n.ts", "./src/quickstart.ts", "./src/setupTests.ts", + "./src/userContext.test.ts", "./src/workers/upload/definitions.ts" ], - "include": [] + "include": [ + "src/Controls/**/*", + "src/Definitions/**/*", + "src/Explorer/Controls/ErrorDisplayComponent/**/*", + "src/Explorer/Controls/RadioSwitchComponent/**/*", + "src/Explorer/Controls/ResizeSensorReactComponent/**/*", + "src/Explorer/Graph/GraphExplorerComponent/__mocks__/**/*", + "src/Explorer/Notebook/NotebookComponent/__mocks__/**/*", + "src/Libs/**/*", + "src/Localization/**/*", + "src/Platform/Emulator/**/*", + "src/Shared/Telemetry/**/*", + "src/Terminal/**/*", + "src/Utils/arm/**/*" + ] }