@duytransipher/gitnexus 1.4.6-sipher.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/LICENSE +73 -0
- package/README.md +261 -0
- package/dist/cli/ai-context.d.ts +23 -0
- package/dist/cli/ai-context.js +265 -0
- package/dist/cli/analyze.d.ts +12 -0
- package/dist/cli/analyze.js +345 -0
- package/dist/cli/augment.d.ts +13 -0
- package/dist/cli/augment.js +33 -0
- package/dist/cli/clean.d.ts +10 -0
- package/dist/cli/clean.js +60 -0
- package/dist/cli/eval-server.d.ts +37 -0
- package/dist/cli/eval-server.js +389 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +137 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +18 -0
- package/dist/cli/list.d.ts +6 -0
- package/dist/cli/list.js +30 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +36 -0
- package/dist/cli/serve.d.ts +4 -0
- package/dist/cli/serve.js +6 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +367 -0
- package/dist/cli/sipher-patched.d.ts +2 -0
- package/dist/cli/sipher-patched.js +77 -0
- package/dist/cli/skill-gen.d.ts +26 -0
- package/dist/cli/skill-gen.js +549 -0
- package/dist/cli/status.d.ts +6 -0
- package/dist/cli/status.js +36 -0
- package/dist/cli/tool.d.ts +60 -0
- package/dist/cli/tool.js +180 -0
- package/dist/cli/wiki.d.ts +15 -0
- package/dist/cli/wiki.js +365 -0
- package/dist/config/ignore-service.d.ts +26 -0
- package/dist/config/ignore-service.js +284 -0
- package/dist/config/supported-languages.d.ts +15 -0
- package/dist/config/supported-languages.js +16 -0
- package/dist/core/augmentation/engine.d.ts +26 -0
- package/dist/core/augmentation/engine.js +240 -0
- package/dist/core/embeddings/embedder.d.ts +60 -0
- package/dist/core/embeddings/embedder.js +251 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +51 -0
- package/dist/core/embeddings/embedding-pipeline.js +356 -0
- package/dist/core/embeddings/index.d.ts +9 -0
- package/dist/core/embeddings/index.js +9 -0
- package/dist/core/embeddings/text-generator.d.ts +24 -0
- package/dist/core/embeddings/text-generator.js +182 -0
- package/dist/core/embeddings/types.d.ts +87 -0
- package/dist/core/embeddings/types.js +32 -0
- package/dist/core/graph/graph.d.ts +2 -0
- package/dist/core/graph/graph.js +66 -0
- package/dist/core/graph/types.d.ts +66 -0
- package/dist/core/graph/types.js +1 -0
- package/dist/core/ingestion/ast-cache.d.ts +11 -0
- package/dist/core/ingestion/ast-cache.js +35 -0
- package/dist/core/ingestion/call-processor.d.ts +23 -0
- package/dist/core/ingestion/call-processor.js +793 -0
- package/dist/core/ingestion/call-routing.d.ts +68 -0
- package/dist/core/ingestion/call-routing.js +129 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +38 -0
- package/dist/core/ingestion/cluster-enricher.js +170 -0
- package/dist/core/ingestion/community-processor.d.ts +39 -0
- package/dist/core/ingestion/community-processor.js +312 -0
- package/dist/core/ingestion/constants.d.ts +16 -0
- package/dist/core/ingestion/constants.js +16 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +40 -0
- package/dist/core/ingestion/entry-point-scoring.js +353 -0
- package/dist/core/ingestion/export-detection.d.ts +18 -0
- package/dist/core/ingestion/export-detection.js +231 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +28 -0
- package/dist/core/ingestion/filesystem-walker.js +81 -0
- package/dist/core/ingestion/framework-detection.d.ts +54 -0
- package/dist/core/ingestion/framework-detection.js +411 -0
- package/dist/core/ingestion/heritage-processor.d.ts +28 -0
- package/dist/core/ingestion/heritage-processor.js +251 -0
- package/dist/core/ingestion/import-processor.d.ts +34 -0
- package/dist/core/ingestion/import-processor.js +398 -0
- package/dist/core/ingestion/language-config.d.ts +46 -0
- package/dist/core/ingestion/language-config.js +167 -0
- package/dist/core/ingestion/mro-processor.d.ts +45 -0
- package/dist/core/ingestion/mro-processor.js +369 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
- package/dist/core/ingestion/named-binding-extraction.js +363 -0
- package/dist/core/ingestion/parsing-processor.d.ts +19 -0
- package/dist/core/ingestion/parsing-processor.js +315 -0
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +401 -0
- package/dist/core/ingestion/process-processor.d.ts +51 -0
- package/dist/core/ingestion/process-processor.js +315 -0
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
- package/dist/core/ingestion/resolvers/csharp.js +109 -0
- package/dist/core/ingestion/resolvers/go.d.ts +19 -0
- package/dist/core/ingestion/resolvers/go.js +42 -0
- package/dist/core/ingestion/resolvers/index.d.ts +18 -0
- package/dist/core/ingestion/resolvers/index.js +13 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
- package/dist/core/ingestion/resolvers/jvm.js +87 -0
- package/dist/core/ingestion/resolvers/php.d.ts +15 -0
- package/dist/core/ingestion/resolvers/php.js +35 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
- package/dist/core/ingestion/resolvers/rust.js +73 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
- package/dist/core/ingestion/resolvers/standard.js +123 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
- package/dist/core/ingestion/resolvers/utils.js +122 -0
- package/dist/core/ingestion/structure-processor.d.ts +2 -0
- package/dist/core/ingestion/structure-processor.js +36 -0
- package/dist/core/ingestion/symbol-table.d.ts +63 -0
- package/dist/core/ingestion/symbol-table.js +85 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +15 -0
- package/dist/core/ingestion/tree-sitter-queries.js +888 -0
- package/dist/core/ingestion/type-env.d.ts +49 -0
- package/dist/core/ingestion/type-env.js +613 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/csharp.js +383 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/go.js +467 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
- package/dist/core/ingestion/type-extractors/index.js +31 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
- package/dist/core/ingestion/type-extractors/jvm.js +681 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/php.js +549 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/python.js +455 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/rust.js +456 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +145 -0
- package/dist/core/ingestion/type-extractors/shared.js +810 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/swift.js +137 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
- package/dist/core/ingestion/type-extractors/types.js +1 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/typescript.js +494 -0
- package/dist/core/ingestion/utils.d.ts +138 -0
- package/dist/core/ingestion/utils.js +1290 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +122 -0
- package/dist/core/ingestion/workers/parse-worker.js +1126 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +16 -0
- package/dist/core/ingestion/workers/worker-pool.js +128 -0
- package/dist/core/lbug/csv-generator.d.ts +33 -0
- package/dist/core/lbug/csv-generator.js +366 -0
- package/dist/core/lbug/lbug-adapter.d.ts +103 -0
- package/dist/core/lbug/lbug-adapter.js +769 -0
- package/dist/core/lbug/schema.d.ts +53 -0
- package/dist/core/lbug/schema.js +430 -0
- package/dist/core/search/bm25-index.d.ts +23 -0
- package/dist/core/search/bm25-index.js +96 -0
- package/dist/core/search/hybrid-search.d.ts +49 -0
- package/dist/core/search/hybrid-search.js +118 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +5 -0
- package/dist/core/tree-sitter/parser-loader.js +63 -0
- package/dist/core/wiki/generator.d.ts +120 -0
- package/dist/core/wiki/generator.js +939 -0
- package/dist/core/wiki/graph-queries.d.ts +80 -0
- package/dist/core/wiki/graph-queries.js +238 -0
- package/dist/core/wiki/html-viewer.d.ts +10 -0
- package/dist/core/wiki/html-viewer.js +297 -0
- package/dist/core/wiki/llm-client.d.ts +43 -0
- package/dist/core/wiki/llm-client.js +186 -0
- package/dist/core/wiki/prompts.d.ts +53 -0
- package/dist/core/wiki/prompts.js +174 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +3 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
- package/dist/mcp/compatible-stdio-transport.js +200 -0
- package/dist/mcp/core/embedder.d.ts +27 -0
- package/dist/mcp/core/embedder.js +108 -0
- package/dist/mcp/core/lbug-adapter.d.ts +57 -0
- package/dist/mcp/core/lbug-adapter.js +455 -0
- package/dist/mcp/local/local-backend.d.ts +181 -0
- package/dist/mcp/local/local-backend.js +1722 -0
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +411 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +296 -0
- package/dist/mcp/staleness.d.ts +15 -0
- package/dist/mcp/staleness.js +29 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +292 -0
- package/dist/server/api.d.ts +10 -0
- package/dist/server/api.js +344 -0
- package/dist/server/mcp-http.d.ts +13 -0
- package/dist/server/mcp-http.js +100 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +35 -0
- package/dist/storage/repo-manager.d.ts +138 -0
- package/dist/storage/repo-manager.js +299 -0
- package/dist/types/pipeline.d.ts +32 -0
- package/dist/types/pipeline.js +18 -0
- package/dist/unreal/bridge.d.ts +4 -0
- package/dist/unreal/bridge.js +113 -0
- package/dist/unreal/config.d.ts +6 -0
- package/dist/unreal/config.js +55 -0
- package/dist/unreal/types.d.ts +105 -0
- package/dist/unreal/types.js +1 -0
- package/hooks/claude/gitnexus-hook.cjs +238 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/package.json +100 -0
- package/scripts/ensure-cli-executable.cjs +21 -0
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/scripts/setup-unreal-gitnexus.ps1 +191 -0
- package/skills/gitnexus-cli.md +82 -0
- package/skills/gitnexus-debugging.md +89 -0
- package/skills/gitnexus-exploring.md +78 -0
- package/skills/gitnexus-guide.md +64 -0
- package/skills/gitnexus-impact-analysis.md +97 -0
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +121 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { extractSimpleTypeName, extractVarName, extractCalleeName, resolveIterableElementType, extractElementTypeFromString } from './shared.js';
|
|
2
|
+
const DECLARATION_NODE_TYPES = new Set([
|
|
3
|
+
'assignment_expression', // For constructor inference: $x = new User()
|
|
4
|
+
'property_declaration', // PHP 7.4+ typed properties: private UserRepo $repo;
|
|
5
|
+
'method_declaration', // PHPDoc @param on class methods
|
|
6
|
+
'function_definition', // PHPDoc @param on top-level functions
|
|
7
|
+
]);
|
|
8
|
+
/** Walk up the AST to find the enclosing class declaration. */
|
|
9
|
+
const findEnclosingClass = (node) => {
|
|
10
|
+
let current = node.parent;
|
|
11
|
+
while (current) {
|
|
12
|
+
if (current.type === 'class_declaration')
|
|
13
|
+
return current;
|
|
14
|
+
current = current.parent;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Resolve PHP self/static/parent to the actual class name.
|
|
20
|
+
* - self/static → enclosing class name
|
|
21
|
+
* - parent → superclass from base_clause
|
|
22
|
+
*/
|
|
23
|
+
const resolvePhpKeyword = (keyword, node) => {
|
|
24
|
+
if (keyword === 'self' || keyword === 'static') {
|
|
25
|
+
const cls = findEnclosingClass(node);
|
|
26
|
+
if (!cls)
|
|
27
|
+
return undefined;
|
|
28
|
+
const nameNode = cls.childForFieldName('name');
|
|
29
|
+
return nameNode?.text;
|
|
30
|
+
}
|
|
31
|
+
if (keyword === 'parent') {
|
|
32
|
+
const cls = findEnclosingClass(node);
|
|
33
|
+
if (!cls)
|
|
34
|
+
return undefined;
|
|
35
|
+
// base_clause contains the parent class name
|
|
36
|
+
for (let i = 0; i < cls.namedChildCount; i++) {
|
|
37
|
+
const child = cls.namedChild(i);
|
|
38
|
+
if (child?.type === 'base_clause') {
|
|
39
|
+
const parentName = child.firstNamedChild;
|
|
40
|
+
if (parentName)
|
|
41
|
+
return extractSimpleTypeName(parentName);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
const normalizePhpType = (raw) => {
|
|
49
|
+
// Strip nullable prefix: ?User → User
|
|
50
|
+
let type = raw.startsWith('?') ? raw.slice(1) : raw;
|
|
51
|
+
// Strip array suffix: User[] → User
|
|
52
|
+
type = type.replace(/\[\]$/, '');
|
|
53
|
+
// Strip union with null/false/void: User|null → User
|
|
54
|
+
const parts = type.split('|').filter(p => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
|
|
55
|
+
if (parts.length !== 1)
|
|
56
|
+
return undefined;
|
|
57
|
+
type = parts[0];
|
|
58
|
+
// Strip namespace: \App\Models\User → User
|
|
59
|
+
const segments = type.split('\\');
|
|
60
|
+
type = segments[segments.length - 1];
|
|
61
|
+
// Skip uninformative types
|
|
62
|
+
if (type === 'mixed' || type === 'void' || type === 'self' || type === 'static' || type === 'object')
|
|
63
|
+
return undefined;
|
|
64
|
+
// Extract element type from generic: Collection<User> → User
|
|
65
|
+
// PHPDoc generics encode the element type in angle brackets. Since PHP's Strategy B
|
|
66
|
+
// uses the scopeEnv value directly as the element type, we must store the inner type,
|
|
67
|
+
// not the container name. This mirrors how User[] → User is handled by the [] strip above.
|
|
68
|
+
const genericMatch = type.match(/^(\w+)\s*</);
|
|
69
|
+
if (genericMatch) {
|
|
70
|
+
const elementType = extractElementTypeFromString(type);
|
|
71
|
+
return elementType ?? undefined;
|
|
72
|
+
}
|
|
73
|
+
if (/^\w+$/.test(type))
|
|
74
|
+
return type;
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
77
|
+
/** Node types to skip when walking backwards to find doc-comments.
|
|
78
|
+
* PHP 8+ attributes (#[Route(...)]) appear as named siblings between PHPDoc and method. */
|
|
79
|
+
const SKIP_NODE_TYPES = new Set(['attribute_list', 'attribute']);
|
|
80
|
+
/** Regex to extract PHPDoc @param annotations: `@param Type $name` (standard order) */
|
|
81
|
+
const PHPDOC_PARAM_RE = /@param\s+(\S+)\s+\$(\w+)/g;
|
|
82
|
+
/** Alternate PHPDoc order: `@param $name Type` (name first) */
|
|
83
|
+
const PHPDOC_PARAM_ALT_RE = /@param\s+\$(\w+)\s+(\S+)/g;
|
|
84
|
+
/** Regex to extract PHPDoc @var annotations: `@var Type` */
|
|
85
|
+
const PHPDOC_VAR_RE = /@var\s+(\S+)/;
|
|
86
|
+
/**
|
|
87
|
+
* Extract the element type for a class property from its PHPDoc @var annotation or
|
|
88
|
+
* PHP 7.4+ native type. Walks backward from the property_declaration node to find
|
|
89
|
+
* an immediately preceding comment containing @var.
|
|
90
|
+
*
|
|
91
|
+
* Returns the normalized element type (e.g. User[] → User, Collection<User> → User).
|
|
92
|
+
* Returns undefined when no usable type annotation is found.
|
|
93
|
+
*/
|
|
94
|
+
const extractClassPropertyElementType = (propDecl) => {
|
|
95
|
+
// Strategy 1: PHPDoc @var annotation on a preceding comment sibling
|
|
96
|
+
let sibling = propDecl.previousSibling;
|
|
97
|
+
while (sibling) {
|
|
98
|
+
if (sibling.type === 'comment') {
|
|
99
|
+
const match = PHPDOC_VAR_RE.exec(sibling.text);
|
|
100
|
+
if (match)
|
|
101
|
+
return normalizePhpType(match[1]);
|
|
102
|
+
}
|
|
103
|
+
else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
sibling = sibling.previousSibling;
|
|
107
|
+
}
|
|
108
|
+
// Strategy 2: PHP 7.4+ native type field — skip generic 'array' since element type is unknown
|
|
109
|
+
const typeNode = propDecl.childForFieldName('type');
|
|
110
|
+
if (!typeNode)
|
|
111
|
+
return undefined;
|
|
112
|
+
const typeName = extractSimpleTypeName(typeNode);
|
|
113
|
+
if (!typeName || typeName === 'array')
|
|
114
|
+
return undefined;
|
|
115
|
+
return typeName;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Scan a class body for a property_declaration matching the given property name,
|
|
119
|
+
* and extract its element type. The class body is the `declaration_list` child of
|
|
120
|
+
* a `class_declaration` node.
|
|
121
|
+
*
|
|
122
|
+
* Used as Strategy C in extractForLoopBinding for `$this->property` iterables
|
|
123
|
+
* where Strategy A (resolveIterableElementType) and Strategy B (scopeEnv lookup)
|
|
124
|
+
* both fail to find the type.
|
|
125
|
+
*/
|
|
126
|
+
const findClassPropertyElementType = (propName, classNode) => {
|
|
127
|
+
const declList = classNode.childForFieldName('body')
|
|
128
|
+
?? (classNode.namedChild(classNode.namedChildCount - 1)?.type === 'declaration_list'
|
|
129
|
+
? classNode.namedChild(classNode.namedChildCount - 1)
|
|
130
|
+
: null); // fallback: last named child, only if it's a declaration_list
|
|
131
|
+
if (!declList)
|
|
132
|
+
return undefined;
|
|
133
|
+
for (let i = 0; i < declList.namedChildCount; i++) {
|
|
134
|
+
const child = declList.namedChild(i);
|
|
135
|
+
if (child?.type !== 'property_declaration')
|
|
136
|
+
continue;
|
|
137
|
+
// Check if any property_element has a variable_name matching '$propName'
|
|
138
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
139
|
+
const elem = child.namedChild(j);
|
|
140
|
+
if (elem?.type !== 'property_element')
|
|
141
|
+
continue;
|
|
142
|
+
const varNameNode = elem.firstNamedChild; // variable_name node
|
|
143
|
+
if (varNameNode?.text === '$' + propName) {
|
|
144
|
+
return extractClassPropertyElementType(child);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Collect PHPDoc @param type bindings from comment nodes preceding a method/function.
|
|
152
|
+
* Returns a map of paramName → typeName (without $ prefix).
|
|
153
|
+
*/
|
|
154
|
+
const collectPhpDocParams = (methodNode) => {
|
|
155
|
+
const commentTexts = [];
|
|
156
|
+
let sibling = methodNode.previousSibling;
|
|
157
|
+
while (sibling) {
|
|
158
|
+
if (sibling.type === 'comment') {
|
|
159
|
+
commentTexts.unshift(sibling.text);
|
|
160
|
+
}
|
|
161
|
+
else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type)) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
sibling = sibling.previousSibling;
|
|
165
|
+
}
|
|
166
|
+
if (commentTexts.length === 0)
|
|
167
|
+
return new Map();
|
|
168
|
+
const params = new Map();
|
|
169
|
+
const commentBlock = commentTexts.join('\n');
|
|
170
|
+
PHPDOC_PARAM_RE.lastIndex = 0;
|
|
171
|
+
let match;
|
|
172
|
+
while ((match = PHPDOC_PARAM_RE.exec(commentBlock)) !== null) {
|
|
173
|
+
const typeName = normalizePhpType(match[1]);
|
|
174
|
+
const paramName = match[2]; // without $ prefix
|
|
175
|
+
if (typeName) {
|
|
176
|
+
// Store with $ prefix to match how PHP variables appear in the env
|
|
177
|
+
params.set('$' + paramName, typeName);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Also check alternate PHPDoc order: @param $name Type
|
|
181
|
+
PHPDOC_PARAM_ALT_RE.lastIndex = 0;
|
|
182
|
+
while ((match = PHPDOC_PARAM_ALT_RE.exec(commentBlock)) !== null) {
|
|
183
|
+
const paramName = match[1];
|
|
184
|
+
if (params.has('$' + paramName))
|
|
185
|
+
continue; // standard format takes priority
|
|
186
|
+
const typeName = normalizePhpType(match[2]);
|
|
187
|
+
if (typeName) {
|
|
188
|
+
params.set('$' + paramName, typeName);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return params;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* PHP: typed class properties (PHP 7.4+): private UserRepo $repo;
|
|
195
|
+
* Also: PHPDoc @param annotations on method/function definitions.
|
|
196
|
+
*/
|
|
197
|
+
const extractDeclaration = (node, env) => {
|
|
198
|
+
// PHPDoc @param on methods/functions — pre-populate env with param types
|
|
199
|
+
if (node.type === 'method_declaration' || node.type === 'function_definition') {
|
|
200
|
+
const phpDocParams = collectPhpDocParams(node);
|
|
201
|
+
for (const [paramName, typeName] of phpDocParams) {
|
|
202
|
+
if (!env.has(paramName))
|
|
203
|
+
env.set(paramName, typeName);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (node.type !== 'property_declaration')
|
|
208
|
+
return;
|
|
209
|
+
const typeNode = node.childForFieldName('type');
|
|
210
|
+
if (!typeNode)
|
|
211
|
+
return;
|
|
212
|
+
const typeName = extractSimpleTypeName(typeNode);
|
|
213
|
+
if (!typeName)
|
|
214
|
+
return;
|
|
215
|
+
// The variable name is inside property_element > variable_name
|
|
216
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
217
|
+
const child = node.namedChild(i);
|
|
218
|
+
if (child?.type === 'property_element') {
|
|
219
|
+
const varNameNode = child.firstNamedChild; // variable_name
|
|
220
|
+
if (varNameNode) {
|
|
221
|
+
const varName = extractVarName(varNameNode);
|
|
222
|
+
if (varName)
|
|
223
|
+
env.set(varName, typeName);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
/** PHP: $x = new User() — infer type from object_creation_expression */
|
|
230
|
+
const extractInitializer = (node, env, _classNames) => {
|
|
231
|
+
if (node.type !== 'assignment_expression')
|
|
232
|
+
return;
|
|
233
|
+
const left = node.childForFieldName('left');
|
|
234
|
+
const right = node.childForFieldName('right');
|
|
235
|
+
if (!left || !right)
|
|
236
|
+
return;
|
|
237
|
+
if (right.type !== 'object_creation_expression')
|
|
238
|
+
return;
|
|
239
|
+
// The class name is the first named child of object_creation_expression
|
|
240
|
+
// (tree-sitter-php uses 'name' or 'qualified_name' nodes here)
|
|
241
|
+
const ctorType = right.firstNamedChild;
|
|
242
|
+
if (!ctorType)
|
|
243
|
+
return;
|
|
244
|
+
const typeName = extractSimpleTypeName(ctorType);
|
|
245
|
+
if (!typeName)
|
|
246
|
+
return;
|
|
247
|
+
// Resolve PHP self/static/parent to actual class names
|
|
248
|
+
const resolvedType = (typeName === 'self' || typeName === 'static' || typeName === 'parent')
|
|
249
|
+
? resolvePhpKeyword(typeName, node)
|
|
250
|
+
: typeName;
|
|
251
|
+
if (!resolvedType)
|
|
252
|
+
return;
|
|
253
|
+
const varName = extractVarName(left);
|
|
254
|
+
if (varName)
|
|
255
|
+
env.set(varName, resolvedType);
|
|
256
|
+
};
|
|
257
|
+
/** PHP: simple_parameter → type $name */
|
|
258
|
+
const extractParameter = (node, env) => {
|
|
259
|
+
let nameNode = null;
|
|
260
|
+
let typeNode = null;
|
|
261
|
+
if (node.type === 'simple_parameter') {
|
|
262
|
+
typeNode = node.childForFieldName('type');
|
|
263
|
+
nameNode = node.childForFieldName('name');
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
nameNode = node.childForFieldName('name') ?? node.childForFieldName('pattern');
|
|
267
|
+
typeNode = node.childForFieldName('type');
|
|
268
|
+
}
|
|
269
|
+
if (!nameNode || !typeNode)
|
|
270
|
+
return;
|
|
271
|
+
const varName = extractVarName(nameNode);
|
|
272
|
+
if (!varName)
|
|
273
|
+
return;
|
|
274
|
+
// Don't overwrite PHPDoc-derived types (e.g. @param User[] $users → User)
|
|
275
|
+
// with the less-specific AST type annotation (e.g. array).
|
|
276
|
+
if (env.has(varName))
|
|
277
|
+
return;
|
|
278
|
+
const typeName = extractSimpleTypeName(typeNode);
|
|
279
|
+
if (typeName)
|
|
280
|
+
env.set(varName, typeName);
|
|
281
|
+
};
|
|
282
|
+
/** PHP: $x = SomeFactory() or $x = $this->getUser() — bind variable to call return type */
|
|
283
|
+
const scanConstructorBinding = (node) => {
|
|
284
|
+
if (node.type !== 'assignment_expression')
|
|
285
|
+
return undefined;
|
|
286
|
+
const left = node.childForFieldName('left');
|
|
287
|
+
const right = node.childForFieldName('right');
|
|
288
|
+
if (!left || !right)
|
|
289
|
+
return undefined;
|
|
290
|
+
if (left.type !== 'variable_name')
|
|
291
|
+
return undefined;
|
|
292
|
+
// Skip object_creation_expression (new User()) — handled by extractInitializer
|
|
293
|
+
if (right.type === 'object_creation_expression')
|
|
294
|
+
return undefined;
|
|
295
|
+
// Handle both standalone function calls and method calls ($this->getUser())
|
|
296
|
+
if (right.type === 'function_call_expression') {
|
|
297
|
+
const calleeName = extractCalleeName(right);
|
|
298
|
+
if (!calleeName)
|
|
299
|
+
return undefined;
|
|
300
|
+
return { varName: left.text, calleeName };
|
|
301
|
+
}
|
|
302
|
+
if (right.type === 'member_call_expression') {
|
|
303
|
+
const methodName = right.childForFieldName('name');
|
|
304
|
+
if (!methodName)
|
|
305
|
+
return undefined;
|
|
306
|
+
// When receiver is $this/self/static, qualify with enclosing class for disambiguation
|
|
307
|
+
const receiver = right.childForFieldName('object');
|
|
308
|
+
const receiverText = receiver?.text;
|
|
309
|
+
let receiverClassName;
|
|
310
|
+
if (receiverText === '$this' || receiverText === 'self' || receiverText === 'static') {
|
|
311
|
+
const cls = findEnclosingClass(node);
|
|
312
|
+
const clsName = cls?.childForFieldName('name');
|
|
313
|
+
if (clsName)
|
|
314
|
+
receiverClassName = clsName.text;
|
|
315
|
+
}
|
|
316
|
+
return { varName: left.text, calleeName: methodName.text, receiverClassName };
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
};
|
|
320
|
+
/** Regex to extract PHPDoc @return annotations: `@return User` */
|
|
321
|
+
const PHPDOC_RETURN_RE = /@return\s+(\S+)/;
|
|
322
|
+
/**
|
|
323
|
+
* Normalize a PHPDoc return type for storage in the SymbolTable.
|
|
324
|
+
* Unlike normalizePhpType (which strips User[] → User for scopeEnv), this preserves
|
|
325
|
+
* array notation so lookupRawReturnType can extract element types for for-loop resolution.
|
|
326
|
+
* \App\Models\User[] → User[]
|
|
327
|
+
* ?User → User
|
|
328
|
+
* Collection<User> → Collection<User> (preserved for extractElementTypeFromString)
|
|
329
|
+
*/
|
|
330
|
+
const normalizePhpReturnType = (raw) => {
|
|
331
|
+
// Strip nullable prefix: ?User[] → User[]
|
|
332
|
+
let type = raw.startsWith('?') ? raw.slice(1) : raw;
|
|
333
|
+
// Strip union with null/false/void: User[]|null → User[]
|
|
334
|
+
const parts = type.split('|').filter(p => p !== 'null' && p !== 'false' && p !== 'void' && p !== 'mixed');
|
|
335
|
+
if (parts.length !== 1)
|
|
336
|
+
return undefined;
|
|
337
|
+
type = parts[0];
|
|
338
|
+
// Strip namespace: \App\Models\User[] → User[]
|
|
339
|
+
const segments = type.split('\\');
|
|
340
|
+
type = segments[segments.length - 1];
|
|
341
|
+
// Skip uninformative types
|
|
342
|
+
if (type === 'mixed' || type === 'void' || type === 'self' || type === 'static' || type === 'object' || type === 'array')
|
|
343
|
+
return undefined;
|
|
344
|
+
if (/^\w+(\[\])?$/.test(type) || /^\w+\s*</.test(type))
|
|
345
|
+
return type;
|
|
346
|
+
return undefined;
|
|
347
|
+
};
|
|
348
|
+
/**
|
|
349
|
+
* Extract return type from PHPDoc `@return Type` annotation preceding a method.
|
|
350
|
+
* Walks backwards through preceding siblings looking for comment nodes.
|
|
351
|
+
* Preserves array notation (e.g., User[]) for for-loop element type extraction.
|
|
352
|
+
*/
|
|
353
|
+
const extractReturnType = (node) => {
|
|
354
|
+
let sibling = node.previousSibling;
|
|
355
|
+
while (sibling) {
|
|
356
|
+
if (sibling.type === 'comment') {
|
|
357
|
+
const match = PHPDOC_RETURN_RE.exec(sibling.text);
|
|
358
|
+
if (match)
|
|
359
|
+
return normalizePhpReturnType(match[1]);
|
|
360
|
+
}
|
|
361
|
+
else if (sibling.isNamed && !SKIP_NODE_TYPES.has(sibling.type))
|
|
362
|
+
break;
|
|
363
|
+
sibling = sibling.previousSibling;
|
|
364
|
+
}
|
|
365
|
+
return undefined;
|
|
366
|
+
};
|
|
367
|
+
/** PHP: $alias = $user → assignment_expression with variable_name left/right.
|
|
368
|
+
* PHP TypeEnv stores variables WITH $ prefix ($user → User), so we keep $ in lhs/rhs. */
|
|
369
|
+
const extractPendingAssignment = (node, scopeEnv) => {
|
|
370
|
+
if (node.type !== 'assignment_expression')
|
|
371
|
+
return undefined;
|
|
372
|
+
const left = node.childForFieldName('left');
|
|
373
|
+
const right = node.childForFieldName('right');
|
|
374
|
+
if (!left || !right)
|
|
375
|
+
return undefined;
|
|
376
|
+
if (left.type !== 'variable_name' || right.type !== 'variable_name')
|
|
377
|
+
return undefined;
|
|
378
|
+
const lhs = left.text;
|
|
379
|
+
const rhs = right.text;
|
|
380
|
+
if (!lhs || !rhs || scopeEnv.has(lhs))
|
|
381
|
+
return undefined;
|
|
382
|
+
return { kind: 'copy', lhs, rhs };
|
|
383
|
+
};
|
|
384
|
+
const FOR_LOOP_NODE_TYPES = new Set([
|
|
385
|
+
'foreach_statement',
|
|
386
|
+
]);
|
|
387
|
+
/** Extract element type from a PHP type annotation AST node.
|
|
388
|
+
* PHP has limited AST-level container types — `array` is a primitive_type with no generic args.
|
|
389
|
+
* Named types (e.g., `Collection`) are returned as-is (container descriptor lookup handles them). */
|
|
390
|
+
const extractPhpElementTypeFromTypeNode = (_typeNode) => {
|
|
391
|
+
// PHP AST type nodes don't carry generic parameters (array<User> is PHPDoc-only).
|
|
392
|
+
// primitive_type 'array' and named_type 'Collection' don't encode element types.
|
|
393
|
+
return undefined;
|
|
394
|
+
};
|
|
395
|
+
/** Walk up from a foreach to the enclosing function and search parameter type annotations.
|
|
396
|
+
* PHP parameter type hints are limited (array, ClassName) — this extracts element type when possible. */
|
|
397
|
+
const findPhpParamElementType = (iterableName, startNode) => {
|
|
398
|
+
let current = startNode.parent;
|
|
399
|
+
while (current) {
|
|
400
|
+
if (current.type === 'method_declaration' || current.type === 'function_definition') {
|
|
401
|
+
const paramsNode = current.childForFieldName('parameters');
|
|
402
|
+
if (paramsNode) {
|
|
403
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
404
|
+
const param = paramsNode.namedChild(i);
|
|
405
|
+
if (!param || param.type !== 'simple_parameter')
|
|
406
|
+
continue;
|
|
407
|
+
const nameNode = param.childForFieldName('name');
|
|
408
|
+
if (nameNode?.text !== iterableName)
|
|
409
|
+
continue;
|
|
410
|
+
const typeNode = param.childForFieldName('type');
|
|
411
|
+
if (typeNode)
|
|
412
|
+
return extractPhpElementTypeFromTypeNode(typeNode);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
current = current.parent;
|
|
418
|
+
}
|
|
419
|
+
return undefined;
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* PHP: foreach ($users as $user) — extract loop variable binding.
|
|
423
|
+
*
|
|
424
|
+
* AST structure (from tree-sitter-php grammar):
|
|
425
|
+
* foreach_statement — no named fields for iterable/value (only 'body')
|
|
426
|
+
* children[0]: expression (iterable, e.g. $users)
|
|
427
|
+
* children[1]: expression (simple value) OR pair ($key => $value)
|
|
428
|
+
* pair children: expression (key), expression (value)
|
|
429
|
+
*
|
|
430
|
+
* PHP's PHPDoc @param normalizes `User[]` → `User` in the env, so the iterable's
|
|
431
|
+
* stored type IS the element type. We first try resolveIterableElementType (for
|
|
432
|
+
* constructor-binding cases that retain container types), then fall back to direct
|
|
433
|
+
* scopeEnv lookup (for PHPDoc-normalized types).
|
|
434
|
+
*/
|
|
435
|
+
const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
|
|
436
|
+
if (node.type !== 'foreach_statement')
|
|
437
|
+
return;
|
|
438
|
+
// Collect non-body named children: first is the iterable, second is value or pair
|
|
439
|
+
const children = [];
|
|
440
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
441
|
+
const child = node.namedChild(i);
|
|
442
|
+
if (child && child !== node.childForFieldName('body')) {
|
|
443
|
+
children.push(child);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (children.length < 2)
|
|
447
|
+
return;
|
|
448
|
+
const iterableNode = children[0];
|
|
449
|
+
const valueOrPair = children[1];
|
|
450
|
+
// Determine the loop variable node
|
|
451
|
+
let loopVarNode;
|
|
452
|
+
if (valueOrPair.type === 'pair') {
|
|
453
|
+
// $key => $value — the value is the last named child of the pair
|
|
454
|
+
const lastChild = valueOrPair.namedChild(valueOrPair.namedChildCount - 1);
|
|
455
|
+
if (!lastChild)
|
|
456
|
+
return;
|
|
457
|
+
// Handle by_ref: foreach ($arr as $k => &$v)
|
|
458
|
+
loopVarNode = lastChild.type === 'by_ref' ? (lastChild.firstNamedChild ?? lastChild) : lastChild;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// Simple: foreach ($users as $user) or foreach ($users as &$user)
|
|
462
|
+
loopVarNode = valueOrPair.type === 'by_ref' ? (valueOrPair.firstNamedChild ?? valueOrPair) : valueOrPair;
|
|
463
|
+
}
|
|
464
|
+
const varName = extractVarName(loopVarNode);
|
|
465
|
+
if (!varName)
|
|
466
|
+
return;
|
|
467
|
+
// Get iterable variable name (PHP vars include $ prefix)
|
|
468
|
+
let iterableName;
|
|
469
|
+
let callExprElementType;
|
|
470
|
+
if (iterableNode.type === 'variable_name') {
|
|
471
|
+
iterableName = iterableNode.text;
|
|
472
|
+
}
|
|
473
|
+
else if (iterableNode?.type === 'member_access_expression') {
|
|
474
|
+
const name = iterableNode.childForFieldName('name');
|
|
475
|
+
// PHP properties are stored in scopeEnv with $ prefix ($users), but
|
|
476
|
+
// member_access_expression.name returns without $ (users). Add $ to match.
|
|
477
|
+
if (name)
|
|
478
|
+
iterableName = '$' + name.text;
|
|
479
|
+
}
|
|
480
|
+
else if (iterableNode?.type === 'function_call_expression') {
|
|
481
|
+
// foreach (getUsers() as $user) — resolve via return type lookup
|
|
482
|
+
const calleeName = extractCalleeName(iterableNode);
|
|
483
|
+
if (calleeName) {
|
|
484
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(calleeName);
|
|
485
|
+
if (rawReturn)
|
|
486
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else if (iterableNode?.type === 'member_call_expression') {
|
|
490
|
+
// foreach ($this->getUsers() as $user) — resolve via return type lookup
|
|
491
|
+
const methodName = iterableNode.childForFieldName('name');
|
|
492
|
+
if (methodName) {
|
|
493
|
+
const rawReturn = returnTypeLookup.lookupRawReturnType(methodName.text);
|
|
494
|
+
if (rawReturn)
|
|
495
|
+
callExprElementType = extractElementTypeFromString(rawReturn);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!iterableName && !callExprElementType)
|
|
499
|
+
return;
|
|
500
|
+
// If we resolved the element type from a call expression, bind and return early
|
|
501
|
+
if (callExprElementType) {
|
|
502
|
+
scopeEnv.set(varName, callExprElementType);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// Strategy A: try resolveIterableElementType (handles constructor-binding container types)
|
|
506
|
+
const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractPhpElementTypeFromTypeNode, findPhpParamElementType, undefined);
|
|
507
|
+
if (elementType) {
|
|
508
|
+
scopeEnv.set(varName, elementType);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// Strategy B: direct scopeEnv lookup — PHP normalizePhpType strips User[] → User,
|
|
512
|
+
// so the iterable's stored type is already the element type from PHPDoc annotations.
|
|
513
|
+
const iterableType = scopeEnv.get(iterableName);
|
|
514
|
+
if (iterableType) {
|
|
515
|
+
scopeEnv.set(varName, iterableType);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Strategy C: $this->property — scan the enclosing class body for the property
|
|
519
|
+
// declaration and extract its element type from @var PHPDoc or native type.
|
|
520
|
+
// This handles the common PHP pattern where the property type is declared on the
|
|
521
|
+
// class body (/** @var User[] */ private $users) but the foreach is in a method
|
|
522
|
+
// whose scopeEnv does not contain the property type.
|
|
523
|
+
if (iterableNode?.type === 'member_access_expression') {
|
|
524
|
+
const obj = iterableNode.childForFieldName('object');
|
|
525
|
+
if (obj?.text === '$this') {
|
|
526
|
+
const nameNode = iterableNode.childForFieldName('name');
|
|
527
|
+
const propName = nameNode?.text;
|
|
528
|
+
if (propName) {
|
|
529
|
+
const classNode = findEnclosingClass(iterableNode);
|
|
530
|
+
if (classNode) {
|
|
531
|
+
const elementType = findClassPropertyElementType(propName, classNode);
|
|
532
|
+
if (elementType)
|
|
533
|
+
scopeEnv.set(varName, elementType);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
export const typeConfig = {
|
|
540
|
+
declarationNodeTypes: DECLARATION_NODE_TYPES,
|
|
541
|
+
forLoopNodeTypes: FOR_LOOP_NODE_TYPES,
|
|
542
|
+
extractDeclaration,
|
|
543
|
+
extractParameter,
|
|
544
|
+
extractInitializer,
|
|
545
|
+
scanConstructorBinding,
|
|
546
|
+
extractReturnType,
|
|
547
|
+
extractForLoopBinding,
|
|
548
|
+
extractPendingAssignment,
|
|
549
|
+
};
|