1import * as path from "node:path";
2
3import * as ts from "typescript-api/unstable/ast";
4import {
5 API,
6 TypeFlags,
7 type Type,
8 type Project,
9} from "typescript-api/unstable/async";
10
11/**
12 * Lints every `packages/alchemy/src/{Cloud}/Providers.ts` file and fails if any
13 * `providers()` factory ends up with `unknown` or `any` in its Layer requirements
14 * (the 3rd `Layer<ROut, E, RIn>` type argument). An `unknown`/`any` RIn means some leaf
15 * provider layer leaked an unsatisfied/undeclared requirement, which silently
16 * poisons `StackServices` inference across every consumer.
17 *
18 * For each offending file it reports the specific leaf provider-layer factory
19 * call(s) whose requirements include `unknown`.
20 */
21
22const tsConfig = path.join(
23 import.meta.dir,
24 "../packages/alchemy/tsconfig.json",
25);
26const srcRoot = path.join(import.meta.dir, "../packages/alchemy/src");
27
28export async function lintProviders(
29 project: Project,
30 srcRoot: string,
31 log: (message: string) => void = console.log,
32): Promise<boolean> {
33 const { checker, program } = project;
34 const providerPaths = (await program.getSourceFileNames())
35 .filter(
36 (file) =>
37 file.startsWith(`${srcRoot}/`) && file.endsWith("/Providers.ts"),
38 )
39 .sort((a, b) => a.localeCompare(b));
40
41 // A Layer's requirements are its 3rd type argument: Layer<ROut, E, RIn>.
42 async function layerRequirements(type: Type): Promise<Type | undefined> {
43 const args = type.isTypeReference()
44 ? await checker.getTypeArguments(type)
45 : [];
46 if (args.length === 3) return args[2];
47 return undefined;
48 }
49
50 // `any` absorbs every other union member, so it must be checked alongside
51 // `unknown` or a leaked `any` silently passes.
52 async function containsUnknown(type: Type): Promise<boolean> {
53 if (type.flags & (TypeFlags.Unknown | TypeFlags.Any)) return true;
54 if (type.isUnionType()) {
55 for (const member of await type.getTypes()) {
56 if (await containsUnknown(member)) return true;
57 }
58 }
59 return false;
60 }
61
62 let hadError = false;
63
64 for (const file of providerPaths) {
65 const sourceFile = await program.getSourceFile(file);
66 if (!sourceFile) throw new Error(`Missing source file ${file}`);
67 const rel = path.relative(process.cwd(), sourceFile.fileName);
68 const providersVar = sourceFile.statements
69 .filter(ts.isVariableStatement)
70 .flatMap((statement) => statement.declarationList.declarations)
71 .find(
72 (declaration) =>
73 ts.isIdentifier(declaration.name) &&
74 declaration.name.text === "providers",
75 );
76 if (!providersVar) continue;
77
78 // Overall requirements of the `providers()` factory return value.
79 const factoryType = await checker.getTypeAtLocation(providersVar);
80 const signature = (await factoryType.getCallSignatures())[0];
81 const returnType = signature
82 ? await checker.getReturnTypeOfSignature(signature)
83 : undefined;
84 const overallReq = returnType
85 ? await layerRequirements(returnType)
86 : undefined;
87
88 if (!overallReq || !(await containsUnknown(overallReq))) {
89 log(`✓ ${rel}`);
90 continue;
91 }
92
93 hadError = true;
94
95 // Localize the leak structurally: every Layer-typed expression whose RIn
96 // is `unknown`/`any`, keeping only the innermost ones (composites such as
97 // `Layer.mergeAll(...)` or `.pipe(...)` merely inherit it from a child).
98 const candidates: ts.Node[] = [];
99 const visit = (node: ts.Node) => {
100 if (
101 ts.isCallExpression(node) ||
102 ts.isPropertyAccessExpression(node) ||
103 (ts.isIdentifier(node) &&
104 !(
105 ts.isPropertyAccessExpression(node.parent) &&
106 node.parent.name === node
107 ))
108 ) {
109 candidates.push(node);
110 }
111 node.forEachChild(visit);
112 };
113 providersVar.forEachChild(visit);
114 const leaking: { node: ts.Node; req: Type }[] = [];
115 for (const node of candidates) {
116 const type = await checker.getTypeAtLocation(node);
117 const req = await layerRequirements(type);
118 if (
119 req &&
120 (await containsUnknown(req)) &&
121 (await checker.typeToString(type)).startsWith("Layer<")
122 ) {
123 leaking.push({ node, req });
124 }
125 }
126 const offenders: { text: string; req: string }[] = [];
127 for (const { node, req } of leaking) {
128 const hasLeakingChild = leaking.some(
129 (other) =>
130 other.node !== node &&
131 other.node.pos >= node.pos &&
132 other.node.end <= node.end,
133 );
134 if (hasLeakingChild) continue;
135 offenders.push({
136 text: node.getText(sourceFile).replace(/\s+/g, " ").slice(0, 120),
137 req: await checker.typeToString(req),
138 });
139 }
140
141 log(
142 `✗ ${rel} -> providers() RIn includes \`unknown\`/\`any\` (${await checker.typeToString(overallReq)})`,
143 );
144 if (offenders.length === 0) {
145 log(
146 " (could not localize a leaf culprit — inspect the composite layers)",
147 );
148 }
149 for (const o of offenders) {
150 log(` ✗ ${o.text} -> RIn = ${o.req}`);
151 }
152 }
153
154 return !hadError;
155}
156
157if (import.meta.main) {
158 await using api = new API();
159 const snapshot = await api.updateSnapshot({ openProjects: [tsConfig] });
160 const project = snapshot.getProject(tsConfig);
161 if (!project) throw new Error(`Failed to open ${tsConfig}`);
162 if (!(await lintProviders(project, srcRoot))) {
163 console.error(
164 "\nProvider lint failed: one or more `providers()` factories have `unknown`/`any` requirements.",
165 );
166 process.exitCode = 1;
167 } else {
168 console.log("\nAll provider factories have fully-resolved requirements.");
169 }
170}