Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/cli/src/repowise/cli/commands/dead_code_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/repowise/cli/commands/workspace_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/repowise/core/ingestion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -52,6 +52,7 @@
# Graph
"GraphBuilder",
"TsconfigResolver",
"wire_tsconfig_resolver",
"Import",
"LanguageConfig",
"PackageInfo",
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/repowise/core/ingestion/tsconfig_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
29 changes: 12 additions & 17 deletions packages/core/src/repowise/core/pipeline/phases/ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions tests/unit/ingestion/test_tsconfig_cli_repro.py
Original file line number Diff line number Diff line change
@@ -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
Loading