septum 0.1.5 → 0.1.7
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 +23 -3
- package/package.json +5 -5
- package/src/cli/commands/check.ts +32 -122
- package/src/cli/commands/hook.ts +45 -12
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/query.ts +45 -5
- package/src/cli/commands/slice.ts +4 -2
- package/src/cli/index.ts +17 -6
- package/src/cli/templates/hooks.ts +6 -6
- package/src/cli/templates/rules.ts +35 -23
- package/src/cli/templates/skill.ts +47 -51
- package/src/core/boundary/evaluator.ts +217 -24
- package/src/core/boundary/staged-auditor.ts +47 -0
- package/src/core/config/loader.ts +29 -12
- package/src/core/database/repositories/dependency.repository.ts +252 -66
- package/src/core/database/repositories/domain.repository.ts +96 -39
- package/src/core/database/repositories/file.repository.ts +20 -19
- package/src/core/database/repositories/symbol.repository.ts +115 -5
- package/src/core/database/repository.ts +19 -2
- package/src/core/database/schema.ts +3 -2
- package/src/core/ingestion/pipeline.ts +96 -60
- package/src/core/parser/boundary-tracker.ts +145 -0
- package/src/core/parser/extractors/frontend.ts +44 -90
- package/src/core/parser/extractors/go.ts +200 -21
- package/src/core/parser/extractors/laravel-semantic.ts +18 -12
- package/src/core/parser/extractors/mcp-cli-semantic.ts +173 -44
- package/src/core/parser/extractors/nestjs-semantic.ts +20 -5
- package/src/core/parser/extractors/php.ts +293 -31
- package/src/core/parser/extractors/python.ts +242 -10
- package/src/core/parser/extractors/typescript.ts +604 -117
- package/src/core/parser/tree-sitter.ts +6 -1
- package/src/core/parser/wasm-assets.ts +17 -0
- package/src/core/parser/wasm-loader.ts +274 -0
- package/src/core/resolver/call-graph-tracer.ts +49 -17
- package/src/core/resolver/module-resolver.ts +88 -26
- package/src/core/resolver/vertical-slice-tracer.ts +34 -11
- package/src/core/session/session-manager.ts +55 -39
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/mcp/schemas.ts +146 -29
- package/src/mcp/server.ts +177 -114
- package/src/mcp/tools/check-boundary.ts +159 -41
- package/src/mcp/tools/get-domain-catalog.ts +30 -15
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +35 -20
- package/src/mcp/tools/get-symbol-hotspots.ts +4 -10
- package/src/mcp/tools/locate-symbol.ts +37 -16
- package/src/mcp/tools/register-domain.ts +3 -12
- package/src/mcp/tools/register-feature.ts +25 -32
- package/src/mcp/tools/trace-vertical-slice.ts +8 -8
- package/src/types/index.ts +56 -6
- package/src/types/wasm.d.ts +4 -0
|
@@ -1,66 +1,122 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import type Parser from "web-tree-sitter";
|
|
2
|
+
import type {
|
|
3
|
+
ExtractedDependency,
|
|
4
|
+
ExtractedSymbol,
|
|
5
|
+
ParsedFileAST,
|
|
6
|
+
SymbolKind,
|
|
7
|
+
Visibility,
|
|
8
|
+
} from "../../../types/index.ts";
|
|
9
|
+
import {
|
|
10
|
+
blankCommentsAndStrings,
|
|
11
|
+
calculateControlFlowNesting,
|
|
12
|
+
findBraceBlockEnd,
|
|
13
|
+
} from "../boundary-tracker.ts";
|
|
14
|
+
import { calculateAstControlFlowNesting, parseWithTreeSitter } from "../wasm-loader.ts";
|
|
3
15
|
import type { CodeExtractor } from "./base.ts";
|
|
4
16
|
|
|
17
|
+
const PHP_CONTROL_FLOW_NODES = new Set([
|
|
18
|
+
"if_statement",
|
|
19
|
+
"for_statement",
|
|
20
|
+
"foreach_statement",
|
|
21
|
+
"while_statement",
|
|
22
|
+
"do_statement",
|
|
23
|
+
"switch_statement",
|
|
24
|
+
"try_statement",
|
|
25
|
+
"catch_clause",
|
|
26
|
+
"match_expression",
|
|
27
|
+
"conditional_expression",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const PHP_FUNCTION_BOUNDARY_NODES = new Set([
|
|
31
|
+
"function_definition",
|
|
32
|
+
"method_declaration",
|
|
33
|
+
"anonymous_function_creation_expression",
|
|
34
|
+
"arrow_function",
|
|
35
|
+
]);
|
|
36
|
+
|
|
5
37
|
export class PHPExtractor implements CodeExtractor {
|
|
6
38
|
public canHandle(filePath: string): boolean {
|
|
7
39
|
return /\.php$/i.test(filePath);
|
|
8
40
|
}
|
|
9
41
|
|
|
10
|
-
public async extract(
|
|
42
|
+
public async extract(_filePath: string, content: string): Promise<ParsedFileAST> {
|
|
43
|
+
const astResult = await parseWithTreeSitter("php", content, (rootNode) =>
|
|
44
|
+
this.extractFromAst(rootNode, content)
|
|
45
|
+
);
|
|
46
|
+
if (astResult) {
|
|
47
|
+
return { ...astResult, status: "parsed" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { ...this.extractFallback(content), status: "degraded" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private isExternalNamespace(cleaned: string): boolean {
|
|
54
|
+
return (
|
|
55
|
+
cleaned.startsWith("Illuminate\\") ||
|
|
56
|
+
cleaned.startsWith("Symfony\\") ||
|
|
57
|
+
cleaned.startsWith("Psr\\") ||
|
|
58
|
+
cleaned.startsWith("Composer\\")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private extractFallback(content: string): ParsedFileAST {
|
|
11
63
|
const symbols: ExtractedSymbol[] = [];
|
|
12
64
|
const dependencies: ExtractedDependency[] = [];
|
|
13
65
|
|
|
14
66
|
const lines = content.split("\n");
|
|
67
|
+
const cleanLines = blankCommentsAndStrings(content, { hashComments: true }).split("\n");
|
|
15
68
|
|
|
16
69
|
// 1. Extract PHP use / import statements
|
|
17
70
|
const useRegex = /^\s*use\s+([^;]+);/;
|
|
18
71
|
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
if (!cleanLines[i]?.trim()) continue;
|
|
19
73
|
const line = lines[i];
|
|
20
|
-
const match = useRegex.exec(
|
|
74
|
+
const match = useRegex.exec(cleanLines[i]);
|
|
21
75
|
if (match) {
|
|
22
76
|
const fullTarget = match[1].trim();
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
cleaned.startsWith("Symfony\\") ||
|
|
28
|
-
cleaned.startsWith("Psr\\") ||
|
|
29
|
-
cleaned.startsWith("Composer\\");
|
|
77
|
+
const cleaned = fullTarget.replace(/^(function|const)\s+/, "").replace(/\s+as\s+[A-Za-z0-9_]+$/i, "").trim();
|
|
78
|
+
const aliasMatch = /\bas\s+([A-Za-z0-9_]+)$/i.exec(fullTarget);
|
|
79
|
+
const shortName = cleaned.split("\\").pop() || cleaned;
|
|
80
|
+
const importedSymbols = Array.from(new Set([shortName, ...(aliasMatch ? [aliasMatch[1]] : [])])).filter(Boolean);
|
|
30
81
|
|
|
31
82
|
dependencies.push({
|
|
32
83
|
target: cleaned,
|
|
33
84
|
statement: line.trim(),
|
|
34
85
|
line_number: i + 1,
|
|
35
|
-
is_external:
|
|
86
|
+
is_external: this.isExternalNamespace(cleaned),
|
|
87
|
+
...(importedSymbols.length > 0 ? { imported_symbols: importedSymbols } : {}),
|
|
36
88
|
});
|
|
37
89
|
}
|
|
38
90
|
}
|
|
39
91
|
|
|
92
|
+
|
|
40
93
|
// 2. Extract PHP Class / Interface / Trait / Enum
|
|
41
94
|
let currentContainer: string | null = null;
|
|
42
95
|
let currentContainerEndLine = 0;
|
|
43
96
|
|
|
44
|
-
for (let i = 0; i <
|
|
45
|
-
const
|
|
46
|
-
const
|
|
97
|
+
for (let i = 0; i < cleanLines.length; i++) {
|
|
98
|
+
const trimmed = cleanLines[i].trim();
|
|
99
|
+
const rawTrimmed = lines[i].trim();
|
|
47
100
|
const lineNum = i + 1;
|
|
48
101
|
|
|
102
|
+
if (!trimmed) continue;
|
|
103
|
+
|
|
49
104
|
if (currentContainer && lineNum > currentContainerEndLine) {
|
|
50
105
|
currentContainer = null;
|
|
51
106
|
currentContainerEndLine = 0;
|
|
52
107
|
}
|
|
53
108
|
|
|
54
|
-
|
|
55
|
-
|
|
109
|
+
const classMatch = /^(?:abstract\s+|final\s+|readonly\s+)*\b(class|interface|trait|enum)\s+([A-Za-z0-9_]+)\b/.exec(
|
|
110
|
+
trimmed
|
|
111
|
+
);
|
|
56
112
|
if (classMatch) {
|
|
57
113
|
currentContainer = classMatch[2];
|
|
58
|
-
const endLine = findBraceBlockEnd(
|
|
114
|
+
const endLine = findBraceBlockEnd(cleanLines, i);
|
|
59
115
|
currentContainerEndLine = endLine;
|
|
60
116
|
symbols.push({
|
|
61
117
|
name: classMatch[2],
|
|
62
|
-
kind: classMatch[1] as
|
|
63
|
-
signature:
|
|
118
|
+
kind: classMatch[1] as SymbolKind,
|
|
119
|
+
signature: rawTrimmed.replace(/\{$/, "").trim(),
|
|
64
120
|
visibility: "public",
|
|
65
121
|
line_start: lineNum,
|
|
66
122
|
line_end: endLine,
|
|
@@ -69,10 +125,8 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
69
125
|
continue;
|
|
70
126
|
}
|
|
71
127
|
|
|
72
|
-
// Methods or Properties in class/interface/trait/enum
|
|
73
128
|
if (currentContainer) {
|
|
74
|
-
|
|
75
|
-
const constMatch = /^(?:(public|protected|private)\s+)?const\s+([A-Za-z0-9_]+)\s*=/.exec(trimmed);
|
|
129
|
+
const constMatch = /^(?:(public|protected|private)\s+)?\bconst\s+([A-Za-z0-9_]+)\s*=/.exec(trimmed);
|
|
76
130
|
if (constMatch) {
|
|
77
131
|
const vis = (constMatch[1] as Visibility) || "public";
|
|
78
132
|
const constName = constMatch[2];
|
|
@@ -88,15 +142,17 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
88
142
|
continue;
|
|
89
143
|
}
|
|
90
144
|
|
|
91
|
-
|
|
92
|
-
|
|
145
|
+
const propMatch =
|
|
146
|
+
/^(?:(public|protected|private)\s+)?(?:readonly\s+)?(?:static\s+)?(?:\??[A-Za-z0-9_\\]+\s+)?\$([A-Za-z0-9_]+)(?:\s*=[^;]+)?;\s*$/.exec(
|
|
147
|
+
trimmed
|
|
148
|
+
);
|
|
93
149
|
if (propMatch && !trimmed.includes("function ")) {
|
|
94
150
|
const vis = (propMatch[1] as Visibility) || "public";
|
|
95
151
|
const propName = `$${propMatch[2]}`;
|
|
96
152
|
symbols.push({
|
|
97
153
|
name: `${currentContainer}::${propName}`,
|
|
98
154
|
kind: "property",
|
|
99
|
-
signature: `${vis} ${
|
|
155
|
+
signature: `${vis} ${rawTrimmed.replace(/;\s*$/, "")}`,
|
|
100
156
|
visibility: vis,
|
|
101
157
|
line_start: lineNum,
|
|
102
158
|
line_end: lineNum,
|
|
@@ -105,13 +161,16 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
105
161
|
continue;
|
|
106
162
|
}
|
|
107
163
|
|
|
108
|
-
const methodMatch =
|
|
164
|
+
const methodMatch =
|
|
165
|
+
/^(?:(public|protected|private)\s+)?(?:static\s+)?(?:final\s+)?\bfunction\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(
|
|
166
|
+
trimmed
|
|
167
|
+
);
|
|
109
168
|
if (methodMatch) {
|
|
110
169
|
const vis = (methodMatch[1] as Visibility) || "public";
|
|
111
170
|
const name = methodMatch[2];
|
|
112
171
|
const params = methodMatch[3].trim();
|
|
113
172
|
const retType = methodMatch[4] ? `: ${methodMatch[4].trim()}` : "";
|
|
114
|
-
const endLine = findBraceBlockEnd(
|
|
173
|
+
const endLine = findBraceBlockEnd(cleanLines, i);
|
|
115
174
|
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
116
175
|
symbols.push({
|
|
117
176
|
name: `${currentContainer}::${name}`,
|
|
@@ -127,13 +186,12 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
127
186
|
}
|
|
128
187
|
}
|
|
129
188
|
|
|
130
|
-
|
|
131
|
-
const funcMatch = /^(?:final\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
|
|
189
|
+
const funcMatch = /^(?:final\s+)?\bfunction\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([^;{]+))?/.exec(trimmed);
|
|
132
190
|
if (funcMatch && !currentContainer) {
|
|
133
191
|
const name = funcMatch[1];
|
|
134
192
|
const params = funcMatch[2].trim();
|
|
135
193
|
const retType = funcMatch[3] ? `: ${funcMatch[3].trim()}` : "";
|
|
136
|
-
const endLine = findBraceBlockEnd(
|
|
194
|
+
const endLine = findBraceBlockEnd(cleanLines, i);
|
|
137
195
|
const nestingDepth = calculateControlFlowNesting(lines, i, endLine - 1);
|
|
138
196
|
symbols.push({
|
|
139
197
|
name,
|
|
@@ -150,4 +208,208 @@ export class PHPExtractor implements CodeExtractor {
|
|
|
150
208
|
|
|
151
209
|
return { symbols, dependencies };
|
|
152
210
|
}
|
|
211
|
+
|
|
212
|
+
private extractFromAst(rootNode: Parser.SyntaxNode, content: string): ParsedFileAST {
|
|
213
|
+
const symbols: ExtractedSymbol[] = [];
|
|
214
|
+
const dependencies: ExtractedDependency[] = [];
|
|
215
|
+
const lines = content.split("\n");
|
|
216
|
+
|
|
217
|
+
const walk = (node: Parser.SyntaxNode): void => {
|
|
218
|
+
if (node.type === "namespace_use_declaration") {
|
|
219
|
+
const rawStmt = (lines[node.startPosition.row] ?? node.text).trim();
|
|
220
|
+
const bodyMatch = /^\s*use\s+([^;]+);/i.exec(node.text.replace(/\r?\n/g, " "));
|
|
221
|
+
if (bodyMatch) {
|
|
222
|
+
const fullTarget = bodyMatch[1].trim();
|
|
223
|
+
const cleaned = fullTarget.replace(/^(function|const)\s+/i, "").replace(/\s+as\s+[A-Za-z0-9_]+$/i, "").trim();
|
|
224
|
+
const aliasMatch = /\bas\s+([A-Za-z0-9_]+)$/i.exec(fullTarget);
|
|
225
|
+
const shortName = cleaned.split("\\").pop() || cleaned;
|
|
226
|
+
const importedSymbols = Array.from(
|
|
227
|
+
new Set([shortName, ...(aliasMatch ? [aliasMatch[1]] : [])])
|
|
228
|
+
).filter(Boolean);
|
|
229
|
+
|
|
230
|
+
dependencies.push({
|
|
231
|
+
target: cleaned,
|
|
232
|
+
statement: rawStmt,
|
|
233
|
+
line_number: node.startPosition.row + 1,
|
|
234
|
+
is_external: this.isExternalNamespace(cleaned),
|
|
235
|
+
...(importedSymbols.length > 0 ? { imported_symbols: importedSymbols } : {}),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (
|
|
242
|
+
node.type === "class_declaration" ||
|
|
243
|
+
node.type === "interface_declaration" ||
|
|
244
|
+
node.type === "trait_declaration" ||
|
|
245
|
+
node.type === "enum_declaration"
|
|
246
|
+
) {
|
|
247
|
+
const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "name");
|
|
248
|
+
if (!nameNode) return;
|
|
249
|
+
|
|
250
|
+
const containerName = nameNode.text;
|
|
251
|
+
const kindMap: Record<string, SymbolKind> = {
|
|
252
|
+
class_declaration: "class",
|
|
253
|
+
interface_declaration: "interface",
|
|
254
|
+
trait_declaration: "trait",
|
|
255
|
+
enum_declaration: "enum",
|
|
256
|
+
};
|
|
257
|
+
const lineStart = node.startPosition.row + 1;
|
|
258
|
+
const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
|
|
259
|
+
const headerLine = (lines[node.startPosition.row] ?? "").trim().replace(/\{$/, "").trim();
|
|
260
|
+
|
|
261
|
+
symbols.push({
|
|
262
|
+
name: containerName,
|
|
263
|
+
kind: kindMap[node.type] ?? "class",
|
|
264
|
+
signature: headerLine || `${kindMap[node.type]} ${containerName}`,
|
|
265
|
+
visibility: "public",
|
|
266
|
+
line_start: lineStart,
|
|
267
|
+
line_end: lineEnd,
|
|
268
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const bodyNode =
|
|
272
|
+
node.childForFieldName("body") ?? node.namedChildren.find((c) => c.type === "declaration_list" || c.type === "enum_declaration_list");
|
|
273
|
+
if (bodyNode) {
|
|
274
|
+
this.extractPhpContainerMembers(containerName, bodyNode, lines, symbols);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (node.type === "function_definition") {
|
|
280
|
+
const nameNode = node.childForFieldName("name") ?? node.namedChildren.find((c) => c.type === "name");
|
|
281
|
+
if (!nameNode) return;
|
|
282
|
+
|
|
283
|
+
const fnName = nameNode.text;
|
|
284
|
+
const paramsNode = node.childForFieldName("parameters") ?? node.namedChildren.find((c) => c.type === "formal_parameters");
|
|
285
|
+
const retNode = node.childForFieldName("return_type");
|
|
286
|
+
const paramsText = paramsNode ? paramsNode.text : "()";
|
|
287
|
+
const retText = retNode ? `: ${retNode.text.replace(/^:\s*/, "")}` : "";
|
|
288
|
+
const lineStart = node.startPosition.row + 1;
|
|
289
|
+
const lineEnd = Math.max(lineStart, node.endPosition.row + 1);
|
|
290
|
+
const nestingDepth = calculateAstControlFlowNesting(
|
|
291
|
+
node,
|
|
292
|
+
PHP_CONTROL_FLOW_NODES,
|
|
293
|
+
PHP_FUNCTION_BOUNDARY_NODES
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
symbols.push({
|
|
297
|
+
name: fnName,
|
|
298
|
+
kind: "function",
|
|
299
|
+
signature: `function ${fnName}${paramsText}${retText}`,
|
|
300
|
+
visibility: "public",
|
|
301
|
+
line_start: lineStart,
|
|
302
|
+
line_end: lineEnd,
|
|
303
|
+
line_count: Math.max(1, lineEnd - lineStart + 1),
|
|
304
|
+
nesting_depth: nestingDepth,
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const child of node.namedChildren) {
|
|
310
|
+
walk(child);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
walk(rootNode);
|
|
315
|
+
return { symbols, dependencies };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private extractPhpContainerMembers(
|
|
319
|
+
containerName: string,
|
|
320
|
+
bodyNode: Parser.SyntaxNode,
|
|
321
|
+
lines: string[],
|
|
322
|
+
symbols: ExtractedSymbol[]
|
|
323
|
+
): void {
|
|
324
|
+
for (const member of bodyNode.namedChildren) {
|
|
325
|
+
const mStart = member.startPosition.row + 1;
|
|
326
|
+
const mEnd = Math.max(mStart, member.endPosition.row + 1);
|
|
327
|
+
const mCount = Math.max(1, mEnd - mStart + 1);
|
|
328
|
+
const vis = this.extractPhpVisibility(member);
|
|
329
|
+
|
|
330
|
+
if (member.type === "method_declaration") {
|
|
331
|
+
const nameNode = member.childForFieldName("name") ?? member.namedChildren.find((c) => c.type === "name");
|
|
332
|
+
if (!nameNode) continue;
|
|
333
|
+
|
|
334
|
+
const methodName = nameNode.text;
|
|
335
|
+
const paramsNode =
|
|
336
|
+
member.childForFieldName("parameters") ??
|
|
337
|
+
member.namedChildren.find((c) => c.type === "formal_parameters");
|
|
338
|
+
const retNode = member.childForFieldName("return_type");
|
|
339
|
+
const paramsText = paramsNode ? paramsNode.text : "()";
|
|
340
|
+
const retText = retNode ? `: ${retNode.text.replace(/^:\s*/, "")}` : "";
|
|
341
|
+
const nestingDepth = calculateAstControlFlowNesting(
|
|
342
|
+
member,
|
|
343
|
+
PHP_CONTROL_FLOW_NODES,
|
|
344
|
+
PHP_FUNCTION_BOUNDARY_NODES
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
symbols.push({
|
|
348
|
+
name: `${containerName}::${methodName}`,
|
|
349
|
+
kind: "method",
|
|
350
|
+
signature: `${vis} function ${methodName}${paramsText}${retText}`,
|
|
351
|
+
visibility: vis,
|
|
352
|
+
line_start: mStart,
|
|
353
|
+
line_end: mEnd,
|
|
354
|
+
line_count: mCount,
|
|
355
|
+
nesting_depth: nestingDepth,
|
|
356
|
+
});
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (member.type === "property_declaration") {
|
|
361
|
+
const propElem = member.namedChildren.find((c) => c.type === "property_element");
|
|
362
|
+
const varNode =
|
|
363
|
+
propElem?.namedChildren.find((c) => c.type === "variable_name") ??
|
|
364
|
+
member.namedChildren.find((c) => c.type === "variable_name");
|
|
365
|
+
if (!varNode) continue;
|
|
366
|
+
|
|
367
|
+
const propName = varNode.text.startsWith("$") ? varNode.text : `$${varNode.text}`;
|
|
368
|
+
const rawLine = (lines[member.startPosition.row] ?? member.text).trim().replace(/;\s*$/, "");
|
|
369
|
+
|
|
370
|
+
symbols.push({
|
|
371
|
+
name: `${containerName}::${propName}`,
|
|
372
|
+
kind: "property",
|
|
373
|
+
signature: rawLine.startsWith(vis) ? rawLine : `${vis} ${rawLine}`,
|
|
374
|
+
visibility: vis,
|
|
375
|
+
line_start: mStart,
|
|
376
|
+
line_end: mEnd,
|
|
377
|
+
line_count: mCount,
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (member.type === "const_declaration") {
|
|
383
|
+
const constElem = member.namedChildren.find((c) => c.type === "const_element");
|
|
384
|
+
const nameNode =
|
|
385
|
+
constElem?.namedChildren.find((c) => c.type === "name") ??
|
|
386
|
+
member.namedChildren.find((c) => c.type === "name");
|
|
387
|
+
if (!nameNode) continue;
|
|
388
|
+
|
|
389
|
+
const constName = nameNode.text;
|
|
390
|
+
symbols.push({
|
|
391
|
+
name: `${containerName}::${constName}`,
|
|
392
|
+
kind: "property",
|
|
393
|
+
signature: `${vis} const ${constName}`,
|
|
394
|
+
visibility: vis,
|
|
395
|
+
line_start: mStart,
|
|
396
|
+
line_end: mEnd,
|
|
397
|
+
line_count: mCount,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private extractPhpVisibility(node: Parser.SyntaxNode): Visibility {
|
|
404
|
+
for (const child of node.children) {
|
|
405
|
+
if (child.type === "visibility_modifier") {
|
|
406
|
+
const t = child.text.trim().toLowerCase();
|
|
407
|
+
if (t === "private" || t === "protected" || t === "public") {
|
|
408
|
+
return t;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return "public";
|
|
413
|
+
}
|
|
153
414
|
}
|
|
415
|
+
|