codemods/analyzers/specifier.target.analyzer.ts
codemods/analyzers/specifier.target.analyzer.ts is a file in GovLab Extension Host. 107 lines of code and 26 definitions.
import {
CODEMOD_TSCONFIGS,
inRepo,
lineOf,
programFor,
relPath,
repoSourceFiles,
toPosix,
} from "../selectors/program.selector.ts";
import { ambiguousTarget, missingTarget } from "../strings/codemod.strings.ts";
import { basename, dirname, extname, relative, resolve } from "node:path";
import type { SpecifierFinding } from "../../types/analyzer.types.ts";
import { existsSync } from "node:fs";
import { memberOf } from "./specifier.analyzer.ts";
import { specifierNodes } from "../../shared/selectors/specifier.selector.ts";
import ts from "typescript";
const CURRENT = ".";
const SEPARATOR = "/";
const QUERY = "?";
export const filePartOf = function filePartOf(specifier: string): string {
const query = specifier.indexOf(QUERY);
return query === -1 ? specifier : specifier.slice(0, query);
};
export const targetCandidates = function targetCandidates(specifier: string, files: readonly string[]): string[] {
const name = basename(filePartOf(specifier));
const bare = extname(name).length === 0;
return files.filter((file) => {
const candidate = basename(file);
return candidate === name || (bare && basename(candidate, extname(candidate)) === name);
});
};
export const relocatedSpecifier = function relocatedSpecifier(
fromFile: string,
specifier: string,
target: string,
): string {
const folder = toPosix(relative(dirname(fromFile), dirname(target)));
if (folder.length === 0) {
return `${CURRENT}${SEPARATOR}${basename(specifier)}`;
}
const prefix = folder.startsWith(CURRENT) ? folder : `${CURRENT}${SEPARATOR}${folder}`;
return `${prefix}${SEPARATOR}${basename(specifier)}`;
};
const reasonFor = function reasonFor(specifier: string, candidates: readonly string[]): string | null {
if (candidates.length === 0) {
return missingTarget(specifier, basename(filePartOf(specifier)));
}
return candidates.length > 1 ? ambiguousTarget(specifier, candidates.map(relPath)) : null;
};
const specifierTargetExists = function specifierTargetExists(
source: ts.SourceFile,
specifier: string,
options: ts.CompilerOptions,
): boolean {
if (existsSync(resolve(dirname(source.fileName), filePartOf(specifier)))) {
return true;
}
return ts.resolveModuleName(specifier, source.fileName, options, ts.sys).resolvedModule !== undefined;
};
const findingFor = function findingFor(
program: ts.Program,
source: ts.SourceFile,
literal: ts.StringLiteral,
): SpecifierFinding | null {
const specifier = literal.text;
if (!specifier.startsWith(CURRENT) || specifierTargetExists(source, specifier, program.getCompilerOptions())) {
return null;
}
const member = memberOf(relPath(source.fileName));
if (member === null) {
return null;
}
const inMember = program
.getRootFileNames()
.filter((file) => inRepo(file) && memberOf(relPath(file))?.rel === member.rel);
const candidates = targetCandidates(specifier, inMember);
const [only] = candidates;
const reason = reasonFor(specifier, candidates);
return {
end: literal.getEnd() - 1,
file: relPath(source.fileName),
from: specifier,
line: lineOf(source, literal),
reason,
start: literal.getStart(source) + 1,
to: only === undefined || reason !== null ? specifier : relocatedSpecifier(source.fileName, specifier, only),
};
};
const findingsInProgram = function findingsInProgram(tsconfig: string): SpecifierFinding[] {
const program = programFor(tsconfig);
return repoSourceFiles(program).flatMap((source) =>
specifierNodes(source)
.map((literal) => findingFor(program, source, literal))
.filter((finding): finding is SpecifierFinding => finding !== null),
);
};
export const collectTargetFindings = function collectTargetFindings(): SpecifierFinding[] {
const seen = new Set<string>();
return CODEMOD_TSCONFIGS.flatMap(findingsInProgram).filter((finding) => {
const key = `${finding.file}:${String(finding.start)}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
};