t3-code-android-nightly/scripts/knip-schemas.ts
2026-09-07 21:00:10 -07:00

97 lines
3.8 KiB
TypeScript

// @effect-diagnostics nodeBuiltinImport:off - Knip and the TypeScript compiler host use synchronous Node paths.
import * as NodePath from "node:path";
import type { Preprocessor } from "knip";
// Knip needs the legacy compiler API, which TypeScript 7 no longer exports.
import ts from "typescript-legacy";
// Effect 4 schemas carry this marker, including aliases and Schema.Class constructors.
// Checking the type avoids evaluating application modules or exempting schema factories/decoders.
const schemaTypeId = "~effect/Schema/Schema";
const preprocess: Preprocessor = (options) => {
const categories = ["exports", "nsExports", "duplicates"] as const;
const projects = new Map<string | undefined, Set<string>>();
for (const category of categories) {
for (const [filePath, issues] of Object.entries(options.issues[category])) {
if (Object.keys(issues).length === 0) continue;
const configPath = ts.findConfigFile(
NodePath.dirname(NodePath.resolve(options.cwd, filePath)),
ts.sys.fileExists,
);
const files = projects.get(configPath) ?? new Set<string>();
files.add(filePath);
projects.set(configPath, files);
}
}
for (const [configPath, files] of projects) {
const config = configPath
? ts.getParsedCommandLineOfConfigFile(
configPath,
{},
{
...ts.sys,
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
},
},
)
: undefined;
if (config?.errors.length) {
throw new Error(
config.errors
.map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n"))
.join("\n"),
);
}
const program = ts.createProgram(
[...files].map((file) => NodePath.resolve(options.cwd, file)),
{
module: ts.ModuleKind.NodeNext,
allowJs: true,
...config?.options,
noEmit: true,
},
);
const checker = program.getTypeChecker();
for (const filePath of files) {
const source = program.getSourceFile(NodePath.resolve(options.cwd, filePath));
const module = source && checker.getSymbolAtLocation(source);
if (!source || !module) continue;
const schemas = new Set(
checker.getExportsOfModule(module).flatMap((symbol) => {
const exported =
symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
// Duplicate exports can name a private value with a public type of the same name.
const target =
exported.flags & ts.SymbolFlags.Value
? exported
: checker.resolveName(symbol.name, source, ts.SymbolFlags.Value, false);
if (!target) return [];
const type = checker.getTypeOfSymbolAtLocation(target, target.valueDeclaration ?? source);
const marker = type.getProperty(schemaTypeId);
if (!marker) return [];
const markerType = checker.getTypeOfSymbolAtLocation(marker, source);
return markerType.isStringLiteral() && markerType.value === schemaTypeId
? [symbol.name]
: [];
}),
);
for (const category of categories) {
const issues = options.issues[category][filePath];
if (!issues) continue;
for (const [key, issue] of Object.entries(issues)) {
const symbols = issue.symbols ?? [{ symbol: issue.symbol }];
if (symbols.length > 0 && symbols.every(({ symbol }) => schemas.has(symbol))) {
delete issues[key];
options.counters[category]--;
}
}
if (Object.keys(issues).length === 0) delete options.issues[category][filePath];
}
}
}
return options;
};
export default preprocess;