|
| 1 | +# Hand-off 1 — Emit `CallSite` nodes from the parser; retire the regex re-tokenizers |
| 2 | + |
| 3 | +**Effort:** L **Risk:** medium **Prerequisite for:** hand-off 2, phase B |
| 4 | + |
| 5 | +## Problem |
| 6 | + |
| 7 | +The parser (`server/src/analysis/ast/parser.ts`) stops at declaration level. Per |
| 8 | +function body it emits only: |
| 9 | +- `bodyTypeRefs: TypeNode[]` — static-call targets (`ClassName.` where ClassName |
| 10 | + is PascalCase), **deduped per body** (first occurrence only). |
| 11 | +- `bodyIdentifierRefs: BodyIdentifierRef[]` — standalone identifiers **not** |
| 12 | + preceded by `.` and **not** followed by `(`, also **deduped per body**. |
| 13 | + |
| 14 | +Neither captures call sites, member accesses, or every occurrence. So the |
| 15 | +diagnostics checkers re-derive call structure from **raw text with regex** on the |
| 16 | +per-keystroke path: |
| 17 | + |
| 18 | +- `checkFunctionCallArgs` (`graph.ts:6365`, ~350 lines) — regex-matches calls, |
| 19 | + then uses brittle heuristics to decide decl-vs-call (`/(?:void|int|float…)\s+$/` |
| 20 | + look-behind, backward `[` walks, `endsWith('new')`). These guards are the top |
| 21 | + false-positive source. |
| 22 | +- `checkTypeMismatches` (`graph.ts:5378`, ~200 lines) — same shape. |
| 23 | +- `parseCallArguments` (`graph.ts:5954`) — manually splits argument text on |
| 24 | + top-level commas (handles nested parens/brackets/strings/templates by hand). |
| 25 | + |
| 26 | +Cost: O(fileLength) regex passes on every keystroke, plus a maintenance maze. |
| 27 | + |
| 28 | +## Goal |
| 29 | + |
| 30 | +Have the parser emit a minimal, **complete** (non-deduped) `CallSite[]` for each |
| 31 | +function body, and migrate the two checkers to consume it — deleting the |
| 32 | +decl-vs-call heuristics and `parseCallArguments`. |
| 33 | + |
| 34 | +```ts |
| 35 | +interface CallSite { |
| 36 | + calleeName: string; |
| 37 | + calleeStart: Position; // start of the callee identifier |
| 38 | + calleeEnd: Position; |
| 39 | + argRanges: { start: number; end: number }[]; // char offsets of each top-level arg |
| 40 | + isDeclaration: boolean; // true for `Type name(...)`-style decls the parser already knows aren't calls |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +Add `callSites: CallSite[]` to `FunctionDeclNode` (parser.ts interface ~173). |
| 45 | + |
| 46 | +## Where to emit it |
| 47 | + |
| 48 | +The body loop already tracks `parenDepth` (parser.ts:702) and walks tokens with |
| 49 | +`prevPrev`/`prev`/`t`. When `prev` is an identifier and `t.value === '('` at the |
| 50 | +statement level, record a `CallSite`: `calleeName = prev.value`, positions from |
| 51 | +`prev.start/end`, and `argRanges` by scanning to the matching `)` and splitting on |
| 52 | +commas at the call's paren depth. `isDeclaration` is derivable from the same |
| 53 | +context the parser already has (a preceding type token / modifier run). |
| 54 | + |
| 55 | +Keep the emission O(n) — it must not regress the hot parse path. |
| 56 | + |
| 57 | +## Plan (incremental — one checker at a time) |
| 58 | + |
| 59 | +1. Add the `CallSite` interface + `callSites` field + emission in the body loop. |
| 60 | + Add **parser unit tests** for tricky inputs: nested calls `f(g(x), y)`, string |
| 61 | + args with commas `f("a,b", c)`, `new X()`, array args `f({1,2})`, chained |
| 62 | + `a.b().c()`. Assert `calleeName`, `argRanges`, `isDeclaration`. |
| 63 | +2. Migrate `checkFunctionCallArgs` to iterate `func.callSites` instead of its |
| 64 | + regex call-detection. **Diff diagnostics output on a corpus** — ranges must be |
| 65 | + byte-for-byte identical (the 303 tests assert exact ranges). Commit only when |
| 66 | + the diff is empty (or the deltas are verified improvements). |
| 67 | +3. Migrate `checkTypeMismatches` the same way. |
| 68 | +4. Delete `parseCallArguments` once no caller remains. |
| 69 | + |
| 70 | +## Do NOT touch |
| 71 | + |
| 72 | +`parseExpressionChainBackward`, `parseChainMembers`, `resolveChainReturnType` |
| 73 | +(the completion/hover path). They run on **incomplete cursor text** where the |
| 74 | +parser has no `CallSite`. Retiring those is hand-off 2, phase B. |
| 75 | + |
| 76 | +## Risks |
| 77 | + |
| 78 | +- **Exact ranges.** The parser-derived offsets must reproduce current highlight |
| 79 | + ranges exactly. Golden-diff before every commit. |
| 80 | +- **Hot path.** Emission runs on every keystroke parse — keep it O(n), no |
| 81 | + backtracking. |
| 82 | +- **`isDeclaration` correctness.** Getting this wrong flips real diagnostics; |
| 83 | + cover it heavily in the parser unit tests. |
| 84 | + |
| 85 | +## Done when |
| 86 | + |
| 87 | +- `checkFunctionCallArgs` and `checkTypeMismatches` no longer regex-scan for calls. |
| 88 | +- `parseCallArguments` is deleted. |
| 89 | +- Parser emits `callSites`; unit tests cover the tricky cases. |
| 90 | +- Diagnostics are byte-identical on the corpus; `npm test` green. |
0 commit comments