codemods/analyzers/specifier.analyzer.ts
codemods/analyzers/specifier.analyzer.ts is a file in GovLab Extension Host. 129 lines of code and 28 definitions.
import {
CODEMOD_TSCONFIGS,
inRepo,
lineOf,
programFor,
relPath,
repoSourceFiles,
toPosix,
} from "../selectors/program.selector.ts";
import type { SpecifierFinding, WorkspaceMember } from "../../types/analyzer.types.ts";
import { isManifestRecord, manifestAt, memberRels } from "../../shared/loaders/manifest.loader.ts";
import { ROOT } from "@ssot/paths";
import path from "node:path";
import { specifierNodes } from "../../shared/selectors/specifier.selector.ts";
import type ts from "typescript";
const ROOT_POSIX = toPosix(ROOT);
const manifestOf = function manifestOf(memberRel: string): WorkspaceMember {
const { exports, name } = manifestAt(path.join(ROOT, memberRel));
return {
exports: isManifestRecord(exports) ? exports : null,
name: typeof name === "string" ? name : "",
rel: memberRel,
};
};
const buildMembers = function buildMembers(): WorkspaceMember[] {
return memberRels()
.map(manifestOf)
.toSorted((a, b) => b.rel.length - a.rel.length);
};
const MEMBERS = buildMembers();
export const memberOf = function memberOf(relative: string): WorkspaceMember | null {
return MEMBERS.find((member) => relative === member.rel || relative.startsWith(`${member.rel}/`)) ?? null;
};
const exactMatch = function exactMatch(
member: WorkspaceMember,
entries: [string, unknown][],
subpath: string,
): string | null {
const hit = entries.find(
([key, target]) => typeof target === "string" && !key.includes("*") && target === `./${subpath}`,
);
if (hit === undefined) {
return null;
}
const [key] = hit;
return key === "." ? member.name : `${member.name}${key.slice(1)}`;
};
const patternMatch = function patternMatch(
member: WorkspaceMember,
entries: [string, unknown][],
subpath: string,
): string | null {
for (const [key, target] of entries) {
if (typeof target !== "string" || !key.includes("*")) {
continue;
}
const targetPrefix = target.slice(2, target.indexOf("*"));
if (subpath.startsWith(targetPrefix)) {
const keyPrefix = key.slice(2, key.indexOf("*"));
return `${member.name}/${keyPrefix}${subpath.slice(targetPrefix.length)}`;
}
}
return null;
};
const publishedSpecifier = function publishedSpecifier(member: WorkspaceMember, subpath: string): string | null {
if (member.name.length === 0) {
return null;
}
if (member.exports === null) {
return `${member.name}/${subpath}`;
}
const entries = Object.entries(member.exports);
return exactMatch(member, entries, subpath) ?? patternMatch(member, entries, subpath);
};
const findingFor = function findingFor(source: ts.SourceFile, literal: ts.StringLiteral): SpecifierFinding | null {
const specifier = literal.text;
if (!specifier.startsWith(".")) {
return null;
}
const fileRel = relPath(source.fileName);
const from = memberOf(fileRel);
if (from === null) {
return null;
}
const absolute = toPosix(path.resolve(path.dirname(source.fileName), specifier));
if (!absolute.startsWith(`${ROOT_POSIX}/`)) {
return null;
}
const targetRel = absolute.slice(ROOT_POSIX.length + 1);
const to = memberOf(targetRel);
if (to === null || to.rel === from.rel) {
return null;
}
const published = publishedSpecifier(to, targetRel.slice(to.rel.length + 1));
return {
end: literal.getEnd() - 1,
file: fileRel,
from: specifier,
line: lineOf(source, literal),
reason:
published === null
? `'${to.name.length > 0 ? to.name : to.rel}' publishes no subpath covering '${targetRel.slice(to.rel.length + 1)}' — add the pattern to its exports map`
: null,
start: literal.getStart(source) + 1,
to: published ?? specifier,
};
};
const findingsInSource = function findingsInSource(source: ts.SourceFile): SpecifierFinding[] {
return specifierNodes(source)
.map((literal) => findingFor(source, literal))
.filter((finding): finding is SpecifierFinding => finding !== null);
};
const findingsInProgram = function findingsInProgram(tsconfig: string): SpecifierFinding[] {
return repoSourceFiles(programFor(tsconfig))
.filter((source) => inRepo(source.fileName))
.flatMap(findingsInSource);
};
export const collectSpecifierFindings = function collectSpecifierFindings(): SpecifierFinding[] {
const seen = new Set<string>();
const isFirstAtSite = function isFirstAtSite(finding: SpecifierFinding): boolean {
const key = `${finding.file}:${String(finding.start)}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
};
return CODEMOD_TSCONFIGS.flatMap(findingsInProgram).filter(isFirstAtSite);
};