@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.
- package/gate-12-file-hygiene.sh +26 -4
- package/gate-13-compiler-strictness.sh +12 -0
- package/gates/gate-3.ts +77 -21
- package/gates/gate-8.ts +39 -26
- package/gates/gate-9.ts +4 -4
- package/hooks/gate-12-file-hygiene.sh +26 -4
- package/hooks/gate-13-compiler-strictness.sh +12 -0
- package/hooks/pre-commit +82 -12
- package/hooks/pre-push +11 -2
- package/lib/__tests__/doctor.test.js +9 -0
- package/lib/__tests__/gate-numbering.test.js +52 -0
- package/lib/__tests__/gate-runner.test.js +59 -14
- package/lib/__tests__/shared-utils.test.js +47 -0
- package/lib/gate-runner.js +98 -18
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-utils.js +27 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/mutation/runners/mutmut-runner.ts +165 -70
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/claude-code/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/dsh/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/src/delphi-findings.test.ts +32 -6
- package/plugins/dsh/src/exit-code.test.ts +6 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/opencode/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/qoder/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/adapters/import-clause.ts +57 -0
- package/principles/adapters/member-scan.ts +73 -0
- package/principles/adapters/position-analysis.ts +43 -0
- package/principles/adapters/quote-state.ts +80 -0
- package/principles/adapters/typescript.ts +57 -116
- package/principles/rules/clean-code/god-class.ts +37 -14
- package/principles/rules/solid/dip.ts +15 -2
- package/scripts/__tests__/sync-package-content.test.js +114 -46
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- 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
|
|
736
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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 {
|