@boyingliu01/xp-gate 0.31.3 → 0.32.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/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -102,6 +102,56 @@ function findBlockEnd(masked: string, open: number): number {
|
|
|
102
102
|
return masked.length;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** Keywords that begin a new top-level declaration. */
|
|
106
|
+
const DECLARATION_KEYWORDS = /^(?:export|import|interface|class|const|let|var|function|type|declare|@)/;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* True when a newline at `index` ends a declaration that has no body: the next
|
|
110
|
+
* line starts another declaration (or a decorator). Without this the scan runs
|
|
111
|
+
* into the following declaration and reports ITS brace as this one's body.
|
|
112
|
+
*/
|
|
113
|
+
function startsNewDeclaration(masked: string, index: number): boolean {
|
|
114
|
+
const rest = masked.substring(index + 1).replace(/^[ \t]*/, '');
|
|
115
|
+
return DECLARATION_KEYWORDS.test(rest);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What the declaration scan should do with the character it just read. */
|
|
119
|
+
type ScanAction =
|
|
120
|
+
| { kind: 'advance' }
|
|
121
|
+
| { kind: 'jump'; to: number }
|
|
122
|
+
| { kind: 'depth'; delta: 1 | -1 }
|
|
123
|
+
| { kind: 'body'; at: number }
|
|
124
|
+
| { kind: 'no-body' };
|
|
125
|
+
|
|
126
|
+
/** Characters that open a generic/bracket group in a declaration header. */
|
|
127
|
+
const HEADER_OPENERS = new Set(['<', '[']);
|
|
128
|
+
/** Characters that close one. */
|
|
129
|
+
const HEADER_CLOSERS = new Set(['>', ']']);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Decide what the declaration scan does at `i`.
|
|
133
|
+
*
|
|
134
|
+
* Table-driven rather than an `if` ladder (#620): the interesting cases are
|
|
135
|
+
* "this character means something", and a per-character decision keeps that list
|
|
136
|
+
* readable and each rule independently testable.
|
|
137
|
+
*/
|
|
138
|
+
function declarationScanAction(masked: string, i: number, depth: number): ScanAction {
|
|
139
|
+
const c = masked[i];
|
|
140
|
+
|
|
141
|
+
// A parenthesised group is skipped whole: parens only ever appear inside a
|
|
142
|
+
// header (`class X extends Y<() => void>`), never as body structure.
|
|
143
|
+
if (c === '(') return { kind: 'jump', to: readParenGroup(masked, i).end - 1 };
|
|
144
|
+
if (HEADER_OPENERS.has(c)) return { kind: 'depth', delta: 1 };
|
|
145
|
+
if (HEADER_CLOSERS.has(c)) return { kind: 'depth', delta: -1 };
|
|
146
|
+
|
|
147
|
+
if (c === '{') return depth === 0 ? { kind: 'body', at: i } : { kind: 'depth', delta: 1 };
|
|
148
|
+
if (c === '}') return { kind: 'depth', delta: -1 };
|
|
149
|
+
|
|
150
|
+
if (c === ';' && depth === 0) return { kind: 'no-body' };
|
|
151
|
+
if (c === '\n' && depth === 0 && startsNewDeclaration(masked, i)) return { kind: 'no-body' };
|
|
152
|
+
return { kind: 'advance' };
|
|
153
|
+
}
|
|
154
|
+
|
|
105
155
|
/**
|
|
106
156
|
* Body brace for declarations that have no parameter list (`class`, `interface`).
|
|
107
157
|
* Scans forward from the end of the declaration header; the first `{` at generic
|
|
@@ -110,27 +160,17 @@ function findBlockEnd(masked: string, open: number): number {
|
|
|
110
160
|
function findDeclarationBrace(masked: string, from: number): number {
|
|
111
161
|
let depth = 0;
|
|
112
162
|
for (let i = from; i < masked.length; i++) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
if (c === '}') { if (depth > 0) depth--; continue; }
|
|
126
|
-
if (c === ';' && depth === 0) return -1;
|
|
127
|
-
// Stop at a blank line / a new top-level keyword so we do not run into the
|
|
128
|
-
// next declaration when this one has no body.
|
|
129
|
-
if (c === '\n') {
|
|
130
|
-
const rest = masked.substring(i + 1).replace(/^[ \t]*/, '');
|
|
131
|
-
if (/^(?:export|import|interface|class|const|let|var|function|type|declare|@)/.test(rest) && depth === 0) {
|
|
132
|
-
return -1;
|
|
133
|
-
}
|
|
163
|
+
const action = declarationScanAction(masked, i, depth);
|
|
164
|
+
switch (action.kind) {
|
|
165
|
+
case 'body': return action.at;
|
|
166
|
+
case 'no-body': return -1;
|
|
167
|
+
case 'jump': i = action.to; break;
|
|
168
|
+
case 'depth':
|
|
169
|
+
// Depth never goes negative: a stray closing bracket in a header must
|
|
170
|
+
// not look like the start of a new group.
|
|
171
|
+
depth = Math.max(0, depth + action.delta);
|
|
172
|
+
break;
|
|
173
|
+
case 'advance': break;
|
|
134
174
|
}
|
|
135
175
|
}
|
|
136
176
|
return -1;
|
|
@@ -140,80 +180,292 @@ function lineOf(src: string, index: number): number {
|
|
|
140
180
|
return src.substring(0, index).split('\n').length;
|
|
141
181
|
}
|
|
142
182
|
|
|
183
|
+
/** The three quoted states shared by the parameter/paren scanners. */
|
|
184
|
+
interface QuoteState {
|
|
185
|
+
inSingle: boolean;
|
|
186
|
+
inDouble: boolean;
|
|
187
|
+
inTemplate: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function newQuoteState(): QuoteState {
|
|
191
|
+
return { inSingle: false, inDouble: false, inTemplate: false };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function inQuotedState(state: QuoteState): boolean {
|
|
195
|
+
return state.inSingle || state.inDouble || state.inTemplate;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The three quote kinds tracked by QuoteState, with the flag that marks each. */
|
|
199
|
+
const QUOTE_KINDS: ReadonlyArray<{ quote: string; flag: 'inSingle' | 'inDouble' | 'inTemplate' }> = [
|
|
200
|
+
{ quote: "'", flag: 'inSingle' },
|
|
201
|
+
{ quote: '"', flag: 'inDouble' },
|
|
202
|
+
{ quote: '`', flag: 'inTemplate' },
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Advance one character of quote tracking.
|
|
207
|
+
*
|
|
208
|
+
* Extracted because `countParams` and `readParenGroup` carried the same
|
|
209
|
+
* six-branch block verbatim (#602): both must ignore delimiters that appear
|
|
210
|
+
* inside a string, and neither may treat an escape as a closing quote.
|
|
211
|
+
*
|
|
212
|
+
* Table-driven (#620): all three quote kinds behave identically -- an escape
|
|
213
|
+
* skips two characters, the matching quote closes the literal -- so they are one
|
|
214
|
+
* rule applied to a list rather than three copies of it.
|
|
215
|
+
*
|
|
216
|
+
* @returns the number of characters consumed (2 when the next char is escaped).
|
|
217
|
+
*/
|
|
218
|
+
function advanceQuoteState(src: string, i: number, state: QuoteState): number {
|
|
219
|
+
const c = src[i];
|
|
220
|
+
|
|
221
|
+
for (const kind of QUOTE_KINDS) {
|
|
222
|
+
if (!state[kind.flag]) continue;
|
|
223
|
+
if (c === '\\') return 2;
|
|
224
|
+
if (c === kind.quote) state[kind.flag] = false;
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Not inside any literal: open the one this character starts, if any.
|
|
229
|
+
const opener = QUOTE_KINDS.find(kind => kind.quote === c);
|
|
230
|
+
if (opener) state[opener.flag] = true;
|
|
231
|
+
return 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Opening delimiters that raise the generic/bracket depth in a param list. */
|
|
235
|
+
const DEPTH_OPENERS = new Set(['(', '[', '{', '<']);
|
|
236
|
+
/** Closing delimiters that lower it. */
|
|
237
|
+
const DEPTH_CLOSERS = new Set([')', ']', '}', '>']);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Adjust `depth` for one delimiter.
|
|
241
|
+
* @returns true when the character was a delimiter (so it cannot also be a
|
|
242
|
+
* separator), false otherwise.
|
|
243
|
+
*/
|
|
244
|
+
function applyDelimiter(c: string, depth: { value: number }): boolean {
|
|
245
|
+
if (DEPTH_OPENERS.has(c)) { depth.value++; return true; }
|
|
246
|
+
if (DEPTH_CLOSERS.has(c)) { depth.value--; return true; }
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** True when the character opens a quote/template the scanners must skip. */
|
|
251
|
+
function isQuote(c: string): boolean {
|
|
252
|
+
return c === "'" || c === '"' || c === '`';
|
|
253
|
+
}
|
|
254
|
+
|
|
143
255
|
/** Count top-level, comma-separated parameters in a raw parameter list. */
|
|
144
256
|
function countParams(rawList: string): number {
|
|
145
257
|
const trimmed = rawList.trim();
|
|
146
258
|
if (trimmed === '') return 0;
|
|
147
|
-
|
|
259
|
+
|
|
260
|
+
const depth = { value: 0 };
|
|
148
261
|
let count = 1;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
let
|
|
152
|
-
|
|
262
|
+
const state = newQuoteState();
|
|
263
|
+
|
|
264
|
+
for (let i = 0; i < trimmed.length;) {
|
|
265
|
+
if (inQuotedState(state)) {
|
|
266
|
+
i += advanceQuoteState(trimmed, i, state);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
153
269
|
const c = trimmed[i];
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (c === '(' || c === '[' || c === '{' || c === '<') depth++;
|
|
161
|
-
else if (c === ')' || c === ']' || c === '}' || c === '>') depth--;
|
|
162
|
-
else if (c === ',' && depth === 0) count++;
|
|
270
|
+
if (isQuote(c)) {
|
|
271
|
+
advanceQuoteState(trimmed, i, state);
|
|
272
|
+
} else if (!applyDelimiter(c, depth) && c === ',' && depth.value === 0) {
|
|
273
|
+
count++;
|
|
274
|
+
}
|
|
275
|
+
i++;
|
|
163
276
|
}
|
|
164
277
|
return count;
|
|
165
278
|
}
|
|
166
279
|
|
|
280
|
+
/** What one step of the paren-group scan did, so the caller can react. */
|
|
281
|
+
type ParenStep =
|
|
282
|
+
| { kind: 'consumed'; next: number }
|
|
283
|
+
| { kind: 'closed'; end: number };
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Advance one step through a paren group.
|
|
287
|
+
*
|
|
288
|
+
* Extracted from `readParenGroup` (#620) so the three-way decision -- inside a
|
|
289
|
+
* literal, opening a literal, or structural bracket -- reads as one rule instead
|
|
290
|
+
* of being folded into the loop that also owns depth and substring slicing.
|
|
291
|
+
*
|
|
292
|
+
* A literal is entered through `advanceQuoteState` at the SAME index rather than
|
|
293
|
+
* consumed: that call flips the state on the opening quote and reports one
|
|
294
|
+
* character consumed, so re-scans resume one past the quote with the state set.
|
|
295
|
+
*
|
|
296
|
+
* @param depth running paren depth, updated in place for structural brackets
|
|
297
|
+
* @returns the next index to read, or the offset just past the closing paren
|
|
298
|
+
*/
|
|
299
|
+
function stepParenGroup(
|
|
300
|
+
src: string,
|
|
301
|
+
i: number,
|
|
302
|
+
state: QuoteState,
|
|
303
|
+
depth: { value: number },
|
|
304
|
+
): ParenStep {
|
|
305
|
+
if (inQuotedState(state)) {
|
|
306
|
+
return { kind: 'consumed', next: i + advanceQuoteState(src, i, state) };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const c = src[i];
|
|
310
|
+
if (isQuote(c)) {
|
|
311
|
+
// Entering a literal: flip state here, resume at the next character.
|
|
312
|
+
advanceQuoteState(src, i, state);
|
|
313
|
+
return { kind: 'consumed', next: i + 1 };
|
|
314
|
+
}
|
|
315
|
+
if (c === '(') {
|
|
316
|
+
depth.value++;
|
|
317
|
+
return { kind: 'consumed', next: i + 1 };
|
|
318
|
+
}
|
|
319
|
+
if (c === ')') {
|
|
320
|
+
depth.value--;
|
|
321
|
+
if (depth.value === 0) return { kind: 'closed', end: i + 1 };
|
|
322
|
+
}
|
|
323
|
+
return { kind: 'consumed', next: i + 1 };
|
|
324
|
+
}
|
|
325
|
+
|
|
167
326
|
/**
|
|
168
327
|
* Extract the balanced `( ... )` group that starts at `open`.
|
|
169
328
|
* Returns the inner text and the index just past the closing paren.
|
|
170
329
|
*/
|
|
171
330
|
function readParenGroup(src: string, open: number): { inner: string; end: number } {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
let
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (inDouble) { if (c === '\\') i++; else if (c === '"') inDouble = false; continue; }
|
|
180
|
-
if (inTemplate) { if (c === '\\') i++; else if (c === '`') inTemplate = false; continue; }
|
|
181
|
-
if (c === "'") { inSingle = true; continue; }
|
|
182
|
-
if (c === '"') { inDouble = true; continue; }
|
|
183
|
-
if (c === '`') { inTemplate = true; continue; }
|
|
184
|
-
if (c === '(') depth++;
|
|
185
|
-
else if (c === ')') {
|
|
186
|
-
depth--;
|
|
187
|
-
if (depth === 0) return { inner: src.substring(open + 1, i), end: i + 1 };
|
|
331
|
+
const state = newQuoteState();
|
|
332
|
+
const depth = { value: 0 };
|
|
333
|
+
|
|
334
|
+
for (let i = open; i < src.length;) {
|
|
335
|
+
const step = stepParenGroup(src, i, state, depth);
|
|
336
|
+
if (step.kind === 'closed') {
|
|
337
|
+
return { inner: src.substring(open + 1, step.end - 1), end: step.end };
|
|
188
338
|
}
|
|
339
|
+
i = step.next;
|
|
189
340
|
}
|
|
190
341
|
return { inner: src.substring(open + 1), end: src.length };
|
|
191
342
|
}
|
|
192
343
|
|
|
344
|
+
/**
|
|
345
|
+
* The lexical states `maxNesting` tracks while counting braces. Braces inside
|
|
346
|
+
* any of these are data, not structure, so they must not change the count.
|
|
347
|
+
*/
|
|
348
|
+
interface NestingScanState {
|
|
349
|
+
inSingle: boolean;
|
|
350
|
+
inDouble: boolean;
|
|
351
|
+
inTemplate: boolean;
|
|
352
|
+
inLineComment: boolean;
|
|
353
|
+
inBlockComment: boolean;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* One handler per lexical state. Each returns how many characters were
|
|
358
|
+
* consumed, and mutates `state` only when the construct actually ends.
|
|
359
|
+
*
|
|
360
|
+
* Split out of a single 22-CCN dispatcher (#602): every state's exit condition
|
|
361
|
+
* is independent, so keeping them in one body bought nothing but branch count.
|
|
362
|
+
*/
|
|
363
|
+
type StateAdvance = (body: string, i: number, state: NestingScanState) => number;
|
|
364
|
+
|
|
365
|
+
const advanceLineComment: StateAdvance = (body, i, state) => {
|
|
366
|
+
if (body[i] === '\n') state.inLineComment = false;
|
|
367
|
+
return 1;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const advanceBlockComment: StateAdvance = (body, i, state) => {
|
|
371
|
+
if (body[i] === '*' && body[i + 1] === '/') {
|
|
372
|
+
state.inBlockComment = false;
|
|
373
|
+
return 2;
|
|
374
|
+
}
|
|
375
|
+
return 1;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/** Shared shape of the three quoted states: honour `\`, close on the delimiter. */
|
|
379
|
+
const advanceSingleQuote: StateAdvance = (body, i, state) => {
|
|
380
|
+
if (body[i] === '\\') return 2;
|
|
381
|
+
if (body[i] === "'") state.inSingle = false;
|
|
382
|
+
return 1;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const advanceDoubleQuote: StateAdvance = (body, i, state) => {
|
|
386
|
+
if (body[i] === '\\') return 2;
|
|
387
|
+
if (body[i] === '"') state.inDouble = false;
|
|
388
|
+
return 1;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const advanceTemplate: StateAdvance = (body, i, state) => {
|
|
392
|
+
if (body[i] === '\\') return 2;
|
|
393
|
+
if (body[i] === '`') state.inTemplate = false;
|
|
394
|
+
return 1;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Enter a construct when the current character opens one.
|
|
399
|
+
* @returns characters consumed, or 0 when nothing was opened.
|
|
400
|
+
*/
|
|
401
|
+
function openConstruct(body: string, i: number, state: NestingScanState): number {
|
|
402
|
+
const c = body[i];
|
|
403
|
+
const next = body[i + 1];
|
|
404
|
+
if (c === '/' && next === '/') { state.inLineComment = true; return 2; }
|
|
405
|
+
if (c === '/' && next === '*') { state.inBlockComment = true; return 2; }
|
|
406
|
+
if (c === "'") { state.inSingle = true; return 1; }
|
|
407
|
+
if (c === '"') { state.inDouble = true; return 1; }
|
|
408
|
+
if (c === '`') { state.inTemplate = true; return 1; }
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Advance the quoting/commenting state machine by one character.
|
|
414
|
+
*
|
|
415
|
+
* Pure with respect to depth: it only updates `state` and reports how many
|
|
416
|
+
* characters were consumed. Extracted from `maxNesting` (#602) so the brace
|
|
417
|
+
* counting and the lexing can be reasoned about separately -- the combined body
|
|
418
|
+
* carried every branch of both.
|
|
419
|
+
*
|
|
420
|
+
* @returns the number of characters consumed (1, or 2 for the escape/close
|
|
421
|
+
* pairs that swallow the next character).
|
|
422
|
+
*/
|
|
423
|
+
function advanceNestingState(
|
|
424
|
+
body: string,
|
|
425
|
+
i: number,
|
|
426
|
+
state: NestingScanState,
|
|
427
|
+
): number {
|
|
428
|
+
if (state.inLineComment) return advanceLineComment(body, i, state);
|
|
429
|
+
if (state.inBlockComment) return advanceBlockComment(body, i, state);
|
|
430
|
+
if (state.inSingle) return advanceSingleQuote(body, i, state);
|
|
431
|
+
if (state.inDouble) return advanceDoubleQuote(body, i, state);
|
|
432
|
+
if (state.inTemplate) return advanceTemplate(body, i, state);
|
|
433
|
+
const opened = openConstruct(body, i, state);
|
|
434
|
+
return opened === 0 ? 1 : opened;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** True when the character at `i` is structural (not inside a literal/comment). */
|
|
438
|
+
function isStructuralAt(state: NestingScanState): boolean {
|
|
439
|
+
return !state.inSingle && !state.inDouble && !state.inTemplate &&
|
|
440
|
+
!state.inLineComment && !state.inBlockComment;
|
|
441
|
+
}
|
|
442
|
+
|
|
193
443
|
/** Max brace nesting depth inside a function body (1 = body itself). */
|
|
194
444
|
function maxNesting(body: string): number {
|
|
195
445
|
let depth = 0;
|
|
196
446
|
let max = 0;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
447
|
+
const state: NestingScanState = {
|
|
448
|
+
inSingle: false,
|
|
449
|
+
inDouble: false,
|
|
450
|
+
inTemplate: false,
|
|
451
|
+
inLineComment: false,
|
|
452
|
+
inBlockComment: false,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < body.length;) {
|
|
456
|
+
const consumed = advanceNestingState(body, i, state);
|
|
457
|
+
if (consumed === 2) {
|
|
458
|
+
// The pair (escape, comment close, comment open) is consumed whole. A
|
|
459
|
+
// comment opener must not also be read as a brace-less character below.
|
|
460
|
+
i += 2;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (isStructuralAt(state)) {
|
|
464
|
+
const c = body[i];
|
|
465
|
+
if (c === '{') { depth++; if (depth > max) max = depth; }
|
|
466
|
+
else if (c === '}') depth--;
|
|
467
|
+
}
|
|
468
|
+
i += 1;
|
|
217
469
|
}
|
|
218
470
|
return max;
|
|
219
471
|
}
|
|
@@ -232,12 +484,121 @@ function ioOperationsIn(body: string): string[] {
|
|
|
232
484
|
return [...found];
|
|
233
485
|
}
|
|
234
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Blank a `//` line comment starting at `start`.
|
|
489
|
+
* Stops AT the newline so line numbering survives (the newline is the caller's
|
|
490
|
+
* to consume as an ordinary character).
|
|
491
|
+
* @returns the index to resume scanning from.
|
|
492
|
+
*/
|
|
493
|
+
function maskLineComment(src: string, out: string[], start: number): number {
|
|
494
|
+
let i = start;
|
|
495
|
+
while (i < src.length && src[i] !== '\n') { out[i] = ' '; i++; }
|
|
496
|
+
return i;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Blank a block comment starting at `start` (which holds `/`).
|
|
501
|
+
*
|
|
502
|
+
* #602: newlines inside the comment must survive. Blanking them shifts every
|
|
503
|
+
* subsequent line number, so `lineOf()` and every line-numbered violation after
|
|
504
|
+
* a multi-line comment would point at the wrong line -- exactly the defect the
|
|
505
|
+
* line-comment branch never had, because it stops AT the newline.
|
|
506
|
+
*
|
|
507
|
+
* @returns the index to resume scanning from.
|
|
508
|
+
*/
|
|
509
|
+
function maskBlockComment(src: string, out: string[], start: number): number {
|
|
510
|
+
let i = start;
|
|
511
|
+
out[i] = ' '; out[i + 1] = ' '; i += 2;
|
|
512
|
+
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
513
|
+
if (src[i] !== '\n') out[i] = ' ';
|
|
514
|
+
i++;
|
|
515
|
+
}
|
|
516
|
+
if (i < src.length) { out[i] = ' '; out[i + 1] = ' '; i += 2; }
|
|
517
|
+
return i;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Decide whether the `/` at `index` opens a regex literal rather than being
|
|
522
|
+
* division. A regex may only start where an expression may start: at the very
|
|
523
|
+
* beginning, after an operator/punctuation, or after a keyword that expects an
|
|
524
|
+
* expression (`return /re/`, `typeof /re/`, ...).
|
|
525
|
+
*/
|
|
526
|
+
function opensRegexLiteral(src: string, index: number): boolean {
|
|
527
|
+
const before = src.substring(0, index).replace(/\s+$/, '');
|
|
528
|
+
const prev = before[before.length - 1];
|
|
529
|
+
if (prev === undefined) return true;
|
|
530
|
+
if (/[=(,:[!&|?{};+\-*%^~<>]/.test(prev)) return true;
|
|
531
|
+
return /(?:^|[^\w$])(return|typeof|case|in|of|new|delete|void|instanceof)$/.test(before);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Find where a regex literal starting at `start` closes, honouring `\` escapes
|
|
536
|
+
* and character classes (`[/]` must not close the literal).
|
|
537
|
+
* @returns the index of the closing `/`, or -1 when the line ends first (so the
|
|
538
|
+
* slash really was division).
|
|
539
|
+
*/
|
|
540
|
+
function findRegexClose(src: string, start: number): number {
|
|
541
|
+
let inClass = false;
|
|
542
|
+
for (let j = start + 1; j < src.length; j++) {
|
|
543
|
+
const d = src[j];
|
|
544
|
+
if (d === '\\') { j++; continue; }
|
|
545
|
+
if (d === '\n') return -1;
|
|
546
|
+
if (d === '[') { inClass = true; continue; }
|
|
547
|
+
if (d === ']') { inClass = false; continue; }
|
|
548
|
+
if (d === '/' && !inClass) return j;
|
|
549
|
+
}
|
|
550
|
+
return -1;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Blank a regex literal (and its trailing flags) starting at `start`.
|
|
555
|
+
* @returns the index to resume scanning from.
|
|
556
|
+
*/
|
|
557
|
+
function maskRegexLiteral(src: string, out: string[], start: number): number {
|
|
558
|
+
const close = findRegexClose(src, start);
|
|
559
|
+
if (close < 0) return start;
|
|
560
|
+
for (let k = start; k <= close; k++) out[k] = ' ';
|
|
561
|
+
let i = close + 1;
|
|
562
|
+
while (i < src.length && /[a-z]/i.test(src[i])) { out[i] = ' '; i++; }
|
|
563
|
+
return i;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Blank a quoted or template literal starting at `start`.
|
|
568
|
+
* A backtick runs to its closing backtick (interpolations included); a single
|
|
569
|
+
* or double quote stops at an unescaped newline, matching a syntax error.
|
|
570
|
+
* @returns the index to resume scanning from.
|
|
571
|
+
*/
|
|
572
|
+
function maskStringLiteral(src: string, out: string[], start: number): number {
|
|
573
|
+
const quote = src[start];
|
|
574
|
+
out[start] = ' ';
|
|
575
|
+
let i = start + 1;
|
|
576
|
+
while (i < src.length) {
|
|
577
|
+
const d = src[i];
|
|
578
|
+
if (d === '\\') {
|
|
579
|
+
out[i] = ' ';
|
|
580
|
+
if (i + 1 < src.length) out[i + 1] = ' ';
|
|
581
|
+
i += 2;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (d === quote) { out[i] = ' '; i++; break; }
|
|
585
|
+
if (d === '\n' && quote !== '`') break;
|
|
586
|
+
out[i] = ' ';
|
|
587
|
+
i++;
|
|
588
|
+
}
|
|
589
|
+
return i;
|
|
590
|
+
}
|
|
591
|
+
|
|
235
592
|
/**
|
|
236
593
|
* Blank out comments and string/template literals, preserving offsets.
|
|
237
594
|
* A single left-to-right scan is used rather than chained `.replace()` calls:
|
|
238
595
|
* sequential regex replacement re-scans text that earlier passes produced and
|
|
239
596
|
* can pair a quote inside a regex literal with a quote in a later comment,
|
|
240
597
|
* swallowing hundreds of lines (observed as a 302-line phantom function).
|
|
598
|
+
*
|
|
599
|
+
* The scan dispatches to one masking routine per construct and otherwise copies
|
|
600
|
+
* the character through, so the loop's own branching stays flat (#602: split
|
|
601
|
+
* from a single 36-CCN body into five routines the tests can pin separately).
|
|
241
602
|
*/
|
|
242
603
|
function maskLiterals(src: string): string {
|
|
243
604
|
const out = src.split('');
|
|
@@ -247,56 +608,20 @@ function maskLiterals(src: string): string {
|
|
|
247
608
|
const next = src[i + 1];
|
|
248
609
|
|
|
249
610
|
if (c === '/' && next === '/') {
|
|
250
|
-
|
|
611
|
+
i = maskLineComment(src, out, i);
|
|
251
612
|
continue;
|
|
252
613
|
}
|
|
253
614
|
if (c === '/' && next === '*') {
|
|
254
|
-
|
|
255
|
-
while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) { out[i] = ' '; i++; }
|
|
256
|
-
if (i < src.length) { out[i] = ' '; out[i + 1] = ' '; i += 2; }
|
|
615
|
+
i = maskBlockComment(src, out, i);
|
|
257
616
|
continue;
|
|
258
617
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const before = src.substring(0, i).replace(/\s+$/, '');
|
|
264
|
-
const prev = before[before.length - 1];
|
|
265
|
-
const prevWord = /(?:^|[^\w$])(return|typeof|case|in|of|new|delete|void|instanceof)$/.test(before);
|
|
266
|
-
if (prev === undefined || /[=(,:[!&|?{};+\-*%^~<>]/.test(prev) || prevWord) {
|
|
267
|
-
let j = i + 1;
|
|
268
|
-
let inClass = false;
|
|
269
|
-
let closed = false;
|
|
270
|
-
while (j < src.length) {
|
|
271
|
-
const d = src[j];
|
|
272
|
-
if (d === '\\') { j += 2; continue; }
|
|
273
|
-
if (d === '\n') break;
|
|
274
|
-
if (d === '[') inClass = true;
|
|
275
|
-
else if (d === ']') inClass = false;
|
|
276
|
-
else if (d === '/' && !inClass) { closed = true; break; }
|
|
277
|
-
j++;
|
|
278
|
-
}
|
|
279
|
-
if (closed) {
|
|
280
|
-
for (let k = i; k <= j; k++) out[k] = ' ';
|
|
281
|
-
i = j + 1;
|
|
282
|
-
while (i < src.length && /[a-z]/i.test(src[i])) { out[i] = ' '; i++; }
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
618
|
+
if (c === '/' && opensRegexLiteral(src, i)) {
|
|
619
|
+
const resumed = maskRegexLiteral(src, out, i);
|
|
620
|
+
// -1/findRegexClose failure keeps start, so `i` only advances on a match.
|
|
621
|
+
if (resumed !== i) { i = resumed; continue; }
|
|
286
622
|
}
|
|
287
|
-
|
|
288
623
|
if (c === "'" || c === '"' || c === '`') {
|
|
289
|
-
|
|
290
|
-
out[i] = ' ';
|
|
291
|
-
i++;
|
|
292
|
-
while (i < src.length) {
|
|
293
|
-
const d = src[i];
|
|
294
|
-
if (d === '\\') { out[i] = ' '; if (i + 1 < src.length) out[i + 1] = ' '; i += 2; continue; }
|
|
295
|
-
if (d === quote) { out[i] = ' '; i++; break; }
|
|
296
|
-
if (d === '\n' && quote !== '`') break;
|
|
297
|
-
out[i] = ' ';
|
|
298
|
-
i++;
|
|
299
|
-
}
|
|
624
|
+
i = maskStringLiteral(src, out, i);
|
|
300
625
|
continue;
|
|
301
626
|
}
|
|
302
627
|
|
|
@@ -513,15 +838,31 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
|
|
|
513
838
|
}
|
|
514
839
|
|
|
515
840
|
extractFunctions(): unknown[] {
|
|
516
|
-
const src = this.fileContent;
|
|
517
841
|
// Match declarations against the masked source so identifiers that appear
|
|
518
842
|
// only in comments or strings are never mistaken for functions, while
|
|
519
843
|
// bodies/positions still come from the original text (same offsets).
|
|
520
|
-
const masked = this.maskedSource;
|
|
521
|
-
const found: FunctionMetrics[] = [];
|
|
522
844
|
const seen = new Set<string>();
|
|
845
|
+
const found: FunctionMetrics[] = [];
|
|
846
|
+
// Three independent syntactic shapes, one collector each. Splitting them out
|
|
847
|
+
// of the single CCN-20 body also keeps each shape's quirks (see the comments
|
|
848
|
+
// on the collectors) next to the code they explain.
|
|
849
|
+
this.collectFunctionDecls(seen, found);
|
|
850
|
+
this.collectClassMethods(seen, found);
|
|
851
|
+
this.collectArrowFunctions(seen, found);
|
|
852
|
+
return found;
|
|
853
|
+
}
|
|
523
854
|
|
|
524
|
-
|
|
855
|
+
/** Claim a `name@offset` key once, so overlapping shapes cannot double-count. */
|
|
856
|
+
private claim(seen: Set<string>, name: string, nameIndex: number): boolean {
|
|
857
|
+
const key = `${name}@${nameIndex}`;
|
|
858
|
+
if (seen.has(key)) return false;
|
|
859
|
+
seen.add(key);
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** `function name(...)` declarations, including export / async / generators. */
|
|
864
|
+
private collectFunctionDecls(seen: Set<string>, found: FunctionMetrics[]): void {
|
|
865
|
+
const masked = this.maskedSource;
|
|
525
866
|
const declRe = /(?:^|[^\w$.])(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(\w+)\s*/g;
|
|
526
867
|
let m: RegExpExecArray | null;
|
|
527
868
|
while ((m = declRe.exec(masked)) !== null) {
|
|
@@ -530,16 +871,20 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
|
|
|
530
871
|
if (parenIndex === -1) continue;
|
|
531
872
|
const bodyOpen = findBodyBrace(masked, parenIndex);
|
|
532
873
|
if (bodyOpen === -1) continue;
|
|
533
|
-
|
|
534
|
-
if (seen.has(key)) continue;
|
|
535
|
-
seen.add(key);
|
|
874
|
+
if (!this.claim(seen, m[1], nameIndex)) continue;
|
|
536
875
|
found.push(this.buildFunction(m[1], 'function', nameIndex, parenIndex, bodyOpen));
|
|
537
876
|
}
|
|
877
|
+
}
|
|
538
878
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
879
|
+
/**
|
|
880
|
+
* Class methods: `name(...) {`. Not anchored to line start so minified
|
|
881
|
+
* single-line bodies count too. Control-flow keywords are excluded -- a bare
|
|
882
|
+
* `if (a > 0) {` matches the shape but is not a method.
|
|
883
|
+
*/
|
|
884
|
+
private collectClassMethods(seen: Set<string>, found: FunctionMetrics[]): void {
|
|
885
|
+
const masked = this.maskedSource;
|
|
542
886
|
const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
|
|
887
|
+
let m: RegExpExecArray | null;
|
|
543
888
|
while ((m = memberRe.exec(masked)) !== null) {
|
|
544
889
|
if (CONTROL_KEYWORDS.has(m[1])) continue;
|
|
545
890
|
// The pattern ends with `\(`, so the match's last character IS the opening
|
|
@@ -550,73 +895,98 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
|
|
|
550
895
|
// Only treat as a method when a body brace follows the parameter list.
|
|
551
896
|
const bodyOpen = findBodyBrace(masked, parenIndex);
|
|
552
897
|
if (bodyOpen === -1) continue;
|
|
553
|
-
|
|
554
|
-
if (seen.has(key)) continue;
|
|
555
|
-
seen.add(key);
|
|
898
|
+
if (!this.claim(seen, m[1], nameIndex)) continue;
|
|
556
899
|
found.push(this.buildFunction(m[1], 'method', nameIndex, parenIndex, bodyOpen));
|
|
557
900
|
}
|
|
901
|
+
}
|
|
558
902
|
|
|
559
|
-
|
|
903
|
+
/** Arrow consts / assignments: `const name = (...) =>` / `name = x =>`. */
|
|
904
|
+
private collectArrowFunctions(seen: Set<string>, found: FunctionMetrics[]): void {
|
|
905
|
+
const masked = this.maskedSource;
|
|
560
906
|
const arrowRe = /(?:const|let|var)\s+(\w+)\s*(?::[^=]+)?=\s*(?:async\s*)?/g;
|
|
907
|
+
let m: RegExpExecArray | null;
|
|
561
908
|
while ((m = arrowRe.exec(masked)) !== null) {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
let parenIndex: number;
|
|
565
|
-
if (parenOffset !== -1) {
|
|
566
|
-
parenIndex = arrowRe.lastIndex + parenOffset;
|
|
567
|
-
} else {
|
|
568
|
-
// Single-identifier param: `x => ...`
|
|
569
|
-
const simple = /^\s*(\w+)\s*=>/.exec(rest);
|
|
570
|
-
if (!simple) continue;
|
|
571
|
-
parenIndex = -1;
|
|
572
|
-
}
|
|
573
|
-
const afterParams = parenIndex === -1
|
|
574
|
-
? arrowRe.lastIndex + (/^\s*(\w+)/.exec(rest) as RegExpExecArray)[0].length
|
|
575
|
-
: readParenGroup(src, parenIndex).end;
|
|
576
|
-
if (!/^\s*(?::[^=]+)?=>/.test(masked.substring(afterParams))) continue;
|
|
577
|
-
|
|
909
|
+
const arrow = this.parseArrowAt(m, arrowRe.lastIndex);
|
|
910
|
+
if (!arrow) continue;
|
|
578
911
|
const nameIndex = m.index + m[0].length - m[1].length;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
let length = 1;
|
|
585
|
-
let nestingDepth = 0;
|
|
586
|
-
let hasTryCatch = false;
|
|
587
|
-
let ioOperations: string[] = [];
|
|
588
|
-
let code = '';
|
|
589
|
-
if (arrowBodyStart !== -1) {
|
|
590
|
-
const bodyEnd = findBlockEnd(masked, arrowBodyStart);
|
|
591
|
-
code = src.substring(arrowBodyStart, bodyEnd);
|
|
592
|
-
const startLine = lineOf(src, nameIndex);
|
|
593
|
-
const endLine = lineOf(src, bodyEnd);
|
|
594
|
-
length = Math.max(1, endLine - startLine + 1);
|
|
595
|
-
nestingDepth = maxNesting(code);
|
|
596
|
-
hasTryCatch = /\btry\s*\{/.test(code);
|
|
597
|
-
ioOperations = ioOperationsIn(code);
|
|
598
|
-
}
|
|
912
|
+
if (!this.claim(seen, m[1], nameIndex)) continue;
|
|
913
|
+
found.push(this.buildArrowMetrics(m[1], nameIndex, arrow));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
599
916
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
917
|
+
/**
|
|
918
|
+
* Resolve an arrow assignment's parameter list and body brace.
|
|
919
|
+
* @returns null when the text after the assignment is not an arrow function.
|
|
920
|
+
*/
|
|
921
|
+
private parseArrowAt(
|
|
922
|
+
m: RegExpExecArray,
|
|
923
|
+
scanFrom: number,
|
|
924
|
+
): { parenIndex: number; afterParams: number; bodyOpen: number } | null {
|
|
925
|
+
const masked = this.maskedSource;
|
|
926
|
+
const rest = masked.substring(scanFrom);
|
|
927
|
+
const parenOffset = /^\s*\(/.test(rest) ? rest.indexOf('(') : -1;
|
|
928
|
+
let parenIndex: number;
|
|
929
|
+
if (parenOffset !== -1) {
|
|
930
|
+
parenIndex = scanFrom + parenOffset;
|
|
931
|
+
} else {
|
|
932
|
+
// Single-identifier param: `x => ...`
|
|
933
|
+
if (!/^\s*(\w+)\s*=>/.test(rest)) return null;
|
|
934
|
+
parenIndex = -1;
|
|
935
|
+
}
|
|
936
|
+
const afterParams = parenIndex === -1
|
|
937
|
+
? scanFrom + (/^\s*(\w+)/.exec(rest) as RegExpExecArray)[0].length
|
|
938
|
+
: readParenGroup(this.fileContent, parenIndex).end;
|
|
939
|
+
if (!/^\s*(?::[^=]+)?=>/.test(masked.substring(afterParams))) return null;
|
|
940
|
+
|
|
941
|
+
const braceAfter = masked.indexOf('{', afterParams);
|
|
942
|
+
const isBlockBody = braceAfter !== -1 &&
|
|
943
|
+
/^\s*(?::[^=]+)?=>\s*\{/.test(masked.substring(afterParams, braceAfter + 1));
|
|
944
|
+
return { parenIndex, afterParams, bodyOpen: isBlockBody ? braceAfter : -1 };
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/** Derive the metrics for one arrow function, whether or not it has a block body. */
|
|
948
|
+
private buildArrowMetrics(
|
|
949
|
+
name: string,
|
|
950
|
+
nameIndex: number,
|
|
951
|
+
arrow: { parenIndex: number; afterParams: number; bodyOpen: number },
|
|
952
|
+
): FunctionMetrics {
|
|
953
|
+
const src = this.fileContent;
|
|
954
|
+
const startLine = lineOf(src, nameIndex);
|
|
955
|
+
|
|
956
|
+
// A concise body (`() => x + 1`) has no block, so it carries no measurable
|
|
957
|
+
// span: length 1, no nesting, no io. Only a block body is inspected.
|
|
958
|
+
if (arrow.bodyOpen === -1) {
|
|
959
|
+
return {
|
|
960
|
+
name,
|
|
606
961
|
type: 'arrow',
|
|
607
962
|
line: startLine,
|
|
608
963
|
startLine,
|
|
609
964
|
endLine: startLine,
|
|
610
|
-
length,
|
|
611
|
-
paramCount: parenIndex === -1 ? 1 : countParams(readParenGroup(src, parenIndex).inner),
|
|
612
|
-
nestingDepth,
|
|
613
|
-
hasTryCatch,
|
|
614
|
-
ioOperations,
|
|
615
|
-
code,
|
|
616
|
-
}
|
|
965
|
+
length: 1,
|
|
966
|
+
paramCount: arrow.parenIndex === -1 ? 1 : countParams(readParenGroup(src, arrow.parenIndex).inner),
|
|
967
|
+
nestingDepth: 0,
|
|
968
|
+
hasTryCatch: false,
|
|
969
|
+
ioOperations: [],
|
|
970
|
+
code: '',
|
|
971
|
+
};
|
|
617
972
|
}
|
|
618
973
|
|
|
619
|
-
|
|
974
|
+
const bodyEnd = findBlockEnd(this.maskedSource, arrow.bodyOpen);
|
|
975
|
+
const code = src.substring(arrow.bodyOpen, bodyEnd);
|
|
976
|
+
const endLine = lineOf(src, bodyEnd);
|
|
977
|
+
return {
|
|
978
|
+
name,
|
|
979
|
+
type: 'arrow',
|
|
980
|
+
line: startLine,
|
|
981
|
+
startLine,
|
|
982
|
+
endLine: startLine,
|
|
983
|
+
length: Math.max(1, endLine - startLine + 1),
|
|
984
|
+
paramCount: arrow.parenIndex === -1 ? 1 : countParams(readParenGroup(src, arrow.parenIndex).inner),
|
|
985
|
+
nestingDepth: maxNesting(code),
|
|
986
|
+
hasTryCatch: /\btry\s*\{/.test(code),
|
|
987
|
+
ioOperations: ioOperationsIn(code),
|
|
988
|
+
code,
|
|
989
|
+
};
|
|
620
990
|
}
|
|
621
991
|
|
|
622
992
|
extractClasses(): unknown[] {
|