forked from colbymchenry/codegraph
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathphp.ts
More file actions
189 lines (178 loc) · 8.42 KB
/
php.ts
File metadata and controls
189 lines (178 loc) · 8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';
// include / require (+ _once) expression node types. These carry the
// file→file dependency in procedural PHP, where `include`/`require` — not
// namespace `use` — is how a file pulls in another (issue #660).
const PHP_INCLUDE_TYPES = new Set([
'include_expression',
'include_once_expression',
'require_expression',
'require_once_expression',
]);
/**
* Extract a static string-literal path from a PHP include/require expression.
*
* Returns null for dynamic forms (`include $var`, `require __DIR__ . '/x'`,
* interpolated strings) — they have no resolvable compile-time path, which
* matches the issue's "static string literals (the common case)" scope.
*/
function phpStaticIncludePath(node: SyntaxNode, source: string): string | null {
// The path argument is the expression's first named child; the call-style
// form `require("x")` wraps it in a parenthesized_expression.
let arg: SyntaxNode | null = node.namedChild(0);
if (arg?.type === 'parenthesized_expression') arg = arg.namedChild(0);
if (!arg || (arg.type !== 'string' && arg.type !== 'encapsed_string')) return null;
// Pure literal only: any non-`string_content` child (interpolated variable,
// escape sequence, …) means the value isn't a static path.
const parts = arg.namedChildren;
if (parts.some((c: SyntaxNode) => c.type !== 'string_content')) return null;
const content = parts.find((c: SyntaxNode) => c.type === 'string_content');
return content ? getNodeText(content, source) : null;
}
/** PHP built-in return types that can't be a method receiver (so no class to chain on). */
const PHP_NON_CLASS_RETURN = new Set([
'array', 'string', 'int', 'integer', 'float', 'double', 'bool', 'boolean',
'void', 'mixed', 'never', 'null', 'false', 'true', 'object', 'callable',
'iterable', 'resource',
]);
/**
* A method/function's declared return type, normalized to the class a chained
* `->method()` could be called on (issue #608). `self` / `static` / `$this` are
* kept as the marker `self` and resolved to the declaring class at resolution
* time; a concrete type returns its short name; primitives / unions / nullable
* non-class types return undefined.
*/
function extractPhpReturnType(node: SyntaxNode, source: string): string | undefined {
let rt = getChildByField(node, 'return_type');
if (!rt) return undefined;
// Unwrap `?Type`. Union / intersection types are ambiguous — skip them.
if (rt.type === 'optional_type') rt = rt.namedChild(0) ?? rt;
if (!rt || rt.type === 'primitive_type') return undefined;
const nameNode = rt.type === 'named_type' ? (rt.namedChild(0) ?? rt) : rt;
const text = getNodeText(nameNode, source).trim().replace(/^\\/, '');
if (!text) return undefined;
const last = text.split('\\').pop() ?? text;
const lc = last.toLowerCase();
if (lc === 'self' || lc === 'static' || lc === 'this' || lc === '$this') return 'self';
if (PHP_NON_CLASS_RETURN.has(lc)) return undefined;
if (!/^[A-Za-z_]\w*$/.test(last)) return undefined; // union/intersection/complex
return last;
}
export const phpExtractor: LanguageExtractor = {
functionTypes: ['function_definition'],
classTypes: ['class_declaration', 'trait_declaration'],
methodTypes: ['method_declaration'],
interfaceTypes: ['interface_declaration'],
structTypes: [],
enumTypes: ['enum_declaration'],
enumMemberTypes: ['enum_case'],
typeAliasTypes: [],
importTypes: ['namespace_use_declaration', ...PHP_INCLUDE_TYPES],
callTypes: ['function_call_expression', 'member_call_expression', 'scoped_call_expression'],
variableTypes: ['const_declaration'],
fieldTypes: ['property_declaration'],
nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
returnField: 'return_type',
getReturnType: extractPhpReturnType,
classifyClassNode: (node) => {
return node.type === 'trait_declaration' ? 'trait' : 'class';
},
getVisibility: (node) => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'visibility_modifier') {
const text = child.text;
if (text === 'public') return 'public';
if (text === 'private') return 'private';
if (text === 'protected') return 'protected';
}
}
return 'public'; // PHP defaults to public
},
isStatic: (node) => {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child?.type === 'static_modifier') return true;
}
return false;
},
visitNode: (node, ctx) => {
// Handle class constants: const_declaration inside classes
// These are skipped by the main visitor because variableTypes check excludes class-like contexts
if (node.type === 'const_declaration') {
const constElements = node.namedChildren.filter((c: SyntaxNode) => c.type === 'const_element');
for (const elem of constElements) {
const nameNode = elem.namedChildren.find((c: SyntaxNode) => c.type === 'name');
if (!nameNode) continue;
const name = getNodeText(nameNode, ctx.source);
ctx.createNode('constant', name, elem, {});
}
return true; // handled
}
// Handle trait usage: use TraitName, OtherTrait; inside classes
// Creates unresolved references that will be resolved to 'implements' edges
if (node.type === 'use_declaration') {
const names = node.namedChildren.filter((c: SyntaxNode) => c.type === 'name' || c.type === 'qualified_name');
const parentId = ctx.nodeStack.length > 0 ? ctx.nodeStack[ctx.nodeStack.length - 1] : undefined;
if (parentId) {
for (const nameNode of names) {
const traitName = getNodeText(nameNode, ctx.source);
ctx.addUnresolvedReference({
fromNodeId: parentId,
referenceName: traitName,
referenceKind: 'implements',
filePath: ctx.filePath,
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
}
}
return true; // handled
}
return false;
},
// PHP `namespace Foo\Bar;` is file-level (like a Java/Kotlin package). Capturing
// it scopes every class under an `Foo\Bar::` qualified name, which is what makes
// `use` imports and same-named types (Laravel has 7+ `Factory` interfaces across
// namespaces) resolvable to the RIGHT definition instead of an arbitrary match.
packageTypes: ['namespace_definition'],
extractPackage: (node, source) => {
const nsName = node.namedChildren.find((c: SyntaxNode) => c.type === 'namespace_name');
// Skip braced `namespace Foo { … }` (has a body) — file-level only.
const hasBody = node.namedChildren.some((c: SyntaxNode) => c.type === 'compound_statement' || c.type === 'declaration_list');
if (!nsName || hasBody) return null;
return getNodeText(nsName, source);
},
extractImport: (node, source) => {
const importText = source.substring(node.startIndex, node.endIndex).trim();
// include / require (+ _once): emit a file→file dependency. The path is a
// static string literal in the common case; dynamic forms resolve to null
// and are skipped (no import node, no edge).
if (PHP_INCLUDE_TYPES.has(node.type)) {
const includePath = phpStaticIncludePath(node, source);
return includePath ? { moduleName: includePath, signature: importText } : null;
}
// Check for grouped imports: use X\{A, B} - return null for core fallback
const namespacePrefix = node.namedChildren.find((c: SyntaxNode) => c.type === 'namespace_name');
const useGroup = node.namedChildren.find((c: SyntaxNode) => c.type === 'namespace_use_group');
if (namespacePrefix && useGroup) {
return null; // Grouped imports create multiple nodes - let core handle
}
// Single import - find namespace_use_clause
const useClause = node.namedChildren.find((c: SyntaxNode) => c.type === 'namespace_use_clause');
if (useClause) {
const qualifiedName = useClause.namedChildren.find((c: SyntaxNode) => c.type === 'qualified_name');
if (qualifiedName) {
return { moduleName: getNodeText(qualifiedName, source), signature: importText };
}
const name = useClause.namedChildren.find((c: SyntaxNode) => c.type === 'name');
if (name) {
return { moduleName: getNodeText(name, source), signature: importText };
}
}
return null;
},
};