Skip to content

refactor(parser): split into parsers/ package (Python verbatim) (#42) - #46

Merged
ggrace519 merged 1 commit into
mainfrom
refactor/parsers-package
Aug 31, 2026
Merged

refactor(parser): split into parsers/ package (Python verbatim) (#42)#46
ggrace519 merged 1 commit into
mainfrom
refactor/parsers-package

Conversation

@ggrace519

Copy link
Copy Markdown
Owner

Summary

Phase 0 of the multi-language AST epic (#42): split the 645-line
graphlm/parser.py into a graphlm/parsers/ package with a registry-driven
tree-sitter backend, so future language packs plug in without touching the
Python resolver. Pure refactor — zero behavior change.

This is the foundation phase; Phases 1–3 (JS/TS, Java, Rust packs) build on the
parsers/ structure and resolver registry this introduces.

What changed

  • graphlm/parsers/base.py (457 lines) — registry-driven
    _TreeSitterBackend (single _backend singleton; lazy per-language grammar
    import via _GRAMMARS/_GrammarSpec; _GrammarUnavailable degrades a missing
    grammar to zero edges for that language and never escapes
    build_dependency_graph/parse_file), ParsedFile, shared helpers, the
    group-by-language dispatch, detect_import_cycles.
  • graphlm/parsers/python.py (359 lines) — the Python resolver moved
    verbatim (queries, import extraction, _source_roots + src-layout
    resolution — AST import graph empty on src-layout / doc-heavy repos (0 deterministic edges) #19 behavior unchanged), registered for "python".
  • graphlm/parsers/__init__.py — re-exports the public contract.
  • graphlm/parser.py (42 lines) — thin backwards-compat shim re-exporting
    the contract (incl. _source_roots explicitly, since import * skips it).
  • tests/test_parser.py — one new test (TestMissingGrammarDegrades) with
    real teeth: a fake language registered in _GRAMMARS, EXT_TO_LANGUAGE, and
    _RESOLVERS yields [] (not None) for that language while Python edges stay
    intact. Existing tests unchanged.
  • CLAUDE.mdparser.py path references updated to the new module
    locations; language claims unchanged (still Python-only).

Why the never-escapes invariant matters

graphlm/__init__.py wraps build_dependency_graph in
except Exception → deterministic_edges = None, and the diff layer reads None
as "AST was off". So a single uninstalled pack grammar, if its exception escaped,
would zero every language's edges and mislabel the diff's AST dimension as
not-compared. The refactor catches _GrammarUnavailable inside the dispatchers
and always returns a list when AST is on.

Verification

  • uv run pytest -q396 passed (395 unmodified + 1 new).
  • uv run mypy graphlm --ignore-missing-imports → clean (18 files).
  • Verbatim-move proof: the new parser run over the pristine main tree
    yields 51 edges, byte-identical edge set to main's own code — the same
    input produces the same output. (A working-tree run shows ~57 only because
    graphlm now scans its own 3 new source files; not a regression.)
  • All new modules < 600 lines.
  • Fresh-context review: verdict SHIP, zero blockers, zero majors. It
    independently confirmed all 29 moved resolver symbols are AST-identical and
    the edge set is byte-identical over the pristine tree.

Notes for later phases (not Phase 0 defects — behavior-preserving vs main)

The review surfaced two latent spec-intent gaps for future packs, to be handled
in Phase 1: (1) the "log once per language" warning dedupe is currently
unreachable because python.py's broad except Exception catches
_GrammarUnavailable first (per-file warning instead); (2) one
resolver.source_roots() call sits just outside the grammar guard. Both are
inherited from main's existing behavior; neither affects Phase 0.

No CHANGELOG entry — a pure internal refactor has no externally observable
effect.

Tracked in #42.

🤖 Generated with Claude Code

https://claude.ai/code/session_018DiZwx1UJrf7wu3suBzq6x

Phase 0 of multi-language AST support: split the 645-line parser.py into a
graphlm/parsers/ package so future languages plug in without touching the
Python resolver. Pure refactor — zero behavior change.

- parsers/base.py: registry-driven _TreeSitterBackend (single _backend
  singleton; lazy per-language grammar import via _GRAMMARS/_GrammarSpec;
  _GrammarUnavailable degrades a missing grammar to zero edges for that
  language, never escapes build_dependency_graph/parse_file), ParsedFile,
  shared helpers, group-by-language dispatch, detect_import_cycles.
- parsers/python.py: Python resolver moved verbatim (queries, import
  extraction, _source_roots + src-layout resolution — #19 behavior
  unchanged), registered for "python".
- parser.py: thin backwards-compat shim re-exporting the public contract
  (incl. _source_roots).
- New test asserts the never-escapes invariant with real teeth: a fake
  language registered in BOTH _GRAMMARS and the extension map yields [] (not
  None) for that language while Python edges stay intact.
- CLAUDE.md: parser.py path references updated to the new module locations.

Verified: 396 tests pass (395 unmodified + 1 new), mypy clean, all modules
under 600 lines, deterministic-edge count on the pristine HEAD tree unchanged
at 51 (verbatim-move proof).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DiZwx1UJrf7wu3suBzq6x
@ggrace519
ggrace519 merged commit 250d7e3 into main Aug 31, 2026
7 checks passed
@ggrace519
ggrace519 deleted the refactor/parsers-package branch August 31, 2026 11:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant