shared/analyzers/specifier.analyzer.ts
shared/analyzers/specifier.analyzer.ts is a file in GovLab Extension Host. 190 lines of code and 52 definitions.
import { PROJECT_ROOT, collapsePath, normalizePath } from "../resolvers/anchor.resolver.ts";
import { WORKSPACE_POSIX, isManifestRecord, manifestAt, memberDirs } from "../loaders/manifest.loader.ts";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { MASTER_EXCLUDE_MARKERS } from "../generated/exclusions.generated.ts";
import { isExcludedPath } from "@govlab/quality/core/matchers/exclusions.matcher.ts";
import path from "node:path";
import { specifiersOf } from "../selectors/specifier.selector.ts";
import { stagesFor } from "@govlab/pipeline/core/factories/plan.factory.ts";
const SOURCE_EXTENSION = ".ts";
const RELATIVE_MARK = ".";
const WILDCARD = "*";
const SUBPATH_MARK = "./";
const TOKEN_SEPARATOR = " ";
const IMPORT_MARK = "#";
const APPLICATION_ROOT = normalizePath(PROJECT_ROOT);
const exportPairsOf = function exportPairsOf(
manifest: Record<string, unknown>,
): readonly (readonly [string, string])[] {
const { exports } = manifest;
if (!isManifestRecord(exports)) {
return [];
}
return Object.entries(exports).flatMap(([key, target]) =>
typeof target === "string" && key.endsWith(WILDCARD) && target.endsWith(WILDCARD)
? [
[
key.slice(SUBPATH_MARK.length, -WILDCARD.length),
target.slice(SUBPATH_MARK.length, -WILDCARD.length),
] as const,
]
: [],
);
};
const packageTargets = function packageTargets(): ReadonlyMap<string, (sub: string) => string | null> {
return new Map(
memberDirs().flatMap((dir) => {
const manifest = manifestAt(dir);
const { name } = manifest;
if (typeof name !== "string") {
return [];
}
const pairs = exportPairsOf(manifest);
const resolve = (sub: string): string | null => {
const pair = pairs.find(([key]) => sub.startsWith(key));
return pair === undefined ? null : `${dir}/${pair[1]}${sub.slice(pair[0].length)}`;
};
return [[name, resolve] as const];
}),
);
};
const PACKAGES = packageTargets();
interface SubpathImport {
readonly key: string;
readonly prefix: string;
readonly suffix: string;
}
const subpathImportsOf = function subpathImportsOf(manifest: Record<string, unknown>): readonly SubpathImport[] {
const { imports } = manifest;
if (!isManifestRecord(imports)) {
return [];
}
return Object.entries(imports).flatMap(([key, target]) => {
if (typeof target !== "string" || !key.endsWith(WILDCARD) || !target.includes(WILDCARD)) {
return [];
}
const star = target.indexOf(WILDCARD);
return [
{
key: key.slice(0, -WILDCARD.length),
prefix: target.slice(SUBPATH_MARK.length, star),
suffix: target.slice(star + WILDCARD.length),
},
];
});
};
const MEMBER_IMPORTS: ReadonlyMap<string, readonly SubpathImport[]> = new Map(
memberDirs().map((dir) => [dir, subpathImportsOf(manifestAt(dir))] as const),
);
const memberOf = function memberOf(file: string): string | null {
const owners = [...MEMBER_IMPORTS.keys()].filter((dir) => file.startsWith(`${dir}/`));
return owners.reduce<string | null>((best, dir) => (best === null || dir.length > best.length ? dir : best), null);
};
const subpathTarget = function subpathTarget(importer: string, specifier: string): string | null {
const member = memberOf(importer);
const entry =
member === null ? undefined : MEMBER_IMPORTS.get(member)?.find((held) => specifier.startsWith(held.key));
return member === null || entry === undefined
? null
: `${member}/${entry.prefix}${specifier.slice(entry.key.length)}${entry.suffix}`;
};
const absoluteOf = function absoluteOf(collapsed: string): string {
return collapsed.startsWith("/") || collapsed.includes(":") ? collapsed : `/${collapsed}`;
};
export const specifierTarget = function specifierTarget(importer: string, specifier: string): string | null {
if (specifier.startsWith(RELATIVE_MARK)) {
const folder = importer.slice(0, importer.lastIndexOf("/"));
return absoluteOf(collapsePath(`${folder}/${specifier}`));
}
if (specifier.startsWith(IMPORT_MARK)) {
return subpathTarget(importer, specifier);
}
const parts = specifier.split("/");
const name = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? "");
const resolve = PACKAGES.get(name);
return resolve === undefined ? null : resolve(specifier.slice(name.length + 1));
};
const sourceFilesUnder = function sourceFilesUnder(dir: string): string[] {
if (!existsSync(dir)) {
return [];
}
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = normalizePath(path.join(dir, entry.name));
if (isExcludedPath(full.slice(WORKSPACE_POSIX.length + 1), MASTER_EXCLUDE_MARKERS)) {
return [];
}
if (entry.isDirectory()) {
return sourceFilesUnder(full);
}
return entry.name.endsWith(SOURCE_EXTENSION) ? [full] : [];
});
};
const applicationEdges = function applicationEdges(): ReadonlyMap<string, readonly string[]> {
const edges = new Map<string, string[]>();
for (const file of sourceFilesUnder(APPLICATION_ROOT)) {
const targets = specifiersOf(file, readFileSync(file, "utf8")).flatMap((specifier) => {
const target = specifierTarget(file, specifier);
return target === null || target === file ? [] : [target];
});
edges.set(file, targets);
}
return edges;
};
export const applicationImporters = function applicationImporters(): ReadonlyMap<string, readonly string[]> {
const importers = new Map<string, string[]>();
for (const [file, targets] of applicationEdges()) {
for (const target of targets) {
importers.set(target, [...(importers.get(target) ?? []), file.slice(APPLICATION_ROOT.length + 1)]);
}
}
return importers;
};
export const reachedUnder = function reachedUnder(root: string, seeds: ReadonlySet<string>): ReadonlySet<string> {
const edges = applicationEdges();
const inside = (file: string): boolean => file.startsWith(root);
const start = [
...[...edges].filter(([file]) => !inside(file)).flatMap(([, targets]) => targets.filter(inside)),
...[...seeds].filter(inside),
];
const reached = new Set<string>();
const queue = [...start];
while (queue.length > 0) {
const file = queue.pop();
if (file !== undefined && !reached.has(file)) {
reached.add(file);
queue.push(...(edges.get(file) ?? []).filter(inside));
}
}
return reached;
};
const scriptTokens = function scriptTokens(command: string): readonly string[] {
return command.split(TOKEN_SEPARATOR).filter((token) => token.endsWith(SOURCE_EXTENSION));
};
export const scriptTargets = function scriptTargets(): ReadonlySet<string> {
const targets = new Set<string>();
for (const dir of [WORKSPACE_POSIX, ...memberDirs()]) {
const { scripts } = manifestAt(dir);
const commands = isManifestRecord(scripts)
? Object.values(scripts).filter((value) => typeof value === "string")
: [];
for (const token of commands.flatMap(scriptTokens)) {
targets.add(absoluteOf(collapsePath(`${dir}/${token}`)));
}
}
return targets;
};
const EMPTY_OPTION = "";
export const gateTargets = function gateTargets(): ReadonlySet<string> {
const { stages } = stagesFor({
cleanCommentsIgnore: EMPTY_OPTION,
hexIgnore: EMPTY_OPTION,
qualityRoot: EMPTY_OPTION,
scope: new Set<string>(),
});
const commands = stages.flatMap((stage) =>
stage.steps.flatMap((step) => (step.parallel ? step.parallel.map((sub) => sub.run) : [step.run ?? ""])),
);
return new Set(
commands.flatMap(scriptTokens).map((token) => absoluteOf(collapsePath(`${WORKSPACE_POSIX}/${token}`))),
);
};