@djodjonx/neosyringe-lsp 0.0.1

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/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @djodjonx/neosyringe-lsp
2
+
3
+ TypeScript Language Service Plugin for NeoSyringe. Provides real-time error detection in VSCode.
4
+
5
+ ## Setup
6
+
7
+ Add to `tsconfig.json`:
8
+
9
+ ```json
10
+ {
11
+ "compilerOptions": {
12
+ "plugins": [{ "name": "@djodjonx/neosyringe-lsp" }]
13
+ }
14
+ }
15
+ ```
16
+
17
+ See [Documentation](https://djodjonx.github.io/neosyringe/guide/ide-plugin).
package/dist/index.cjs ADDED
@@ -0,0 +1,610 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let typescript = require("typescript");
29
+ typescript = __toESM(typescript);
30
+ let node_crypto = require("node:crypto");
31
+ node_crypto = __toESM(node_crypto);
32
+ let node_path = require("node:path");
33
+ node_path = __toESM(node_path);
34
+
35
+ //#region ../core/src/analyzer/Analyzer.ts
36
+ /**
37
+ * Generates a unique, deterministic Token ID for a symbol.
38
+ * Uses the symbol name and a short hash of its relative file path.
39
+ */
40
+ function generateTokenId(symbol, sourceFile) {
41
+ const name = symbol.getName();
42
+ let relativePath = node_path.relative(process.cwd(), sourceFile.fileName);
43
+ relativePath = relativePath.split(node_path.sep).join("/");
44
+ return `${name}_${node_crypto.createHash("md5").update(relativePath).digest("hex").substring(0, 8)}`;
45
+ }
46
+ /**
47
+ * Analyzes TypeScript source code to extract the dependency injection graph.
48
+ *
49
+ * This class uses the TypeScript Compiler API to:
50
+ * 1. Locate `createContainer` calls.
51
+ * 2. Parse the fluent chain of `bind` and `register` calls.
52
+ * 3. Resolve symbols and types for services and their dependencies.
53
+ * 4. Build a `DependencyGraph` representing the system.
54
+ */
55
+ var Analyzer = class {
56
+ program;
57
+ checker;
58
+ /** Set of variable names that are parent containers (should not be added to main graph) */
59
+ parentContainerNames = /* @__PURE__ */ new Set();
60
+ /**
61
+ * Creates a new Analyzer instance.
62
+ * @param program - The TypeScript Program instance containing the source files to analyze.
63
+ */
64
+ constructor(program) {
65
+ this.program = program;
66
+ this.checker = program.getTypeChecker();
67
+ }
68
+ /**
69
+ * Extracts the dependency graph from the program's source files.
70
+ *
71
+ * It scans all non-declaration source files for container configurations.
72
+ *
73
+ * @returns A `DependencyGraph` containing all registered services and their dependencies.
74
+ */
75
+ extract() {
76
+ const graph = {
77
+ nodes: /* @__PURE__ */ new Map(),
78
+ roots: [],
79
+ buildArguments: []
80
+ };
81
+ for (const sourceFile of this.program.getSourceFiles()) {
82
+ if (sourceFile.isDeclarationFile) continue;
83
+ this.identifyParentContainers(sourceFile);
84
+ }
85
+ for (const sourceFile of this.program.getSourceFiles()) {
86
+ if (sourceFile.isDeclarationFile) continue;
87
+ this.visitNode(sourceFile, graph);
88
+ }
89
+ this.resolveAllDependencies(graph);
90
+ return graph;
91
+ }
92
+ /**
93
+ * First pass: identify containers used as parents so we can skip them in visitNode.
94
+ */
95
+ identifyParentContainers(node) {
96
+ if (typescript.isPropertyAssignment(node) && typescript.isIdentifier(node.name) && node.name.text === "useContainer" && typescript.isIdentifier(node.initializer)) this.parentContainerNames.add(node.initializer.text);
97
+ typescript.forEachChild(node, (child) => this.identifyParentContainers(child));
98
+ }
99
+ /**
100
+ * Visits an AST node to find container calls.
101
+ * @param node - The AST node to visit.
102
+ * @param graph - The graph to populate.
103
+ */
104
+ visitNode(node, graph) {
105
+ if (typescript.isCallExpression(node)) {
106
+ if (this.isDefineBuilderConfigCall(node)) {
107
+ const parent = node.parent;
108
+ if (typescript.isVariableDeclaration(parent) && typescript.isIdentifier(parent.name)) {
109
+ if (this.parentContainerNames.has(parent.name.text)) return;
110
+ graph.exportedVariableName = parent.name.text;
111
+ let current = parent;
112
+ while (current && !typescript.isVariableStatement(current)) current = current.parent;
113
+ if (current && typescript.isVariableStatement(current)) graph.variableStatementStart = current.getStart();
114
+ }
115
+ graph.defineBuilderConfigStart = node.getStart();
116
+ graph.defineBuilderConfigEnd = node.getEnd();
117
+ this.parseBuilderConfig(node, graph);
118
+ }
119
+ }
120
+ typescript.forEachChild(node, (child) => this.visitNode(child, graph));
121
+ }
122
+ isDefineBuilderConfigCall(node) {
123
+ const expression = node.expression;
124
+ if (typescript.isIdentifier(expression)) return expression.text === "defineBuilderConfig";
125
+ return false;
126
+ }
127
+ parseBuilderConfig(node, graph) {
128
+ const args = node.arguments;
129
+ if (args.length < 1) return;
130
+ const configObj = args[0];
131
+ if (!typescript.isObjectLiteralExpression(configObj)) return;
132
+ const nameProp = configObj.properties.find((p) => p.name && typescript.isIdentifier(p.name) && p.name.text === "name");
133
+ if (nameProp && typescript.isPropertyAssignment(nameProp) && typescript.isStringLiteral(nameProp.initializer)) graph.containerName = nameProp.initializer.text;
134
+ const injectionsProp = configObj.properties.find((p) => p.name && typescript.isIdentifier(p.name) && p.name.text === "injections");
135
+ if (injectionsProp && typescript.isPropertyAssignment(injectionsProp) && typescript.isArrayLiteralExpression(injectionsProp.initializer)) this.parseInjectionsArray(injectionsProp.initializer, graph);
136
+ const extendsProp = configObj.properties.find((p) => p.name && typescript.isIdentifier(p.name) && p.name.text === "extends");
137
+ if (extendsProp && typescript.isPropertyAssignment(extendsProp) && typescript.isArrayLiteralExpression(extendsProp.initializer)) this.parseExtendsArray(extendsProp.initializer, graph);
138
+ const useContainerProp = configObj.properties.find((p) => p.name && typescript.isIdentifier(p.name) && p.name.text === "useContainer");
139
+ if (useContainerProp && typescript.isPropertyAssignment(useContainerProp)) {
140
+ if (!graph.legacyContainers) graph.legacyContainers = [];
141
+ if (!graph.parentProvidedTokens) graph.parentProvidedTokens = /* @__PURE__ */ new Set();
142
+ const containerExpr = useContainerProp.initializer;
143
+ if (typescript.isIdentifier(containerExpr)) {
144
+ graph.legacyContainers.push(containerExpr.text);
145
+ this.extractParentContainerTokens(containerExpr, graph);
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Extracts tokens provided by a parent container.
151
+ * Handles both NeoSyringe containers (defineBuilderConfig) and
152
+ * declared legacy containers (declareContainerTokens).
153
+ */
154
+ extractParentContainerTokens(containerIdentifier, graph) {
155
+ const symbol = this.checker.getSymbolAtLocation(containerIdentifier);
156
+ if (!symbol) return;
157
+ this.parentContainerNames.add(containerIdentifier.text);
158
+ const resolvedSymbol = this.resolveSymbol(symbol);
159
+ const declaration = resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0];
160
+ if (!declaration) return;
161
+ if (typescript.isVariableDeclaration(declaration) && declaration.initializer) {
162
+ const init$1 = declaration.initializer;
163
+ if (typescript.isCallExpression(init$1)) {
164
+ if (this.isDefineBuilderConfigCall(init$1) || this.isDefinePartialConfigCall(init$1)) {
165
+ const parentGraph = {
166
+ nodes: /* @__PURE__ */ new Map(),
167
+ roots: []
168
+ };
169
+ this.parseBuilderConfig(init$1, parentGraph);
170
+ for (const tokenId of parentGraph.nodes.keys()) graph.parentProvidedTokens.add(tokenId);
171
+ if (parentGraph.parentProvidedTokens) for (const tokenId of parentGraph.parentProvidedTokens) graph.parentProvidedTokens.add(tokenId);
172
+ return;
173
+ }
174
+ if (this.isDeclareContainerTokensCall(init$1)) {
175
+ this.extractDeclaredTokens(init$1, graph);
176
+ return;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ /**
182
+ * Checks if a call expression is declareContainerTokens<T>().
183
+ */
184
+ isDeclareContainerTokensCall(node) {
185
+ if (typescript.isIdentifier(node.expression)) return node.expression.text === "declareContainerTokens";
186
+ return false;
187
+ }
188
+ /**
189
+ * Extracts tokens from declareContainerTokens<{ Token: Type }>().
190
+ * The type argument contains the token names.
191
+ */
192
+ extractDeclaredTokens(node, graph) {
193
+ if (!node.typeArguments || node.typeArguments.length === 0) return;
194
+ const typeArg = node.typeArguments[0];
195
+ const properties = this.checker.getTypeFromTypeNode(typeArg).getProperties();
196
+ for (const prop of properties) {
197
+ const propType = this.checker.getTypeOfSymbol(prop);
198
+ if (propType) {
199
+ const tokenId = this.getTypeId(propType);
200
+ graph.parentProvidedTokens.add(tokenId);
201
+ } else graph.parentProvidedTokens.add(prop.getName());
202
+ }
203
+ }
204
+ parseExtendsArray(arrayLiteral, graph) {
205
+ for (const element of arrayLiteral.elements) if (typescript.isIdentifier(element)) this.parsePartialConfig(element, graph);
206
+ }
207
+ parsePartialConfig(identifier, graph) {
208
+ const symbol = this.checker.getSymbolAtLocation(identifier);
209
+ if (!symbol) return;
210
+ const resolvedSymbol = this.resolveSymbol(symbol);
211
+ const declaration = resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0];
212
+ if (!declaration) return;
213
+ if (typescript.isVariableDeclaration(declaration) && declaration.initializer && typescript.isCallExpression(declaration.initializer)) {
214
+ const callExpr = declaration.initializer;
215
+ if (this.isDefinePartialConfigCall(callExpr)) this.parseBuilderConfig(callExpr, graph);
216
+ }
217
+ if (typescript.isExportSpecifier(declaration)) {}
218
+ }
219
+ isDefinePartialConfigCall(node) {
220
+ const expression = node.expression;
221
+ if (typescript.isIdentifier(expression)) return expression.text === "definePartialConfig";
222
+ return false;
223
+ }
224
+ parseInjectionsArray(arrayLiteral, graph) {
225
+ for (const element of arrayLiteral.elements) if (typescript.isObjectLiteralExpression(element)) this.parseInjectionObject(element, graph);
226
+ }
227
+ parseInjectionObject(obj, graph) {
228
+ let tokenNode;
229
+ let providerNode;
230
+ let lifecycle = "singleton";
231
+ let useFactory = false;
232
+ let isScoped = false;
233
+ for (const prop of obj.properties) {
234
+ if (!typescript.isPropertyAssignment(prop) || !typescript.isIdentifier(prop.name)) continue;
235
+ if (prop.name.text === "token") tokenNode = prop.initializer;
236
+ else if (prop.name.text === "provider") providerNode = prop.initializer;
237
+ else if (prop.name.text === "lifecycle" && typescript.isStringLiteral(prop.initializer)) {
238
+ if (prop.initializer.text === "transient") lifecycle = "transient";
239
+ } else if (prop.name.text === "useFactory") {
240
+ if (prop.initializer.kind === typescript.SyntaxKind.TrueKeyword) useFactory = true;
241
+ } else if (prop.name.text === "scoped") {
242
+ if (prop.initializer.kind === typescript.SyntaxKind.TrueKeyword) isScoped = true;
243
+ }
244
+ }
245
+ if (!tokenNode) return;
246
+ if (providerNode && (typescript.isArrowFunction(providerNode) || typescript.isFunctionExpression(providerNode))) useFactory = true;
247
+ let tokenId;
248
+ let implementationSymbol;
249
+ let tokenSymbol;
250
+ let type = "autowire";
251
+ let isInterfaceToken = false;
252
+ let isValueToken = false;
253
+ let factorySource;
254
+ let resolvedTokenNode = tokenNode;
255
+ const resolved = this.resolveToInitializer(tokenNode);
256
+ if (resolved) resolvedTokenNode = resolved;
257
+ if (typescript.isCallExpression(resolvedTokenNode) && this.isUseInterfaceCall(resolvedTokenNode)) {
258
+ tokenId = this.extractInterfaceTokenId(resolvedTokenNode);
259
+ type = "explicit";
260
+ isInterfaceToken = true;
261
+ } else if (typescript.isCallExpression(resolvedTokenNode) && this.isUsePropertyCall(resolvedTokenNode)) {
262
+ const propertyInfo = this.extractPropertyTokenId(resolvedTokenNode);
263
+ tokenId = propertyInfo.tokenId;
264
+ type = "explicit";
265
+ isValueToken = true;
266
+ if (!providerNode) throw new Error(`useProperty(${propertyInfo.className}, '${propertyInfo.paramName}') requires a provider (factory).`);
267
+ useFactory = true;
268
+ } else {
269
+ const tokenType = this.checker.getTypeAtLocation(tokenNode);
270
+ tokenId = this.getTypeIdFromConstructor(tokenType);
271
+ if (typescript.isIdentifier(tokenNode)) tokenSymbol = this.checker.getSymbolAtLocation(tokenNode);
272
+ }
273
+ if (useFactory && providerNode) {
274
+ factorySource = providerNode.getText();
275
+ type = "factory";
276
+ if (tokenId) {
277
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
278
+ const definition = {
279
+ tokenId,
280
+ tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
281
+ registrationNode: obj,
282
+ type: "factory",
283
+ lifecycle,
284
+ isInterfaceToken,
285
+ isValueToken,
286
+ isFactory: true,
287
+ factorySource,
288
+ isScoped
289
+ };
290
+ graph.nodes.set(tokenId, {
291
+ service: definition,
292
+ dependencies: []
293
+ });
294
+ }
295
+ return;
296
+ }
297
+ if (providerNode) {
298
+ implementationSymbol = this.checker.getSymbolAtLocation(providerNode);
299
+ type = "explicit";
300
+ } else if (type === "explicit" && !providerNode) {
301
+ if (typescript.isIdentifier(tokenNode)) {
302
+ implementationSymbol = this.checker.getSymbolAtLocation(tokenNode);
303
+ type = "autowire";
304
+ }
305
+ } else if (typescript.isIdentifier(tokenNode)) {
306
+ implementationSymbol = this.checker.getSymbolAtLocation(tokenNode);
307
+ type = "autowire";
308
+ }
309
+ if (tokenId && implementationSymbol) {
310
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
311
+ const definition = {
312
+ tokenId,
313
+ implementationSymbol: this.resolveSymbol(implementationSymbol),
314
+ tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
315
+ registrationNode: obj,
316
+ type,
317
+ lifecycle,
318
+ isInterfaceToken: isInterfaceToken || typescript.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode),
319
+ isScoped
320
+ };
321
+ graph.nodes.set(tokenId, {
322
+ service: definition,
323
+ dependencies: []
324
+ });
325
+ }
326
+ }
327
+ isUseInterfaceCall(node) {
328
+ if (typescript.isIdentifier(node.expression)) return node.expression.text === "useInterface";
329
+ return false;
330
+ }
331
+ isUsePropertyCall(node) {
332
+ if (typescript.isIdentifier(node.expression)) return node.expression.text === "useProperty";
333
+ return false;
334
+ }
335
+ /**
336
+ * Resolves a node to its original initializer expression.
337
+ * Handles identifiers, property access, imports, and unwraps type assertions/casts.
338
+ */
339
+ resolveToInitializer(node) {
340
+ let expr = node;
341
+ while (typescript.isParenthesizedExpression(expr) || typescript.isAsExpression(expr) || typescript.isTypeAssertionExpression(expr) || typescript.isSatisfiesExpression && typescript.isSatisfiesExpression(expr)) expr = expr.expression;
342
+ if (typescript.isCallExpression(expr)) return expr;
343
+ if (typescript.isIdentifier(expr)) return this.resolveIdentifierToInitializer(expr);
344
+ if (typescript.isPropertyAccessExpression(expr)) return this.resolvePropertyAccessToInitializer(expr);
345
+ }
346
+ resolveIdentifierToInitializer(identifier) {
347
+ const symbol = this.checker.getSymbolAtLocation(identifier);
348
+ if (!symbol) return void 0;
349
+ const declarations = this.resolveSymbol(symbol).getDeclarations();
350
+ if (!declarations || declarations.length === 0) return void 0;
351
+ for (const decl of declarations) if (typescript.isVariableDeclaration(decl) && decl.initializer) return this.resolveToInitializer(decl.initializer) ?? decl.initializer;
352
+ }
353
+ resolvePropertyAccessToInitializer(node) {
354
+ const objectInitializer = this.resolveToInitializer(node.expression);
355
+ if (objectInitializer && typescript.isObjectLiteralExpression(objectInitializer)) {
356
+ const propName = node.name.text;
357
+ const prop = objectInitializer.properties.find((p) => p.name && typescript.isIdentifier(p.name) && p.name.text === propName);
358
+ if (prop && typescript.isPropertyAssignment(prop)) return this.resolveToInitializer(prop.initializer) ?? prop.initializer;
359
+ }
360
+ }
361
+ extractPropertyTokenId(node) {
362
+ if (node.arguments.length < 2) throw new Error("useProperty requires two arguments: (Class, paramName)");
363
+ const classArg = node.arguments[0];
364
+ const nameArg = node.arguments[1];
365
+ if (!typescript.isIdentifier(classArg)) throw new Error("useProperty first argument must be a class identifier.");
366
+ if (!typescript.isStringLiteral(nameArg)) throw new Error("useProperty second argument must be a string literal.");
367
+ const className = classArg.text;
368
+ const paramName = nameArg.text;
369
+ return {
370
+ tokenId: `PropertyToken:${className}.${paramName}`,
371
+ className,
372
+ paramName
373
+ };
374
+ }
375
+ extractInterfaceTokenId(node) {
376
+ if (!node.typeArguments || node.typeArguments.length === 0) throw new Error("useInterface must have a type argument.");
377
+ const typeNode = node.typeArguments[0];
378
+ const symbol = this.checker.getTypeFromTypeNode(typeNode).getSymbol();
379
+ if (!symbol) return "AnonymousInterface";
380
+ const declarations = symbol.getDeclarations();
381
+ if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
382
+ return symbol.getName();
383
+ }
384
+ getTypeIdFromConstructor(type) {
385
+ const constructSignatures = type.getConstructSignatures();
386
+ let instanceType;
387
+ if (constructSignatures.length > 0) instanceType = constructSignatures[0].getReturnType();
388
+ else instanceType = type;
389
+ return this.getTypeId(instanceType);
390
+ }
391
+ extractScope(optionsNode) {
392
+ for (const prop of optionsNode.properties) if (typescript.isPropertyAssignment(prop) && typescript.isIdentifier(prop.name) && prop.name.text === "scope" && typescript.isStringLiteral(prop.initializer)) {
393
+ if (prop.initializer.text === "transient") return "transient";
394
+ }
395
+ return "singleton";
396
+ }
397
+ /**
398
+ * Resolves dependencies for all nodes in the graph.
399
+ * @param graph - The dependency graph.
400
+ */
401
+ resolveAllDependencies(graph) {
402
+ for (const node of graph.nodes.values()) this.resolveDependencies(node, graph);
403
+ }
404
+ /**
405
+ * Resolves dependencies for a single service node by inspecting its constructor.
406
+ * @param node - The dependency node to resolve.
407
+ */
408
+ resolveDependencies(node, graph) {
409
+ if (node.service.isFactory || node.service.type === "factory") return;
410
+ const symbol = node.service.implementationSymbol;
411
+ if (!symbol) return;
412
+ const declarations = symbol.getDeclarations();
413
+ if (!declarations || declarations.length === 0) return;
414
+ const classDecl = declarations.find((d) => typescript.isClassDeclaration(d));
415
+ if (!classDecl) return;
416
+ const className = classDecl.name?.getText() ?? "Anonymous";
417
+ const constructor = classDecl.members.find((m) => typescript.isConstructorDeclaration(m));
418
+ if (!constructor) return;
419
+ for (const param of constructor.parameters) {
420
+ const paramName = param.name.getText();
421
+ const typeNode = param.type;
422
+ if (!typeNode) continue;
423
+ const type = this.checker.getTypeFromTypeNode(typeNode);
424
+ const propertyTokenId = `PropertyToken:${className}.${paramName}`;
425
+ if (graph.nodes.has(propertyTokenId)) {
426
+ node.dependencies.push(propertyTokenId);
427
+ continue;
428
+ }
429
+ const depTokenId = this.getTypeId(type);
430
+ node.dependencies.push(depTokenId);
431
+ }
432
+ }
433
+ /**
434
+ * Generates a unique Token ID for a given Type.
435
+ * Uses file path hash + name for consistency and collision avoidance.
436
+ *
437
+ * @param type - The TypeScript Type.
438
+ * @returns A string identifier for the token.
439
+ */
440
+ getTypeId(type) {
441
+ const symbol = type.getSymbol();
442
+ if (!symbol) return this.checker.typeToString(type);
443
+ const name = symbol.getName();
444
+ if (name === "__type" || name === "InterfaceToken" || name === "__brand") return this.checker.typeToString(type);
445
+ const declarations = symbol.getDeclarations();
446
+ if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
447
+ return symbol.getName();
448
+ }
449
+ /**
450
+ * Resolves a symbol, following aliases if necessary.
451
+ * @param symbol - The symbol to resolve.
452
+ * @returns The resolved symbol.
453
+ */
454
+ resolveSymbol(symbol) {
455
+ if (symbol.flags & typescript.SymbolFlags.Alias) return this.resolveSymbol(this.checker.getAliasedSymbol(symbol));
456
+ return symbol;
457
+ }
458
+ };
459
+
460
+ //#endregion
461
+ //#region ../core/src/generator/GraphValidator.ts
462
+ /**
463
+ * Validates the dependency graph for correctness.
464
+ * Detects circular dependencies, missing bindings, and duplicates.
465
+ */
466
+ var GraphValidator = class {
467
+ /**
468
+ * Validates the graph structure.
469
+ *
470
+ * @param graph - The dependency graph to validate.
471
+ * @throws {Error} If a circular dependency or missing binding is detected.
472
+ */
473
+ validate(graph) {
474
+ const visited = /* @__PURE__ */ new Set();
475
+ const recursionStack = /* @__PURE__ */ new Set();
476
+ const parentTokens = graph.parentProvidedTokens ?? /* @__PURE__ */ new Set();
477
+ for (const [nodeId, node] of graph.nodes) if (parentTokens.has(nodeId)) {
478
+ if (node.service.isScoped) continue;
479
+ throw new Error(`Duplicate registration: '${nodeId}' is already registered in the parent container. Use 'scoped: true' to override the parent's registration intentionally.`);
480
+ }
481
+ for (const [nodeId, node] of graph.nodes) for (const depId of node.dependencies) {
482
+ const isProvidedLocally = graph.nodes.has(depId);
483
+ const isProvidedByParent = parentTokens.has(depId);
484
+ if (!isProvidedLocally && !isProvidedByParent) {
485
+ const serviceName = nodeId;
486
+ throw new Error(`Missing binding: Service '${serviceName}' depends on '${depId}', but no provider was registered.`);
487
+ }
488
+ }
489
+ for (const nodeId of graph.nodes.keys()) if (!visited.has(nodeId)) this.detectCycle(nodeId, graph, visited, recursionStack, parentTokens);
490
+ }
491
+ /**
492
+ * Recursive Helper for Cycle Detection (DFS).
493
+ *
494
+ * @param nodeId - The current node ID.
495
+ * @param graph - The graph.
496
+ * @param visited - Set of all visited nodes.
497
+ * @param stack - Set of nodes in the current recursion stack (path).
498
+ * @param parentTokens - Tokens from parent container (skip traversal).
499
+ */
500
+ detectCycle(nodeId, graph, visited, stack, parentTokens) {
501
+ visited.add(nodeId);
502
+ stack.add(nodeId);
503
+ const node = graph.nodes.get(nodeId);
504
+ if (node) for (const depId of node.dependencies) {
505
+ if (parentTokens.has(depId)) continue;
506
+ if (!visited.has(depId)) this.detectCycle(depId, graph, visited, stack, parentTokens);
507
+ else if (stack.has(depId)) throw new Error(`Circular dependency detected: ${[...stack, depId].join(" -> ")}`);
508
+ }
509
+ stack.delete(nodeId);
510
+ }
511
+ };
512
+
513
+ //#endregion
514
+ //#region src/index.ts
515
+ /**
516
+ * Initializes the TypeScript Language Service Plugin.
517
+ *
518
+ * This plugin wraps the standard TypeScript Language Service to provide
519
+ * additional diagnostics for NeoSyringe usage (e.g., circular dependencies).
520
+ *
521
+ * @param modules - The typescript module passed by the tsserver.
522
+ * @returns An object containing the `create` factory.
523
+ */
524
+ function init(modules) {
525
+ const ts = modules.typescript;
526
+ /**
527
+ * Creates the proxy language service.
528
+ * @param info - Plugin configuration and context.
529
+ */
530
+ function create(info) {
531
+ const proxy = Object.create(null);
532
+ for (const k of Object.keys(info.languageService)) {
533
+ const x = info.languageService[k];
534
+ proxy[k] = (...args) => x.apply(info.languageService, args);
535
+ }
536
+ /**
537
+ * Intercepts semantic diagnostics to add NeoSyringe validation errors.
538
+ * @param fileName - The file being analyzed.
539
+ */
540
+ proxy.getSemanticDiagnostics = (fileName) => {
541
+ const prior = info.languageService.getSemanticDiagnostics(fileName);
542
+ const log = (msg) => {
543
+ if (info.project?.projectService?.logger) info.project.projectService.logger.info(`[NeoSyringe LSP] ${msg}`);
544
+ };
545
+ if (fileName.includes("container")) log(`Checking file: ${fileName}`);
546
+ try {
547
+ const program = info.languageService.getProgram();
548
+ if (!program) {
549
+ if (fileName.includes("container")) log(`No program available`);
550
+ return prior;
551
+ }
552
+ const sourceFile = program.getSourceFile(fileName);
553
+ if (!sourceFile) {
554
+ if (fileName.includes("container")) log(`No source file`);
555
+ return prior;
556
+ }
557
+ if (!sourceFile.getText().includes("defineBuilderConfig")) {
558
+ if (fileName.includes("container")) log(`No defineBuilderConfig found in file`);
559
+ return prior;
560
+ }
561
+ log(`Running analysis on: ${fileName}`);
562
+ const graph = new Analyzer(program).extract();
563
+ log(`Graph extracted with ${graph.nodes.size} nodes`);
564
+ const validator = new GraphValidator();
565
+ try {
566
+ validator.validate(graph);
567
+ log(`Validation passed`);
568
+ } catch (e) {
569
+ if (e instanceof Error) {
570
+ const msg = e.message;
571
+ log(`Validation error: ${msg}`);
572
+ prior.push({
573
+ file: sourceFile,
574
+ start: 0,
575
+ length: 10,
576
+ messageText: `[NeoSyringe] ${msg}`,
577
+ category: ts.DiagnosticCategory.Error,
578
+ code: 9999
579
+ });
580
+ }
581
+ }
582
+ } catch (e) {
583
+ if (e instanceof Error) {
584
+ log(`Analyzer exception: ${e.message}`);
585
+ if (e.message.includes("Duplicate")) {
586
+ const file = info.languageService.getProgram()?.getSourceFile(fileName);
587
+ if (file) {
588
+ log(`Adding duplicate diagnostic`);
589
+ prior.push({
590
+ file,
591
+ start: 0,
592
+ length: 10,
593
+ messageText: `[NeoSyringe] ${e.message}`,
594
+ category: ts.DiagnosticCategory.Error,
595
+ code: 9998
596
+ });
597
+ }
598
+ }
599
+ }
600
+ }
601
+ return prior;
602
+ };
603
+ return proxy;
604
+ }
605
+ return { create };
606
+ }
607
+ var src_default = init;
608
+
609
+ //#endregion
610
+ module.exports = src_default;
@@ -0,0 +1,18 @@
1
+ import * as typescript0 from "typescript";
2
+
3
+ //#region src/index.d.ts
4
+ /**
5
+ * Initializes the TypeScript Language Service Plugin.
6
+ *
7
+ * This plugin wraps the standard TypeScript Language Service to provide
8
+ * additional diagnostics for NeoSyringe usage (e.g., circular dependencies).
9
+ *
10
+ * @param modules - The typescript module passed by the tsserver.
11
+ * @returns An object containing the `create` factory.
12
+ */
13
+ declare function init(modules: {
14
+ typescript: typeof typescript0;
15
+ }): {
16
+ create: (info: typescript0.server.PluginCreateInfo) => typescript0.LanguageService;
17
+ };
18
+ export = init;
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@djodjonx/neosyringe-lsp",
3
+ "version": "0.0.1",
4
+ "description": "TypeScript Language Service Plugin for NeoSyringe",
5
+ "main": "dist/index.cjs",
6
+ "types": "dist/index.d.cts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/index.cjs",
10
+ "types": "./dist/index.d.cts"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@djodjonx/neosyringe-core": "0.1.0"
21
+ },
22
+ "peerDependencies": {
23
+ "typescript": ">=5.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "tsdown": "0.20.0-beta.3",
27
+ "typescript": "^5.9.3",
28
+ "vitest": "^4.0.17"
29
+ },
30
+ "scripts": {
31
+ "build": "tsdown",
32
+ "test": "vitest run"
33
+ }
34
+ }