@djodjonx/neosyringe-core 0.1.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Analyzer-CsTQzjGb.mjs +1356 -0
- package/dist/Analyzer-gDRpk7ei.cjs +1458 -0
- package/dist/analyzer/index.cjs +14 -461
- package/dist/analyzer/index.d.cts +324 -2
- package/dist/analyzer/index.d.mts +324 -2
- package/dist/analyzer/index.mjs +2 -430
- package/dist/generator/index.cjs +111 -20
- package/dist/generator/index.d.cts +49 -9
- package/dist/generator/index.d.mts +49 -9
- package/dist/generator/index.mjs +112 -20
- package/dist/types-C6tJfwp1.d.cts +197 -0
- package/dist/types-oDS1jdOF.d.mts +197 -0
- package/package.json +1 -1
- package/dist/types-DeLOEpiR.d.cts +0 -84
- package/dist/types-EnrcFw9C.d.mts +0 -84
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region rolldown:runtime
|
|
7
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/analyzer/collectors/ConfigCollector.ts
|
|
11
|
+
/**
|
|
12
|
+
* Collects defineBuilderConfig and definePartialConfig calls from the program.
|
|
13
|
+
*/
|
|
14
|
+
var ConfigCollector = class {
|
|
15
|
+
constructor(program, checker) {
|
|
16
|
+
this.program = program;
|
|
17
|
+
this.checker = checker;
|
|
18
|
+
}
|
|
19
|
+
collect() {
|
|
20
|
+
const configs = /* @__PURE__ */ new Map();
|
|
21
|
+
const containerIdsByFile = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
23
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
24
|
+
const fileConfigs = /* @__PURE__ */ new Map();
|
|
25
|
+
this.visitNode(sourceFile, sourceFile, configs, fileConfigs);
|
|
26
|
+
if (fileConfigs.size > 0) containerIdsByFile.set(sourceFile.fileName, fileConfigs);
|
|
27
|
+
}
|
|
28
|
+
this.validateContainerIdCollisions(containerIdsByFile);
|
|
29
|
+
return configs;
|
|
30
|
+
}
|
|
31
|
+
validateContainerIdCollisions(containerIdsByFile) {
|
|
32
|
+
for (const [fileName, fileConfigs] of containerIdsByFile) {
|
|
33
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const config of fileConfigs.values()) {
|
|
35
|
+
const existing = seenIds.get(config.containerId);
|
|
36
|
+
if (existing) throw new Error(`Duplicate container name '${config.containerId}' found in ${fileName}.\nEach container must have a unique 'name' field within the same file.\nFirst occurrence: line ${config.sourceFile.getLineAndCharacterOfPosition(existing.node.getStart()).line + 1}\nSecond occurrence: line ${config.sourceFile.getLineAndCharacterOfPosition(config.node.getStart()).line + 1}`);
|
|
37
|
+
seenIds.set(config.containerId, config);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
visitNode(node, sourceFile, configs, fileConfigs) {
|
|
42
|
+
if (ts.isCallExpression(node)) {
|
|
43
|
+
const config = this.tryParseConfig(node, sourceFile);
|
|
44
|
+
if (config) {
|
|
45
|
+
const uniqueKey = `${sourceFile.fileName}:${config.name}`;
|
|
46
|
+
configs.set(uniqueKey, config);
|
|
47
|
+
if (fileConfigs) {
|
|
48
|
+
const fileUniqueKey = `${config.name}:${node.getStart()}`;
|
|
49
|
+
fileConfigs.set(fileUniqueKey, config);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
ts.forEachChild(node, (child) => this.visitNode(child, sourceFile, configs, fileConfigs));
|
|
54
|
+
}
|
|
55
|
+
tryParseConfig(node, sourceFile) {
|
|
56
|
+
const funcName = this.getFunctionName(node);
|
|
57
|
+
if (funcName === "defineBuilderConfig") return this.parseConfig(node, sourceFile, "builder");
|
|
58
|
+
if (funcName === "definePartialConfig") return this.parseConfig(node, sourceFile, "partial");
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
getFunctionName(node) {
|
|
62
|
+
const expression = node.expression;
|
|
63
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
parseConfig(node, sourceFile, type) {
|
|
67
|
+
const name = this.getConfigName(node);
|
|
68
|
+
if (!name) return null;
|
|
69
|
+
const configArg = node.arguments[0];
|
|
70
|
+
if (!configArg || !ts.isObjectLiteralExpression(configArg)) return null;
|
|
71
|
+
const containerId = this.generateContainerId(configArg, sourceFile, node.getStart());
|
|
72
|
+
const { injections: localInjections, duplicates } = this.collectInjections(configArg, sourceFile);
|
|
73
|
+
const extendsRefs = type === "builder" ? this.getExtendsRefs(configArg) : [];
|
|
74
|
+
const useContainerRef = type === "builder" ? this.getUseContainerRef(configArg) : null;
|
|
75
|
+
return {
|
|
76
|
+
containerId,
|
|
77
|
+
name,
|
|
78
|
+
type,
|
|
79
|
+
sourceFile,
|
|
80
|
+
node,
|
|
81
|
+
localInjections,
|
|
82
|
+
duplicates,
|
|
83
|
+
extendsRefs,
|
|
84
|
+
useContainerRef,
|
|
85
|
+
legacyParentTokens: type === "builder" && useContainerRef ? this.extractLegacyParentTokens(useContainerRef, sourceFile) : void 0,
|
|
86
|
+
containerName: this.getContainerName(configArg)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
getConfigName(node) {
|
|
90
|
+
const parent = node.parent;
|
|
91
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) return parent.name.text;
|
|
92
|
+
if (ts.isExportAssignment(parent)) return "__default__";
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
collectInjections(configObj, sourceFile) {
|
|
96
|
+
const injections = /* @__PURE__ */ new Map();
|
|
97
|
+
const duplicates = [];
|
|
98
|
+
const injectionsProperty = this.findProperty(configObj, "injections");
|
|
99
|
+
if (!injectionsProperty || !ts.isArrayLiteralExpression(injectionsProperty.initializer)) return {
|
|
100
|
+
injections,
|
|
101
|
+
duplicates
|
|
102
|
+
};
|
|
103
|
+
for (const element of injectionsProperty.initializer.elements) {
|
|
104
|
+
if (!ts.isObjectLiteralExpression(element)) continue;
|
|
105
|
+
const info = this.parseInjection(element, sourceFile);
|
|
106
|
+
if (info) if (injections.has(info.definition.tokenId)) duplicates.push(info);
|
|
107
|
+
else injections.set(info.definition.tokenId, info);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
injections,
|
|
111
|
+
duplicates
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
parseInjection(obj, sourceFile) {
|
|
115
|
+
let tokenNode;
|
|
116
|
+
let providerNode;
|
|
117
|
+
let lifecycle = "singleton";
|
|
118
|
+
let useFactory = false;
|
|
119
|
+
let isScoped = false;
|
|
120
|
+
for (const prop of obj.properties) {
|
|
121
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
|
|
122
|
+
switch (prop.name.text) {
|
|
123
|
+
case "token":
|
|
124
|
+
tokenNode = prop.initializer;
|
|
125
|
+
break;
|
|
126
|
+
case "provider":
|
|
127
|
+
providerNode = prop.initializer;
|
|
128
|
+
break;
|
|
129
|
+
case "lifecycle":
|
|
130
|
+
if (ts.isStringLiteral(prop.initializer) && prop.initializer.text === "transient") lifecycle = "transient";
|
|
131
|
+
break;
|
|
132
|
+
case "useFactory":
|
|
133
|
+
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) useFactory = true;
|
|
134
|
+
break;
|
|
135
|
+
case "scoped":
|
|
136
|
+
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) isScoped = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!tokenNode) return null;
|
|
141
|
+
const tokenText = tokenNode.getText(sourceFile);
|
|
142
|
+
const tokenId = this.resolveTokenId(tokenNode);
|
|
143
|
+
if (!tokenId) return null;
|
|
144
|
+
if (providerNode && (ts.isArrowFunction(providerNode) || ts.isFunctionExpression(providerNode))) useFactory = true;
|
|
145
|
+
let registrationType = "autowire";
|
|
146
|
+
if (useFactory) registrationType = "factory";
|
|
147
|
+
else if (providerNode) registrationType = "explicit";
|
|
148
|
+
const implementationSymbol = providerNode ? this.getSymbolForNode(providerNode) : this.getSymbolForNode(tokenNode);
|
|
149
|
+
const isInterfaceToken = this.isUseInterfaceCall(tokenNode);
|
|
150
|
+
return {
|
|
151
|
+
definition: {
|
|
152
|
+
tokenId,
|
|
153
|
+
implementationSymbol,
|
|
154
|
+
registrationNode: obj,
|
|
155
|
+
type: registrationType,
|
|
156
|
+
lifecycle,
|
|
157
|
+
isInterfaceToken,
|
|
158
|
+
isFactory: useFactory,
|
|
159
|
+
factorySource: useFactory && providerNode ? providerNode.getText(sourceFile) : void 0,
|
|
160
|
+
isScoped
|
|
161
|
+
},
|
|
162
|
+
node: obj,
|
|
163
|
+
tokenText,
|
|
164
|
+
isScoped
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
resolveTokenId(tokenNode) {
|
|
168
|
+
const node = this.resolveToInitializer(tokenNode) || tokenNode;
|
|
169
|
+
if (ts.isCallExpression(node) && this.isUseInterfaceCall(node)) return this.extractInterfaceTokenId(node);
|
|
170
|
+
if (ts.isCallExpression(node) && this.isUsePropertyCall(node)) return this.extractPropertyTokenId(node);
|
|
171
|
+
const type = this.checker.getTypeAtLocation(tokenNode);
|
|
172
|
+
return this.getTypeId(type);
|
|
173
|
+
}
|
|
174
|
+
resolveToInitializer(node) {
|
|
175
|
+
if (ts.isIdentifier(node)) {
|
|
176
|
+
const symbol = this.checker.getSymbolAtLocation(node);
|
|
177
|
+
if (!symbol) return null;
|
|
178
|
+
const declarations = symbol.getDeclarations();
|
|
179
|
+
if (!declarations || declarations.length === 0) return null;
|
|
180
|
+
const decl = declarations[0];
|
|
181
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer) return decl.initializer;
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
185
|
+
const objectSymbol = this.checker.getSymbolAtLocation(node.expression);
|
|
186
|
+
if (!objectSymbol) return null;
|
|
187
|
+
const declarations = objectSymbol.getDeclarations();
|
|
188
|
+
if (!declarations || declarations.length === 0) return null;
|
|
189
|
+
const decl = declarations[0];
|
|
190
|
+
if (ts.isVariableDeclaration(decl) && decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
|
|
191
|
+
const propName = node.name.text;
|
|
192
|
+
for (const prop of decl.initializer.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === propName) return prop.initializer;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
isUseInterfaceCall(node) {
|
|
199
|
+
if (!ts.isCallExpression(node)) return false;
|
|
200
|
+
const expr = node.expression;
|
|
201
|
+
return ts.isIdentifier(expr) && expr.text === "useInterface";
|
|
202
|
+
}
|
|
203
|
+
isUsePropertyCall(node) {
|
|
204
|
+
if (!ts.isCallExpression(node)) return false;
|
|
205
|
+
const expr = node.expression;
|
|
206
|
+
return ts.isIdentifier(expr) && expr.text === "useProperty";
|
|
207
|
+
}
|
|
208
|
+
extractInterfaceTokenId(node) {
|
|
209
|
+
const typeArgs = node.typeArguments;
|
|
210
|
+
if (!typeArgs || typeArgs.length === 0) return "UnknownInterface";
|
|
211
|
+
const typeArg = typeArgs[0];
|
|
212
|
+
const type = this.checker.getTypeAtLocation(typeArg);
|
|
213
|
+
return `useInterface<${this.getTypeId(type)}>()`;
|
|
214
|
+
}
|
|
215
|
+
extractPropertyTokenId(node) {
|
|
216
|
+
const args = node.arguments;
|
|
217
|
+
if (args.length < 2) return "UnknownProperty";
|
|
218
|
+
return `useProperty<${args[0].getText()}>('${ts.isStringLiteral(args[1]) ? args[1].text : args[1].getText()}')`;
|
|
219
|
+
}
|
|
220
|
+
getTypeId(type) {
|
|
221
|
+
const symbol = type.getSymbol() || type.aliasSymbol;
|
|
222
|
+
if (!symbol) return this.checker.typeToString(type);
|
|
223
|
+
const declarations = symbol.getDeclarations();
|
|
224
|
+
if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
|
|
225
|
+
return symbol.getName();
|
|
226
|
+
}
|
|
227
|
+
simpleHash(str) {
|
|
228
|
+
let hash = 0;
|
|
229
|
+
for (let i = 0; i < str.length; i++) {
|
|
230
|
+
const char = str.charCodeAt(i);
|
|
231
|
+
hash = (hash << 5) - hash + char;
|
|
232
|
+
hash = hash & hash;
|
|
233
|
+
}
|
|
234
|
+
return Math.abs(hash).toString(16).slice(0, 8);
|
|
235
|
+
}
|
|
236
|
+
getSymbolForNode(node) {
|
|
237
|
+
return this.checker.getSymbolAtLocation(node);
|
|
238
|
+
}
|
|
239
|
+
getExtendsRefs(configObj) {
|
|
240
|
+
const refs = [];
|
|
241
|
+
const extendsProp = this.findProperty(configObj, "extends");
|
|
242
|
+
if (extendsProp && ts.isArrayLiteralExpression(extendsProp.initializer)) {
|
|
243
|
+
for (const element of extendsProp.initializer.elements) if (ts.isIdentifier(element)) refs.push(element.text);
|
|
244
|
+
}
|
|
245
|
+
return refs;
|
|
246
|
+
}
|
|
247
|
+
getUseContainerRef(configObj) {
|
|
248
|
+
const useContainerProp = this.findProperty(configObj, "useContainer");
|
|
249
|
+
if (useContainerProp && ts.isIdentifier(useContainerProp.initializer)) return useContainerProp.initializer.text;
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
getContainerName(configObj) {
|
|
253
|
+
const nameProp = this.findProperty(configObj, "name");
|
|
254
|
+
if (nameProp && ts.isStringLiteral(nameProp.initializer)) return nameProp.initializer.text;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Extracts tokens from a legacy parent container (declareContainerTokens).
|
|
258
|
+
* @param useContainerRef - Variable name of the parent container
|
|
259
|
+
* @param sourceFile - Source file containing the reference
|
|
260
|
+
* @returns Set of token IDs provided by the legacy container, or undefined if not a legacy container
|
|
261
|
+
*/
|
|
262
|
+
extractLegacyParentTokens(useContainerRef, sourceFile) {
|
|
263
|
+
const identifier = this.findIdentifierInFile(useContainerRef, sourceFile);
|
|
264
|
+
if (!identifier) return void 0;
|
|
265
|
+
const symbol = this.checker.getSymbolAtLocation(identifier);
|
|
266
|
+
if (!symbol) return void 0;
|
|
267
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
268
|
+
if (!declaration) return void 0;
|
|
269
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isCallExpression(declaration.initializer)) {
|
|
270
|
+
const callExpr = declaration.initializer;
|
|
271
|
+
if (ts.isIdentifier(callExpr.expression) && callExpr.expression.text === "declareContainerTokens") return this.extractDeclaredTokens(callExpr);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Extracts tokens from declareContainerTokens<{ Token: Type }>().
|
|
276
|
+
*/
|
|
277
|
+
extractDeclaredTokens(node) {
|
|
278
|
+
if (!node.typeArguments || node.typeArguments.length === 0) return;
|
|
279
|
+
const typeArg = node.typeArguments[0];
|
|
280
|
+
const type = this.checker.getTypeFromTypeNode(typeArg);
|
|
281
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
282
|
+
const properties = type.getProperties();
|
|
283
|
+
for (const prop of properties) {
|
|
284
|
+
const propType = this.checker.getTypeOfSymbol(prop);
|
|
285
|
+
if (propType) {
|
|
286
|
+
const tokenId = this.getTypeId(propType);
|
|
287
|
+
tokens.add(tokenId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return tokens.size > 0 ? tokens : void 0;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Helper to find an identifier in a source file.
|
|
294
|
+
*/
|
|
295
|
+
findIdentifierInFile(name, sourceFile) {
|
|
296
|
+
let result;
|
|
297
|
+
const visit = (node) => {
|
|
298
|
+
if (result) return;
|
|
299
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
300
|
+
result = node;
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
ts.forEachChild(node, visit);
|
|
304
|
+
};
|
|
305
|
+
visit(sourceFile);
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
findProperty(obj, name) {
|
|
309
|
+
for (const prop of obj.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === name) return prop;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Generates a unique container ID from the 'name' field or a hash.
|
|
313
|
+
* Priority 1: Use the 'name' field from the config object
|
|
314
|
+
* Priority 2: Generate a stable hash based on file + position + content
|
|
315
|
+
*/
|
|
316
|
+
generateContainerId(configObject, sourceFile, position) {
|
|
317
|
+
const configName = this.extractConfigName(configObject);
|
|
318
|
+
if (configName) return configName;
|
|
319
|
+
const fileName = __require("path").basename(sourceFile.fileName, ".ts");
|
|
320
|
+
const configText = configObject.getText();
|
|
321
|
+
return `Container_${this.createHash(`${fileName}:${position}:${configText}`).substring(0, 8)}`;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Extracts the value of the 'name' field from a config object.
|
|
325
|
+
*/
|
|
326
|
+
extractConfigName(configObject) {
|
|
327
|
+
for (const prop of configObject.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "name") {
|
|
328
|
+
if (ts.isStringLiteral(prop.initializer)) return prop.initializer.text;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Creates a simple hash from a string.
|
|
333
|
+
*/
|
|
334
|
+
createHash(str) {
|
|
335
|
+
let hash = 0;
|
|
336
|
+
for (let i = 0; i < str.length; i++) {
|
|
337
|
+
const char = str.charCodeAt(i);
|
|
338
|
+
hash = (hash << 5) - hash + char;
|
|
339
|
+
hash = hash & hash;
|
|
340
|
+
}
|
|
341
|
+
return Math.abs(hash).toString(16);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/analyzer/errors/CycleError.ts
|
|
347
|
+
/**
|
|
348
|
+
* Custom error for cycle detection.
|
|
349
|
+
*/
|
|
350
|
+
var CycleError = class extends Error {
|
|
351
|
+
constructor(chain) {
|
|
352
|
+
super(`Circular dependency detected: ${chain.join(" -> ")}`);
|
|
353
|
+
this.chain = chain;
|
|
354
|
+
this.name = "CycleError";
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/analyzer/resolvers/TokenResolver.ts
|
|
360
|
+
/**
|
|
361
|
+
* Resolves inherited tokens from useContainer and extends.
|
|
362
|
+
*
|
|
363
|
+
* Priority order:
|
|
364
|
+
* 1. useContainer (parent) - highest priority, recursive
|
|
365
|
+
* 2. extends (partials) - in array order
|
|
366
|
+
*/
|
|
367
|
+
var TokenResolver = class {
|
|
368
|
+
resolveInheritedTokens(config, allConfigs) {
|
|
369
|
+
const inherited = /* @__PURE__ */ new Map();
|
|
370
|
+
const visited = /* @__PURE__ */ new Set();
|
|
371
|
+
this.resolveRecursive(config, allConfigs, inherited, visited, []);
|
|
372
|
+
return inherited;
|
|
373
|
+
}
|
|
374
|
+
resolveRecursive(config, allConfigs, inherited, visited, chain) {
|
|
375
|
+
if (visited.has(config.name)) throw new CycleError([...chain, config.name]);
|
|
376
|
+
visited.add(config.name);
|
|
377
|
+
if (config.useContainerRef) {
|
|
378
|
+
const parent = this.findConfigByName(allConfigs, config.useContainerRef);
|
|
379
|
+
if (parent) {
|
|
380
|
+
this.resolveRecursive(parent, allConfigs, inherited, new Set(visited), [...chain, config.name]);
|
|
381
|
+
this.addTokensFromConfig(parent, inherited, "parent", chain);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
for (const partialName of config.extendsRefs) {
|
|
385
|
+
const partial = this.findConfigByName(allConfigs, partialName);
|
|
386
|
+
if (partial) this.addTokensFromConfig(partial, inherited, "extends", chain);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Find a config by its variable name.
|
|
391
|
+
* Config keys are now "fileName:variableName", so we need to search for it.
|
|
392
|
+
*/
|
|
393
|
+
findConfigByName(allConfigs, variableName) {
|
|
394
|
+
for (const [_key, config] of allConfigs) if (config.name === variableName) return config;
|
|
395
|
+
}
|
|
396
|
+
addTokensFromConfig(config, inherited, type, chain) {
|
|
397
|
+
for (const [tokenId, info] of config.localInjections) if (!inherited.has(tokenId)) inherited.set(tokenId, {
|
|
398
|
+
tokenId,
|
|
399
|
+
tokenText: info.tokenText,
|
|
400
|
+
source: {
|
|
401
|
+
name: config.name,
|
|
402
|
+
type,
|
|
403
|
+
chain: chain.length > 0 ? [...chain] : void 0
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region src/analyzer/validators/Validator.ts
|
|
411
|
+
/**
|
|
412
|
+
* Composite validator that combines multiple validators.
|
|
413
|
+
* Use this to compose validation rules.
|
|
414
|
+
*/
|
|
415
|
+
var CompositeValidator = class {
|
|
416
|
+
name = "CompositeValidator";
|
|
417
|
+
constructor(validators = []) {
|
|
418
|
+
this.validators = validators;
|
|
419
|
+
}
|
|
420
|
+
validate(config, context) {
|
|
421
|
+
const errors = [];
|
|
422
|
+
for (const validator of this.validators) errors.push(...validator.validate(config, context));
|
|
423
|
+
return errors;
|
|
424
|
+
}
|
|
425
|
+
/** Add a validator (for extensibility) */
|
|
426
|
+
addValidator(validator) {
|
|
427
|
+
this.validators.push(validator);
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
/** Remove a validator by name */
|
|
431
|
+
removeValidator(name) {
|
|
432
|
+
this.validators = this.validators.filter((v) => v.name !== name);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
/** Get all validators */
|
|
436
|
+
getValidators() {
|
|
437
|
+
return this.validators;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/analyzer/validators/DuplicateValidator.ts
|
|
443
|
+
/**
|
|
444
|
+
* Validates duplicate registrations.
|
|
445
|
+
* - Internal duplicates (same config)
|
|
446
|
+
* - Inherited duplicates (from parent/extends) for builders
|
|
447
|
+
*/
|
|
448
|
+
var DuplicateValidator = class {
|
|
449
|
+
name = "DuplicateValidator";
|
|
450
|
+
constructor(errorFormatter) {
|
|
451
|
+
this.errorFormatter = errorFormatter;
|
|
452
|
+
}
|
|
453
|
+
validate(config, context) {
|
|
454
|
+
const errors = [];
|
|
455
|
+
errors.push(...this.validateInternalDuplicates(config));
|
|
456
|
+
if (config.type === "builder" && context.inheritedTokens) errors.push(...this.validateInheritedDuplicates(config, context.inheritedTokens));
|
|
457
|
+
return errors;
|
|
458
|
+
}
|
|
459
|
+
validateInternalDuplicates(config) {
|
|
460
|
+
const errors = [];
|
|
461
|
+
for (const duplicate of config.duplicates) errors.push(this.errorFormatter.formatDuplicateError(duplicate, {
|
|
462
|
+
name: config.name,
|
|
463
|
+
type: "internal"
|
|
464
|
+
}));
|
|
465
|
+
return errors;
|
|
466
|
+
}
|
|
467
|
+
validateInheritedDuplicates(config, inheritedTokens) {
|
|
468
|
+
const errors = [];
|
|
469
|
+
for (const [tokenId, info] of config.localInjections) {
|
|
470
|
+
if (info.isScoped) continue;
|
|
471
|
+
const inherited = inheritedTokens.get(tokenId);
|
|
472
|
+
if (inherited) errors.push(this.errorFormatter.formatDuplicateError(info, inherited.source));
|
|
473
|
+
}
|
|
474
|
+
return errors;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/analyzer/validators/TypeValidator.ts
|
|
480
|
+
/**
|
|
481
|
+
* Validates type compatibility between tokens and providers.
|
|
482
|
+
* Ensures that the provider implements the interface/class expected by the token.
|
|
483
|
+
*/
|
|
484
|
+
var TypeValidator = class {
|
|
485
|
+
name = "TypeValidator";
|
|
486
|
+
constructor(checker, errorFormatter) {
|
|
487
|
+
this.checker = checker;
|
|
488
|
+
this.errorFormatter = errorFormatter;
|
|
489
|
+
}
|
|
490
|
+
validate(config, _context) {
|
|
491
|
+
const errors = [];
|
|
492
|
+
for (const [_tokenId, info] of config.localInjections) {
|
|
493
|
+
const typeError = this.checkTypeCompatibility(info);
|
|
494
|
+
if (typeError) errors.push(typeError);
|
|
495
|
+
}
|
|
496
|
+
return errors;
|
|
497
|
+
}
|
|
498
|
+
checkTypeCompatibility(info) {
|
|
499
|
+
const { definition } = info;
|
|
500
|
+
if (!definition.implementationSymbol) return null;
|
|
501
|
+
if (!definition.isInterfaceToken) return null;
|
|
502
|
+
const tokenType = this.getTokenType(info);
|
|
503
|
+
if (!tokenType) return null;
|
|
504
|
+
const implType = this.getImplementationType(info);
|
|
505
|
+
if (!implType) return null;
|
|
506
|
+
if (!this.checker.isTypeAssignableTo(implType, tokenType)) {
|
|
507
|
+
const expectedTypeName = this.checker.typeToString(tokenType);
|
|
508
|
+
const actualTypeName = this.checker.typeToString(implType);
|
|
509
|
+
return this.errorFormatter.formatTypeMismatchError(info, expectedTypeName, actualTypeName);
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
getTokenType(_info) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
getImplementationType(info) {
|
|
517
|
+
const { definition } = info;
|
|
518
|
+
if (!definition.implementationSymbol) return null;
|
|
519
|
+
const declarations = definition.implementationSymbol.getDeclarations();
|
|
520
|
+
if (!declarations || declarations.length === 0) return null;
|
|
521
|
+
return this.checker.getTypeOfSymbolAtLocation(definition.implementationSymbol, declarations[0]);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/analyzer/validators/MissingDependencyValidator.ts
|
|
527
|
+
/**
|
|
528
|
+
* Validates that all required dependencies are available in the container.
|
|
529
|
+
* - For partialConfig: checks only local injections
|
|
530
|
+
* - For defineBuilder: checks local + parent + extends (recursive)
|
|
531
|
+
*/
|
|
532
|
+
var MissingDependencyValidator = class {
|
|
533
|
+
name = "MissingDependencyValidator";
|
|
534
|
+
constructor(errorFormatter, dependencyAnalyzer) {
|
|
535
|
+
this.errorFormatter = errorFormatter;
|
|
536
|
+
this.dependencyAnalyzer = dependencyAnalyzer;
|
|
537
|
+
}
|
|
538
|
+
validate(config, context) {
|
|
539
|
+
const errors = [];
|
|
540
|
+
const availableTokens = this.collectAvailableTokens(config, context);
|
|
541
|
+
for (const [_tokenId, info] of config.localInjections) {
|
|
542
|
+
const requiredDeps = this.dependencyAnalyzer.getRequiredDependencies(info.definition);
|
|
543
|
+
for (const depTokenId of requiredDeps) if (!(availableTokens.has(depTokenId) || availableTokens.has(`useInterface<${depTokenId}>()`))) {
|
|
544
|
+
const errorNode = this.findTokenNode(info.node) || info.node;
|
|
545
|
+
const sourceFile = config.sourceFile;
|
|
546
|
+
errors.push({
|
|
547
|
+
type: "missing",
|
|
548
|
+
message: `Missing injection: '${depTokenId}' required by '${info.tokenText}' is not registered in this ${config.type === "builder" ? "builder nor its parents/extends" : "partial config"}`,
|
|
549
|
+
node: errorNode,
|
|
550
|
+
sourceFile,
|
|
551
|
+
context: { tokenText: depTokenId }
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return errors;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Finds the token property node within an injection object for better error positioning.
|
|
559
|
+
* Returns the entire property assignment (e.g., "token: UserService") not just the value,
|
|
560
|
+
* because the value might be an imported symbol whose AST node is in a different file.
|
|
561
|
+
*/
|
|
562
|
+
findTokenNode(injectionNode) {
|
|
563
|
+
if (!ts.isObjectLiteralExpression(injectionNode)) return null;
|
|
564
|
+
for (const prop of injectionNode.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "token") return prop;
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Collects all available tokens in the current context.
|
|
569
|
+
* - partialConfig: only local tokens
|
|
570
|
+
* - defineBuilder: local + inherited from parent/extends + legacy parent tokens
|
|
571
|
+
*/
|
|
572
|
+
collectAvailableTokens(config, context) {
|
|
573
|
+
const available = /* @__PURE__ */ new Set();
|
|
574
|
+
for (const tokenId of config.localInjections.keys()) available.add(tokenId);
|
|
575
|
+
if (config.type === "builder" && context.inheritedTokens) for (const tokenId of context.inheritedTokens.keys()) available.add(tokenId);
|
|
576
|
+
if (config.type === "builder" && config.legacyParentTokens) for (const tokenId of config.legacyParentTokens) available.add(tokenId);
|
|
577
|
+
return available;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
//#endregion
|
|
582
|
+
//#region src/analyzer/validators/DependencyAnalyzer.ts
|
|
583
|
+
/**
|
|
584
|
+
* Analyzes class constructors and factory functions to extract required dependencies.
|
|
585
|
+
*/
|
|
586
|
+
var DependencyAnalyzer = class {
|
|
587
|
+
constructor(checker) {
|
|
588
|
+
this.checker = checker;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Extracts all dependencies required by a service definition.
|
|
592
|
+
* @param definition - Service definition to analyze
|
|
593
|
+
* @returns Array of token IDs required by this service
|
|
594
|
+
*/
|
|
595
|
+
getRequiredDependencies(definition) {
|
|
596
|
+
if (definition.isFactory || definition.type === "factory") return [];
|
|
597
|
+
if (!definition.implementationSymbol) return [];
|
|
598
|
+
return this.getConstructorDependencies(definition.implementationSymbol);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Extracts dependencies from a class constructor.
|
|
602
|
+
* @param symbol - Class symbol to analyze
|
|
603
|
+
* @returns Array of token IDs required by the constructor
|
|
604
|
+
*/
|
|
605
|
+
getConstructorDependencies(symbol) {
|
|
606
|
+
const dependencies = [];
|
|
607
|
+
const declarations = symbol.getDeclarations();
|
|
608
|
+
if (!declarations || declarations.length === 0) return dependencies;
|
|
609
|
+
const classDecl = declarations.find((d) => ts.isClassDeclaration(d));
|
|
610
|
+
if (!classDecl) return dependencies;
|
|
611
|
+
const className = classDecl.name?.getText() ?? "Anonymous";
|
|
612
|
+
const constructor = classDecl.members.find((m) => ts.isConstructorDeclaration(m));
|
|
613
|
+
if (!constructor) return dependencies;
|
|
614
|
+
for (const param of constructor.parameters) {
|
|
615
|
+
const paramName = param.name.getText();
|
|
616
|
+
const typeNode = param.type;
|
|
617
|
+
if (!typeNode) continue;
|
|
618
|
+
const type = this.checker.getTypeFromTypeNode(typeNode);
|
|
619
|
+
const depTokenId = this.getTypeId(type, className, paramName);
|
|
620
|
+
dependencies.push(depTokenId);
|
|
621
|
+
}
|
|
622
|
+
return dependencies;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Generates a unique Token ID for a given Type.
|
|
626
|
+
* Uses symbol name and file path hash for consistency.
|
|
627
|
+
*
|
|
628
|
+
* @param type - The TypeScript Type
|
|
629
|
+
* @param _className - Parent class name (for property tokens) - reserved for future use
|
|
630
|
+
* @param _paramName - Parameter name (for property tokens) - reserved for future use
|
|
631
|
+
* @returns A string identifier for the token
|
|
632
|
+
*/
|
|
633
|
+
getTypeId(type, _className, _paramName) {
|
|
634
|
+
const symbol = type.getSymbol();
|
|
635
|
+
if (!symbol) return this.checker.typeToString(type);
|
|
636
|
+
const name = symbol.getName();
|
|
637
|
+
if (name === "__type" || name === "InterfaceToken" || name === "__brand") return this.checker.typeToString(type);
|
|
638
|
+
const declarations = symbol.getDeclarations();
|
|
639
|
+
if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
|
|
640
|
+
return name;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
//#endregion
|
|
645
|
+
//#region src/analyzer/errors/ErrorFormatter.ts
|
|
646
|
+
/**
|
|
647
|
+
* Default error formatter with descriptive messages.
|
|
648
|
+
*/
|
|
649
|
+
var ErrorFormatter = class {
|
|
650
|
+
/**
|
|
651
|
+
* Finds the token property node within an injection object for better error positioning.
|
|
652
|
+
* Returns the entire property assignment (e.g., "token: UserService") not just the value,
|
|
653
|
+
* because the value might be an imported symbol whose AST node is in a different file.
|
|
654
|
+
*/
|
|
655
|
+
findTokenNode(injectionNode) {
|
|
656
|
+
if (!ts.isObjectLiteralExpression(injectionNode)) return null;
|
|
657
|
+
for (const prop of injectionNode.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "token") return prop;
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
formatDuplicateError(injection, source) {
|
|
661
|
+
let message;
|
|
662
|
+
if (source.type === "internal") message = `Duplicate registration: '${injection.tokenText}' is already registered.`;
|
|
663
|
+
else if (source.type === "parent") message = `Duplicate registration: '${injection.tokenText}' is already registered in parent container '${source.name}'.`;
|
|
664
|
+
else message = `Duplicate registration: '${injection.tokenText}' is already registered in partial '${source.name}'.`;
|
|
665
|
+
const errorNode = this.findTokenNode(injection.node) || injection.node;
|
|
666
|
+
return {
|
|
667
|
+
type: "duplicate",
|
|
668
|
+
message,
|
|
669
|
+
node: errorNode,
|
|
670
|
+
sourceFile: errorNode.getSourceFile(),
|
|
671
|
+
context: {
|
|
672
|
+
tokenText: injection.tokenText,
|
|
673
|
+
conflictSource: source.name
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
formatTypeMismatchError(injection, expectedType, actualType) {
|
|
678
|
+
const errorNode = this.findTokenNode(injection.node) || injection.node;
|
|
679
|
+
return {
|
|
680
|
+
type: "type-mismatch",
|
|
681
|
+
message: `Type mismatch: Provider '${actualType}' is not assignable to token type '${expectedType}'.`,
|
|
682
|
+
node: errorNode,
|
|
683
|
+
sourceFile: errorNode.getSourceFile(),
|
|
684
|
+
context: { tokenText: injection.tokenText }
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
formatCycleError(chain, node, sourceFile) {
|
|
688
|
+
return {
|
|
689
|
+
type: "cycle",
|
|
690
|
+
message: `Circular dependency detected: ${chain.join(" -> ")}`,
|
|
691
|
+
node,
|
|
692
|
+
sourceFile,
|
|
693
|
+
context: { chain }
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/analyzer/Analyzer.ts
|
|
700
|
+
/**
|
|
701
|
+
* Error thrown when a duplicate registration is detected.
|
|
702
|
+
* Includes the position of the duplicate registration node.
|
|
703
|
+
*/
|
|
704
|
+
var DuplicateRegistrationError = class extends Error {
|
|
705
|
+
constructor(message, node, sourceFile) {
|
|
706
|
+
super(message);
|
|
707
|
+
this.node = node;
|
|
708
|
+
this.sourceFile = sourceFile;
|
|
709
|
+
this.name = "DuplicateRegistrationError";
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* Error thrown when a provider type is incompatible with the token type.
|
|
714
|
+
* Includes the position of the registration node.
|
|
715
|
+
*/
|
|
716
|
+
var TypeMismatchError = class extends Error {
|
|
717
|
+
constructor(message, node, sourceFile) {
|
|
718
|
+
super(message);
|
|
719
|
+
this.node = node;
|
|
720
|
+
this.sourceFile = sourceFile;
|
|
721
|
+
this.name = "TypeMismatchError";
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
/**
|
|
725
|
+
* Generates a unique, deterministic Token ID for a symbol.
|
|
726
|
+
* Uses the symbol name and a short hash of its relative file path.
|
|
727
|
+
*/
|
|
728
|
+
function generateTokenId(symbol, sourceFile) {
|
|
729
|
+
const name = symbol.getName();
|
|
730
|
+
let relativePath = path.relative(process.cwd(), sourceFile.fileName);
|
|
731
|
+
relativePath = relativePath.split(path.sep).join("/");
|
|
732
|
+
return `${name}_${crypto.createHash("md5").update(relativePath).digest("hex").substring(0, 8)}`;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Analyzes TypeScript source code to extract the dependency injection graph.
|
|
736
|
+
*
|
|
737
|
+
* This class uses the TypeScript Compiler API to:
|
|
738
|
+
* 1. Locate `createContainer` calls.
|
|
739
|
+
* 2. Parse the fluent chain of `bind` and `register` calls.
|
|
740
|
+
* 3. Resolve symbols and types for services and their dependencies.
|
|
741
|
+
* 4. Build a `DependencyGraph` representing the system.
|
|
742
|
+
*/
|
|
743
|
+
var Analyzer = class {
|
|
744
|
+
program;
|
|
745
|
+
checker;
|
|
746
|
+
/** Set of variable names that are parent containers (should not be added to main graph) */
|
|
747
|
+
parentContainerNames = /* @__PURE__ */ new Set();
|
|
748
|
+
/**
|
|
749
|
+
* Creates a new Analyzer instance.
|
|
750
|
+
* @param program - The TypeScript Program instance containing the source files to analyze.
|
|
751
|
+
*/
|
|
752
|
+
constructor(program) {
|
|
753
|
+
this.program = program;
|
|
754
|
+
this.checker = program.getTypeChecker();
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Extracts the dependency graph from the program's source files.
|
|
758
|
+
*
|
|
759
|
+
* It scans all non-declaration source files for container configurations.
|
|
760
|
+
* Each defineBuilderConfig gets its own isolated graph to avoid false positives.
|
|
761
|
+
*
|
|
762
|
+
* @returns A `DependencyGraph` containing all registered services and their dependencies.
|
|
763
|
+
*/
|
|
764
|
+
extract() {
|
|
765
|
+
const graph = {
|
|
766
|
+
containerId: "DefaultContainer",
|
|
767
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
768
|
+
roots: [],
|
|
769
|
+
buildArguments: [],
|
|
770
|
+
errors: []
|
|
771
|
+
};
|
|
772
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
773
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
774
|
+
this.identifyParentContainers(sourceFile);
|
|
775
|
+
}
|
|
776
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
777
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
778
|
+
this.visitNode(sourceFile, graph);
|
|
779
|
+
}
|
|
780
|
+
this.resolveAllDependencies(graph);
|
|
781
|
+
return graph;
|
|
782
|
+
}
|
|
783
|
+
configCollector = null;
|
|
784
|
+
tokenResolver = null;
|
|
785
|
+
validator = null;
|
|
786
|
+
collectedConfigs = null;
|
|
787
|
+
collectionError = null;
|
|
788
|
+
/**
|
|
789
|
+
* Lazily initialize the modular components.
|
|
790
|
+
*/
|
|
791
|
+
initModularComponents() {
|
|
792
|
+
if (this.configCollector) return;
|
|
793
|
+
const errorFormatter = new ErrorFormatter();
|
|
794
|
+
const dependencyAnalyzer = new DependencyAnalyzer(this.checker);
|
|
795
|
+
this.configCollector = new ConfigCollector(this.program, this.checker);
|
|
796
|
+
this.tokenResolver = new TokenResolver();
|
|
797
|
+
this.validator = new CompositeValidator([
|
|
798
|
+
new DuplicateValidator(errorFormatter),
|
|
799
|
+
new TypeValidator(this.checker, errorFormatter),
|
|
800
|
+
new MissingDependencyValidator(errorFormatter, dependencyAnalyzer)
|
|
801
|
+
]);
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Get collected configs (with caching).
|
|
805
|
+
*/
|
|
806
|
+
getCollectedConfigs() {
|
|
807
|
+
if (this.collectionError) throw this.collectionError;
|
|
808
|
+
if (!this.collectedConfigs) {
|
|
809
|
+
this.initModularComponents();
|
|
810
|
+
try {
|
|
811
|
+
this.collectedConfigs = this.configCollector.collect();
|
|
812
|
+
} catch (e) {
|
|
813
|
+
if (e instanceof Error) this.collectionError = e;
|
|
814
|
+
throw e;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return this.collectedConfigs;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Entry point for LSP - analyzes a specific file.
|
|
821
|
+
* Uses the modular architecture for isolated validation.
|
|
822
|
+
*
|
|
823
|
+
* @param fileName - The file to analyze
|
|
824
|
+
* @returns Analysis result with errors for this file only
|
|
825
|
+
*/
|
|
826
|
+
extractForFile(fileName) {
|
|
827
|
+
this.initModularComponents();
|
|
828
|
+
const allConfigs = this.getCollectedConfigs();
|
|
829
|
+
const errors = [];
|
|
830
|
+
const configsInFile = [...allConfigs.values()].filter((c) => c.sourceFile.fileName === fileName);
|
|
831
|
+
for (const config of configsInFile) {
|
|
832
|
+
const context = { allConfigs };
|
|
833
|
+
if (config.type === "builder") try {
|
|
834
|
+
context.inheritedTokens = this.tokenResolver.resolveInheritedTokens(config, allConfigs);
|
|
835
|
+
} catch (e) {
|
|
836
|
+
if (e instanceof CycleError) {
|
|
837
|
+
errors.push({
|
|
838
|
+
type: "cycle",
|
|
839
|
+
message: e.message,
|
|
840
|
+
node: config.node,
|
|
841
|
+
sourceFile: config.sourceFile,
|
|
842
|
+
context: { chain: e.chain }
|
|
843
|
+
});
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
throw e;
|
|
847
|
+
}
|
|
848
|
+
errors.push(...this.validator.validate(config, context));
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
configs: allConfigs,
|
|
852
|
+
errors
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* First pass: identify containers used as parents so we can skip them in visitNode.
|
|
857
|
+
*/
|
|
858
|
+
identifyParentContainers(node) {
|
|
859
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "useContainer" && ts.isIdentifier(node.initializer)) this.parentContainerNames.add(node.initializer.text);
|
|
860
|
+
ts.forEachChild(node, (child) => this.identifyParentContainers(child));
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Visits an AST node to find container calls.
|
|
864
|
+
* @param node - The AST node to visit.
|
|
865
|
+
* @param graph - The graph to populate.
|
|
866
|
+
*/
|
|
867
|
+
visitNode(node, graph) {
|
|
868
|
+
if (ts.isCallExpression(node)) {
|
|
869
|
+
if (this.isDefineBuilderConfigCall(node)) {
|
|
870
|
+
const parent = node.parent;
|
|
871
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
872
|
+
if (this.parentContainerNames.has(parent.name.text)) return;
|
|
873
|
+
graph.exportedVariableName = parent.name.text;
|
|
874
|
+
let current = parent;
|
|
875
|
+
while (current && !ts.isVariableStatement(current)) current = current.parent;
|
|
876
|
+
if (current && ts.isVariableStatement(current)) {
|
|
877
|
+
graph.variableStatementStart = current.getStart();
|
|
878
|
+
const modifiers = ts.canHaveModifiers(current) ? ts.getModifiers(current) : void 0;
|
|
879
|
+
if (modifiers) {
|
|
880
|
+
const hasExport = modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
881
|
+
const hasDefault = modifiers.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
882
|
+
if (hasExport && hasDefault) graph.variableExportModifier = "export default";
|
|
883
|
+
else if (hasExport) graph.variableExportModifier = "export";
|
|
884
|
+
else graph.variableExportModifier = "none";
|
|
885
|
+
} else graph.variableExportModifier = "none";
|
|
886
|
+
}
|
|
887
|
+
} else if (ts.isExportAssignment(parent) && parent.isExportEquals === false) {
|
|
888
|
+
graph.variableExportModifier = "export default";
|
|
889
|
+
graph.variableStatementStart = parent.getStart();
|
|
890
|
+
}
|
|
891
|
+
graph.defineBuilderConfigStart = node.getStart();
|
|
892
|
+
graph.defineBuilderConfigEnd = node.getEnd();
|
|
893
|
+
this.parseBuilderConfig(node, graph);
|
|
894
|
+
} else if (this.isDefinePartialConfigCall(node)) {
|
|
895
|
+
const parent = node.parent;
|
|
896
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
897
|
+
const partialName = parent.name.text;
|
|
898
|
+
if (!this.isPartialUsedInExtends(partialName)) {
|
|
899
|
+
const partialGraph = {
|
|
900
|
+
containerId: partialName,
|
|
901
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
902
|
+
roots: [],
|
|
903
|
+
errors: []
|
|
904
|
+
};
|
|
905
|
+
this.parseBuilderConfig(node, partialGraph);
|
|
906
|
+
if (partialGraph.errors && partialGraph.errors.length > 0) {
|
|
907
|
+
if (!graph.errors) graph.errors = [];
|
|
908
|
+
graph.errors.push(...partialGraph.errors);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
ts.forEachChild(node, (child) => this.visitNode(child, graph));
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Check if a partial config is used in any extends array.
|
|
918
|
+
*/
|
|
919
|
+
partialNamesUsedInExtends = null;
|
|
920
|
+
isPartialUsedInExtends(partialName) {
|
|
921
|
+
if (this.partialNamesUsedInExtends === null) {
|
|
922
|
+
this.partialNamesUsedInExtends = /* @__PURE__ */ new Set();
|
|
923
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
924
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
925
|
+
this.collectPartialsInExtends(sourceFile);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return this.partialNamesUsedInExtends.has(partialName);
|
|
929
|
+
}
|
|
930
|
+
collectPartialsInExtends(node) {
|
|
931
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "extends" && ts.isArrayLiteralExpression(node.initializer)) {
|
|
932
|
+
for (const element of node.initializer.elements) if (ts.isIdentifier(element)) this.partialNamesUsedInExtends.add(element.text);
|
|
933
|
+
}
|
|
934
|
+
ts.forEachChild(node, (child) => this.collectPartialsInExtends(child));
|
|
935
|
+
}
|
|
936
|
+
isDefineBuilderConfigCall(node) {
|
|
937
|
+
const expression = node.expression;
|
|
938
|
+
if (ts.isIdentifier(expression)) return expression.text === "defineBuilderConfig";
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
parseBuilderConfig(node, graph) {
|
|
942
|
+
const args = node.arguments;
|
|
943
|
+
if (args.length < 1) return;
|
|
944
|
+
const configObj = args[0];
|
|
945
|
+
if (!ts.isObjectLiteralExpression(configObj)) return;
|
|
946
|
+
const nameProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "name");
|
|
947
|
+
if (nameProp && ts.isPropertyAssignment(nameProp) && ts.isStringLiteral(nameProp.initializer)) {
|
|
948
|
+
graph.containerName = nameProp.initializer.text;
|
|
949
|
+
graph.containerId = nameProp.initializer.text;
|
|
950
|
+
} else graph.containerId = this.generateHashBasedContainerId(node);
|
|
951
|
+
const injectionsProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "injections");
|
|
952
|
+
if (injectionsProp && ts.isPropertyAssignment(injectionsProp) && ts.isArrayLiteralExpression(injectionsProp.initializer)) this.parseInjectionsArray(injectionsProp.initializer, graph);
|
|
953
|
+
const extendsProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "extends");
|
|
954
|
+
if (extendsProp && ts.isPropertyAssignment(extendsProp) && ts.isArrayLiteralExpression(extendsProp.initializer)) this.parseExtendsArray(extendsProp.initializer, graph);
|
|
955
|
+
const useContainerProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "useContainer");
|
|
956
|
+
if (useContainerProp && ts.isPropertyAssignment(useContainerProp)) {
|
|
957
|
+
if (!graph.legacyContainers) graph.legacyContainers = [];
|
|
958
|
+
if (!graph.parentProvidedTokens) graph.parentProvidedTokens = /* @__PURE__ */ new Set();
|
|
959
|
+
const containerExpr = useContainerProp.initializer;
|
|
960
|
+
if (ts.isIdentifier(containerExpr)) {
|
|
961
|
+
graph.legacyContainers.push(containerExpr.text);
|
|
962
|
+
this.extractParentContainerTokens(containerExpr, graph);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Extracts tokens provided by a parent container.
|
|
968
|
+
* Handles both NeoSyringe containers (defineBuilderConfig) and
|
|
969
|
+
* declared legacy containers (declareContainerTokens).
|
|
970
|
+
*/
|
|
971
|
+
extractParentContainerTokens(containerIdentifier, graph) {
|
|
972
|
+
const symbol = this.checker.getSymbolAtLocation(containerIdentifier);
|
|
973
|
+
if (!symbol) return;
|
|
974
|
+
this.parentContainerNames.add(containerIdentifier.text);
|
|
975
|
+
const resolvedSymbol = this.resolveSymbol(symbol);
|
|
976
|
+
const declaration = resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0];
|
|
977
|
+
if (!declaration) return;
|
|
978
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
979
|
+
const init = declaration.initializer;
|
|
980
|
+
if (ts.isCallExpression(init)) {
|
|
981
|
+
if (this.isDefineBuilderConfigCall(init) || this.isDefinePartialConfigCall(init)) {
|
|
982
|
+
const parentGraph = {
|
|
983
|
+
containerId: containerIdentifier.text,
|
|
984
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
985
|
+
roots: []
|
|
986
|
+
};
|
|
987
|
+
this.parseBuilderConfig(init, parentGraph);
|
|
988
|
+
for (const tokenId of parentGraph.nodes.keys()) graph.parentProvidedTokens.add(tokenId);
|
|
989
|
+
if (parentGraph.parentProvidedTokens) for (const tokenId of parentGraph.parentProvidedTokens) graph.parentProvidedTokens.add(tokenId);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (this.isDeclareContainerTokensCall(init)) {
|
|
993
|
+
this.extractDeclaredTokens(init, graph);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Checks if a call expression is declareContainerTokens<T>().
|
|
1001
|
+
*/
|
|
1002
|
+
isDeclareContainerTokensCall(node) {
|
|
1003
|
+
if (ts.isIdentifier(node.expression)) return node.expression.text === "declareContainerTokens";
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Extracts tokens from declareContainerTokens<{ Token: Type }>().
|
|
1008
|
+
* The type argument contains the token names.
|
|
1009
|
+
*/
|
|
1010
|
+
extractDeclaredTokens(node, graph) {
|
|
1011
|
+
if (!node.typeArguments || node.typeArguments.length === 0) return;
|
|
1012
|
+
const typeArg = node.typeArguments[0];
|
|
1013
|
+
const properties = this.checker.getTypeFromTypeNode(typeArg).getProperties();
|
|
1014
|
+
for (const prop of properties) {
|
|
1015
|
+
const propType = this.checker.getTypeOfSymbol(prop);
|
|
1016
|
+
if (propType) {
|
|
1017
|
+
const tokenId = this.getTypeId(propType);
|
|
1018
|
+
graph.parentProvidedTokens.add(tokenId);
|
|
1019
|
+
} else graph.parentProvidedTokens.add(prop.getName());
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
parseExtendsArray(arrayLiteral, graph) {
|
|
1023
|
+
for (const element of arrayLiteral.elements) if (ts.isIdentifier(element)) this.parsePartialConfig(element, graph);
|
|
1024
|
+
}
|
|
1025
|
+
parsePartialConfig(identifier, graph) {
|
|
1026
|
+
const symbol = this.checker.getSymbolAtLocation(identifier);
|
|
1027
|
+
if (!symbol) return;
|
|
1028
|
+
const resolvedSymbol = this.resolveSymbol(symbol);
|
|
1029
|
+
const declaration = resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0];
|
|
1030
|
+
if (!declaration) return;
|
|
1031
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isCallExpression(declaration.initializer)) {
|
|
1032
|
+
const callExpr = declaration.initializer;
|
|
1033
|
+
if (this.isDefinePartialConfigCall(callExpr)) this.parseBuilderConfig(callExpr, graph);
|
|
1034
|
+
}
|
|
1035
|
+
if (ts.isExportSpecifier(declaration)) {}
|
|
1036
|
+
}
|
|
1037
|
+
isDefinePartialConfigCall(node) {
|
|
1038
|
+
const expression = node.expression;
|
|
1039
|
+
if (ts.isIdentifier(expression)) return expression.text === "definePartialConfig";
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
parseInjectionsArray(arrayLiteral, graph) {
|
|
1043
|
+
for (const element of arrayLiteral.elements) if (ts.isObjectLiteralExpression(element)) this.parseInjectionObject(element, graph);
|
|
1044
|
+
}
|
|
1045
|
+
parseInjectionObject(obj, graph) {
|
|
1046
|
+
let tokenNode;
|
|
1047
|
+
let providerNode;
|
|
1048
|
+
let lifecycle = "singleton";
|
|
1049
|
+
let useFactory = false;
|
|
1050
|
+
let isScoped = false;
|
|
1051
|
+
for (const prop of obj.properties) {
|
|
1052
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
|
|
1053
|
+
if (prop.name.text === "token") tokenNode = prop.initializer;
|
|
1054
|
+
else if (prop.name.text === "provider") providerNode = prop.initializer;
|
|
1055
|
+
else if (prop.name.text === "lifecycle" && ts.isStringLiteral(prop.initializer)) {
|
|
1056
|
+
if (prop.initializer.text === "transient") lifecycle = "transient";
|
|
1057
|
+
} else if (prop.name.text === "useFactory") {
|
|
1058
|
+
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) useFactory = true;
|
|
1059
|
+
} else if (prop.name.text === "scoped") {
|
|
1060
|
+
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) isScoped = true;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (!tokenNode) return;
|
|
1064
|
+
if (providerNode && (ts.isArrowFunction(providerNode) || ts.isFunctionExpression(providerNode))) useFactory = true;
|
|
1065
|
+
let tokenId;
|
|
1066
|
+
let implementationSymbol;
|
|
1067
|
+
let tokenSymbol;
|
|
1068
|
+
let type = "autowire";
|
|
1069
|
+
let isInterfaceToken = false;
|
|
1070
|
+
let isValueToken = false;
|
|
1071
|
+
let factorySource;
|
|
1072
|
+
let resolvedTokenNode = tokenNode;
|
|
1073
|
+
const resolved = this.resolveToInitializer(tokenNode);
|
|
1074
|
+
if (resolved) resolvedTokenNode = resolved;
|
|
1075
|
+
if (ts.isCallExpression(resolvedTokenNode) && this.isUseInterfaceCall(resolvedTokenNode)) {
|
|
1076
|
+
tokenId = this.extractInterfaceTokenId(resolvedTokenNode);
|
|
1077
|
+
type = "explicit";
|
|
1078
|
+
isInterfaceToken = true;
|
|
1079
|
+
} else if (ts.isCallExpression(resolvedTokenNode) && this.isUsePropertyCall(resolvedTokenNode)) {
|
|
1080
|
+
const propertyInfo = this.extractPropertyTokenId(resolvedTokenNode);
|
|
1081
|
+
tokenId = propertyInfo.tokenId;
|
|
1082
|
+
type = "explicit";
|
|
1083
|
+
isValueToken = true;
|
|
1084
|
+
if (!providerNode) throw new Error(`useProperty(${propertyInfo.className}, '${propertyInfo.paramName}') requires a provider (factory).`);
|
|
1085
|
+
useFactory = true;
|
|
1086
|
+
} else {
|
|
1087
|
+
const tokenType = this.checker.getTypeAtLocation(tokenNode);
|
|
1088
|
+
tokenId = this.getTypeIdFromConstructor(tokenType);
|
|
1089
|
+
if (ts.isIdentifier(tokenNode)) tokenSymbol = this.checker.getSymbolAtLocation(tokenNode);
|
|
1090
|
+
}
|
|
1091
|
+
if (useFactory && providerNode) {
|
|
1092
|
+
factorySource = providerNode.getText();
|
|
1093
|
+
type = "factory";
|
|
1094
|
+
if (tokenId) {
|
|
1095
|
+
if (graph.nodes.has(tokenId) && !isScoped) {
|
|
1096
|
+
const sourceFile = obj.getSourceFile();
|
|
1097
|
+
const tokenText = tokenNode.getText(sourceFile);
|
|
1098
|
+
if (!graph.errors) graph.errors = [];
|
|
1099
|
+
graph.errors.push({
|
|
1100
|
+
type: "duplicate",
|
|
1101
|
+
message: `Duplicate registration: '${tokenText}' is already registered.`,
|
|
1102
|
+
node: obj,
|
|
1103
|
+
sourceFile
|
|
1104
|
+
});
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const definition = {
|
|
1108
|
+
tokenId,
|
|
1109
|
+
tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
|
|
1110
|
+
registrationNode: obj,
|
|
1111
|
+
type: "factory",
|
|
1112
|
+
lifecycle,
|
|
1113
|
+
isInterfaceToken,
|
|
1114
|
+
isValueToken,
|
|
1115
|
+
isFactory: true,
|
|
1116
|
+
factorySource,
|
|
1117
|
+
isScoped
|
|
1118
|
+
};
|
|
1119
|
+
graph.nodes.set(tokenId, {
|
|
1120
|
+
service: definition,
|
|
1121
|
+
dependencies: []
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (providerNode) {
|
|
1127
|
+
implementationSymbol = this.checker.getSymbolAtLocation(providerNode);
|
|
1128
|
+
type = "explicit";
|
|
1129
|
+
} else if (type === "explicit" && !providerNode) {
|
|
1130
|
+
if (ts.isIdentifier(tokenNode)) {
|
|
1131
|
+
implementationSymbol = this.checker.getSymbolAtLocation(tokenNode);
|
|
1132
|
+
type = "autowire";
|
|
1133
|
+
}
|
|
1134
|
+
} else if (ts.isIdentifier(tokenNode)) {
|
|
1135
|
+
implementationSymbol = this.checker.getSymbolAtLocation(tokenNode);
|
|
1136
|
+
type = "autowire";
|
|
1137
|
+
}
|
|
1138
|
+
if (tokenId && implementationSymbol) {
|
|
1139
|
+
if (graph.nodes.has(tokenId) && !isScoped) {
|
|
1140
|
+
const sourceFile = obj.getSourceFile();
|
|
1141
|
+
const tokenText = tokenNode.getText(sourceFile);
|
|
1142
|
+
if (!graph.errors) graph.errors = [];
|
|
1143
|
+
graph.errors.push({
|
|
1144
|
+
type: "duplicate",
|
|
1145
|
+
message: `Duplicate registration: '${tokenText}' is already registered.`,
|
|
1146
|
+
node: obj,
|
|
1147
|
+
sourceFile
|
|
1148
|
+
});
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (type === "explicit" && isInterfaceToken && providerNode) this.validateTypeCompatibility(tokenNode, providerNode, obj, graph);
|
|
1152
|
+
const definition = {
|
|
1153
|
+
tokenId,
|
|
1154
|
+
implementationSymbol: this.resolveSymbol(implementationSymbol),
|
|
1155
|
+
tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
|
|
1156
|
+
registrationNode: obj,
|
|
1157
|
+
type,
|
|
1158
|
+
lifecycle,
|
|
1159
|
+
isInterfaceToken: isInterfaceToken || ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode),
|
|
1160
|
+
isScoped
|
|
1161
|
+
};
|
|
1162
|
+
graph.nodes.set(tokenId, {
|
|
1163
|
+
service: definition,
|
|
1164
|
+
dependencies: []
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
isUseInterfaceCall(node) {
|
|
1169
|
+
if (ts.isIdentifier(node.expression)) return node.expression.text === "useInterface";
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
isUsePropertyCall(node) {
|
|
1173
|
+
if (ts.isIdentifier(node.expression)) return node.expression.text === "useProperty";
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Resolves a node to its original initializer expression.
|
|
1178
|
+
* Handles identifiers, property access, imports, and unwraps type assertions/casts.
|
|
1179
|
+
*/
|
|
1180
|
+
resolveToInitializer(node) {
|
|
1181
|
+
let expr = node;
|
|
1182
|
+
while (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr) || ts.isSatisfiesExpression && ts.isSatisfiesExpression(expr)) expr = expr.expression;
|
|
1183
|
+
if (ts.isCallExpression(expr)) return expr;
|
|
1184
|
+
if (ts.isIdentifier(expr)) return this.resolveIdentifierToInitializer(expr);
|
|
1185
|
+
if (ts.isPropertyAccessExpression(expr)) return this.resolvePropertyAccessToInitializer(expr);
|
|
1186
|
+
}
|
|
1187
|
+
resolveIdentifierToInitializer(identifier) {
|
|
1188
|
+
const symbol = this.checker.getSymbolAtLocation(identifier);
|
|
1189
|
+
if (!symbol) return void 0;
|
|
1190
|
+
const declarations = this.resolveSymbol(symbol).getDeclarations();
|
|
1191
|
+
if (!declarations || declarations.length === 0) return void 0;
|
|
1192
|
+
for (const decl of declarations) if (ts.isVariableDeclaration(decl) && decl.initializer) return this.resolveToInitializer(decl.initializer) ?? decl.initializer;
|
|
1193
|
+
}
|
|
1194
|
+
resolvePropertyAccessToInitializer(node) {
|
|
1195
|
+
const objectInitializer = this.resolveToInitializer(node.expression);
|
|
1196
|
+
if (objectInitializer && ts.isObjectLiteralExpression(objectInitializer)) {
|
|
1197
|
+
const propName = node.name.text;
|
|
1198
|
+
const prop = objectInitializer.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === propName);
|
|
1199
|
+
if (prop && ts.isPropertyAssignment(prop)) return this.resolveToInitializer(prop.initializer) ?? prop.initializer;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Validates that the provider type is compatible with the token type.
|
|
1204
|
+
* @param tokenNode - The token node (e.g., useInterface<ILogger>())
|
|
1205
|
+
* @param providerNode - The provider node (e.g., ConsoleLogger)
|
|
1206
|
+
* @param registrationNode - The full registration object for error reporting
|
|
1207
|
+
* @param graph - The dependency graph to collect errors
|
|
1208
|
+
*/
|
|
1209
|
+
validateTypeCompatibility(tokenNode, providerNode, registrationNode, graph) {
|
|
1210
|
+
let tokenType;
|
|
1211
|
+
let resolvedTokenNode = tokenNode;
|
|
1212
|
+
if (ts.isExpression(tokenNode)) {
|
|
1213
|
+
const resolved = this.resolveToInitializer(tokenNode);
|
|
1214
|
+
if (resolved) resolvedTokenNode = resolved;
|
|
1215
|
+
}
|
|
1216
|
+
if (ts.isCallExpression(resolvedTokenNode) && this.isUseInterfaceCall(resolvedTokenNode)) {
|
|
1217
|
+
const typeArgs = resolvedTokenNode.typeArguments;
|
|
1218
|
+
if (typeArgs && typeArgs.length > 0) tokenType = this.checker.getTypeFromTypeNode(typeArgs[0]);
|
|
1219
|
+
}
|
|
1220
|
+
if (!tokenType) return;
|
|
1221
|
+
const providerType = this.checker.getTypeAtLocation(providerNode);
|
|
1222
|
+
let providerInstanceType;
|
|
1223
|
+
const constructSignatures = providerType.getConstructSignatures();
|
|
1224
|
+
if (constructSignatures.length > 0) providerInstanceType = constructSignatures[0].getReturnType();
|
|
1225
|
+
else providerInstanceType = providerType;
|
|
1226
|
+
if (!providerInstanceType) return;
|
|
1227
|
+
if (!this.checker.isTypeAssignableTo(providerInstanceType, tokenType)) {
|
|
1228
|
+
const sourceFile = registrationNode.getSourceFile();
|
|
1229
|
+
const tokenTypeName = this.checker.typeToString(tokenType);
|
|
1230
|
+
const providerTypeName = this.checker.typeToString(providerInstanceType);
|
|
1231
|
+
if (!graph.errors) graph.errors = [];
|
|
1232
|
+
graph.errors.push({
|
|
1233
|
+
type: "type-mismatch",
|
|
1234
|
+
message: `Type mismatch: Provider '${providerTypeName}' is not assignable to token type '${tokenTypeName}'.`,
|
|
1235
|
+
node: registrationNode,
|
|
1236
|
+
sourceFile
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
extractPropertyTokenId(node) {
|
|
1241
|
+
if (node.arguments.length < 2) throw new Error("useProperty requires two arguments: (Class, paramName)");
|
|
1242
|
+
const classArg = node.arguments[0];
|
|
1243
|
+
const nameArg = node.arguments[1];
|
|
1244
|
+
if (!ts.isIdentifier(classArg)) throw new Error("useProperty first argument must be a class identifier.");
|
|
1245
|
+
if (!ts.isStringLiteral(nameArg)) throw new Error("useProperty second argument must be a string literal.");
|
|
1246
|
+
const className = classArg.text;
|
|
1247
|
+
const paramName = nameArg.text;
|
|
1248
|
+
return {
|
|
1249
|
+
tokenId: `PropertyToken:${className}.${paramName}`,
|
|
1250
|
+
className,
|
|
1251
|
+
paramName
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
extractInterfaceTokenId(node) {
|
|
1255
|
+
if (!node.typeArguments || node.typeArguments.length === 0) throw new Error("useInterface must have a type argument.");
|
|
1256
|
+
const typeNode = node.typeArguments[0];
|
|
1257
|
+
const symbol = this.checker.getTypeFromTypeNode(typeNode).getSymbol();
|
|
1258
|
+
if (!symbol) return "AnonymousInterface";
|
|
1259
|
+
const declarations = symbol.getDeclarations();
|
|
1260
|
+
if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
|
|
1261
|
+
return symbol.getName();
|
|
1262
|
+
}
|
|
1263
|
+
getTypeIdFromConstructor(type) {
|
|
1264
|
+
const constructSignatures = type.getConstructSignatures();
|
|
1265
|
+
let instanceType;
|
|
1266
|
+
if (constructSignatures.length > 0) instanceType = constructSignatures[0].getReturnType();
|
|
1267
|
+
else instanceType = type;
|
|
1268
|
+
return this.getTypeId(instanceType);
|
|
1269
|
+
}
|
|
1270
|
+
extractScope(optionsNode) {
|
|
1271
|
+
for (const prop of optionsNode.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "scope" && ts.isStringLiteral(prop.initializer)) {
|
|
1272
|
+
if (prop.initializer.text === "transient") return "transient";
|
|
1273
|
+
}
|
|
1274
|
+
return "singleton";
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Resolves dependencies for all nodes in the graph.
|
|
1278
|
+
* @param graph - The dependency graph.
|
|
1279
|
+
*/
|
|
1280
|
+
resolveAllDependencies(graph) {
|
|
1281
|
+
for (const node of graph.nodes.values()) this.resolveDependencies(node, graph);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Resolves dependencies for a single service node by inspecting its constructor.
|
|
1285
|
+
* @param node - The dependency node to resolve.
|
|
1286
|
+
*/
|
|
1287
|
+
resolveDependencies(node, graph) {
|
|
1288
|
+
if (node.service.isFactory || node.service.type === "factory") return;
|
|
1289
|
+
const symbol = node.service.implementationSymbol;
|
|
1290
|
+
if (!symbol) return;
|
|
1291
|
+
const declarations = symbol.getDeclarations();
|
|
1292
|
+
if (!declarations || declarations.length === 0) return;
|
|
1293
|
+
const classDecl = declarations.find((d) => ts.isClassDeclaration(d));
|
|
1294
|
+
if (!classDecl) return;
|
|
1295
|
+
const className = classDecl.name?.getText() ?? "Anonymous";
|
|
1296
|
+
const constructor = classDecl.members.find((m) => ts.isConstructorDeclaration(m));
|
|
1297
|
+
if (!constructor) return;
|
|
1298
|
+
for (const param of constructor.parameters) {
|
|
1299
|
+
const paramName = param.name.getText();
|
|
1300
|
+
const typeNode = param.type;
|
|
1301
|
+
if (!typeNode) continue;
|
|
1302
|
+
const type = this.checker.getTypeFromTypeNode(typeNode);
|
|
1303
|
+
const propertyTokenId = `PropertyToken:${className}.${paramName}`;
|
|
1304
|
+
if (graph.nodes.has(propertyTokenId)) {
|
|
1305
|
+
node.dependencies.push(propertyTokenId);
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
const depTokenId = this.getTypeId(type);
|
|
1309
|
+
node.dependencies.push(depTokenId);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Generates a unique Token ID for a given Type.
|
|
1314
|
+
* Uses file path hash + name for consistency and collision avoidance.
|
|
1315
|
+
*
|
|
1316
|
+
* @param type - The TypeScript Type.
|
|
1317
|
+
* @returns A string identifier for the token.
|
|
1318
|
+
*/
|
|
1319
|
+
getTypeId(type) {
|
|
1320
|
+
const symbol = type.getSymbol();
|
|
1321
|
+
if (!symbol) return this.checker.typeToString(type);
|
|
1322
|
+
const name = symbol.getName();
|
|
1323
|
+
if (name === "__type" || name === "InterfaceToken" || name === "__brand") return this.checker.typeToString(type);
|
|
1324
|
+
const declarations = symbol.getDeclarations();
|
|
1325
|
+
if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
|
|
1326
|
+
return symbol.getName();
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Resolves a symbol, following aliases if necessary.
|
|
1330
|
+
* @param symbol - The symbol to resolve.
|
|
1331
|
+
* @returns The resolved symbol.
|
|
1332
|
+
*/
|
|
1333
|
+
resolveSymbol(symbol) {
|
|
1334
|
+
if (symbol.flags & ts.SymbolFlags.Alias) return this.resolveSymbol(this.checker.getAliasedSymbol(symbol));
|
|
1335
|
+
return symbol;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Generates a hash-based container ID when no 'name' field is provided.
|
|
1339
|
+
* @param node - The defineBuilderConfig call expression
|
|
1340
|
+
* @returns A unique container ID like "Container_a1b2c3d4"
|
|
1341
|
+
*/
|
|
1342
|
+
generateHashBasedContainerId(node) {
|
|
1343
|
+
const sourceFile = node.getSourceFile();
|
|
1344
|
+
const hashInput = `${path.basename(sourceFile.fileName, ".ts")}:${node.getStart()}:${node.getText()}`;
|
|
1345
|
+
let hash = 0;
|
|
1346
|
+
for (let i = 0; i < hashInput.length; i++) {
|
|
1347
|
+
const char = hashInput.charCodeAt(i);
|
|
1348
|
+
hash = (hash << 5) - hash + char;
|
|
1349
|
+
hash = hash & hash;
|
|
1350
|
+
}
|
|
1351
|
+
return `Container_${Math.abs(hash).toString(16).substring(0, 8)}`;
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
//#endregion
|
|
1356
|
+
export { ErrorFormatter as a, TypeValidator as c, TokenResolver as d, CycleError as f, generateTokenId as i, DuplicateValidator as l, DuplicateRegistrationError as n, DependencyAnalyzer as o, ConfigCollector as p, TypeMismatchError as r, MissingDependencyValidator as s, Analyzer as t, CompositeValidator as u };
|