You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Phase 2c: Extend walker to all six anti-patterns, make it default (#357)
## Summary
Completes Phase 2 of the tree-sitter AST walker by extending it from
detecting only `side-effect-import` (Phase 2b) to all six anti-patterns
identified in the idaptik Wave 3 pilot. The walker now detects the
remaining three Phase-1 kinds (`raw-js`, `untyped-exception`,
`mutable-global`) plus the two kinds explicitly deferred from Phase 1
because they require real AST (`inline-callback-record`,
`oversized-function`). The `--engine=walker` flag becomes the CLI
default.
## Key Changes
- **Extended anti-pattern detection** in `walker.ml`:
- `detect_raw_js`: flags any `extension_expression` node (`%raw`,
`%bs.raw`, etc.)
- `detect_untyped_exception`: detects `try_expression`, `raise()` calls,
`Js.Exn.*` references, and `Promise.catch` member access
- `detect_mutable_global`: flags top-level `let x = ref(...)` bindings
and top-level `mutation_expression` (`:=` operator)
- `detect_inline_callback_record`: counts inline `function` values in
record literals and call argument lists; flags when ≥3 appear in a
single container
- `detect_oversized_function`: flags functions whose row span exceeds 50
source lines (proxy for >50 LOC body)
- **Structural improvements**:
- Refactored `at_module_toplevel` as a reusable predicate to distinguish
module-scoped state from local bindings inside function bodies
- Added `dedupe` filter to eliminate structurally-overlapping AST
matches on the same line (e.g., a `Js.Exn` reference nested inside a
`try_expression`)
- Extracted helper functions (`node_text`, `starts_with`, `ends_with`,
`mk_finding`) for cleaner detector implementations
- **CLI and documentation**:
- `main.ml`: flipped `--engine=walker` to the default;
`--engine=scanner` is now the fallback
- `README.md`: updated usage examples, coverage table, and Phase 2
architecture notes to reflect Phase 2c completion
- `scanner.mli` and `scanner.ml`: added `Inline_callback_record` and
`Oversized_function` to the `kind` type and guidance text (for
consistency, though the scanner itself does not detect these)
- **Test coverage**:
- Added `test_walker_finds_raw_js`, `test_walker_finds_mutable_global`,
`test_walker_finds_untyped_exception` to verify Phase 2c parity with
Phase-1 scanner on the four shared kinds
- Added `test_walker_finds_inline_callback_record` and
`test_walker_finds_oversized_function` to verify the two walker-only
kinds
- New synthetic fixture `phase2c.res` exercises inline callback records
(record literal with 4 lambdas, call site with 3 labelled-argument
lambdas) and an oversized function (60-row span)
## Notable Implementation Details
- **Module-toplevel detection**: The walker now correctly distinguishes
`let _ = Mod.value` at module scope (anti-pattern) from the same shape
inside a function body (normal "discard return value" idiom). This
eliminates the false-positive class that the Phase-1 regex had to
band-aid in #319.
- **Deduplication**: Because the AST walker visits structural overlaps
(e.g., a `Js.Exn` member expression nested inside a `try_expression`
yields two `Untyped_exception` findings on the same line), findings are
deduplicated by `(kind, line)` before sorting. This keeps the output
consistent with what the line-based scanner would produce.
- **Row-span proxy for oversized functions**: Rather than counting lines
in the function body subtree, the walker uses `node.stop.row -
node.start.row + 1` as a cheap proxy. This is sufficient for surfacing
decomposition candidates; precise line counting belongs to Phase 3 where
the
https://claude.ai/code/session_01WNkH8UucP3PppG5R36kGcu
---------
Co-authored-by: Claude <noreply@anthropic.com>
The output is **not compilable**. It is a starting point for the human:
@@ -36,8 +36,8 @@ needs re-decomposing.
36
36
37
37
|`--engine`| Implementation | When to use |
38
38
|---|---|---|
39
-
|`scanner` (default) |Line-anchored regex over the raw source (`scanner.ml`). | Default — no prerequisites, ships with the binary. |
40
-
|`walker`|Shells out to the vendored `tree-sitter` CLI, walks the AST (`walker.ml`). |When the regex's false-positive surface matters — eliminates the `let _ = chained.call()` class of misfire that the column-0 anchor in #319 had to band-aid. |
39
+
|`walker` (default) |Shells out to the vendored `tree-sitter` CLI, walks the AST (`walker.ml`). | Default since Phase 2c — covers all six anti-patterns including the two that the scanner cannot see (inline callback records, oversized functions) and eliminates the `let _ = chained.call()` / line-anchored false-positive classes. |
40
+
|`scanner`|Line-anchored regex over the raw source (`scanner.ml`). |Fallback when the vendored grammar is unavailable (no `tree-sitter` CLI, missing `tools/vendor/tree-sitter-rescript/`). Detects four of the six anti-patterns only. |
41
41
42
42
The walker requires the vendored `tree-sitter-rescript` grammar to be
43
43
built first:
@@ -50,42 +50,37 @@ just install-grammar
50
50
If the grammar isn't built or the `tree-sitter` CLI isn't on PATH, the
51
51
walker auto-falls-back to the scanner and prints the reason to stderr.
|`mutable-global`| Top-level `let x = ref(...)` (call-of-`ref` body) OR top-level `mutation_expression` (`x := y`) | Affine record threaded through |
64
+
|`inline-callback-record`| ≥ 3 inline `function` values in one `record` literal OR one call's `arguments` list (via `labeled_argument` or direct) | Row-polymorphic handler record (LESSONS.md §callback-record) |
65
+
|`oversized-function`|`function` node whose row span exceeds 50 source lines | Re-decompose before porting; do not transliterate |
0 commit comments