Extract bare side-effect TypeScript imports - #17
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes TypeScript import extraction in codebase_analyzer so that bare side-effect imports (e.g., import "./polyfill.ts") are included in the extracted module list. That improves both dependency graph accuracy (SourceFile.imports) and coverage mapping inputs (TestFile.imported_modules) for TS/JS projects that rely on side-effect-only modules.
Changes:
- Add
_TS_SIDE_EFFECT_IMPORT_REto match bareimport "spec"/import 'spec'lines. - Update
_extract_ts_importsto merge matches from both the existingfrom-based pattern and the new side-effect pattern, returning specifiers in source order. - Add a focused test suite covering positive cases and regression guards for non-matches.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/pact/codebase_analyzer.py |
Adds a side-effect import regex and merges it into _extract_ts_imports while preserving source order. |
tests/test_typescript_support.py |
Adds import-extraction tests validating side-effect imports and guarding against false positives. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`_extract_ts_imports` only ran `_TS_IMPORT_FROM_RE`, which requires the
`from` keyword. A side-effect import has no clause and no `from`:
import "reflect-metadata"
import "./polyfill.ts"
so a module pulled in purely for what loading it does — installing a
polyfill, registering a plugin or a codec, running a decorator shim — was
invisible to the analyzer. It appears in no `SourceFile.imports` list, and
since `_extract_ts_imports` also feeds `TestFile.imported_modules`, a test
that reaches a module only through a side-effect import contributes no
edge toward its coverage. The module reads as dead: no inbound import,
no covering test, while removing it would break the build.
`_TS_SIDE_EFFECT_IMPORT_RE` matches a line whose `import` keyword is
followed directly by a quoted specifier. Requiring the quote to come next
is what keeps the pattern off every other import form — a clause, a
default binding, a namespace binding, and `type` all put a non-quote token
in that position — so no import is counted twice by the two patterns.
Line-anchoring, matching the existing `from` pattern, keeps the specifier
of a clause wrapped onto its own line from being read as a bare
side-effect import.
The two match sets are merged by source offset, so a file's specifiers
come back in the order they appear whichever form each one takes.
Duplicates are preserved, matching the existing behavior for a module
imported twice.
Not handled: two statements on one line (`import "./a.ts"; import
"./b.ts"`) yields only the first, and an import inside a comment or a
string literal is still matched. Both limits are inherited from the
existing line-anchored `from` pattern rather than introduced here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MrJoy
force-pushed
the
fix/ts-side-effect-imports
branch
from
August 20, 2026 18:53
7fbce21 to
f33bdbc
Compare
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.
Defect
_extract_ts_importsruns only_TS_IMPORT_FROM_RE, which requires thefromkeyword. A bare side-effect import has no clause and no
from, so it matchesnothing:
A module imported purely for what loading it does — installing a polyfill,
registering a plugin or codec, running a decorator shim — therefore appears in
no
SourceFile.importslist. Because_extract_ts_importsalso feedsTestFile.imported_modules, a test that reaches such a module only through aside-effect import contributes no edge toward its coverage either. The module
reads as dead code with no inbound import and no covering test, while deleting
it would break the build.
Fix
A second pattern,
_TS_SIDE_EFFECT_IMPORT_RE, matches a line whoseimportkeyword is followed directly by a quoted specifier:
Requiring the quote to come next is what keeps the pattern off every other
import form — a clause, a default binding, a namespace binding, and
typeallput a non-quote token in that position — so nothing is counted twice by the two
patterns. Line-anchoring, matching the existing
frompattern, keeps thespecifier of a clause wrapped onto its own line from being read as a bare
side-effect import.
The two match sets are merged and sorted by source offset, so a file's
specifiers come back in the order they appear whichever form each one takes.
Duplicates are preserved, matching existing behavior for a module imported
twice.
Deliberately not handled
import "./a.ts"; import "./b.ts") yields onlythe first. The line anchor is inherited from
_TS_IMPORT_FROM_RE, which hasthe same limit; lifting the anchor for one pattern and not the other would be
an inconsistency, and the form is vanishingly rare in real source.
Same pre-existing limit, unchanged here.
import "./x.ts" with { type: "json" }) capture thespecifier correctly; the attribute clause is ignored.
Tests
All in
tests/test_typescript_support.py, in a newTestTypeScriptImportExtractionclass.
Failed before the fix (8):
test_relative_side_effect_importtest_bare_package_side_effect_importtest_side_effect_import_single_quotestest_side_effect_import_with_semicolontest_indented_side_effect_importtest_side_effect_import_with_trailing_commenttest_side_effect_and_static_imports_are_returned_in_source_ordertest_repeated_side_effect_import_is_not_deduplicatedGuards, passing before and after (7) — these pin what the new pattern must not
break or swallow:
test_static_named_import_is_unchangedtest_static_default_and_namespace_imports_are_unchangedtest_static_import_is_not_counted_twice(via_extract_ts_imports)test_static_import_is_not_matched_as_a_side_effect_importtest_multiline_clause_is_not_matched_as_a_side_effect_importtest_export_from_is_not_matched_as_a_side_effect_importtest_import_inside_an_identifier_is_not_a_side_effect_importThe three "is not matched as a side-effect import" guards assert against
_TS_SIDE_EFFECT_IMPORT_REdirectly rather than against_extract_ts_imports.That is deliberate: those inputs currently extract to
[]onmain, but whatthe guard is really about is the new pattern not firing on them, and asserting
at the pattern keeps the guard true whatever the
frompattern is later taughtto match.
Suite
Baseline on
origin/main, in a fresh worktree venv:After:
+15 tests, all passing. The FAILED set is byte-identical before and after
(captured with
grep '^FAILED' | sortand diffed). The 15 pre-existingfailures are environmental and untouched by this change: tree-sitter and cscope
are not installed, the
openaipackage is absent, and two tests shell out to apython3on PATH that has no pytest.Run as:
This repo has no CI, so a local run is the only evidence here — nothing will go
green on the PR itself.
Conflicts with the sibling TypeScript PRs
Branched from
origin/main, independently reviewable.git merge-tree --write-treeagainst each sibling:fix/ts-dynamic-imports— conflicts, in bothsrc/pact/codebase_analyzer.pyandtests/test_typescript_support.py. BothPRs add a pattern in the same spot and rewrite
_extract_ts_importsthe sameway, and both add a
TestTypeScriptImportExtractionclass at the same offset.The conflict is purely textual and additive. I resolved it in a scratch merge
to check: the source resolution is one merged
matcheslist over all threepatterns, the tests are a union of the two classes, and all 69 tests pass
afterward. The two patterns do not overlap — a dynamic import puts
(wheremine requires a quote, and a bare side-effect import has no paren — so nothing
is double-counted.
fix/ts-multiline-imports— conflicts in the test file only;codebase_analyzer.pyauto-merges. Same cause: two new classes at the sameoffset. I ran my 15 tests against the auto-merged source and all pass, so
teaching the
frompattern about wrapped clauses does not disturb this fix.fix/ts-relative-import-coverage— no conflict.feat/ts-cyclomatic-complexity— no conflict.Whichever of #16 or #13 lands first, the other and this one need a trivial
rebase.