code-review-graph version
2.3.6
Operating system
Windows
Python version
3.14.3
AI platform
Claude Code
Output of code-review-graph status
Nodes: 7913
Edges: 67514
Files: 691
Languages: tsx, powershell, typescript, javascript
Last updated: 2026-08-22T00:22:16
Built on branch: main
Built at commit: cd8e3c0c4020
Steps to reproduce
- In a TypeScript/React repo that uses Storybook, add a component with zero real callers — e.g.
components/Foo.tsx exporting Foo, imported by nothing else in the application.
- Add
components/Foo.stories.tsx that imports and renders Foo (a completely ordinary Storybook story — no special naming beyond the standard .stories.tsx suffix).
- Run
refactor_tool in dead_code mode (or the CLI equivalent) scoped to the component directory.
Expected vs actual behavior
Expected: Foo is reported as dead code — nothing in the application imports it, only its own story file does.
Actual: Foo is never reported, because the story's import edge counts as a live caller. refactor.py's _TEST_FILE_RE (used by find_dead_code) and parser.py's separate _TEST_FILE_PATTERNS (used to set is_test on every node) both match common test-file conventions (.test., .spec., __tests__/, test_*.py, e2e[-_]?tests?/, test[-_]utils?/) but neither list includes .stories. — the Storybook convention (Component Story Format) used by essentially every React/Vue/Svelte project with Storybook.
Because .stories.tsx files get is_test: false, they're treated as production source. Any component with a story — which in a maintained Storybook setup is most components — can never be flagged as dead no matter how disconnected it is from the actual application, since its story always supplies one "real" importer.
We hit this concretely: three components in our app (ArchiveSection, GroupCardDense, StickyRightRail) sat unreferenced by any page or route for 3–4 weeks. dead_code reported 0 results the whole time because each had a .stories.tsx. We only found them via a manual grep restricted to source directories, explicitly excluding each component's own story/test file.
Suggested fix — add a stories pattern to both lists:
parser.py _TEST_FILE_PATTERNS: re.compile(r".*\.stories\.[jt]sx?$")
refactor.py _TEST_FILE_RE: add |\.stories\.[jt]sx?$ to the alternation
(Whether story files should be classified as is_test: true outright, or dead_code should just special-case excluding them from the "has a caller" check via a separate flag, is worth a design decision either way — happy to open a PR against whichever approach you'd prefer.)
Additional context
This doesn't affect dead_code's Python/backend detection — only apparent for JS/TS ecosystems using Storybook (Component Story Format), which is the de facto standard for component libraries and design systems. Given how common Storybook is, I'd guess this false-negative affects most JS/TS repos that have adopted code-review-graph.
Also noticed a smaller duplication while tracing this: is_test classification lives in two independent places — parser.py's _TEST_FILE_PATTERNS and refactor.py's own _TEST_FILE_RE — with slightly different pattern sets (e.g. refactor.py matches e2e[-_]?tests?/ and test[-_]utils?/, which parser.py doesn't). Not sure if that's intentional, but it means a fix to one doesn't propagate to the other, and this issue is itself an example of that.
code-review-graph version
2.3.6
Operating system
Windows
Python version
3.14.3
AI platform
Claude Code
Output of
code-review-graph statusSteps to reproduce
components/Foo.tsxexportingFoo, imported by nothing else in the application.components/Foo.stories.tsxthat imports and rendersFoo(a completely ordinary Storybook story — no special naming beyond the standard.stories.tsxsuffix).refactor_toolindead_codemode (or the CLI equivalent) scoped to the component directory.Expected vs actual behavior
Expected:
Foois reported as dead code — nothing in the application imports it, only its own story file does.Actual:
Foois never reported, because the story's import edge counts as a live caller.refactor.py's_TEST_FILE_RE(used byfind_dead_code) andparser.py's separate_TEST_FILE_PATTERNS(used to setis_teston every node) both match common test-file conventions (.test.,.spec.,__tests__/,test_*.py,e2e[-_]?tests?/,test[-_]utils?/) but neither list includes.stories.— the Storybook convention (Component Story Format) used by essentially every React/Vue/Svelte project with Storybook.Because
.stories.tsxfiles getis_test: false, they're treated as production source. Any component with a story — which in a maintained Storybook setup is most components — can never be flagged as dead no matter how disconnected it is from the actual application, since its story always supplies one "real" importer.We hit this concretely: three components in our app (
ArchiveSection,GroupCardDense,StickyRightRail) sat unreferenced by any page or route for 3–4 weeks.dead_codereported 0 results the whole time because each had a.stories.tsx. We only found them via a manual grep restricted to source directories, explicitly excluding each component's own story/test file.Suggested fix — add a stories pattern to both lists:
parser.py_TEST_FILE_PATTERNS:re.compile(r".*\.stories\.[jt]sx?$")refactor.py_TEST_FILE_RE: add|\.stories\.[jt]sx?$to the alternation(Whether story files should be classified as
is_test: trueoutright, ordead_codeshould just special-case excluding them from the "has a caller" check via a separate flag, is worth a design decision either way — happy to open a PR against whichever approach you'd prefer.)Additional context
This doesn't affect
dead_code's Python/backend detection — only apparent for JS/TS ecosystems using Storybook (Component Story Format), which is the de facto standard for component libraries and design systems. Given how common Storybook is, I'd guess this false-negative affects most JS/TS repos that have adoptedcode-review-graph.Also noticed a smaller duplication while tracing this:
is_testclassification lives in two independent places —parser.py's_TEST_FILE_PATTERNSandrefactor.py's own_TEST_FILE_RE— with slightly different pattern sets (e.g.refactor.pymatchese2e[-_]?tests?/andtest[-_]utils?/, whichparser.pydoesn't). Not sure if that's intentional, but it means a fix to one doesn't propagate to the other, and this issue is itself an example of that.