septum 0.1.4 → 0.1.6
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 +21 -21
- package/README.md +105 -105
- package/bin/septum.ts +4 -4
- package/package.json +63 -63
- package/src/cli/commands/check.ts +161 -161
- package/src/cli/commands/hook.ts +216 -188
- package/src/cli/commands/ingest.ts +29 -29
- package/src/cli/commands/init.ts +213 -211
- package/src/cli/commands/locate.ts +61 -61
- package/src/cli/commands/query.ts +104 -64
- package/src/cli/commands/serve.ts +5 -5
- package/src/cli/commands/slice.ts +50 -48
- package/src/cli/commands/sync.ts +31 -31
- package/src/cli/index.ts +220 -210
- package/src/cli/templates/hooks.ts +75 -75
- package/src/cli/templates/rules.ts +50 -38
- package/src/cli/templates/skill.ts +65 -69
- package/src/core/boundary/evaluator.ts +429 -210
- package/src/core/config/loader.ts +117 -97
- package/src/core/config/schema.ts +39 -39
- package/src/core/database/client.ts +44 -44
- package/src/core/database/repositories/dependency.repository.ts +552 -349
- package/src/core/database/repositories/domain.repository.ts +275 -198
- package/src/core/database/repositories/file.repository.ts +117 -106
- package/src/core/database/repositories/meta.repository.ts +33 -33
- package/src/core/database/repositories/symbol.repository.ts +482 -443
- package/src/core/database/repository.ts +281 -260
- package/src/core/database/schema.ts +154 -153
- package/src/core/discovery/topology-detector.ts +486 -486
- package/src/core/ingestion/hasher.ts +5 -5
- package/src/core/ingestion/pipeline.ts +606 -535
- package/src/core/parser/boundary-tracker.ts +433 -288
- package/src/core/parser/extractors/base.ts +62 -62
- package/src/core/parser/extractors/blade.ts +158 -158
- package/src/core/parser/extractors/frontend.ts +147 -194
- package/src/core/parser/extractors/go.ts +408 -180
- package/src/core/parser/extractors/laravel-semantic.ts +1045 -901
- package/src/core/parser/extractors/mcp-cli-semantic.ts +328 -199
- package/src/core/parser/extractors/nestjs-semantic.ts +134 -119
- package/src/core/parser/extractors/php.ts +415 -153
- package/src/core/parser/extractors/python.ts +460 -228
- package/src/core/parser/extractors/registry.ts +70 -70
- package/src/core/parser/extractors/semantic-extractor.interface.ts +21 -21
- package/src/core/parser/extractors/typescript.ts +668 -181
- package/src/core/parser/tree-sitter.ts +41 -41
- package/src/core/parser/wasm-loader.ts +271 -0
- package/src/core/resolver/call-graph-tracer.ts +698 -668
- package/src/core/resolver/module-resolver.ts +479 -342
- package/src/core/resolver/path-resolver.ts +147 -147
- package/src/core/resolver/symbol-locator.ts +543 -543
- package/src/core/resolver/vertical-slice-tracer.ts +323 -285
- package/src/core/session/session-manager.ts +56 -56
- package/src/core/telemetry/telemetry.ts +86 -86
- package/src/core/topology/environment-detector.ts +109 -6
- package/src/index.ts +21 -21
- package/src/mcp/schemas.ts +695 -286
- package/src/mcp/server.ts +927 -716
- package/src/mcp/tools/check-boundary.ts +307 -152
- package/src/mcp/tools/get-domain-catalog.ts +155 -121
- package/src/mcp/tools/get-environment-topology.ts +3 -6
- package/src/mcp/tools/get-feature-context.ts +227 -116
- package/src/mcp/tools/get-symbol-hotspots.ts +54 -59
- package/src/mcp/tools/get-symbol-impact.ts +28 -26
- package/src/mcp/tools/get-symbol.ts +65 -63
- package/src/mcp/tools/locate-symbol.ts +169 -190
- package/src/mcp/tools/register-domain.ts +59 -64
- package/src/mcp/tools/register-feature.ts +91 -0
- package/src/mcp/tools/trace-vertical-slice.ts +74 -48
- package/src/types/index.ts +366 -315
- package/src/version.ts +3 -3
|
@@ -1,288 +1,433 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility to find the end line (1-based index) of a code block.
|
|
3
|
-
* Handles brace matching with string and comment awareness for C-like languages (TS, JS, PHP, Go).
|
|
4
|
-
*/
|
|
5
|
-
export function findBraceBlockEnd(lines: string[], startLineIndex: number): number {
|
|
6
|
-
let depth = 0;
|
|
7
|
-
let hasOpened = false;
|
|
8
|
-
let inMultiLineComment = false;
|
|
9
|
-
|
|
10
|
-
for (let i = startLineIndex; i < lines.length; i++) {
|
|
11
|
-
const line = lines[i];
|
|
12
|
-
let inSingleQuote = false;
|
|
13
|
-
let inDoubleQuote = false;
|
|
14
|
-
let inTemplateString = false;
|
|
15
|
-
|
|
16
|
-
for (let j = 0; j < line.length; j++) {
|
|
17
|
-
const char = line[j];
|
|
18
|
-
const prevChar = j > 0 ? line[j - 1] : "";
|
|
19
|
-
const nextChar = j + 1 < line.length ? line[j + 1] : "";
|
|
20
|
-
|
|
21
|
-
// Multi-line comment boundary
|
|
22
|
-
if (inMultiLineComment) {
|
|
23
|
-
if (char === "*" && nextChar === "/") {
|
|
24
|
-
inMultiLineComment = false;
|
|
25
|
-
j++; // skip '/'
|
|
26
|
-
}
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Check start of multi-line comment /*
|
|
31
|
-
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
|
|
32
|
-
inMultiLineComment = true;
|
|
33
|
-
j++;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check single-line comment //
|
|
38
|
-
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
|
|
39
|
-
break; // ignore rest of line
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// String quote handling (with escape character check)
|
|
43
|
-
if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
|
|
44
|
-
inSingleQuote = !inSingleQuote;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
|
|
48
|
-
inDoubleQuote = !inDoubleQuote;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
|
|
52
|
-
inTemplateString = !inTemplateString;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Brace tracking
|
|
61
|
-
if (char === "{") {
|
|
62
|
-
depth++;
|
|
63
|
-
hasOpened = true;
|
|
64
|
-
} else if (char === "}") {
|
|
65
|
-
if (hasOpened) {
|
|
66
|
-
depth--;
|
|
67
|
-
if (depth === 0) {
|
|
68
|
-
return i + 1; // 1-based line number
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Safety guard: If we haven't seen an opening brace within 40 lines or hit a declaration-only statement
|
|
75
|
-
if (!hasOpened) {
|
|
76
|
-
const trimmed = line.trim();
|
|
77
|
-
if (trimmed.endsWith(";") && i > startLineIndex) {
|
|
78
|
-
return i + 1;
|
|
79
|
-
}
|
|
80
|
-
if (i - startLineIndex > 40) {
|
|
81
|
-
return startLineIndex + 1;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Fallback: If braces were unclosed or not found, return start line
|
|
87
|
-
return startLineIndex + 1;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Utility to find the end line (1-based index) of an indentation-based block (Python).
|
|
92
|
-
*/
|
|
93
|
-
export function findPythonBlockEnd(lines: string[], startLineIndex: number): number {
|
|
94
|
-
if (startLineIndex >= lines.length) return startLineIndex + 1;
|
|
95
|
-
|
|
96
|
-
const startLine = lines[startLineIndex];
|
|
97
|
-
const baseIndentMatch = startLine.match(/^(\s*)/);
|
|
98
|
-
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
99
|
-
|
|
100
|
-
let lastContentLine = startLineIndex + 1;
|
|
101
|
-
let bodyStarted = false;
|
|
102
|
-
|
|
103
|
-
for (let i = startLineIndex + 1; i < lines.length; i++) {
|
|
104
|
-
const line = lines[i];
|
|
105
|
-
const trimmed = line.trim();
|
|
106
|
-
|
|
107
|
-
// Skip empty lines or pure comment lines
|
|
108
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const currentIndentMatch = line.match(/^(\s*)/);
|
|
113
|
-
const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
|
|
114
|
-
|
|
115
|
-
if (currentIndent > baseIndent) {
|
|
116
|
-
bodyStarted = true;
|
|
117
|
-
lastContentLine = i + 1;
|
|
118
|
-
} else {
|
|
119
|
-
// Returned to same or lesser indentation -> block ended
|
|
120
|
-
if (bodyStarted) {
|
|
121
|
-
return lastContentLine;
|
|
122
|
-
}
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return lastContentLine;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Calculates the maximum control flow nesting depth within a code block (PHP, TS, JS, Go).
|
|
132
|
-
* Base depth is 1 (the function body itself).
|
|
133
|
-
* Nested control structures (if, else if, for, foreach, while, do, switch, try, catch)
|
|
134
|
-
* that open a block increase the active depth.
|
|
135
|
-
*/
|
|
136
|
-
export function calculateControlFlowNesting(
|
|
137
|
-
lines: string[],
|
|
138
|
-
startLineIndex: number,
|
|
139
|
-
endLineIndex: number
|
|
140
|
-
): number {
|
|
141
|
-
if (startLineIndex >= endLineIndex) return 1;
|
|
142
|
-
|
|
143
|
-
let maxDepth = 1;
|
|
144
|
-
let currentDepth = 1;
|
|
145
|
-
let inMultiLineComment = false;
|
|
146
|
-
let insideBody = false;
|
|
147
|
-
|
|
148
|
-
const controlStack: number[] = [];
|
|
149
|
-
let braceDepth = 0;
|
|
150
|
-
let pendingControl = false;
|
|
151
|
-
|
|
152
|
-
const controlRegex = /\b(if|else\s+if|else|for|foreach|while|do|switch|try|catch)\b/;
|
|
153
|
-
|
|
154
|
-
const startIdx = Math.max(0, startLineIndex);
|
|
155
|
-
const endIdx = Math.min(lines.length - 1, endLineIndex);
|
|
156
|
-
|
|
157
|
-
for (let i = startIdx; i <= endIdx; i++) {
|
|
158
|
-
const rawLine = lines[i];
|
|
159
|
-
|
|
160
|
-
let cleanLine = "";
|
|
161
|
-
let inSingleQuote = false;
|
|
162
|
-
let inDoubleQuote = false;
|
|
163
|
-
let inTemplateString = false;
|
|
164
|
-
|
|
165
|
-
for (let j = 0; j < rawLine.length; j++) {
|
|
166
|
-
const char = rawLine[j];
|
|
167
|
-
const prevChar = j > 0 ? rawLine[j - 1] : "";
|
|
168
|
-
const nextChar = j + 1 < rawLine.length ? rawLine[j + 1] : "";
|
|
169
|
-
|
|
170
|
-
if (inMultiLineComment) {
|
|
171
|
-
if (char === "*" && nextChar === "/") {
|
|
172
|
-
inMultiLineComment = false;
|
|
173
|
-
j++;
|
|
174
|
-
}
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
|
|
179
|
-
inMultiLineComment = true;
|
|
180
|
-
j++;
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
|
|
185
|
-
break; // Single line comment: ignore rest of line
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
|
|
189
|
-
inSingleQuote = !inSingleQuote;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
|
|
193
|
-
inDoubleQuote = !inDoubleQuote;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
|
|
197
|
-
inTemplateString = !inTemplateString;
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
cleanLine += char;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const trimmedClean = cleanLine.trim();
|
|
209
|
-
if (!trimmedClean) continue;
|
|
210
|
-
|
|
211
|
-
if (insideBody && controlRegex.test(trimmedClean)) {
|
|
212
|
-
pendingControl = true;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
for (let j = 0; j < cleanLine.length; j++) {
|
|
216
|
-
const char = cleanLine[j];
|
|
217
|
-
if (char === "{") {
|
|
218
|
-
braceDepth++;
|
|
219
|
-
if (!insideBody) {
|
|
220
|
-
insideBody = true;
|
|
221
|
-
} else if (pendingControl) {
|
|
222
|
-
currentDepth++;
|
|
223
|
-
controlStack.push(braceDepth);
|
|
224
|
-
if (currentDepth > maxDepth) {
|
|
225
|
-
maxDepth = currentDepth;
|
|
226
|
-
}
|
|
227
|
-
pendingControl = false;
|
|
228
|
-
}
|
|
229
|
-
} else if (char === "}") {
|
|
230
|
-
if (controlStack.length > 0 && controlStack[controlStack.length - 1] === braceDepth) {
|
|
231
|
-
controlStack.pop();
|
|
232
|
-
currentDepth = Math.max(1, currentDepth - 1);
|
|
233
|
-
}
|
|
234
|
-
braceDepth = Math.max(0, braceDepth - 1);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (trimmedClean.endsWith(";")) {
|
|
239
|
-
pendingControl = false;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return maxDepth;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Calculates the maximum control flow nesting depth in indentation-based languages (Python).
|
|
248
|
-
*/
|
|
249
|
-
export function calculatePythonNesting(
|
|
250
|
-
lines: string[],
|
|
251
|
-
startLineIndex: number,
|
|
252
|
-
endLineIndex: number
|
|
253
|
-
): number {
|
|
254
|
-
if (startLineIndex >= endLineIndex) return 1;
|
|
255
|
-
|
|
256
|
-
const startLine = lines[startLineIndex];
|
|
257
|
-
const baseIndentMatch = startLine.match(/^(\s*)/);
|
|
258
|
-
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
259
|
-
|
|
260
|
-
const controlRegex = /^\s*(if|elif|else|for|while|try|except|finally|match|case)\b/;
|
|
261
|
-
const indentStack: number[] = [baseIndent];
|
|
262
|
-
let maxDepth = 1;
|
|
263
|
-
|
|
264
|
-
const startIdx = Math.max(0, startLineIndex + 1);
|
|
265
|
-
const endIdx = Math.min(lines.length - 1, endLineIndex);
|
|
266
|
-
|
|
267
|
-
for (let i = startIdx; i <= endIdx; i++) {
|
|
268
|
-
const line = lines[i];
|
|
269
|
-
const trimmed = line.trim();
|
|
270
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
271
|
-
|
|
272
|
-
const currentIndentMatch = line.match(/^(\s*)/);
|
|
273
|
-
const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
|
|
274
|
-
|
|
275
|
-
while (indentStack.length > 1 && currentIndent <= indentStack[indentStack.length - 1]) {
|
|
276
|
-
indentStack.pop();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (controlRegex.test(line)) {
|
|
280
|
-
indentStack.push(currentIndent);
|
|
281
|
-
if (indentStack.length > maxDepth) {
|
|
282
|
-
maxDepth = indentStack.length;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return maxDepth;
|
|
288
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Utility to find the end line (1-based index) of a code block.
|
|
3
|
+
* Handles brace matching with string and comment awareness for C-like languages (TS, JS, PHP, Go).
|
|
4
|
+
*/
|
|
5
|
+
export function findBraceBlockEnd(lines: string[], startLineIndex: number): number {
|
|
6
|
+
let depth = 0;
|
|
7
|
+
let hasOpened = false;
|
|
8
|
+
let inMultiLineComment = false;
|
|
9
|
+
|
|
10
|
+
for (let i = startLineIndex; i < lines.length; i++) {
|
|
11
|
+
const line = lines[i];
|
|
12
|
+
let inSingleQuote = false;
|
|
13
|
+
let inDoubleQuote = false;
|
|
14
|
+
let inTemplateString = false;
|
|
15
|
+
|
|
16
|
+
for (let j = 0; j < line.length; j++) {
|
|
17
|
+
const char = line[j];
|
|
18
|
+
const prevChar = j > 0 ? line[j - 1] : "";
|
|
19
|
+
const nextChar = j + 1 < line.length ? line[j + 1] : "";
|
|
20
|
+
|
|
21
|
+
// Multi-line comment boundary
|
|
22
|
+
if (inMultiLineComment) {
|
|
23
|
+
if (char === "*" && nextChar === "/") {
|
|
24
|
+
inMultiLineComment = false;
|
|
25
|
+
j++; // skip '/'
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check start of multi-line comment /*
|
|
31
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
|
|
32
|
+
inMultiLineComment = true;
|
|
33
|
+
j++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check single-line comment //
|
|
38
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
|
|
39
|
+
break; // ignore rest of line
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// String quote handling (with escape character check)
|
|
43
|
+
if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
|
|
44
|
+
inSingleQuote = !inSingleQuote;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
|
|
48
|
+
inDoubleQuote = !inDoubleQuote;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
|
|
52
|
+
inTemplateString = !inTemplateString;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Brace tracking
|
|
61
|
+
if (char === "{") {
|
|
62
|
+
depth++;
|
|
63
|
+
hasOpened = true;
|
|
64
|
+
} else if (char === "}") {
|
|
65
|
+
if (hasOpened) {
|
|
66
|
+
depth--;
|
|
67
|
+
if (depth === 0) {
|
|
68
|
+
return i + 1; // 1-based line number
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Safety guard: If we haven't seen an opening brace within 40 lines or hit a declaration-only statement
|
|
75
|
+
if (!hasOpened) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (trimmed.endsWith(";") && i > startLineIndex) {
|
|
78
|
+
return i + 1;
|
|
79
|
+
}
|
|
80
|
+
if (i - startLineIndex > 40) {
|
|
81
|
+
return startLineIndex + 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback: If braces were unclosed or not found, return start line
|
|
87
|
+
return startLineIndex + 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Utility to find the end line (1-based index) of an indentation-based block (Python).
|
|
92
|
+
*/
|
|
93
|
+
export function findPythonBlockEnd(lines: string[], startLineIndex: number): number {
|
|
94
|
+
if (startLineIndex >= lines.length) return startLineIndex + 1;
|
|
95
|
+
|
|
96
|
+
const startLine = lines[startLineIndex];
|
|
97
|
+
const baseIndentMatch = startLine.match(/^(\s*)/);
|
|
98
|
+
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
99
|
+
|
|
100
|
+
let lastContentLine = startLineIndex + 1;
|
|
101
|
+
let bodyStarted = false;
|
|
102
|
+
|
|
103
|
+
for (let i = startLineIndex + 1; i < lines.length; i++) {
|
|
104
|
+
const line = lines[i];
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
|
|
107
|
+
// Skip empty lines or pure comment lines
|
|
108
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const currentIndentMatch = line.match(/^(\s*)/);
|
|
113
|
+
const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
|
|
114
|
+
|
|
115
|
+
if (currentIndent > baseIndent) {
|
|
116
|
+
bodyStarted = true;
|
|
117
|
+
lastContentLine = i + 1;
|
|
118
|
+
} else {
|
|
119
|
+
// Returned to same or lesser indentation -> block ended
|
|
120
|
+
if (bodyStarted) {
|
|
121
|
+
return lastContentLine;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lastContentLine;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Calculates the maximum control flow nesting depth within a code block (PHP, TS, JS, Go).
|
|
132
|
+
* Base depth is 1 (the function body itself).
|
|
133
|
+
* Nested control structures (if, else if, for, foreach, while, do, switch, try, catch)
|
|
134
|
+
* that open a block increase the active depth.
|
|
135
|
+
*/
|
|
136
|
+
export function calculateControlFlowNesting(
|
|
137
|
+
lines: string[],
|
|
138
|
+
startLineIndex: number,
|
|
139
|
+
endLineIndex: number
|
|
140
|
+
): number {
|
|
141
|
+
if (startLineIndex >= endLineIndex) return 1;
|
|
142
|
+
|
|
143
|
+
let maxDepth = 1;
|
|
144
|
+
let currentDepth = 1;
|
|
145
|
+
let inMultiLineComment = false;
|
|
146
|
+
let insideBody = false;
|
|
147
|
+
|
|
148
|
+
const controlStack: number[] = [];
|
|
149
|
+
let braceDepth = 0;
|
|
150
|
+
let pendingControl = false;
|
|
151
|
+
|
|
152
|
+
const controlRegex = /\b(if|else\s+if|else|for|foreach|while|do|switch|try|catch)\b/;
|
|
153
|
+
|
|
154
|
+
const startIdx = Math.max(0, startLineIndex);
|
|
155
|
+
const endIdx = Math.min(lines.length - 1, endLineIndex);
|
|
156
|
+
|
|
157
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
158
|
+
const rawLine = lines[i];
|
|
159
|
+
|
|
160
|
+
let cleanLine = "";
|
|
161
|
+
let inSingleQuote = false;
|
|
162
|
+
let inDoubleQuote = false;
|
|
163
|
+
let inTemplateString = false;
|
|
164
|
+
|
|
165
|
+
for (let j = 0; j < rawLine.length; j++) {
|
|
166
|
+
const char = rawLine[j];
|
|
167
|
+
const prevChar = j > 0 ? rawLine[j - 1] : "";
|
|
168
|
+
const nextChar = j + 1 < rawLine.length ? rawLine[j + 1] : "";
|
|
169
|
+
|
|
170
|
+
if (inMultiLineComment) {
|
|
171
|
+
if (char === "*" && nextChar === "/") {
|
|
172
|
+
inMultiLineComment = false;
|
|
173
|
+
j++;
|
|
174
|
+
}
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "*") {
|
|
179
|
+
inMultiLineComment = true;
|
|
180
|
+
j++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateString && char === "/" && nextChar === "/") {
|
|
185
|
+
break; // Single line comment: ignore rest of line
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (char === "'" && !inDoubleQuote && !inTemplateString && prevChar !== "\\") {
|
|
189
|
+
inSingleQuote = !inSingleQuote;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (char === '"' && !inSingleQuote && !inTemplateString && prevChar !== "\\") {
|
|
193
|
+
inDoubleQuote = !inDoubleQuote;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (char === "`" && !inSingleQuote && !inDoubleQuote && prevChar !== "\\") {
|
|
197
|
+
inTemplateString = !inTemplateString;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (inSingleQuote || inDoubleQuote || inTemplateString) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
cleanLine += char;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const trimmedClean = cleanLine.trim();
|
|
209
|
+
if (!trimmedClean) continue;
|
|
210
|
+
|
|
211
|
+
if (insideBody && controlRegex.test(trimmedClean)) {
|
|
212
|
+
pendingControl = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (let j = 0; j < cleanLine.length; j++) {
|
|
216
|
+
const char = cleanLine[j];
|
|
217
|
+
if (char === "{") {
|
|
218
|
+
braceDepth++;
|
|
219
|
+
if (!insideBody) {
|
|
220
|
+
insideBody = true;
|
|
221
|
+
} else if (pendingControl) {
|
|
222
|
+
currentDepth++;
|
|
223
|
+
controlStack.push(braceDepth);
|
|
224
|
+
if (currentDepth > maxDepth) {
|
|
225
|
+
maxDepth = currentDepth;
|
|
226
|
+
}
|
|
227
|
+
pendingControl = false;
|
|
228
|
+
}
|
|
229
|
+
} else if (char === "}") {
|
|
230
|
+
if (controlStack.length > 0 && controlStack[controlStack.length - 1] === braceDepth) {
|
|
231
|
+
controlStack.pop();
|
|
232
|
+
currentDepth = Math.max(1, currentDepth - 1);
|
|
233
|
+
}
|
|
234
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (trimmedClean.endsWith(";")) {
|
|
239
|
+
pendingControl = false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return maxDepth;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculates the maximum control flow nesting depth in indentation-based languages (Python).
|
|
248
|
+
*/
|
|
249
|
+
export function calculatePythonNesting(
|
|
250
|
+
lines: string[],
|
|
251
|
+
startLineIndex: number,
|
|
252
|
+
endLineIndex: number
|
|
253
|
+
): number {
|
|
254
|
+
if (startLineIndex >= endLineIndex) return 1;
|
|
255
|
+
|
|
256
|
+
const startLine = lines[startLineIndex];
|
|
257
|
+
const baseIndentMatch = startLine.match(/^(\s*)/);
|
|
258
|
+
const baseIndent = baseIndentMatch ? baseIndentMatch[1].length : 0;
|
|
259
|
+
|
|
260
|
+
const controlRegex = /^\s*(if|elif|else|for|while|try|except|finally|match|case)\b/;
|
|
261
|
+
const indentStack: number[] = [baseIndent];
|
|
262
|
+
let maxDepth = 1;
|
|
263
|
+
|
|
264
|
+
const startIdx = Math.max(0, startLineIndex + 1);
|
|
265
|
+
const endIdx = Math.min(lines.length - 1, endLineIndex);
|
|
266
|
+
|
|
267
|
+
for (let i = startIdx; i <= endIdx; i++) {
|
|
268
|
+
const line = lines[i];
|
|
269
|
+
const trimmed = line.trim();
|
|
270
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
271
|
+
|
|
272
|
+
const currentIndentMatch = line.match(/^(\s*)/);
|
|
273
|
+
const currentIndent = currentIndentMatch ? currentIndentMatch[1].length : 0;
|
|
274
|
+
|
|
275
|
+
while (indentStack.length > 1 && currentIndent <= indentStack[indentStack.length - 1]) {
|
|
276
|
+
indentStack.pop();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (controlRegex.test(line)) {
|
|
280
|
+
indentStack.push(currentIndent);
|
|
281
|
+
if (indentStack.length > maxDepth) {
|
|
282
|
+
maxDepth = indentStack.length;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return maxDepth;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Replaces the contents of comments and string/template literals with spaces while
|
|
292
|
+
* preserving exact character offsets, outer quote delimiters, and line breaks.
|
|
293
|
+
*/
|
|
294
|
+
export function blankCommentsAndStrings(
|
|
295
|
+
content: string,
|
|
296
|
+
options?: { hashComments?: boolean }
|
|
297
|
+
): string {
|
|
298
|
+
const hashComments = options?.hashComments ?? false;
|
|
299
|
+
const out = content.split("");
|
|
300
|
+
const len = out.length;
|
|
301
|
+
|
|
302
|
+
let i = 0;
|
|
303
|
+
let inSingle = false;
|
|
304
|
+
let inDouble = false;
|
|
305
|
+
let inTemplate = false;
|
|
306
|
+
let inLineComment = false;
|
|
307
|
+
let inBlockComment = false;
|
|
308
|
+
|
|
309
|
+
while (i < len) {
|
|
310
|
+
const ch = out[i];
|
|
311
|
+
const next = i + 1 < len ? out[i + 1] : "";
|
|
312
|
+
|
|
313
|
+
if (inLineComment) {
|
|
314
|
+
if (ch === "\n") {
|
|
315
|
+
inLineComment = false;
|
|
316
|
+
} else if (ch !== "\r") {
|
|
317
|
+
out[i] = " ";
|
|
318
|
+
}
|
|
319
|
+
i++;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (inBlockComment) {
|
|
324
|
+
if (ch === "*" && next === "/") {
|
|
325
|
+
out[i] = " ";
|
|
326
|
+
out[i + 1] = " ";
|
|
327
|
+
inBlockComment = false;
|
|
328
|
+
i += 2;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (ch !== "\n" && ch !== "\r") {
|
|
332
|
+
out[i] = " ";
|
|
333
|
+
}
|
|
334
|
+
i++;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (inSingle) {
|
|
339
|
+
if (ch === "\\" && i + 1 < len) {
|
|
340
|
+
if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
|
|
341
|
+
if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
|
|
342
|
+
i += 2;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (ch === "'") {
|
|
346
|
+
inSingle = false;
|
|
347
|
+
} else if (ch !== "\n" && ch !== "\r") {
|
|
348
|
+
out[i] = " ";
|
|
349
|
+
}
|
|
350
|
+
i++;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (inDouble) {
|
|
355
|
+
if (ch === "\\" && i + 1 < len) {
|
|
356
|
+
if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
|
|
357
|
+
if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
|
|
358
|
+
i += 2;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (ch === '"') {
|
|
362
|
+
inDouble = false;
|
|
363
|
+
} else if (ch !== "\n" && ch !== "\r") {
|
|
364
|
+
out[i] = " ";
|
|
365
|
+
}
|
|
366
|
+
i++;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (inTemplate) {
|
|
371
|
+
if (ch === "\\" && i + 1 < len) {
|
|
372
|
+
if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
|
|
373
|
+
if (out[i + 1] !== "\n" && out[i + 1] !== "\r") out[i + 1] = " ";
|
|
374
|
+
i += 2;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (ch === "`") {
|
|
378
|
+
inTemplate = false;
|
|
379
|
+
} else if (ch !== "\n" && ch !== "\r") {
|
|
380
|
+
out[i] = " ";
|
|
381
|
+
}
|
|
382
|
+
i++;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Normal code state
|
|
387
|
+
if (ch === "/" && next === "/") {
|
|
388
|
+
out[i] = " ";
|
|
389
|
+
out[i + 1] = " ";
|
|
390
|
+
inLineComment = true;
|
|
391
|
+
i += 2;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (ch === "/" && next === "*") {
|
|
396
|
+
out[i] = " ";
|
|
397
|
+
out[i + 1] = " ";
|
|
398
|
+
inBlockComment = true;
|
|
399
|
+
i += 2;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (hashComments && ch === "#") {
|
|
404
|
+
out[i] = " ";
|
|
405
|
+
inLineComment = true;
|
|
406
|
+
i++;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (ch === "'") {
|
|
411
|
+
inSingle = true;
|
|
412
|
+
i++;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (ch === '"') {
|
|
417
|
+
inDouble = true;
|
|
418
|
+
i++;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (ch === "`") {
|
|
423
|
+
inTemplate = true;
|
|
424
|
+
i++;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
i++;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return out.join("");
|
|
432
|
+
}
|
|
433
|
+
|