@boyingliu01/xp-gate 0.35.16 → 0.35.18

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.
Files changed (75) hide show
  1. package/gate-12-file-hygiene.sh +26 -4
  2. package/gate-13-compiler-strictness.sh +12 -0
  3. package/gates/gate-3.ts +77 -21
  4. package/gates/gate-8.ts +39 -26
  5. package/gates/gate-9.ts +4 -4
  6. package/hooks/gate-12-file-hygiene.sh +26 -4
  7. package/hooks/gate-13-compiler-strictness.sh +12 -0
  8. package/hooks/pre-commit +82 -12
  9. package/hooks/pre-push +11 -2
  10. package/lib/__tests__/doctor.test.js +9 -0
  11. package/lib/__tests__/gate-numbering.test.js +52 -0
  12. package/lib/__tests__/gate-runner.test.js +59 -14
  13. package/lib/__tests__/shared-utils.test.js +47 -0
  14. package/lib/gate-runner.js +98 -18
  15. package/lib/mattpocock-license-verdict.json +1 -1
  16. package/lib/shared-utils.js +27 -1
  17. package/mock-policy/AGENTS.md +2 -2
  18. package/mutation/AGENTS.md +2 -2
  19. package/mutation/runners/mutmut-runner.ts +165 -70
  20. package/package.json +1 -1
  21. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  22. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  23. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  24. package/plugins/claude-code/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  25. package/plugins/claude-code/skills/sprint-flow/references/definition-of-done.md +47 -0
  26. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +16 -0
  27. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +14 -0
  28. package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  29. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  30. package/plugins/dsh/package.json +1 -1
  31. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  32. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  33. package/plugins/dsh/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  34. package/plugins/dsh/skills/sprint-flow/references/definition-of-done.md +47 -0
  35. package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +16 -0
  36. package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +14 -0
  37. package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  38. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  39. package/plugins/dsh/src/delphi-findings.test.ts +32 -6
  40. package/plugins/dsh/src/exit-code.test.ts +6 -0
  41. package/plugins/opencode/package.json +1 -1
  42. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  43. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  44. package/plugins/opencode/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  45. package/plugins/opencode/skills/sprint-flow/references/definition-of-done.md +47 -0
  46. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +16 -0
  47. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +14 -0
  48. package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  49. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  50. package/plugins/qoder/plugin.json +1 -1
  51. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  52. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  53. package/plugins/qoder/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  54. package/plugins/qoder/skills/sprint-flow/references/definition-of-done.md +47 -0
  55. package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +16 -0
  56. package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +14 -0
  57. package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  58. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  59. package/principles/AGENTS.md +2 -2
  60. package/principles/adapters/import-clause.ts +57 -0
  61. package/principles/adapters/member-scan.ts +73 -0
  62. package/principles/adapters/position-analysis.ts +43 -0
  63. package/principles/adapters/quote-state.ts +80 -0
  64. package/principles/adapters/typescript.ts +57 -116
  65. package/principles/rules/clean-code/god-class.ts +37 -14
  66. package/principles/rules/solid/dip.ts +15 -2
  67. package/scripts/__tests__/sync-package-content.test.js +114 -46
  68. package/skills/delphi-review/AGENTS.md +2 -2
  69. package/skills/sprint-flow/AGENTS.md +2 -2
  70. package/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  71. package/skills/sprint-flow/references/definition-of-done.md +47 -0
  72. package/skills/sprint-flow/references/phase-4-verify.md +16 -0
  73. package/skills/sprint-flow/references/phase-5-ship.md +14 -0
  74. package/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  75. package/skills/test-specification-alignment/AGENTS.md +2 -2
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Import-clause parsing for the TypeScript adapter's `imports` getter.
3
+ *
4
+ * Extracted from `typescript.ts` so that one function does not have to carry
5
+ * both the parsing rules and the usage-scoring loop: together they pushed the
6
+ * getter past the 50-line budget and to 5 nesting levels. Splitting also lets
7
+ * the clause grammar be tested without reading a file.
8
+ */
9
+
10
+ /** One symbol bound by an import statement. */
11
+ export interface ImportBinding {
12
+ name: string;
13
+ type?: string;
14
+ }
15
+
16
+ /** A plain identifier -- these are the only names an import can bind. */
17
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
18
+
19
+ /**
20
+ * Parse the clause between `import` and `from` into the symbols it binds.
21
+ *
22
+ * Covers the four shapes TypeScript allows: a default binding, a named brace
23
+ * list (with per-member `type` and `as` renames), and a namespace binding.
24
+ *
25
+ * @param clause text between `import` / `import type` and `from`
26
+ * @param isTypeOnly whether the whole statement was `import type ...`
27
+ */
28
+ export function parseImportClause(clause: string, isTypeOnly: boolean): ImportBinding[] {
29
+ const bindings: ImportBinding[] = [];
30
+ const marker = isTypeOnly ? 'type' : undefined;
31
+ const push = (name: string, type?: string): void => {
32
+ if (IDENTIFIER.test(name)) bindings.push({ name, ...(type ? { type } : {}) });
33
+ };
34
+
35
+ // default import
36
+ const def = /^(\w+)/.exec(clause);
37
+ if (def) push(def[1], marker);
38
+
39
+ // named imports { a, b as c }
40
+ const braces = /\{([\s\S]*?)\}/.exec(clause);
41
+ if (braces) {
42
+ for (const raw of braces[1].split(',')) {
43
+ const part = raw.trim();
44
+ if (!part) continue;
45
+ const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
46
+ const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
47
+ const memberTypeOnly = /^type\s+/.test(part);
48
+ push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
49
+ }
50
+ }
51
+
52
+ // namespace import * as ns
53
+ const ns = /\*\s+as\s+(\w+)/.exec(clause);
54
+ if (ns) push(ns[1], marker);
55
+
56
+ return bindings;
57
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Class-member scanning for the TypeScript adapter.
3
+ *
4
+ * Extracted from `typescript.ts` to keep that file inside its line budget, and
5
+ * because this is the piece that was wrong: counting members by looking at the
6
+ * character BEFORE a name reported every call site inside a method body as a
7
+ * member. `{ helper(1); }` and `{ async run() { ... }` differ in what follows the
8
+ * name, not in what precedes it -- so depth is what separates them.
9
+ */
10
+
11
+ /** Words that look like `name(` but are control flow, never members. */
12
+ const CONTROL_KEYWORDS = new Set([
13
+ 'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new',
14
+ ]);
15
+
16
+ /** Class body members live one level in: the body string starts AT its `{`. */
17
+ const MEMBER_DEPTH = 1;
18
+
19
+ /**
20
+ * Names of the members declared directly in a class body.
21
+ *
22
+ * Depth-aware rather than prefix-aware: a member declaration sits at brace
23
+ * depth 1 of the body, and a call inside a method sits deeper. Nesting is
24
+ * tracked over the MASKED body, so braces inside strings and comments cannot
25
+ * shift the depth -- prose in a JSDoc line like
26
+ * "three separate events (close / error / timeout)" otherwise reads as
27
+ * `events(` and is counted as a method, as is `unknown` from `(): unknown {`.
28
+ *
29
+ * Arrow-function properties (`handler = () => {}`) carry no `name(` shape and
30
+ * are not counted; they were not counted before either.
31
+ */
32
+ export function countTopLevelMembers(body: string): string[] {
33
+ const names: string[] = [];
34
+ let depth = 0;
35
+ for (let i = 0; i < body.length; i += 1) {
36
+ const c = body[i];
37
+ if (c === '{') { depth += 1; continue; }
38
+ if (c === '}') { depth -= 1; continue; }
39
+ if (depth !== MEMBER_DEPTH || c !== '(') continue;
40
+ const name = memberNameBefore(body, i);
41
+ if (name && !CONTROL_KEYWORDS.has(name.value)) names.push(name.value);
42
+ }
43
+ return names;
44
+ }
45
+
46
+ /**
47
+ * The identifier immediately before the `(` at `parenIndex`, skipping modifiers
48
+ * (`public`, `static`, `async`, `readonly`, `get`, `set`, `*`).
49
+ *
50
+ * @returns the name and its offset, or null when no identifier precedes the paren.
51
+ */
52
+ function memberNameBefore(
53
+ body: string,
54
+ parenIndex: number,
55
+ ): { value: string; index: number } | null {
56
+ let end = parenIndex;
57
+ while (end > 0 && /\s/.test(body[end - 1])) end -= 1;
58
+ let start = end;
59
+ while (start > 0 && /[\w$]/.test(body[start - 1])) start -= 1;
60
+ if (start === end) return null;
61
+ return { value: body.substring(start, end), index: start };
62
+ }
63
+
64
+ /**
65
+ * Claim a `name@offset` key once, so overlapping extraction shapes cannot
66
+ * double-count the same declaration.
67
+ */
68
+ export function claimMember(seen: Set<string>, name: string, nameIndex: number): boolean {
69
+ const key = `${name}@${nameIndex}`;
70
+ if (seen.has(key)) return false;
71
+ seen.add(key);
72
+ return true;
73
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Position analysis for TypeScript's regex-based extraction.
3
+ *
4
+ * The extraction regexes match `name(` for both a method DECLARATION and a CALL.
5
+ * This module answers which one it is, keeping `typescript.ts` inside its line
6
+ * budget and giving the rule a place to be tested directly.
7
+ */
8
+
9
+ /**
10
+ * Characters that can only precede a CALL, never a declaration.
11
+ *
12
+ * `.` covers member calls (`this.c()`, `obj.method()`), `(` a call used as an
13
+ * argument (`if (existsSync(p))`), `,` a call in a list (`foo(a, bar(b))`), and
14
+ * the operators cover a call in a condition or expression
15
+ * (`hadExisting && existsSync(p)`, `x = read(p)`, `!isReady(q)`). No member
16
+ * declaration begins with any of them.
17
+ *
18
+ * Deliberately NOT included: `{`, `;`, `}` and plain whitespace. Those precede
19
+ * BOTH shapes -- `{ helper(1); }` is a call, `{ run(): void {` is a declaration
20
+ * -- so using them makes the answer depend on formatting. Depth is what
21
+ * separates those two, which is why extractClasses counts by brace depth.
22
+ */
23
+ const CALL_PRECEDERS = new Set([
24
+ '.', '(', ',',
25
+ '&', '|', '=', '!', '?', ':', '+', '-', '*', '/', '<', '>', '[',
26
+ ]);
27
+
28
+ /**
29
+ * Is the identifier starting at `nameIndex` a call rather than a declaration?
30
+ *
31
+ * Without this check `if (existsSync(p))` was extracted as a method named
32
+ * `existsSync` -- the extraction regex's lookbehind permits `(` before the name
33
+ * -- which then borrowed the enclosing block's brace as a "body" and reported a
34
+ * phantom `missing-error-handling` warning that no edit could clear. It also
35
+ * inflated `extractClasses().methodCount` with the names of whatever the class
36
+ * body happened to call (66 reported for a class declaring 30).
37
+ */
38
+ export function isCallPosition(masked: string, nameIndex: number): boolean {
39
+ let i = nameIndex - 1;
40
+ while (i >= 0 && /\s/.test(masked[i])) i -= 1;
41
+ return i >= 0 && CALL_PRECEDERS.has(masked[i]);
42
+ }
43
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Character-level quote and delimiter tracking for TypeScript source scanning.
3
+ *
4
+ * The param-list and brace scanners all need "is this delimiter inside a string
5
+ * or template?" before they can act on it. That logic is identical for every
6
+ * caller, so it lives here instead of being repeated -- and it keeps
7
+ * `typescript.ts` inside the 1050-line budget.
8
+ */
9
+
10
+ export interface QuoteState {
11
+ inSingle: boolean;
12
+ inDouble: boolean;
13
+ inTemplate: boolean;
14
+ }
15
+
16
+ export function newQuoteState(): QuoteState {
17
+ return { inSingle: false, inDouble: false, inTemplate: false };
18
+ }
19
+
20
+ export function inQuotedState(state: QuoteState): boolean {
21
+ return state.inSingle || state.inDouble || state.inTemplate;
22
+ }
23
+
24
+ /** The three quote kinds tracked by QuoteState, with the flag that marks each. */
25
+ const QUOTE_KINDS: ReadonlyArray<{ quote: string; flag: 'inSingle' | 'inDouble' | 'inTemplate' }> = [
26
+ { quote: "'", flag: 'inSingle' },
27
+ { quote: '"', flag: 'inDouble' },
28
+ { quote: '`', flag: 'inTemplate' },
29
+ ];
30
+
31
+ /**
32
+ * Advance one character of quote tracking.
33
+ *
34
+ * Extracted because `countParams` and `readParenGroup` carried the same
35
+ * six-branch block verbatim (#602): both must ignore delimiters that appear
36
+ * inside a string, and neither may treat an escape as a closing quote.
37
+ *
38
+ * Table-driven (#620): all three quote kinds behave identically -- an escape
39
+ * skips two characters, the matching quote closes the literal -- so they are one
40
+ * rule applied to a list rather than three copies of it.
41
+ *
42
+ * @returns the number of characters consumed (2 when the next char is escaped).
43
+ */
44
+ export function advanceQuoteState(src: string, i: number, state: QuoteState): number {
45
+ const c = src[i];
46
+
47
+ for (const kind of QUOTE_KINDS) {
48
+ if (!state[kind.flag]) continue;
49
+ if (c === '\\') return 2;
50
+ if (c === kind.quote) state[kind.flag] = false;
51
+ return 1;
52
+ }
53
+
54
+ // Not inside any literal: open the one this character starts, if any.
55
+ const opener = QUOTE_KINDS.find(kind => kind.quote === c);
56
+ if (opener) state[opener.flag] = true;
57
+ return 1;
58
+ }
59
+
60
+ /** Opening delimiters that raise the generic/bracket depth in a param list. */
61
+ const DEPTH_OPENERS = new Set(['(', '[', '{', '<']);
62
+ /** Closing delimiters that lower it. */
63
+ const DEPTH_CLOSERS = new Set([')', ']', '}', '>']);
64
+
65
+ /**
66
+ * Adjust `depth` for one delimiter.
67
+ * @returns true when the character was a delimiter (so it cannot also be a
68
+ * separator), false otherwise.
69
+ */
70
+ export function applyDelimiter(c: string, depth: { value: number }): boolean {
71
+ if (DEPTH_OPENERS.has(c)) { depth.value++; return true; }
72
+ if (DEPTH_CLOSERS.has(c)) { depth.value--; return true; }
73
+ return false;
74
+ }
75
+
76
+ /** True when the character opens a quote/template the scanners must skip. */
77
+ export function isQuote(c: string): boolean {
78
+ return c === "'" || c === '"' || c === '`';
79
+ }
80
+
@@ -1,5 +1,11 @@
1
1
  import { BaseAdapter } from './base';
2
2
  import { Adapter } from '../types';
3
+ import { isCallPosition } from './position-analysis';
4
+ import { claimMember, countTopLevelMembers } from './member-scan';
5
+ import { parseImportClause } from './import-clause';
6
+ import {
7
+ type QuoteState, newQuoteState, inQuotedState, advanceQuoteState, applyDelimiter, isQuote,
8
+ } from './quote-state';
3
9
 
4
10
  /** IO-ish callees that the missing-error-handling rule cares about. */
5
11
  const IO_CALLEES = [
@@ -181,77 +187,6 @@ function lineOf(src: string, index: number): number {
181
187
  }
182
188
 
183
189
  /** 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
-
255
190
  /** Count top-level, comma-separated parameters in a raw parameter list. */
256
191
  function countParams(rawList: string): number {
257
192
  const trimmed = rawList.trim();
@@ -726,37 +661,13 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
726
661
  const importRe = /^[ \t]*import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"][^'"]+['"]/gm;
727
662
  const importSpans: Array<[number, number]> = [];
728
663
  let m: RegExpExecArray | null;
729
- const idRe = /^[A-Za-z_$][\w$]*$/;
730
664
 
731
665
  while ((m = importRe.exec(src)) !== null) {
732
- const clause = m[1].trim();
733
666
  const line = lineOf(src, m.index);
734
667
  const isTypeOnly = /^import\s+type\b/.test(m[0]);
735
- const push = (name: string, type?: string) => {
736
- if (idRe.test(name)) entries.push({ name, line, used: false, ...(type ? { type } : {}) });
737
- };
738
-
739
- // default import
740
- const def = /^(\w+)/.exec(clause);
741
- if (def) push(def[1], isTypeOnly ? 'type' : undefined);
742
-
743
- // named imports { a, b as c }
744
- const braces = /\{([\s\S]*?)\}/.exec(clause);
745
- if (braces) {
746
- for (const raw of braces[1].split(',')) {
747
- const part = raw.trim();
748
- if (!part) continue;
749
- const asMatch = /\bas\s+(\w+)\s*$/.exec(part);
750
- const local = asMatch ? asMatch[1] : part.replace(/^type\s+/, '').trim();
751
- const memberTypeOnly = /^type\s+/.test(part);
752
- push(local, isTypeOnly || memberTypeOnly ? 'type' : undefined);
753
- }
668
+ for (const binding of parseImportClause(m[1].trim(), isTypeOnly)) {
669
+ entries.push({ ...binding, line, used: false });
754
670
  }
755
-
756
- // namespace import * as ns
757
- const ns = /\*\s+as\s+(\w+)/.exec(clause);
758
- if (ns) push(ns[1], isTypeOnly ? 'type' : undefined);
759
-
760
671
  importSpans.push([m.index, m.index + m[0].length]);
761
672
  }
762
673
 
@@ -831,7 +742,9 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
831
742
  length: hasBraces ? Math.max(1, endLine - startLine + 1) : 1,
832
743
  paramCount: countParams(inner),
833
744
  nestingDepth: hasBraces ? maxNesting(maskLiterals(body)) : 0,
834
- hasTryCatch: /\btry\s*\{/.test(masked.substring(bodyOpen, bodyEnd)),
745
+ // The RAW body: `on('error', ...)` needs its string literal intact, and
746
+ // masking blanks string bodies. try/catch is a keyword and is unaffected.
747
+ hasTryCatch: hasErrorHandling(body),
835
748
  ioOperations: ioOperationsIn(masked.substring(bodyOpen, bodyEnd)),
836
749
  code: body,
837
750
  };
@@ -853,13 +766,6 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
853
766
  }
854
767
 
855
768
  /** 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
769
  /** `function name(...)` declarations, including export / async / generators. */
864
770
  private collectFunctionDecls(seen: Set<string>, found: FunctionMetrics[]): void {
865
771
  const masked = this.maskedSource;
@@ -871,7 +777,7 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
871
777
  if (parenIndex === -1) continue;
872
778
  const bodyOpen = findBodyBrace(masked, parenIndex);
873
779
  if (bodyOpen === -1) continue;
874
- if (!this.claim(seen, m[1], nameIndex)) continue;
780
+ if (!claimMember(seen, m[1], nameIndex)) continue;
875
781
  found.push(this.buildFunction(m[1], 'function', nameIndex, parenIndex, bodyOpen));
876
782
  }
877
783
  }
@@ -892,10 +798,11 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
892
798
  // parameter list or a type annotation, which corrupted the reported span.
893
799
  const parenIndex = m.index + m[0].length - 1;
894
800
  const nameIndex = parenIndex - m[1].length;
801
+ if (isCallPosition(masked, nameIndex)) continue;
895
802
  // Only treat as a method when a body brace follows the parameter list.
896
803
  const bodyOpen = findBodyBrace(masked, parenIndex);
897
804
  if (bodyOpen === -1) continue;
898
- if (!this.claim(seen, m[1], nameIndex)) continue;
805
+ if (!claimMember(seen, m[1], nameIndex)) continue;
899
806
  found.push(this.buildFunction(m[1], 'method', nameIndex, parenIndex, bodyOpen));
900
807
  }
901
808
  }
@@ -909,7 +816,7 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
909
816
  const arrow = this.parseArrowAt(m, arrowRe.lastIndex);
910
817
  if (!arrow) continue;
911
818
  const nameIndex = m.index + m[0].length - m[1].length;
912
- if (!this.claim(seen, m[1], nameIndex)) continue;
819
+ if (!claimMember(seen, m[1], nameIndex)) continue;
913
820
  found.push(this.buildArrowMetrics(m[1], nameIndex, arrow));
914
821
  }
