inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
449 lines (417 loc) • 11.6 kB
text/typescript
/*!
* This scripts ensures that files annotated with @noExternalImports don't import
* anything from outside the monorepo.
*/
import {
blueBright,
bold,
gray,
greenBright,
redBright,
yellow,
} from "ansi-colors";
import highlight, { fromJson as themeFromJson } from "cli-highlight";
import globrex from "globrex";
import path from "path";
import ts from "typescript";
import yargs from "yargs";
import { loadTSConfig, projectRoot } from "./tsAPITools";
function relativeToProject(filename: string): string {
return path.relative(projectRoot, filename).replace(/\\/g, "/");
}
export interface CodeFindQuery {
filePatterns?: string[];
excludeFilePatterns?: string[];
codePatterns?: string[];
excludeCodePatterns?: string[];
search: RegExp | string;
options?: Partial<{
additionalLines: number;
}>;
}
export interface Result {
file: string;
line: number;
character: number;
codePath: string;
formatted: string;
match: string;
}
const highlightTheme = themeFromJson({
keyword: "blue",
// function: ["yellow", "dim"],
built_in: ["cyan", "dim"],
string: "red",
type: ["cyan"],
comment: ["green", "dim"],
class: ["green"],
// default: "gray",
});
function formatResult(
sourceFile: ts.SourceFile,
result: Omit<Result, "formatted">,
additionalLines: number = 3,
): Result {
const ret: Result = {
...result,
formatted: "",
};
let source = sourceFile.text;
// Remember the leading tabs for each line
const leadingTabs = source
.split("\n")
.map((line) => line.match(/^\t*/)?.[0]?.length ?? 0);
// Then replace them with 4 spaces for formatting
source = source
.split("\n")
.map((line, i) => line.replace(/^\t*/, " ".repeat(leadingTabs[i])))
.join("\n");
let formattedSourceLines = highlight(source, {
language: "typescript",
theme: highlightTheme,
}).split("\n");
const lineIndicatorLength = Math.ceil(
Math.log10(formattedSourceLines.length),
);
formattedSourceLines = formattedSourceLines.map((line, i) => {
let ret = line;
if (i === result.line) ret = bold(ret);
const prefix = `${i + 1}`.padStart(lineIndicatorLength);
if (i === result.line) {
ret = `${bold(greenBright(prefix))} | ${ret}`;
ret +=
"\n" +
" ".repeat(prefix.length) +
" | " +
// Leading tabs are counted as a single character by TS, but we want to
// display them as 4 spaces. This means we need to offset the column number
// by 3 times the no. of tabs.
" ".repeat(3 * leadingTabs[i] + result.character) +
bold(greenBright("^".repeat(result.match.length)));
} else {
ret = `${gray(prefix)} | ${ret}`;
}
return ret;
});
const startLine = Math.max(0, result.line - additionalLines);
const endLine = Math.min(
formattedSourceLines.length,
result.line + 1 + additionalLines,
);
ret.formatted = formattedSourceLines.slice(startLine, endLine).join("\n");
return ret;
}
function getNodeName(
sourceFile: ts.SourceFile,
node: ts.Node,
): string | undefined {
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.initializer &&
(ts.isArrowFunction(node.initializer) ||
ts.isFunctionExpression(node.initializer) ||
ts.isClassExpression(node.initializer)) &&
!node.initializer.name
) {
// const foo = function() { ... }
// const foo = () => { ... }
// const foo = class { ... }
return node.name?.getText(sourceFile);
} else if (
(ts.isFunctionDeclaration(node) ||
ts.isFunctionExpression(node) ||
ts.isMethodDeclaration(node) ||
ts.isClassDeclaration(node)) &&
node.name
) {
// function foo() { ... }
// class foo { ... } (or its methods)
return node.name.getText(sourceFile);
} else if (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.arguments.some(
(arg) => ts.isArrowFunction(arg) || ts.isFunctionExpression(arg),
)
) {
// Call expressions where at least one of the arguments is a function
// beforeAll(() => { ... })
return node.expression.text;
} else if (ts.isConstructorDeclaration(node)) {
// class Foo { constructor() { ... } }
return "constructor";
}
}
function getCodePathAtPosition(
sourceFile: ts.SourceFile,
position: number,
separator: string = "/",
): string {
function visit(node: ts.Node, path: string[]) {
const start = node.getStart(sourceFile);
if (start > position) return;
const end = node.getEnd();
if (end < position) return;
// The position is somewhere inside this node, recurse!
const nodeName = getNodeName(sourceFile, node);
if (nodeName) path.push(nodeName);
ts.forEachChild(node, (member) => visit(member, path));
}
const path: string[] = [];
ts.forEachChild(sourceFile, (node) => visit(node, path));
return path.join(separator);
}
export function codefind(query: CodeFindQuery): Result[] {
const tsConfig = loadTSConfig(undefined, false);
const program = ts.createProgram(tsConfig.fileNames, {
...tsConfig.options,
preserveSymlinks: false,
});
// const checker = program.getTypeChecker();
const results: Result[] = [];
// Scan all source files
for (const sourceFile of program.getSourceFiles()) {
const relativePath = relativeToProject(sourceFile.fileName);
const relativePathMatchesPattern = (pattern: string) => {
const { regex } = globrex(pattern, { extended: true });
return regex.test(relativePath);
};
// If an include pattern is given, make sure the relative path matches at least one
if (
query.filePatterns &&
!query.filePatterns.some((pattern) =>
relativePathMatchesPattern(pattern),
)
) {
continue;
}
// If an exclude pattern is given, make sure the relative path matches none
if (
query.excludeFilePatterns &&
query.excludeFilePatterns.some((pattern) =>
relativePathMatchesPattern(pattern),
)
) {
continue;
}
let codePatterns: RegExp[] | undefined;
if (query.codePatterns) {
codePatterns = query.codePatterns.map(
(pattern) =>
globrex(pattern, {
extended: true,
}).regex,
);
}
let excludeCodePatterns: RegExp[] | undefined;
if (query.excludeCodePatterns) {
excludeCodePatterns = query.excludeCodePatterns.map(
(pattern) =>
globrex(pattern, {
extended: true,
}).regex,
);
}
const searchNodes: [path: string, node: ts.Node][] = [];
if (codePatterns || excludeCodePatterns) {
const pathMatches = (path: string[]): boolean | "unknown" => {
const fullPath = path.join("/");
if (
excludeCodePatterns?.some((pattern) =>
pattern.test(fullPath),
)
) {
// This code pattern is excluded
return false;
} else if (
codePatterns?.some((pattern) => pattern.test(fullPath))
) {
// This code pattern is included
return true;
} else {
return "unknown";
}
};
function visit(node: ts.Node, path: string[]) {
const nodeName = getNodeName(sourceFile, node);
if (nodeName) {
// This node has a name, check if it is a match
const newPath = [...path, nodeName];
const match = pathMatches(newPath);
if (match === false) {
// This node is excluded, do not look further
return;
} else if (match === true) {
// This node is of interest
searchNodes.push([newPath.join("/"), node]);
} else {
// no match, but new path segment to remember
// Iterate through children
ts.forEachChild(node, (member) =>
visit(member, newPath),
);
}
} else {
// No name, iterate through children with the same name
ts.forEachChild(node, (member) => visit(member, path));
}
}
ts.forEachChild(sourceFile, (node) => visit(node, []));
} else {
// Simply look at the entire source file
searchNodes.push(["/", sourceFile]);
}
for (const [, node] of searchNodes) {
const text = node.getText(sourceFile);
if (typeof query.search === "string") {
// Find all occurrences of simple strings in the node
let startIndex = 0;
let foundIndex = -1;
while (
((foundIndex = text.indexOf(query.search, startIndex)),
foundIndex !== -1)
) {
const matchPosition =
node.getStart(sourceFile) + foundIndex;
const location = ts.getLineAndCharacterOfPosition(
sourceFile,
matchPosition,
);
const pathAtPosition = getCodePathAtPosition(
sourceFile,
matchPosition,
" / ",
);
results.push(
formatResult(
sourceFile,
{
file: relativePath,
codePath: pathAtPosition,
...location,
match: query.search,
},
query.options?.additionalLines,
),
);
startIndex = foundIndex + query.search.length;
}
} else {
// Find all occurrences of regex in the node
const matches = text.matchAll(query.search);
for (const match of matches) {
const matchPosition =
node.getStart(sourceFile) + match.index!;
const location = ts.getLineAndCharacterOfPosition(
sourceFile,
matchPosition,
);
const pathAtPosition = getCodePathAtPosition(
sourceFile,
matchPosition,
" / ",
);
results.push(
formatResult(
sourceFile,
{
file: relativePath,
codePath: pathAtPosition,
...location,
match: match[0],
},
query.options?.additionalLines,
),
);
}
}
}
}
return results;
}
if (require.main === module) {
const argv = yargs
.usage("Code search utility\n\nUsage: $0 [options]")
.options({
include: {
alias: "i",
describe:
"Glob of files to include in the search. Default: all",
type: "string",
array: true,
},
exclude: {
alias: "e",
describe:
"Glob of files to exclude from the search. Default: none",
type: "string",
array: true,
},
codePattern: {
alias: "c",
describe:
"Glob of code paths to include in the search, e.g. 'class*/methodName'. Default: all",
type: "string",
array: true,
},
excludeCodePatterns: {
alias: "x",
describe:
"Glob of code paths to exclude from the search, e.g. 'class*/methodName'. Default: none",
type: "string",
array: true,
},
regex: {
alias: "r",
describe:
"Whether the search should be interpreted as a regex (true) or a simple string (false). Default: false",
type: "boolean",
},
search: {
alias: "s",
describe: "What to search for",
type: "string",
},
lines: {
alias: "l",
describe: "How many additional lines around the match to show",
type: "number",
default: 3,
},
})
.wrap(Math.min(100, yargs.terminalWidth()))
.demandOption("search", "Please specify a search query")
.parseSync();
const query: CodeFindQuery = {
filePatterns: argv.include,
excludeFilePatterns: argv.exclude,
codePatterns: argv.codePattern,
excludeCodePatterns: argv.excludeCodePatterns,
search: argv.regex ? new RegExp(argv.search) : argv.search,
options: {
additionalLines: Math.max(0, argv.lines),
},
};
const start = Date.now();
const results = codefind(query);
const duration = Date.now() - start;
console.log();
for (const result of results) {
console.log(
`${blueBright(bold(result.file))}:${yellow(
(result.line + 1).toString(),
)}:${yellow((result.character + 1).toString())}`,
);
console.log(redBright(bold(`⤷ ${result.codePath}`)));
console.log(result.formatted);
console.log();
console.log();
}
console.log(
`Found ${bold(greenBright(results.length.toString()))} result${
results.length === 1 ? "" : "s"
} in ${bold(yellow(duration.toString()))} ms`,
);
console.log();
}