refactor(parser): split into parsers/ package (Python verbatim) (#42) - #46
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 0 of the multi-language AST epic (#42): split the 645-line
graphlm/parser.pyinto agraphlm/parsers/package with a registry-driventree-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_backendsingleton; lazy per-language grammarimport via
_GRAMMARS/_GrammarSpec;_GrammarUnavailabledegrades a missinggrammar to zero edges for that language and never escapes
build_dependency_graph/parse_file),ParsedFile, shared helpers, thegroup-by-language dispatch,
detect_import_cycles.graphlm/parsers/python.py(359 lines) — the Python resolver movedverbatim (queries, import extraction,
_source_roots+ src-layoutresolution — 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-exportingthe contract (incl.
_source_rootsexplicitly, sinceimport *skips it).tests/test_parser.py— one new test (TestMissingGrammarDegrades) withreal teeth: a fake language registered in
_GRAMMARS,EXT_TO_LANGUAGE, and_RESOLVERSyields[](notNone) for that language while Python edges stayintact. Existing tests unchanged.
CLAUDE.md—parser.pypath references updated to the new modulelocations; language claims unchanged (still Python-only).
Why the never-escapes invariant matters
graphlm/__init__.pywrapsbuild_dependency_graphinexcept Exception → deterministic_edges = None, and the diff layer readsNoneas "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
_GrammarUnavailableinside the dispatchersand always returns a list when AST is on.
Verification
uv run pytest -q→ 396 passed (395 unmodified + 1 new).uv run mypy graphlm --ignore-missing-imports→ clean (18 files).maintreeyields 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.)
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 broadexcept Exceptioncatches_GrammarUnavailablefirst (per-file warning instead); (2) oneresolver.source_roots()call sits just outside the grammar guard. Both areinherited 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