Skip to content
Open
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ repowise dead-code
✗ analytics/v1/tracker.ts file 0.41 recent activity — review first
```

Conservative by design. `safe_to_delete` requires confidence ≥ 0.70 and excludes dynamically-loaded patterns (`*Plugin`, `*Handler`, `*Adapter`, `*Middleware`). Dynamic import detection (`importlib.import_module()`, `__import__()`) and framework decorator awareness (Flask/FastAPI/Django routes) further reduce false positives. repowise surfaces candidates. Engineers decide.
Conservative by design. `safe_to_delete` requires confidence ≥ 0.70 and excludes dynamically-loaded patterns (`*Plugin`, `*Handler`, `*Adapter`, `*Middleware`). Dynamic import detection (`importlib.import_module()`, `__import__()`) and framework awareness (Flask/FastAPI/Django/Rails/Laravel/TYPO3 routes and convention files) further reduce false positives. repowise surfaces candidates. Engineers decide.

---

Expand Down Expand Up @@ -550,7 +550,7 @@ repowise reindex # rebuild vector store (no LLM calls)
| Tier | Languages | What works |
|------|-----------|------------|
| **Full** | Python · TypeScript · JavaScript · Java · Go · Rust · C++ · C# | AST parsing, import resolution, named bindings, call resolution, heritage extraction, docstrings; multi-project workspace resolvers (`.csproj`/`.sln` for C#, `Cargo.toml [workspace]` for Rust, `go.mod` multi-module, `package.json` workspaces, etc.); framework-aware edges (Django, FastAPI, Flask, ASP.NET, Spring Boot, Express/NestJS, Gin/Echo/Chi, Axum/Actix); per-language dynamic-hint extractors for runtime-resolved DI / reflection / plugins |
| **Good** | C · Kotlin · Ruby · Swift · Scala · PHP | AST parsing, import resolution, named bindings, call resolution, heritage (mixins, derive, extensions, traits), docstrings, dedicated workspace-aware resolvers (Gradle subprojects, Rails / Zeitwerk, SPM, SBT/Mill, composer PSR-4); Rails / Laravel framework edges for Ruby and PHP; per-language dynamic-hint extractors |
| **Good** | C · Kotlin · Ruby · Swift · Scala · PHP | AST parsing, import resolution, named bindings, call resolution, heritage (mixins, derive, extensions, traits), docstrings, dedicated workspace-aware resolvers (Gradle subprojects, Rails / Zeitwerk, SPM, SBT/Mill, composer PSR-4); Rails / Laravel / TYPO3 framework edges for Ruby and PHP; per-language dynamic-hint extractors |
| **Config / data** | OpenAPI · Protobuf · GraphQL · Dockerfile · Makefile · YAML · JSON · TOML · SQL · Terraform | Included in the file tree; special handlers extract endpoints/targets where applicable |

14 languages with full AST support, 8 of them at the Full tier. Adding a new language requires one `.scm` tree-sitter query file and one config entry. No changes to the parser core. See [Language Support](docs/LANGUAGE_SUPPORT.md) for details.
Expand Down
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Spring Boot (Java/Kotlin)** — `@Component`/`@Service`/`@Repository`/`@Controller`/`@RestController`/`@Configuration` bean classes wire to their injection sites via `@Autowired` field/constructor analysis. Interface-typed dependencies fall back to `parsed.heritage` to find implementing classes. `@Bean` factory methods in `@Configuration` classes link to their return-type files.
- **Rails (Ruby)** — `config/routes.rb` is line-walked with namespace-stack tracking: `resources :users`, `get "/foo", to: "users#index"`, and nested `namespace :admin do … end` all resolve to controller files via the Zeitwerk autoload index. ActiveRecord `belongs_to`/`has_many`/`has_one` relationships link model files (with simple inflector-style singularisation).
- **Laravel (PHP)** — `routes/web.php` and `routes/api.php` parse modern `[Foo::class, 'method']` and legacy `'Foo@method'` syntaxes, plus `Route::resource`. Service-provider `bind`/`singleton`/`instance` calls link providers to bound classes. Eloquent `hasMany`/`belongsTo`/`hasOne` link models. Class resolution uses the composer PSR-4 map first, falling back to stem.
- **TYPO3 (PHP)** — extension discovery via `composer.json` `"type": "typo3-cms-extension"` (canonical for v11–v14) with legacy fallback to `ext_emconf.php`; project-mode (`vendor/<vendor>/<pkg>/composer.json`) is also walked. Convention-loaded files (`ext_localconf.php`, `ext_emconf.php`, `ext_tables.sql`, `Configuration/TCA/*.php`, `Configuration/Backend/*.php`, `Configuration/JavaScriptModules.php`, `Configuration/ContentSecurityPolicies.php`, `Configuration/RequestMiddlewares.php`, `Configuration/Services.php`, `Configuration/Icons.php`) get incoming edges from a synthetic `framework:typo3-core` anchor, so they are no longer flagged as unreachable. `Configuration/JavaScriptModules.php` is parsed for `EXT:<key>/...js` references and edges are added to the registered JS modules. `tech_stack.detect_tech_stack` recognises `typo3/cms-core` and `symfony/framework-bundle` / `laravel/framework` from `composer.json`.
- **`framework:` synthetic-node prefix in dead-code analysis** — distinguishes framework-mediated wiring from third-party `external:` imports. `framework:` predecessors *do* count as cross-package importers (preventing legitimate convention dirs like `Configuration/` from showing up as zombie packages); `external:` predecessors do not.
- **`repowise dead-code` now invokes `add_framework_edges`** — the CLI previously skipped framework-aware edge synthesis, so even Django/Laravel/Rails repos showed false positives. The dead-code command now calls `detect_tech_stack` and adds framework edges before running the analyzer.
- **Express / NestJS (TS/JS)** — Express `app.use(routerVar)` mirrors the FastAPI router-var pattern (resolves imported names ending in `Router`/`router` to source file). NestJS `@Module({ controllers: [...], providers: [...], imports: [...] })` arrays parse into module → target edges using a class-name → file map.
- **Gin / Echo / Chi (Go)** — `r.GET("/p", users.Index)` style handler references resolve via the Go import list (using the multi-module resolver) for package-qualified handlers, or via a function-name → file map for receiver methods. Lambda handlers are accepted as missed.
- **Axum / Actix (Rust)** — Axum `Router::new().route("/p", get(handler))`, Actix `web::resource("/p").route(web::get().to(handler))` / `.service(handler)` / `.configure(routes::register)` all resolve to handler files via a function-name → file map.
Expand Down
4 changes: 2 additions & 2 deletions docs/LANGUAGE_SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ All eight languages support:
- Named binding extraction (mapping imported names to source symbols)
- Heritage extraction (class/interface/trait/record inheritance chains)
- Docstring extraction (Python, JSDoc, GoDoc, Rustdoc, Javadoc, Doxygen, XML doc)
- Framework-aware edges (Django, FastAPI, Flask for Python; tsconfig path aliases for TS/JS; pytest fixture detection; ASP.NET controllers / minimal API / EF Core DbContext for C#; Spring Boot DI + `@Bean` factories for Java/Kotlin; Rails routes + ActiveRecord relationships; Laravel routes + service providers + Eloquent; Express `app.use(router)` + NestJS `@Module` arrays; Gin/Echo/Chi router → handler files for Go; Axum/Actix `.route` → handler files for Rust)
- Framework-aware edges (Django, FastAPI, Flask for Python; tsconfig path aliases for TS/JS; pytest fixture detection; ASP.NET controllers / minimal API / EF Core DbContext for C#; Spring Boot DI + `@Bean` factories for Java/Kotlin; Rails routes + ActiveRecord relationships; Laravel routes + service providers + Eloquent; TYPO3 convention files (`ext_localconf.php`, `Configuration/TCA/*`, `JavaScriptModules.php` registrations) for PHP; Express `app.use(router)` + NestJS `@Module` arrays; Gin/Echo/Chi router → handler files for Go; Axum/Actix `.route` → handler files for Rust)
- Per-language dynamic-hint extractors (Django/Pytest/Node for Python+JS/TS; .NET DI/Activator/InternalsVisibleTo for C#; Spring `getBean`/`@Bean` factories for Java/Kotlin; Ruby `send`/`const_get`/`define_method`/`delegate`; PHP `call_user_func`/`ReflectionClass`/container `get`; Scala `Class.forName`/`given`/`implicit val`; Swift `NSClassFromString`/`Selector`/`#selector`/KVC; C function-pointer assignment + `dlopen`/`dlsym`; Luau `game:GetService`/`setmetatable __index`; Go `reflect.TypeOf`/`plugin.Open`/`plugin.Lookup`)
- For C# only: MSBuild project graph (`<ProjectReference>` / `<PackageReference>`), namespace → file mapping across projects, `global using` / `using static` / `using alias` propagation, ASP.NET HTTP and gRPC-dotnet contract extraction in workspace mode, cross-repo `<ProjectReference>` and internal-NuGet detection

Expand Down Expand Up @@ -315,7 +315,7 @@ ingestion/
scala.py # package-to-directory mapping
php.py # namespace/PSR-4 resolution
generic.py # stem-matching fallback
framework_edges.py # Django, FastAPI, Flask, pytest, ASP.NET detection
framework_edges.py # Django, FastAPI, Flask, pytest, ASP.NET, Rails, Laravel, TYPO3, Spring, Express, Gin, Axum detection
dynamic_hints/ # Per-language dynamic-edge extractors
base.py # DynamicHintExtractor + DynamicEdge
registry.py # HintRegistry
Expand Down
11 changes: 11 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 @@ -90,6 +90,17 @@ def dead_code_command(
pass
graph_builder.build()

# Framework-aware synthetic edges (Django, Laravel, TYPO3, ...). Without
# this, convention-loaded files appear as in_degree=0 unreachable — false
# positives in the dead-code report.
try:
from repowise.core.generation.editor_files.tech_stack import detect_tech_stack

tech_items = detect_tech_stack(repo_path)
graph_builder.add_framework_edges([item.name for item in tech_items])
except Exception:
pass

# Git metadata (best effort)
git_meta_map: dict = {}
try:
Expand Down
32 changes: 28 additions & 4 deletions packages/core/src/repowise/core/analysis/dead_code/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@
logger = structlog.get_logger(__name__)


def _is_synthetic_node(node: str) -> bool:
"""True for non-file graph nodes that should be skipped in 'is this dead?' passes.

Two synthetic prefixes exist:
- ``external:`` — third-party / unresolved imports.
- ``framework:`` — anchors added by ``framework_edges`` to model
convention-based loading (e.g. TYPO3 core loading ``ext_localconf.php``).

Both are skipped when the analyzer asks "is this node itself dead?",
but they are treated differently in the zombie-package pass: ``framework:``
predecessors *do* count as cross-package importers (real framework-
mediated dependencies), whereas ``external:`` predecessors do not.
"""
return node.startswith("external:") or node.startswith("framework:")


class DeadCodeAnalyzer:
"""Detects unreachable files, unused exports, unused internals, and
zombie packages using the dependency graph and git metadata.
Expand Down Expand Up @@ -148,7 +164,7 @@ def _detect_unreachable_files(
findings = []

for node in self.graph.nodes():
if str(node).startswith("external:"):
if _is_synthetic_node(str(node)):
continue

node_data = self.graph.nodes[node]
Expand Down Expand Up @@ -246,7 +262,7 @@ def _detect_unused_exports(
findings = []

for node in self.graph.nodes():
if str(node).startswith("external:"):
if _is_synthetic_node(str(node)):
continue

node_data = self.graph.nodes[node]
Expand Down Expand Up @@ -399,12 +415,18 @@ def _detect_unused_internals(
return findings

def _detect_zombie_packages(self, whitelist: set[str]) -> list[DeadCodeFindingData]:
"""Detect monorepo packages with no incoming inter_package edges."""
"""Detect monorepo packages with no incoming inter_package edges.

``framework:`` predecessors (synthetic anchors added by
``framework_edges``) count as cross-package importers — TYPO3 / Django
/ etc. wiring is a real cross-cutting dependency. ``external:``
predecessors do not count (they represent third-party imports).
"""
findings = []

packages: dict[str, list[str]] = {}
for node in self.graph.nodes():
if str(node).startswith("external:"):
if _is_synthetic_node(str(node)):
continue
parts = Path(str(node)).parts
if len(parts) > 1:
Expand All @@ -423,6 +445,8 @@ def _detect_zombie_packages(self, whitelist: set[str]) -> list[DeadCodeFindingDa
for pred in self.graph.predecessors(f):
pred_str = str(pred)
if pred_str.startswith("external:"):
# Third-party imports don't count as cross-package
# importers; framework: synthetic anchors do.
continue
pred_parts = Path(pred_str).parts
if len(pred_parts) > 0 and pred_parts[0] != pkg:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,27 @@ def add(name: str, version: str | None, category: str) -> None:
add("Ruby", None, "language")

# --- composer.json (PHP) ---
if (repo_path / "composer.json").exists():
composer_json = repo_path / "composer.json"
if composer_json.exists():
add("PHP", None, "language")
try:
composer = json.loads(composer_json.read_text(encoding="utf-8"))
except Exception:
composer = None
if isinstance(composer, dict):
requires = {
**(composer.get("require") or {}),
**(composer.get("require-dev") or {}),
}
if (
composer.get("type") == "typo3-cms-extension"
or "typo3/cms-core" in requires
):
add("TYPO3", None, "framework")
elif "symfony/framework-bundle" in requires or "symfony/symfony" in requires:
add("Symfony", None, "framework")
elif "laravel/framework" in requires:
add("Laravel", None, "framework")

# --- Docker ---
if (repo_path / "Dockerfile").exists():
Expand Down
Loading