attocode-code-intel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"""Dead code detection tools for the code-intel MCP server.
|
|
2
|
+
|
|
3
|
+
Provides a ``dead_code`` tool that identifies unreferenced symbols, files,
|
|
4
|
+
and modules using the cross-reference index. Three analysis levels:
|
|
5
|
+
|
|
6
|
+
- ``symbol``: Functions, classes, and methods with zero references.
|
|
7
|
+
- ``file``: Files with zero importers that are not entry points.
|
|
8
|
+
- ``module``: Directories with no external imports from outside.
|
|
9
|
+
|
|
10
|
+
Entry points (tests, CLI mains, framework routes, etc.) are automatically
|
|
11
|
+
excluded using filename and decorator heuristics.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from attocode_intel._shared import (
|
|
22
|
+
_get_ast_service,
|
|
23
|
+
_get_context_mgr,
|
|
24
|
+
_get_project_dir,
|
|
25
|
+
_get_remote_service,
|
|
26
|
+
mcp,
|
|
27
|
+
)
|
|
28
|
+
except (ImportError, AttributeError):
|
|
29
|
+
# Deferred: will be available when server.py finishes loading.
|
|
30
|
+
# Private helpers (_find_dead_*) don't need these imports.
|
|
31
|
+
_get_ast_service = None # type: ignore[assignment]
|
|
32
|
+
_get_context_mgr = None # type: ignore[assignment]
|
|
33
|
+
_get_project_dir = None # type: ignore[assignment]
|
|
34
|
+
_get_remote_service = None # type: ignore[assignment]
|
|
35
|
+
|
|
36
|
+
class _StubMCP:
|
|
37
|
+
"""Stub so @mcp.tool() doesn't crash during deferred import."""
|
|
38
|
+
def tool(self):
|
|
39
|
+
return lambda f: f
|
|
40
|
+
mcp = _StubMCP() # type: ignore[assignment]
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from attocode_intel._internal.integrations.context.cross_references import CrossRefIndex, SymbolLocation
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Entry-point heuristics
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
_ENTRY_POINT_FILE_PATTERNS: set[str] = {
|
|
51
|
+
"main.py",
|
|
52
|
+
"__main__.py",
|
|
53
|
+
"__init__.py",
|
|
54
|
+
"setup.py",
|
|
55
|
+
"manage.py",
|
|
56
|
+
"conftest.py",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_ENTRY_POINT_FILE_PREFIXES = ("test_",)
|
|
60
|
+
_ENTRY_POINT_FILE_SUFFIXES = ("_test.py",)
|
|
61
|
+
|
|
62
|
+
_ENTRY_POINT_DECORATOR_KEYWORDS: set[str] = {
|
|
63
|
+
"route",
|
|
64
|
+
"endpoint",
|
|
65
|
+
"fixture",
|
|
66
|
+
"command",
|
|
67
|
+
"click",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_ENTRY_POINT_FUNCTION_NAMES: set[str] = {
|
|
71
|
+
"main",
|
|
72
|
+
"setup",
|
|
73
|
+
"teardown",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_entry_point_file(rel_path: str) -> bool:
|
|
78
|
+
"""Return True if *rel_path* looks like a test, CLI main, or config file."""
|
|
79
|
+
basename = os.path.basename(rel_path)
|
|
80
|
+
if basename in _ENTRY_POINT_FILE_PATTERNS:
|
|
81
|
+
return True
|
|
82
|
+
if any(basename.startswith(p) for p in _ENTRY_POINT_FILE_PREFIXES):
|
|
83
|
+
return True
|
|
84
|
+
return any(basename.endswith(s) for s in _ENTRY_POINT_FILE_SUFFIXES)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _is_entry_point_symbol(
|
|
88
|
+
loc: SymbolLocation,
|
|
89
|
+
ast_cache: dict,
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""Return True if the symbol at *loc* looks like a framework entry point.
|
|
92
|
+
|
|
93
|
+
Checks decorator names and special function names.
|
|
94
|
+
"""
|
|
95
|
+
# Special function names
|
|
96
|
+
base_name = loc.name.rsplit(".", 1)[-1] if "." in loc.name else loc.name
|
|
97
|
+
if base_name in _ENTRY_POINT_FUNCTION_NAMES:
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
# Symbols defined in __init__.py are considered re-exports
|
|
101
|
+
if os.path.basename(loc.file_path) == "__init__.py":
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
# Check decorators from AST cache
|
|
105
|
+
file_ast = ast_cache.get(loc.file_path)
|
|
106
|
+
if file_ast is None:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
# Search functions and class methods for matching decorator keywords
|
|
110
|
+
for func in file_ast.functions:
|
|
111
|
+
if func.name == base_name or loc.qualified_name.endswith(f".{func.name}"):
|
|
112
|
+
for dec in func.decorators:
|
|
113
|
+
if any(kw in dec.lower() for kw in _ENTRY_POINT_DECORATOR_KEYWORDS):
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
for cls in file_ast.classes:
|
|
117
|
+
if cls.name == base_name or loc.qualified_name.endswith(f".{cls.name}"):
|
|
118
|
+
for dec in cls.decorators:
|
|
119
|
+
if any(kw in dec.lower() for kw in _ENTRY_POINT_DECORATOR_KEYWORDS):
|
|
120
|
+
return True
|
|
121
|
+
# Check methods inside the class
|
|
122
|
+
for method in cls.methods:
|
|
123
|
+
qname_suffix = f"{cls.name}.{method.name}"
|
|
124
|
+
if loc.qualified_name.endswith(qname_suffix):
|
|
125
|
+
for dec in method.decorators:
|
|
126
|
+
if any(kw in dec.lower() for kw in _ENTRY_POINT_DECORATOR_KEYWORDS):
|
|
127
|
+
return True
|
|
128
|
+
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Confidence scoring
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _compute_confidence(
|
|
138
|
+
loc: SymbolLocation,
|
|
139
|
+
index: CrossRefIndex,
|
|
140
|
+
project_dir: str,
|
|
141
|
+
) -> float:
|
|
142
|
+
"""Compute a confidence score (0.0-1.0) that *loc* is truly dead code.
|
|
143
|
+
|
|
144
|
+
Higher confidence = more likely to be safe to remove.
|
|
145
|
+
"""
|
|
146
|
+
name = loc.name
|
|
147
|
+
is_private = name.startswith("_") and not name.startswith("__")
|
|
148
|
+
|
|
149
|
+
# Base score
|
|
150
|
+
confidence = 0.8 if is_private else 0.7
|
|
151
|
+
|
|
152
|
+
# Penalty: symbol defined in __init__.py (likely a re-export)
|
|
153
|
+
if os.path.basename(loc.file_path) == "__init__.py":
|
|
154
|
+
confidence -= 0.3
|
|
155
|
+
|
|
156
|
+
# Penalty: looks like a public API (no underscore prefix + exported)
|
|
157
|
+
if not name.startswith("_"):
|
|
158
|
+
confidence -= 0.2
|
|
159
|
+
|
|
160
|
+
# Bonus: file hasn't been modified recently (> 180 days)
|
|
161
|
+
try:
|
|
162
|
+
abs_path = os.path.join(project_dir, loc.file_path)
|
|
163
|
+
mtime = os.path.getmtime(abs_path)
|
|
164
|
+
age_days = (time.time() - mtime) / 86400
|
|
165
|
+
if age_days > 180:
|
|
166
|
+
confidence += 0.1
|
|
167
|
+
except OSError:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return max(0.0, min(1.0, round(confidence, 2)))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Dead-code finders
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _find_dead_symbols(
|
|
179
|
+
index: CrossRefIndex,
|
|
180
|
+
ast_cache: dict,
|
|
181
|
+
project_dir: str,
|
|
182
|
+
scope: str,
|
|
183
|
+
entry_points: set[str],
|
|
184
|
+
min_confidence: float,
|
|
185
|
+
top_n: int,
|
|
186
|
+
) -> list[dict]:
|
|
187
|
+
"""Find symbols with 0 references, excluding entry points."""
|
|
188
|
+
candidates: list[dict] = []
|
|
189
|
+
|
|
190
|
+
for qname, locs in index.definitions.items():
|
|
191
|
+
for loc in locs:
|
|
192
|
+
# Skip if outside scope
|
|
193
|
+
if scope and not loc.file_path.startswith(scope):
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
# Skip user-specified entry points
|
|
197
|
+
if qname in entry_points or loc.name in entry_points:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# Skip auto-detected entry points
|
|
201
|
+
if _is_entry_point_file(loc.file_path):
|
|
202
|
+
continue
|
|
203
|
+
if _is_entry_point_symbol(loc, ast_cache):
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# Check references: look up by both qualified name and base name
|
|
207
|
+
refs_by_qname = index.references.get(qname, [])
|
|
208
|
+
base_name = qname.rsplit(".", 1)[-1] if "." in qname else qname
|
|
209
|
+
refs_by_base = index.references.get(base_name, [])
|
|
210
|
+
|
|
211
|
+
# Filter out self-references (references from the same file)
|
|
212
|
+
external_refs_q = [
|
|
213
|
+
r for r in refs_by_qname if r.file_path != loc.file_path
|
|
214
|
+
]
|
|
215
|
+
external_refs_b = [
|
|
216
|
+
r for r in refs_by_base if r.file_path != loc.file_path
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# If there are any external references, it's not dead
|
|
220
|
+
if external_refs_q or external_refs_b:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# Also check if the base name has any references at all (suffix match)
|
|
224
|
+
# For methods like "ClassName.method", check if "method" is referenced
|
|
225
|
+
has_any_ref = False
|
|
226
|
+
if "." in qname:
|
|
227
|
+
for ref_name, refs in index.references.items():
|
|
228
|
+
if ref_name == base_name or ref_name.endswith(f".{base_name}"):
|
|
229
|
+
external = [
|
|
230
|
+
r for r in refs if r.file_path != loc.file_path
|
|
231
|
+
]
|
|
232
|
+
if external:
|
|
233
|
+
has_any_ref = True
|
|
234
|
+
break
|
|
235
|
+
if has_any_ref:
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# Compute confidence
|
|
239
|
+
confidence = _compute_confidence(loc, index, project_dir)
|
|
240
|
+
if confidence < min_confidence:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
# Determine reason
|
|
244
|
+
reasons: list[str] = []
|
|
245
|
+
if loc.kind == "method":
|
|
246
|
+
reasons.append("method with no external callers")
|
|
247
|
+
elif loc.kind == "class":
|
|
248
|
+
reasons.append("class never instantiated or referenced")
|
|
249
|
+
else:
|
|
250
|
+
reasons.append("function never called or imported")
|
|
251
|
+
|
|
252
|
+
if loc.name.startswith("_"):
|
|
253
|
+
reasons.append("private symbol")
|
|
254
|
+
|
|
255
|
+
candidates.append({
|
|
256
|
+
"symbol": loc.qualified_name,
|
|
257
|
+
"name": loc.name,
|
|
258
|
+
"kind": loc.kind,
|
|
259
|
+
"file": loc.file_path,
|
|
260
|
+
"line": loc.start_line,
|
|
261
|
+
"end_line": loc.end_line,
|
|
262
|
+
"confidence": confidence,
|
|
263
|
+
"reason": "; ".join(reasons),
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
# Sort by confidence descending, then by file/line
|
|
267
|
+
candidates.sort(key=lambda c: (-c["confidence"], c["file"], c["line"]))
|
|
268
|
+
return candidates[:top_n]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _find_dead_files(
|
|
272
|
+
index: CrossRefIndex,
|
|
273
|
+
all_file_paths: set[str],
|
|
274
|
+
scope: str,
|
|
275
|
+
entry_points: set[str],
|
|
276
|
+
min_confidence: float,
|
|
277
|
+
top_n: int,
|
|
278
|
+
project_dir: str,
|
|
279
|
+
) -> list[dict]:
|
|
280
|
+
"""Find files with 0 importers that are not entry points."""
|
|
281
|
+
candidates: list[dict] = []
|
|
282
|
+
|
|
283
|
+
for rel_path in sorted(all_file_paths):
|
|
284
|
+
# Skip if outside scope
|
|
285
|
+
if scope and not rel_path.startswith(scope):
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Skip user-specified entry points
|
|
289
|
+
if rel_path in entry_points:
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Skip auto-detected entry-point files
|
|
293
|
+
if _is_entry_point_file(rel_path):
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Check importers
|
|
297
|
+
importers = index.file_dependents.get(rel_path, set())
|
|
298
|
+
if importers:
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
# Compute file-level confidence
|
|
302
|
+
basename = os.path.basename(rel_path)
|
|
303
|
+
is_private = basename.startswith("_") and basename != "__init__.py"
|
|
304
|
+
confidence = 0.8 if is_private else 0.7
|
|
305
|
+
|
|
306
|
+
# Penalty for __init__.py
|
|
307
|
+
if basename == "__init__.py":
|
|
308
|
+
confidence -= 0.3
|
|
309
|
+
|
|
310
|
+
# Penalty for public-looking files
|
|
311
|
+
if not basename.startswith("_"):
|
|
312
|
+
confidence -= 0.2
|
|
313
|
+
|
|
314
|
+
# Bonus for old files
|
|
315
|
+
try:
|
|
316
|
+
abs_path = os.path.join(project_dir, rel_path)
|
|
317
|
+
mtime = os.path.getmtime(abs_path)
|
|
318
|
+
age_days = (time.time() - mtime) / 86400
|
|
319
|
+
if age_days > 180:
|
|
320
|
+
confidence += 0.1
|
|
321
|
+
except OSError:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
confidence = max(0.0, min(1.0, round(confidence, 2)))
|
|
325
|
+
if confidence < min_confidence:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Count symbols in the file
|
|
329
|
+
symbol_count = len(index.file_symbols.get(rel_path, set()))
|
|
330
|
+
deps_count = len(index.file_dependencies.get(rel_path, set()))
|
|
331
|
+
|
|
332
|
+
reasons: list[str] = ["no files import this module"]
|
|
333
|
+
if deps_count == 0:
|
|
334
|
+
reasons.append("also has no imports (orphan)")
|
|
335
|
+
|
|
336
|
+
candidates.append({
|
|
337
|
+
"file": rel_path,
|
|
338
|
+
"symbol_count": symbol_count,
|
|
339
|
+
"dependency_count": deps_count,
|
|
340
|
+
"confidence": confidence,
|
|
341
|
+
"reason": "; ".join(reasons),
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
candidates.sort(key=lambda c: (-c["confidence"], c["file"]))
|
|
345
|
+
return candidates[:top_n]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _find_dead_modules(
|
|
349
|
+
index: CrossRefIndex,
|
|
350
|
+
all_file_paths: set[str],
|
|
351
|
+
scope: str,
|
|
352
|
+
entry_points: set[str],
|
|
353
|
+
min_confidence: float,
|
|
354
|
+
top_n: int,
|
|
355
|
+
project_dir: str,
|
|
356
|
+
) -> list[dict]:
|
|
357
|
+
"""Find directories where no file is imported from outside the directory."""
|
|
358
|
+
# Group files by directory
|
|
359
|
+
dir_files: dict[str, set[str]] = {}
|
|
360
|
+
for rel_path in all_file_paths:
|
|
361
|
+
dir_name = os.path.dirname(rel_path)
|
|
362
|
+
if not dir_name:
|
|
363
|
+
continue # skip root-level files
|
|
364
|
+
dir_files.setdefault(dir_name, set()).add(rel_path)
|
|
365
|
+
|
|
366
|
+
candidates: list[dict] = []
|
|
367
|
+
|
|
368
|
+
for dir_path, files_in_dir in sorted(dir_files.items()):
|
|
369
|
+
# Skip if outside scope
|
|
370
|
+
if scope and not dir_path.startswith(scope):
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
# Skip user-specified entry points
|
|
374
|
+
if dir_path in entry_points:
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
# Check if any file in this directory is imported from outside
|
|
378
|
+
has_external_importer = False
|
|
379
|
+
for f in files_in_dir:
|
|
380
|
+
importers = index.file_dependents.get(f, set())
|
|
381
|
+
for imp in importers:
|
|
382
|
+
if os.path.dirname(imp) != dir_path:
|
|
383
|
+
has_external_importer = True
|
|
384
|
+
break
|
|
385
|
+
if has_external_importer:
|
|
386
|
+
break
|
|
387
|
+
|
|
388
|
+
if has_external_importer:
|
|
389
|
+
continue
|
|
390
|
+
|
|
391
|
+
# Check if any file in the directory is an entry point
|
|
392
|
+
has_entry_point = any(_is_entry_point_file(f) for f in files_in_dir)
|
|
393
|
+
|
|
394
|
+
# Base confidence
|
|
395
|
+
confidence = 0.7
|
|
396
|
+
if has_entry_point:
|
|
397
|
+
confidence -= 0.3 # might be a standalone executable package
|
|
398
|
+
|
|
399
|
+
# Bonus for old directories
|
|
400
|
+
try:
|
|
401
|
+
newest_mtime = 0.0
|
|
402
|
+
for f in files_in_dir:
|
|
403
|
+
abs_path = os.path.join(project_dir, f)
|
|
404
|
+
try:
|
|
405
|
+
mt = os.path.getmtime(abs_path)
|
|
406
|
+
newest_mtime = max(newest_mtime, mt)
|
|
407
|
+
except OSError:
|
|
408
|
+
pass
|
|
409
|
+
if newest_mtime > 0:
|
|
410
|
+
age_days = (time.time() - newest_mtime) / 86400
|
|
411
|
+
if age_days > 180:
|
|
412
|
+
confidence += 0.1
|
|
413
|
+
except OSError:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
confidence = max(0.0, min(1.0, round(confidence, 2)))
|
|
417
|
+
if confidence < min_confidence:
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
# Count total symbols across all files in directory
|
|
421
|
+
total_symbols = sum(
|
|
422
|
+
len(index.file_symbols.get(f, set())) for f in files_in_dir
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
reasons: list[str] = [
|
|
426
|
+
f"no external imports into this directory ({len(files_in_dir)} files)"
|
|
427
|
+
]
|
|
428
|
+
if has_entry_point:
|
|
429
|
+
reasons.append("contains entry-point files (lower confidence)")
|
|
430
|
+
|
|
431
|
+
candidates.append({
|
|
432
|
+
"module": dir_path,
|
|
433
|
+
"file_count": len(files_in_dir),
|
|
434
|
+
"symbol_count": total_symbols,
|
|
435
|
+
"confidence": confidence,
|
|
436
|
+
"reason": "; ".join(reasons),
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
candidates.sort(key=lambda c: (-c["confidence"], c["module"]))
|
|
440
|
+
return candidates[:top_n]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# ---------------------------------------------------------------------------
|
|
444
|
+
# MCP Tool
|
|
445
|
+
# ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@mcp.tool()
|
|
449
|
+
def dead_code(
|
|
450
|
+
scope: str = "",
|
|
451
|
+
entry_points: list[str] | None = None,
|
|
452
|
+
level: str = "symbol",
|
|
453
|
+
min_confidence: float = 0.5,
|
|
454
|
+
top_n: int = 30,
|
|
455
|
+
) -> str:
|
|
456
|
+
"""Detect potentially dead (unreferenced) code in the project.
|
|
457
|
+
|
|
458
|
+
Scans the cross-reference index for symbols, files, or modules that
|
|
459
|
+
have zero external references and are not detected as entry points.
|
|
460
|
+
Results are ranked by confidence score.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
scope: Restrict analysis to files under this directory prefix
|
|
464
|
+
(e.g. "src/mypackage/"). Empty string means whole project.
|
|
465
|
+
entry_points: Additional qualified names or file paths to treat as
|
|
466
|
+
entry points (never flagged as dead). Auto-detected
|
|
467
|
+
entry points (tests, mains, framework routes) are
|
|
468
|
+
always excluded.
|
|
469
|
+
level: Analysis granularity. One of:
|
|
470
|
+
- "symbol": individual functions, classes, methods (default)
|
|
471
|
+
- "file": entire files with no importers
|
|
472
|
+
- "module": directories with no external imports
|
|
473
|
+
min_confidence: Minimum confidence threshold (0.0-1.0). Only report
|
|
474
|
+
items at or above this score. Default 0.5.
|
|
475
|
+
top_n: Maximum number of results to return. Default 30.
|
|
476
|
+
"""
|
|
477
|
+
if _get_remote_service is not None:
|
|
478
|
+
remote = _get_remote_service()
|
|
479
|
+
if remote is not None:
|
|
480
|
+
return remote.dead_code(
|
|
481
|
+
scope=scope,
|
|
482
|
+
entry_points=entry_points,
|
|
483
|
+
level=level,
|
|
484
|
+
min_confidence=min_confidence,
|
|
485
|
+
top_n=top_n,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
valid_levels = {"symbol", "file", "module"}
|
|
489
|
+
if level not in valid_levels:
|
|
490
|
+
opts = ", ".join(sorted(valid_levels))
|
|
491
|
+
return f"Error: invalid level '{level}'. Must be one of: {opts}"
|
|
492
|
+
|
|
493
|
+
min_confidence = max(0.0, min(1.0, min_confidence))
|
|
494
|
+
top_n = max(1, min(200, top_n))
|
|
495
|
+
|
|
496
|
+
svc = _get_ast_service()
|
|
497
|
+
index = svc._index
|
|
498
|
+
ast_cache = svc._ast_cache
|
|
499
|
+
project_dir = _get_project_dir()
|
|
500
|
+
|
|
501
|
+
ctx = _get_context_mgr()
|
|
502
|
+
all_file_paths = {fi.relative_path for fi in ctx._files}
|
|
503
|
+
|
|
504
|
+
ep_set = set(entry_points) if entry_points else set()
|
|
505
|
+
|
|
506
|
+
# Normalize scope
|
|
507
|
+
if scope and not scope.endswith("/"):
|
|
508
|
+
scope = scope + "/"
|
|
509
|
+
|
|
510
|
+
if level == "symbol":
|
|
511
|
+
items = _find_dead_symbols(
|
|
512
|
+
index, ast_cache, project_dir, scope, ep_set,
|
|
513
|
+
min_confidence, top_n,
|
|
514
|
+
)
|
|
515
|
+
return _format_symbol_results(items, scope)
|
|
516
|
+
elif level == "file":
|
|
517
|
+
items = _find_dead_files(
|
|
518
|
+
index, all_file_paths, scope, ep_set,
|
|
519
|
+
min_confidence, top_n, project_dir,
|
|
520
|
+
)
|
|
521
|
+
return _format_file_results(items, scope)
|
|
522
|
+
else: # module
|
|
523
|
+
items = _find_dead_modules(
|
|
524
|
+
index, all_file_paths, scope, ep_set,
|
|
525
|
+
min_confidence, top_n, project_dir,
|
|
526
|
+
)
|
|
527
|
+
return _format_module_results(items, scope)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ---------------------------------------------------------------------------
|
|
531
|
+
# Output formatters
|
|
532
|
+
# ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _format_symbol_results(items: list[dict], scope: str) -> str:
|
|
536
|
+
"""Format dead symbol results as ranked text output."""
|
|
537
|
+
scope_label = f" in {scope}" if scope else ""
|
|
538
|
+
if not items:
|
|
539
|
+
return f"No dead symbols detected{scope_label}."
|
|
540
|
+
|
|
541
|
+
lines = [f"Dead symbols detected{scope_label} ({len(items)} results):\n"]
|
|
542
|
+
for i, item in enumerate(items, 1):
|
|
543
|
+
lines.append(
|
|
544
|
+
f" {i:2d}. [{item['confidence']:.2f}] {item['kind']} {item['symbol']}\n"
|
|
545
|
+
f" {item['file']}:{item['line']}\n"
|
|
546
|
+
f" Reason: {item['reason']}"
|
|
547
|
+
)
|
|
548
|
+
return "\n".join(lines)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _format_file_results(items: list[dict], scope: str) -> str:
|
|
552
|
+
"""Format dead file results as ranked text output."""
|
|
553
|
+
scope_label = f" in {scope}" if scope else ""
|
|
554
|
+
if not items:
|
|
555
|
+
return f"No dead files detected{scope_label}."
|
|
556
|
+
|
|
557
|
+
lines = [f"Dead files detected{scope_label} ({len(items)} results):\n"]
|
|
558
|
+
for i, item in enumerate(items, 1):
|
|
559
|
+
lines.append(
|
|
560
|
+
f" {i:2d}. [{item['confidence']:.2f}] {item['file']}\n"
|
|
561
|
+
f" {item['symbol_count']} symbols, "
|
|
562
|
+
f"{item['dependency_count']} dependencies\n"
|
|
563
|
+
f" Reason: {item['reason']}"
|
|
564
|
+
)
|
|
565
|
+
return "\n".join(lines)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _format_module_results(items: list[dict], scope: str) -> str:
|
|
569
|
+
"""Format dead module results as ranked text output."""
|
|
570
|
+
scope_label = f" in {scope}" if scope else ""
|
|
571
|
+
if not items:
|
|
572
|
+
return f"No dead modules detected{scope_label}."
|
|
573
|
+
|
|
574
|
+
lines = [f"Dead modules detected{scope_label} ({len(items)} results):\n"]
|
|
575
|
+
for i, item in enumerate(items, 1):
|
|
576
|
+
lines.append(
|
|
577
|
+
f" {i:2d}. [{item['confidence']:.2f}] {item['module']}/\n"
|
|
578
|
+
f" {item['file_count']} files, "
|
|
579
|
+
f"{item['symbol_count']} symbols\n"
|
|
580
|
+
f" Reason: {item['reason']}"
|
|
581
|
+
)
|
|
582
|
+
return "\n".join(lines)
|