mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-11 13:38:46 +00:00
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
This commit is contained in:
8
strict-null-checks/README.md
Normal file
8
strict-null-checks/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
Licensed under the MIT License.
|
||||
-->
|
||||
|
||||
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)
|
||||
100
strict-null-checks/auto-add.js
Normal file
100
strict-null-checks/auto-add.js
Normal file
@@ -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);
|
||||
});
|
||||
83
strict-null-checks/collapse-completed-directories.js
Normal file
83
strict-null-checks/collapse-completed-directories.js
Normal file
@@ -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,
|
||||
};
|
||||
11
strict-null-checks/config.js
Normal file
11
strict-null-checks/config.js
Normal file
@@ -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([]),
|
||||
};
|
||||
70
strict-null-checks/eligible-file-finder.js
Normal file
70
strict-null-checks/eligible-file-finder.js
Normal file
@@ -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,
|
||||
};
|
||||
63
strict-null-checks/find.js
Normal file
63
strict-null-checks/find.js
Normal file
@@ -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);
|
||||
});
|
||||
34
strict-null-checks/import-finder.js
Normal file
34
strict-null-checks/import-finder.js
Normal file
@@ -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,
|
||||
};
|
||||
12
strict-null-checks/write-tsconfig.js
Normal file
12
strict-null-checks/write-tsconfig.js
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user