From aed3f920bf3a2e42999d4dce28529425c5aff03d Mon Sep 17 00:00:00 2001 From: SAI KISHAN A <222060771+kishansaaai@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:56:19 +0530 Subject: [PATCH] Fix #648: Wire TsconfigResolver in CLI commands --- .../repowise/cli/commands/dead_code_cmd.py | 9 ++ .../cli/commands/health_cmd/command.py | 9 ++ .../repowise/cli/commands/workspace_cmd.py | 4 + .../src/repowise/core/ingestion/__init__.py | 3 +- .../core/ingestion/tsconfig_resolver.py | 30 ++++++ .../core/pipeline/phases/ingestion.py | 29 +++--- .../unit/ingestion/test_tsconfig_cli_repro.py | 96 +++++++++++++++++++ 7 files changed, 162 insertions(+), 18 deletions(-) create mode 100644 tests/unit/ingestion/test_tsconfig_cli_repro.py diff --git a/packages/cli/src/repowise/cli/commands/dead_code_cmd.py b/packages/cli/src/repowise/cli/commands/dead_code_cmd.py index 404cd4059..5ca6d3ff6 100644 --- a/packages/cli/src/repowise/cli/commands/dead_code_cmd.py +++ b/packages/cli/src/repowise/cli/commands/dead_code_cmd.py @@ -153,6 +153,15 @@ def dead_code_command( graph_builder.add_file(parsed) except Exception: pass + + from repowise.core.ingestion import wire_tsconfig_resolver + + wire_tsconfig_resolver( + graph_builder, + repo_path, + include_submodules=include_submodules, + include_nested_repos=include_nested_repos, + ) graph_builder.build() # Framework-aware synthetic edges (Django, Laravel, TYPO3, ...). Without diff --git a/packages/cli/src/repowise/cli/commands/health_cmd/command.py b/packages/cli/src/repowise/cli/commands/health_cmd/command.py index aa49d8a09..96c54d83c 100644 --- a/packages/cli/src/repowise/cli/commands/health_cmd/command.py +++ b/packages/cli/src/repowise/cli/commands/health_cmd/command.py @@ -194,6 +194,15 @@ def health_command( parsed_files.append(parsed) except Exception: continue + + from repowise.core.ingestion import wire_tsconfig_resolver + + wire_tsconfig_resolver( + graph_builder, + repo_path, + include_submodules=include_submodules, + include_nested_repos=include_nested_repos, + ) graph_builder.build() git_meta_map: dict = {} diff --git a/packages/cli/src/repowise/cli/commands/workspace_cmd.py b/packages/cli/src/repowise/cli/commands/workspace_cmd.py index 2759452ba..9594451cb 100644 --- a/packages/cli/src/repowise/cli/commands/workspace_cmd.py +++ b/packages/cli/src/repowise/cli/commands/workspace_cmd.py @@ -660,6 +660,10 @@ def _generate_docs_for_added_repo( graph_builder.add_file(parsed) except Exception: continue + + from repowise.core.ingestion import wire_tsconfig_resolver + + wire_tsconfig_resolver(graph_builder, repo_path) graph_builder.build() from repowise.core.repo_config import load_repo_config diff --git a/packages/core/src/repowise/core/ingestion/__init__.py b/packages/core/src/repowise/core/ingestion/__init__.py index a892b6c69..ab5caa99e 100644 --- a/packages/core/src/repowise/core/ingestion/__init__.py +++ b/packages/core/src/repowise/core/ingestion/__init__.py @@ -29,7 +29,7 @@ ) from .parser import LANGUAGE_CONFIGS, ASTParser, LanguageConfig, parse_file from .traverser import FileTraverser, TraversalStats -from .tsconfig_resolver import TsconfigResolver +from .tsconfig_resolver import TsconfigResolver, wire_tsconfig_resolver __all__ = [ # Models @@ -52,6 +52,7 @@ # Graph "GraphBuilder", "TsconfigResolver", + "wire_tsconfig_resolver", "Import", "LanguageConfig", "PackageInfo", diff --git a/packages/core/src/repowise/core/ingestion/tsconfig_resolver.py b/packages/core/src/repowise/core/ingestion/tsconfig_resolver.py index 341cf0658..5e0b72039 100644 --- a/packages/core/src/repowise/core/ingestion/tsconfig_resolver.py +++ b/packages/core/src/repowise/core/ingestion/tsconfig_resolver.py @@ -442,3 +442,33 @@ def _parse_json_lenient(config_path: Path) -> dict[str, Any] | None: except Exception as exc: log.debug("tsconfig_parse_failed", path=str(config_path), error=str(exc)) return None + + +def wire_tsconfig_resolver( + graph_builder: Any, + repo_path: str | Path, + *, + include_submodules: bool = False, + include_nested_repos: bool = False, +) -> None: + """Wire up the TsconfigResolver for the given GraphBuilder, if TS/JS files are present. + + Needs to be called after add_file() but before build(). + """ + _ts_langs = {"typescript", "javascript"} + + # We duck-type graph_builder to avoid circular imports. It has ._parsed_files. + parsed_files = graph_builder._parsed_files.values() + if not any(pf.file_info.language in _ts_langs for pf in parsed_files): + return + + try: + _path_set = set(graph_builder._parsed_files.keys()) + _resolver = TsconfigResolver( + repo_path=Path(repo_path), + path_set=_path_set, + prune_nested_git=not (include_submodules or include_nested_repos), + ) + graph_builder.set_tsconfig_resolver(_resolver) + except Exception as _resolver_exc: + log.warning("tsconfig_resolver_init_failed", error=str(_resolver_exc)) diff --git a/packages/core/src/repowise/core/pipeline/phases/ingestion.py b/packages/core/src/repowise/core/pipeline/phases/ingestion.py index d4a020ebe..04aecf9a0 100644 --- a/packages/core/src/repowise/core/pipeline/phases/ingestion.py +++ b/packages/core/src/repowise/core/pipeline/phases/ingestion.py @@ -341,23 +341,18 @@ async def _run_ingestion( # Only runs when the repo has TS/JS files. On large TS monorepos the # resolver indexes hundreds of tsconfig files up-front; without a phase # label this shows up as a silent gap right after parsing. - try: - from repowise.core.ingestion.tsconfig_resolver import TsconfigResolver - - _ts_langs = {"typescript", "javascript"} - if any(pf.file_info.language in _ts_langs for pf in parsed_files): - if progress: - progress.on_phase_start("tsconfig", None) - _path_set = set(graph_builder._parsed_files.keys()) - _resolver = TsconfigResolver( - repo_path=repo_path, - path_set=_path_set, - prune_nested_git=not (include_submodules or include_nested_repos), - ) - graph_builder.set_tsconfig_resolver(_resolver) - _phase_done(progress, "tsconfig") - except Exception as _resolver_exc: - logger.warning("tsconfig_resolver_init_failed", error=str(_resolver_exc)) + _ts_langs = {"typescript", "javascript"} + if any(pf.file_info.language in _ts_langs for pf in parsed_files): + if progress: + progress.on_phase_start("tsconfig", None) + from repowise.core.ingestion import wire_tsconfig_resolver + wire_tsconfig_resolver( + graph_builder, + repo_path, + include_submodules=include_submodules, + include_nested_repos=include_nested_repos, + ) + _phase_done(progress, "tsconfig") # ---- Graph build phase ------------------------------------------------- # Sub-phases (graph.imports / graph.heritage / graph.calls) are emitted diff --git a/tests/unit/ingestion/test_tsconfig_cli_repro.py b/tests/unit/ingestion/test_tsconfig_cli_repro.py new file mode 100644 index 000000000..0abd23ab0 --- /dev/null +++ b/tests/unit/ingestion/test_tsconfig_cli_repro.py @@ -0,0 +1,96 @@ +"""Regression test for #648: false-positive unreachable files for TS/JS path aliases.""" + +from __future__ import annotations + +import json +from datetime import datetime +from pathlib import Path + +from repowise.core.ingestion.graph import GraphBuilder +from repowise.core.ingestion.models import FileInfo, Import, ParsedFile +from repowise.core.ingestion import wire_tsconfig_resolver + + +def test_tsconfig_alias_cli_rebuild(tmp_path: Path) -> None: + # 1. Setup mock tsconfig.json + tsconfig_file = tmp_path / "tsconfig.json" + tsconfig_file.write_text( + json.dumps({ + "compilerOptions": { + "baseUrl": ".", + "paths": {"@/*": ["./src/*"]}, + } + }), + encoding="utf-8", + ) + + # 2. Setup mock files + layout_path = "src/routes/layout.tsx" + sensor_path = "src/components/SensorProvider.tsx" + + # Mock ParsedFile for layout.tsx (imports SensorProvider via alias) + layout_import = Import( + raw_statement="import Sensor from '@/components/SensorProvider'", + module_path="@/components/SensorProvider", + imported_names=["Sensor"], + is_relative=False, + resolved_file=None, + ) + layout_parsed = ParsedFile( + file_info=FileInfo( + path=layout_path, + abs_path=str(tmp_path / layout_path), + language="typescript", + size_bytes=100, + git_hash="", + last_modified=datetime.now(), + is_test=False, + is_config=False, + is_api_contract=False, + is_entry_point=True, + ), + symbols=[], + imports=[layout_import], + exports=[], + docstring=None, + parse_errors=[], + content_hash="", + ) + + # Mock ParsedFile for SensorProvider.tsx (the target) + sensor_parsed = ParsedFile( + file_info=FileInfo( + path=sensor_path, + abs_path=str(tmp_path / sensor_path), + language="typescript", + size_bytes=100, + git_hash="", + last_modified=datetime.now(), + is_test=False, + is_config=False, + is_api_contract=False, + is_entry_point=False, + ), + symbols=[], + imports=[], + exports=[], + docstring=None, + parse_errors=[], + content_hash="", + ) + + # 3. Build graph (mimic CLI command) + graph_builder = GraphBuilder(repo_path=tmp_path) + graph_builder.add_file(layout_parsed) + graph_builder.add_file(sensor_parsed) + + # The fix: call wire_tsconfig_resolver before build + wire_tsconfig_resolver( + graph_builder, tmp_path, include_submodules=False, include_nested_repos=False + ) + + graph_builder.build() + + # 4. Assert SensorProvider is reached + assert graph_builder.graph().has_edge(layout_path, sensor_path) + assert graph_builder.graph().in_degree(sensor_path) > 0