@boyingliu01/xp-gate 0.35.15 → 0.35.17
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/bin/xp-gate.js +67 -6
- package/gates/gate-9.ts +26 -4
- package/hooks/pre-commit +33 -10
- package/lib/__tests__/audit-cli-contract.test.ts +117 -0
- package/lib/__tests__/gate-coverage-hook-floor.test.js +94 -0
- package/lib/__tests__/gate-entry-provenance.test.js +71 -0
- package/lib/gate-coverage.js +46 -3
- package/lib/gate-runner.js +10 -2
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- 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 +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/package.json +1 -1
- 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/opencode/README.md +22 -1
- package/plugins/opencode/contract-v2.test.ts +125 -0
- package/plugins/opencode/exec-injection.test.ts +110 -0
- package/plugins/opencode/index.ts +39 -4
- package/plugins/opencode/package.json +2 -7
- 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/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/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -470,11 +470,24 @@ function gateArchTool(directory: string) {
|
|
|
470
470
|
|
|
471
471
|
function readCurrentSessionModel(dbPath: string, sessionId: string): string {
|
|
472
472
|
try {
|
|
473
|
+
// execFileSync, NOT execSync (#639). `sessionId` comes from the caller's
|
|
474
|
+
// argument, and the previous form interpolated it into a shell string:
|
|
475
|
+
//
|
|
476
|
+
// `sqlite3 "${dbPath}" "SELECT model FROM session WHERE id = '${escapedId}'"`
|
|
477
|
+
//
|
|
478
|
+
// Escaping only the SQL single quote left every SHELL metacharacter live --
|
|
479
|
+
// a session id containing `"`, `$( )` or a backtick escaped the quoting and
|
|
480
|
+
// executed. Passing argv as an array removes the shell from the path
|
|
481
|
+
// entirely, so no escaping is needed or possible to get wrong.
|
|
482
|
+
//
|
|
483
|
+
// The SQL single quote is still escaped: that is a SQL concern, not a shell
|
|
484
|
+
// one, and it survives the move to argv unchanged.
|
|
473
485
|
const escapedId = sessionId.replace(/'/g, "''")
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
486
|
+
const sql = `SELECT model FROM session WHERE id = '${escapedId}'`
|
|
487
|
+
const currentRaw = execFileSync("sqlite3", [dbPath, sql], {
|
|
488
|
+
encoding: "utf8",
|
|
489
|
+
timeout: SQLITE_TIMEOUT_MS,
|
|
490
|
+
}).trim()
|
|
478
491
|
if (!currentRaw) return "unknown"
|
|
479
492
|
try {
|
|
480
493
|
const parsed = JSON.parse(currentRaw)
|
|
@@ -664,9 +677,31 @@ export const XpGatePlugin = async (input: OpenCodePluginInput) => {
|
|
|
664
677
|
}
|
|
665
678
|
}
|
|
666
679
|
|
|
680
|
+
/**
|
|
681
|
+
* The plugin module, shaped for BOTH OpenCode generations (#639).
|
|
682
|
+
*
|
|
683
|
+
* v1 read `server`. v2 validates the default export against a schema that
|
|
684
|
+
* requires `id` plus exactly one of `effect` / `setup`, and rejects anything
|
|
685
|
+
* else outright:
|
|
686
|
+
*
|
|
687
|
+
* PluginModule.LoadError: Plugin must export a default definition with an id
|
|
688
|
+
* and an effect or setup function.
|
|
689
|
+
* (cause: SchemaError(Missing key at ["default"]["effect"] / ["default"]["setup"]))
|
|
690
|
+
*
|
|
691
|
+
* The failure mode is total, not partial: the plugin does not load at all, so
|
|
692
|
+
* `gate-check` / `gate-principles` / `gate-arch` simply vanish from v2 sessions
|
|
693
|
+
* and every startup prints a load error.
|
|
694
|
+
*
|
|
695
|
+
* `setup` and the v1 `server` take the same input and return the same shape, so
|
|
696
|
+
* the same function serves both keys. Both are exported deliberately: dropping
|
|
697
|
+
* `server` would break every v1 user on upgrade, which is a migration nobody
|
|
698
|
+
* asked for. `pluginModule` is not frozen, so this stays a plain object literal
|
|
699
|
+
* that both runtimes can read.
|
|
700
|
+
*/
|
|
667
701
|
const pluginModule = {
|
|
668
702
|
id: "xp-gate",
|
|
669
703
|
server: XpGatePlugin,
|
|
704
|
+
setup: XpGatePlugin,
|
|
670
705
|
}
|
|
671
706
|
|
|
672
707
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/opencode-plugin",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
-
"description": "XP-Gate quality gates + AI workflow skills
|
|
6
|
+
"description": "XP-Gate quality gates + AI workflow skills for OpenCode",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/boyingliu01/xp-gate",
|
|
@@ -17,15 +17,10 @@
|
|
|
17
17
|
".": {
|
|
18
18
|
"import": "./index.ts",
|
|
19
19
|
"types": "./index.ts"
|
|
20
|
-
},
|
|
21
|
-
"./tui": {
|
|
22
|
-
"import": "./tui-plugin.tsx",
|
|
23
|
-
"types": "./tui-plugin.tsx"
|
|
24
20
|
}
|
|
25
21
|
},
|
|
26
22
|
"files": [
|
|
27
23
|
"index.ts",
|
|
28
|
-
"tui-plugin.ts",
|
|
29
24
|
"skills/",
|
|
30
25
|
"tsconfig.json",
|
|
31
26
|
"README.md"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.17.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.17.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.17.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.17",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.17.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.17.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.17.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-31
|
|
4
|
+
**Commit:** 26a741de
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.17.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -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
|
+
|