Skip to content

Commit 37d6f78

Browse files
committed
More refactoring
1 parent cc20ae2 commit 37d6f78

5 files changed

Lines changed: 284 additions & 920 deletions

File tree

docs/todo/refactor.md

Lines changed: 181 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -214,112 +214,143 @@ Each item must include:
214214

215215
# Outstanding items
216216

217-
## Shared AST walker — remaining candidates (needs a decision)
218-
219-
**Done so far.** The stateless boolean/name-set/collection walkers have
220-
been migrated to the generated `mago-syntax` `Walker` trait (each is now
221-
a small visitor over a shared traversal): the six detector pairs in
222-
`diagnostics/undefined_variables/` (dynamic-vars, `extract()`,
223-
`compact()`, `get_defined_vars()`, `@`-suppression, isset/empty guards),
224-
the statement-dispatch halves of `diagnostics/unused_variables.rs` and
225-
`diagnostics/type_errors.rs`, and the anonymous-class walker in
226-
`parser/anonymous.rs`. That removed ~1,900 lines of hand-rolled
227-
traversal.
228-
229-
**What remains — and why it is not a clean fit.** The remaining
230-
hand-rolled walkers are *not* stateless collectors; they are
231-
positional/order-sensitive, which the typed `Walker` cannot express
232-
without re-typing per-node logic (it has no single "visit any node"
233-
hook, only per-node-type `walk_in_*`):
234-
235-
- `selection_range.rs` pushes **synthetic** spans (brace pairs, paren
236-
pairs, block interiors) that are not AST-node spans, and would need a
237-
`walk_in_*` override for essentially every node type to reproduce
238-
them — no net reduction.
239-
- `symbol_map/extraction.rs` records position-keyed symbol spans and
240-
subject text; order and per-kind span shaping dominate, so the same
241-
per-node-override problem applies.
242-
- The `property_access.rs` / `property_narrowing.rs` pair walk **from
243-
the method start down to the cursor**, accumulating narrowing state in
244-
source order with early returns. That interleaved-mutation,
245-
cursor-bounded shape is exactly why the forward walker is out of scope
246-
(below).
247-
248-
The forward walker is explicitly out of scope (its traversal is
249-
interleaved with scope-state mutation).
250-
251-
**Decision needed.** These three are arguably better left as-is: the
252-
`Walker` migration's payoff (delete child-dispatch boilerplate) does not
253-
apply when the walk pushes synthetic/positional data or mutates state in
254-
source order. If the goal is purely "one traversal so new AST variants
255-
can't create blind spots," a thin `visit_all_nodes(&Node, &mut FnMut)`
256-
helper over mago's `Node` enum would serve `selection_range` and
257-
`symbol_map/extraction` better than the typed `Walker`. Confirm whether
258-
to (a) leave these as-is, (b) migrate `selection_range` /
259-
`symbol_map/extraction` via a `Node`-enum visitor, or (c) migrate the
260-
property pair despite the state-threading cost.
261-
262-
**Why it matters.** Every new mago AST node variant (new PHP syntax)
263-
still needs matching arms in these remaining walkers; missing one
264-
produces a silent blind spot in exactly one feature.
265-
266-
---
267-
268-
## `diagnostics/mod.rs` is a grab-bag around a thin orchestrator
269-
270-
**What to do.** The actual orchestrator
271-
(`collect_slow_diagnostics`, ~85 lines) is fine, but it is buried in
272-
2,928 lines of unrelated logic. Carve out of `src/diagnostics/mod.rs`:
273-
274-
- `external/{phpstan,phpcs,mago}.rs` — the four `schedule_*`
275-
subprocess spawn/debounce/parse pipelines (~700 lines, self-contained).
276-
- `stale.rs``is_stale_phpstan_diagnostic` plus its helpers
277-
(~335 lines reconciling cached external diagnostics with edits).
278-
- `suppression.rs``filter_suppressed`,
279-
`suppress_imprecise_overlaps`, `is_full_line_range` (~230 lines of
280-
post-processing policy).
281-
282-
While in there, introduce a shared `FileDiagnosticContext` built once
283-
in `collect_slow_diagnostics` and passed to collectors — seven
284-
symbol-span collectors (`unknown_classes.rs`, `unknown_functions.rs`,
217+
Recommended order: do tasks 2 → 3 → 4 in that order (4 renames fields
218+
across the whole crate, so it goes last to avoid churning code that 2
219+
and 3 are about to move).
220+
221+
All three tasks are pure internal refactors. None of them gets a
222+
changelog entry unless the work uncovers and fixes a concrete
223+
user-visible bug. Run the Rust CI checks (`cargo nextest run`, `cargo
224+
clippy -- -D warnings`, `cargo clippy --tests -- -D warnings`, `cargo
225+
fmt`) after each task.
226+
227+
## 2. Split `diagnostics/mod.rs` around its thin orchestrator
228+
229+
**What to do.** The actual orchestrator (`collect_slow_diagnostics`,
230+
at ~line 240, ~85 lines) is fine, but it is buried in a 3,391-line
231+
`src/diagnostics/mod.rs`. This is pure code motion plus one shared
232+
context struct — no behavior changes. Rust allows `impl Backend`
233+
blocks in any file of the crate, so moving a method means moving it
234+
into a new `impl Backend` block in the new file; nothing needs to
235+
become free functions.
236+
237+
**Step 1 — external-tool pipelines → `diagnostics/external/`.**
238+
Create `src/diagnostics/external/{mod,phpstan,phpcs,mago}.rs` and move
239+
the four subprocess spawn/debounce/parse pipelines out of `mod.rs`:
240+
`schedule_phpstan` (~line 1299), `schedule_phpcs` (~1474),
241+
`schedule_mago_lint` (~1632), and `schedule_mago_analyze` (~1764,
242+
both mago pipelines go in `mago.rs`) — roughly 730 lines — each with
243+
the private helpers only it uses. Helpers shared by two or more
244+
pipelines go in `external/mod.rs`. Do **not** move
245+
`schedule_diagnostics`, `schedule_external_diagnostics`, or
246+
`schedule_diagnostics_for_open_files` (~1077–1298): those are the
247+
native debounce orchestration and stay in `mod.rs`.
248+
249+
**Step 2 — staleness reconciliation → `diagnostics/stale.rs`.**
250+
Move `is_stale_phpstan_diagnostic` (~line 467) plus every helper only
251+
it uses (~335 lines reconciling cached external diagnostics with
252+
edits).
253+
254+
**Step 3 — post-processing policy → `diagnostics/suppression.rs`.**
255+
Move `filter_suppressed` (~line 2034), `suppress_imprecise_overlaps`
256+
(~2071), and `is_full_line_range` (~2159) — ~230 lines.
257+
258+
**Step 4 — shared `FileDiagnosticContext`.** Seven symbol-span
259+
collectors (`unknown_classes.rs`, `unknown_functions.rs`,
285260
`deprecated.rs`, `implementation_errors.rs`, `invalid_class_kind.rs`,
286-
`unknown_members/mod.rs`, `unused_imports.rs`) currently open with a
287-
near-verbatim 15-20 line lock-gathering preamble (symbol map, resolved
288-
names, use map, namespace, local classes), and a shared snapshot also
289-
guarantees they observe consistent state.
290-
291-
Also split `src/diagnostics/type_errors.rs`: `is_type_compatible` is a
292-
single ~607-line function (plus four helper predicates) implementing
293-
the diagnostic-policy layer over `util::is_subtype_of_typed`. Move it
294-
to `type_errors/compatibility.rs`, and audit whether some of its
295-
"MAYBE escape hatches" (IntRange, union handling, Generic/Traversable)
296-
belong in `is_subtype_of_typed` where all callers would benefit.
261+
`unknown_members/mod.rs`, `unused_imports.rs`) open with variations of
262+
the same lock-gathering preamble. Define, next to the existing
263+
`diagnostics/helpers.rs` utilities, a struct holding the per-file
264+
snapshot they gather: `symbol_map: Arc<SymbolMap>`,
265+
`resolved_names: Option<Arc<OwnedResolvedNames>>`, the file's use map
266+
(from `file_imports`), its `Vec<NamespaceSpan>` (from
267+
`file_namespaces`), and its local classes (from `uri_classes_index`).
268+
Give it a constructor like `FileDiagnosticContext::gather(&Backend,
269+
uri) -> Option<Self>` (returning `None` when no symbol map exists,
270+
which is every collector's current early-out). Build it **once** in
271+
`collect_slow_diagnostics` and pass `&FileDiagnosticContext` to the
272+
seven collectors; collectors that need only some fields just ignore
273+
the rest. Before changing collector signatures, grep for callers
274+
outside `collect_slow_diagnostics` (the `analyse/` CLI path, code
275+
actions, and tests call some collectors directly); any such caller
276+
builds its own context via `gather`. Besides deduplication, the shared
277+
snapshot guarantees all collectors observe consistent state instead of
278+
re-reading locks that a concurrent parse may update between
279+
collectors.
280+
281+
**Step 5 — split `type_errors.rs`.** Convert
282+
`src/diagnostics/type_errors.rs` (1,538 lines) into a directory and
283+
move `is_type_compatible` (starts at ~line 76, ~730 lines) plus the
284+
predicates only it uses (`is_bare_array`, `is_refined_scalar_pair`,
285+
etc.) to `type_errors/compatibility.rs`. It is the diagnostic-policy
286+
layer over `crate::class_lookup::is_subtype_of_typed`.
287+
288+
**Step 6 — file the MAYBE audit, do not act on it.** While moving
289+
`is_type_compatible`, list its "MAYBE escape hatches" (IntRange,
290+
union handling, iterable/Traversable, bare Closure/callable, bare
291+
array, nullable → non-nullable) and assess which ones describe real
292+
subtype relationships that belong in
293+
`class_lookup::is_subtype_of_typed` where all callers would benefit,
294+
versus diagnostic-only leniency that must stay in the policy layer.
295+
Do **not** move any semantics in this task — file the candidates with
296+
reasoning in `docs/todo/type-inference.md`.
297+
298+
**Acceptance.** `cargo nextest run` passes with no test edits (except
299+
mechanical import-path updates); `mod.rs` shrinks to the orchestrator,
300+
scheduling, and publishing logic; behavior is unchanged.
297301

298302
**Why it matters.** Diagnostics is the most actively developed area;
299303
new collectors keep landing in a module whose entry file is 75%
300-
unrelated scheduling and filtering code.
304+
unrelated scheduling and filtering code. The `external/` split also
305+
directly de-risks the scheduled D10 task (PHPMD proxy — a fifth
306+
external-tool pipeline that gets a formulaic home instead of growing
307+
`mod.rs` further).
301308

302309
---
303310

304-
## `server.rs` carries a ~950-line workspace-init block
305-
306-
**What to do.** `src/server.rs` (2,821 lines) contains an
307-
`impl Backend` block of workspace initialization and indexing that has
308-
nothing to do with LSP dispatch: `init_single_project`,
309-
`init_monorepo`, `init_no_composer`, `add_vendor_dir`,
310-
`apply_watched_file_changes`, `rescan_composer_indexes`,
311-
`scan_autoload_files`, `preload_autoload_files`, `scan_phar_archive`,
312-
`build_self_scan_composer`, `populate_autoload_indices`. Move it to a
313-
dedicated module (e.g. `src/workspace_init.rs` or `src/indexing/`).
314-
`warm_laravel_completion_cache` belongs in `virtual_members/laravel/`.
315-
Also move the pull-diagnostics resultId-cache logic embedded in the
316-
`diagnostic` and `workspace_diagnostic` handlers into `diagnostics/`,
317-
leaving the handlers as thin delegations like the rest.
318-
319-
Relatedly, `references/mod.rs` hosts `ensure_workspace_indexed`,
320-
`parse_files_parallel`, and `parse_paths_parallel` — workspace
321-
indexing, not reference finding. They should land in the same new
322-
module.
311+
## 3. Move workspace init/indexing out of `server.rs` and `references/mod.rs`
312+
313+
**What to do.** `src/server.rs` (4,007 lines) ends with an
314+
`impl Backend` block of workspace initialization and indexing
315+
(~lines 2696–4007, ~1,300 lines) that has nothing to do with LSP
316+
dispatch, and `src/references/mod.rs` hosts workspace-indexing
317+
functions that have nothing to do with reference finding. Consolidate
318+
both into a new `src/indexing/` module (declare `mod indexing;` in
319+
`lib.rs`). Pure code motion via `impl Backend` blocks in the new
320+
files; approximate current line numbers given for finding things.
321+
322+
- `indexing/init.rs``init_single_project` (~2696),
323+
`init_monorepo` (~2933), `init_no_composer` (~3102).
324+
- `indexing/scan.rs``add_vendor_dir` (~3153),
325+
`rescan_composer_indexes` (~3374), `scan_autoload_files` (~3469),
326+
`scan_phar_archive` (~3686), `build_self_scan_composer` (~3776),
327+
`populate_autoload_indices` (~3877).
328+
- `indexing/preload.rs``preload_autoload_files` (~3608) and
329+
`preload_autoload_files_with_progress` (~3614) from `server.rs`,
330+
plus from `src/references/mod.rs`: `ensure_workspace_indexed`
331+
(~125), `ensure_workspace_indexed_for_request` (~137),
332+
`ensure_workspace_indexed_with_progress` (~166),
333+
`parse_files_parallel_with_progress` (~314), and
334+
`parse_paths_parallel_with_progress` (~430).
335+
- `indexing/watch.rs``apply_watched_file_changes` (~3194).
336+
337+
Two more relocations while in there:
338+
339+
- `warm_laravel_completion_cache` (`server.rs` ~2081) belongs in
340+
`src/virtual_members/laravel/`.
341+
- The pull-diagnostics resultId-cache logic embedded in the
342+
`diagnostic` (~1589) and `workspace_diagnostic` (~1651) handlers
343+
belongs in a new `src/diagnostics/pull.rs`, leaving both handlers
344+
as thin delegations like the rest of `server.rs`.
345+
346+
**Constraint.** Several of these functions spawn threads sized with
347+
`PARSE_WORKER_STACK_SIZE` (see the "Performance Anti-Patterns" §3
348+
note in `CLAUDE.md`). Move that code verbatim — do not "simplify"
349+
thread spawning or stack sizing while relocating it.
350+
351+
**Acceptance.** `cargo nextest run` passes with no test edits (except
352+
import paths); `server.rs` is reduced to protocol handlers and
353+
dispatch (~1,700 lines); behavior is unchanged.
323354

324355
**Why it matters.** Full background indexing has already shipped on
325356
top of init logic scattered across `server.rs` and
@@ -328,26 +359,57 @@ sprawl as the feature continues to grow.
328359

329360
---
330361

331-
## Group `Backend`'s remaining fields into sub-systems
332-
333-
**What to do.** `struct Backend` in `src/lib.rs` still has fields that
334-
cluster into implicit sub-systems (the four external-tool field triples
335-
have already been consolidated into an `ExternalToolWorker` struct —
336-
`phpstan_tool`, `phpcs_tool`, `mago_lint_tool`, `mago_analyze_tool`):
337-
338-
1. Diagnostic state (`diag_version`, `diag_notify`, `diag_pending_uris`,
339-
`diag_last_*`, `diag_result_ids`, `diag_suppressed`) →
340-
`DiagnosticState`.
341-
2. Symbol/class indexes (`uri_classes_index`, `fqn_class_index`,
342-
`fqn_uri_index`, `gti_index`, `method_store`, `global_functions`,
343-
…) → `SymbolIndex`.
344-
3. Workspace config (`workspace_root`, `psr4_mappings`, `vendor_*`,
345-
`php_version`, `config`) → `WorkspaceConfig`.
346-
347-
**Why it matters.** Item 1 directly de-risks the scheduled D10 task;
362+
## 4. Group `Backend`'s remaining fields into sub-systems
363+
364+
**What to do.** `struct Backend` in `src/lib.rs` (starts at ~line 377)
365+
still has fields that cluster into implicit sub-systems. The four
366+
external-tool fields are already grouped (`ExternalToolWorker`); this
367+
task introduces three more groups. It is a mechanical, crate-wide
368+
field rename — do it as three separate passes (one per group), running
369+
`cargo check` between passes, and run it as the **sole active agent**
370+
(no parallel sub-agents; see "Never run project-wide rewrites in
371+
parallel" in `CLAUDE.md`).
372+
373+
For each group: define the struct with a `fn new()` used by both
374+
`Backend` constructors (~lines 1096 and 1206) and include the group in
375+
the per-request clone (~line 1836); the `Arc`/`Mutex` wrappers move
376+
into the new struct unchanged (clone semantics of a `Backend` clone
377+
must not change).
378+
379+
**Group 1 — `DiagnosticState`** (define it in `src/diagnostics/`,
380+
field name e.g. `diag`): `diag_version`, `diag_notify`,
381+
`diag_pending_uris`, `diag_last_slow`, `diag_last_fast`,
382+
`diag_last_full`, `diag_result_ids`, `diag_suppressed`,
383+
`workspace_diags`, `workspace_diag_pass_started`. Drop the `diag_`
384+
prefix on the struct's fields (`self.diag_version`
385+
`self.diag.version`). Leave `supports_pull_diagnostics` with the other
386+
client-capability flags.
387+
388+
**Group 2 — `SymbolIndex`** (field name e.g. `symbols`):
389+
`uri_classes_index`, `fqn_uri_index`, `fqn_origin_index`,
390+
`fqn_class_index`, `class_not_found_cache`, `gti_index`,
391+
`method_store`, `global_functions`, `global_defines`,
392+
`uri_globals_index`, `autoload_function_index`,
393+
`autoload_function_origin_index`, `autoload_constant_index`,
394+
`autoload_constant_origin_index`, `autoload_file_paths`. Do **not**
395+
pull in the per-file parse artifacts (`symbol_maps`, `resolved_names`,
396+
`file_imports`, `file_namespaces`, `parse_errors`, `parsed_uris`,
397+
`parse_inflight`, `phar_archives`) or the `stub_*` indexes — they have
398+
a different lifecycle and are out of scope here.
399+
400+
**Group 3 — `WorkspaceEnv`** (field name e.g. `workspace` — not
401+
"WorkspaceConfig", to avoid colliding with the existing
402+
`config: Mutex<config::Config>` field, which becomes a member of this
403+
group): `workspace_root`, `psr4_mappings`, `vendor_uri_prefixes`,
404+
`vendor_dir_paths`, `vendor_package_origin_roots`, `php_version`,
405+
`config`.
406+
407+
**Acceptance.** Pure renames: `cargo nextest run` passes with only
408+
mechanical field-path updates in tests; no lock types, wrapper types,
409+
or clone semantics change.
410+
411+
**Why it matters.** Group 1 directly de-risks the scheduled D10 task;
348412
the rest makes the Backend's state graph legible and shrinks the
349-
constructor.
413+
constructors.
350414

351415
---
352-
353-

0 commit comments

Comments
 (0)