915
822
  }
@@ -1003,14 +910,19 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
1003
910
  if (bodyOpen === -1) continue;
1004
911
  const bodyEnd = findBlockEnd(masked, bodyOpen);
1005
912
  const body = src.substring(bodyOpen, bodyEnd);
1006
-
1007
- // Count method-ish members. Not anchored to line start: minified /
1008
- // single-line class bodies (`m1() {} m2() {}`) are valid TS and must count.
1009
- // `(?<![\w$.])` avoids matching call sites like `foo.bar(`.
1010
- const memberRe = /(?<![\w$.])(?:public\s+|private\s+|protected\s+|static\s+|async\s+|readonly\s+|get\s+|set\s+|\*\s*)*(\w+)\s*\(/g;
1011
- const methods = [...body.matchAll(memberRe)]
1012
- .map(x => x[1])
1013
- .filter(n => !['if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'new'].includes(n));
913
+ // Members are counted on the MASKED body: the prose in a JSDoc line like
914
+ // "three separate events (close / error / timeout)" reads as `events(` and
915
+ // was counted as a method, as did `unknown` from a `(): unknown {` return
916
+ // type. Masking blanks comments and string bodies, so only real code has
917
+ // braces and parens left.
918
+ const maskedBody = masked.substring(bodyOpen, bodyEnd);
919
+
920
+ // Count members declared at the class body's TOP brace depth. A call
921
+ // inside a method body sits one level deeper, which is the only signal
922
+ // that separates `{ helper(1); }` from `{ async run() { ... }`.
923
+ // Earlier attempts compared the preceding character and reported 66
924
+ // methods for a class declaring 30 -- every call site counted once.
925
+ const methods = countTopLevelMembers(maskedBody);
1014
926
 
1015
927
  results.push({
1016
928
  name,
@@ -1065,3 +977,32 @@ export class TypeScriptAdapter extends BaseAdapter implements Adapter {
1065
977
  return exportMatches;
1066
978
  }
1067
979
  }
980
+
981
+
982
+ /**
983
+ * Does this function handle failures?
984
+ *
985
+ * `try`/`catch` is one way, and for a function that owns a child process it is
986
+ * not the idiomatic one: Node reports spawn failures through an `'error'`
987
+ * event, and a `try` around `spawn()` would never see them. Treating the event
988
+ * listener as unhandled reported `runMutmut` for having "I/O without error
989
+ * handling" while it was in fact the only handler for that failure mode --
990
+ * a warning no correct edit could clear, which is how a rule loses its meaning.
991
+ *
992
+ * `'error'` is matched as a QUOTED argument (`on('error', ...)`) so that a
993
+ * string that merely contains the word does not qualify.
994
+ */
995
+ function hasErrorHandling(body: string): boolean {
996
+ return TRY_BLOCK.test(body) || ERROR_LISTENER.test(body);
997
+ }
998
+
999
+ /**
1000
+ * The two handler shapes, as module-level constants.
1001
+ *
1002
+ * ccn-report locates a function body by counting brackets on its lines; a regex
1003
+ * literal INSIDE the body makes that extent ambiguous, and the row comes back
1004
+ * "CCN could not be span-verified" -- a Gate 3 block. Brackets and braces in the
1005
+ * patterns are written as character classes for the same reason.
1006
+ */
1007
+ const TRY_BLOCK = /\btry\s*[{]/;
1008
+ const ERROR_LISTENER = /\bon\s*[(]\s*['"]error['"]\s*,/;
@@ -15,25 +15,26 @@ export const godClassRule: Rule = {
15
15
  code?: string;
16
16
  line: number;
17
17
  name: string;
18
+ /** Method count when the adapter parsed the class structurally. */
19
+ methodCount?: number;
18
20
  }
19
-
21
+
20
22
  interface TypedAdapter {
21
23
  extractClasses?: () => ClassObject[] | undefined;
22
24
  }
23
-
25
+
24
26
  const typedAdapter = adapter as TypedAdapter;
25
27
  const classes = typedAdapter.extractClasses?.() || [];
26
-
28
+
27
29
  for (const cls of classes) {
28
- const methodMatches = cls.code?.match(/(get|set)\s+\w+\s*\(|\w+\s*\([^)]*\)\s*{/g) || [];
29
-
30
- const methodCount = methodMatches.filter((m: string) => {
31
- if (m.startsWith('get ') || m.startsWith('set ')) {
32
- return false;
33
- }
34
- return true;
35
- }).length;
36
-
30
+ // Prefer the adapter's structural count. The regex fallback below cannot
31
+ // see methods whose signature spans lines or carries a return type, so on
32
+ // `TypeScriptAdapter` (66 methods) it counted 0 while `solid.srp` read the
33
+ // adapter's own figure and reported it -- the same class, two different
34
+ // answers, and the wrong one was silent. A count the adapter already
35
+ // computed must not be recomputed by guessing at source text.
36
+ const methodCount = cls.methodCount ?? countMethodsByRegex(cls.code);
37
+
37
38
  if (methodCount > (settings.threshold ?? 0)) {
38
39
  violations.push({
39
40
  file,
@@ -46,7 +47,29 @@ export const godClassRule: Rule = {
46
47
  });
47
48
  }
48
49
  }
49
-
50
+
50
51
  return violations;
51
52
  }
52
- };
53
+ };
54
+
55
+ /**
56
+ * Source-text method matcher.
57
+ *
58
+ * Kept at module scope on purpose: ccn-report locates a function body by counting
59
+ * brackets on its lines, so a regex literal inside the body makes the extent
60
+ * ambiguous and the row comes back "CCN could not be span-verified".
61
+ */
62
+ const METHOD_PATTERN = /(?:get|set)\s+\w+\s*[(]|\w+\s*[(][^)]*[)]\s*[{]/g;
63
+
64
+ /** A matched accessor keyword pair rather than a real method. */
65
+ const ACCESSOR_PATTERN = /^(?:get|set)\s/;
66
+
67
+ /**
68
+ * Count methods by matching source text. Only used when the adapter does not
69
+ * report `methodCount` (Java, C++ and other regex-backed adapters), so the
70
+ * getter/setter exclusion stays meaningful.
71
+ */
72
+ function countMethodsByRegex(code: string | undefined): number {
73
+ const methodMatches = code?.match(METHOD_PATTERN) ?? [];
74
+ return methodMatches.filter((m: string) => !ACCESSOR_PATTERN.test(m)).length;
75
+ }
@@ -1,8 +1,14 @@
1
1
  import { Rule, Violation, Severity } from '../../types';
2
2
  import { getActiveConfig } from '../../config';
3
3
 
4
+ // Value objects, not collaborators. Every entry here is a type that ordinary
5
+ // code CONSTRUCTS from data it already has, and that has no injectable
6
+ // alternative: you cannot pass in a Date/Map/RegExp factory any more usefully
7
+ // than you can pass in the number 1. Constructing one is not the inversion being
8
+ // bypassed, so reporting it teaches the wrong lesson (#676).
4
9
  const DEFAULT_EXCLUDED_CLASSES = [
5
- 'Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise'
10
+ 'Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise',
11
+ 'RegExp', 'Number', 'String', 'Boolean', 'BigInt', 'Symbol', 'WeakMap', 'WeakSet',
6
12
  ];
7
13
 
8
14
  /**
@@ -10,9 +16,16 @@ const DEFAULT_EXCLUDED_CLASSES = [
10
16
  *
11
17
  * A module-load snapshot was #457: a project's `.principlesrc` override was
12
18
  * parsed but never consulted.
19
+ *
20
+ * The config entry EXTENDS the built-in value-object list rather than replacing
21
+ * it (#676). Replacing meant a project that added one name silently lost every
22
+ * default exemption -- `.principlesrc` here lists 7 entries, so `new RegExp()`
23
+ * was reported even though the default list would have excused it. A project
24
+ * cannot reasonably be expected to restate the built-ins it never knew about.
13
25
  */
14
26
  function excludedClasses(): (string | number)[] {
15
- return getActiveConfig().rules['solid']['dip'].exclude ?? DEFAULT_EXCLUDED_CLASSES;
27
+ const configured = getActiveConfig().rules['solid']['dip'].exclude ?? [];
28
+ return [...new Set([...DEFAULT_EXCLUDED_CLASSES, ...configured])];
16
29
  }
17
30
 
18
31
  function shouldSkipInstantiation(className: string): boolean {