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,528 @@
|
|
|
1
|
+
"""Code analyzer using optional tree-sitter for AST parsing.
|
|
2
|
+
|
|
3
|
+
Extracts code chunks (functions, classes, imports) from source files
|
|
4
|
+
for intelligent context selection. Falls back to regex-based parsing
|
|
5
|
+
when tree-sitter is not available.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _djb2_hash(s: str) -> int:
|
|
17
|
+
"""DJB2 hash function for content-based cache invalidation."""
|
|
18
|
+
h = 5381
|
|
19
|
+
for ch in s:
|
|
20
|
+
h = ((h << 5) + h + ord(ch)) & 0xFFFFFFFF
|
|
21
|
+
return h
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class CodeChunk:
|
|
26
|
+
"""A chunk of code extracted from a file."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
kind: str # 'function', 'class', 'method', 'import', 'constant'
|
|
30
|
+
start_line: int
|
|
31
|
+
end_line: int
|
|
32
|
+
content: str
|
|
33
|
+
file_path: str = ""
|
|
34
|
+
language: str = ""
|
|
35
|
+
signature: str = "" # Function/method signature
|
|
36
|
+
parent: str = "" # Parent class for methods
|
|
37
|
+
docstring: str = ""
|
|
38
|
+
importance: float = 0.5
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def line_count(self) -> int:
|
|
42
|
+
return self.end_line - self.start_line + 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True)
|
|
46
|
+
class FileAnalysis:
|
|
47
|
+
"""Analysis results for a single file."""
|
|
48
|
+
|
|
49
|
+
path: str
|
|
50
|
+
language: str
|
|
51
|
+
chunks: list[CodeChunk]
|
|
52
|
+
imports: list[str] = field(default_factory=list)
|
|
53
|
+
exports: list[str] = field(default_factory=list)
|
|
54
|
+
line_count: int = 0
|
|
55
|
+
has_main: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Tree-sitter availability flag
|
|
59
|
+
_TREE_SITTER_AVAILABLE = False
|
|
60
|
+
try:
|
|
61
|
+
import tree_sitter # type: ignore[import-untyped] # noqa: F401
|
|
62
|
+
_TREE_SITTER_AVAILABLE = True
|
|
63
|
+
except ImportError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_tree_sitter_available() -> bool:
|
|
68
|
+
"""Check if tree-sitter is available."""
|
|
69
|
+
return _TREE_SITTER_AVAILABLE
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CodeAnalyzer:
|
|
73
|
+
"""Analyzes source code files to extract structured chunks.
|
|
74
|
+
|
|
75
|
+
Uses tree-sitter when available, falls back to regex-based
|
|
76
|
+
extraction otherwise. Caches results using content hashing
|
|
77
|
+
to avoid re-analysis of unchanged files.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
self._parsers: dict[str, Any] = {}
|
|
82
|
+
self._cache: dict[str, tuple[int, FileAnalysis]] = {} # path -> (hash, result)
|
|
83
|
+
self._cache_hits: int = 0
|
|
84
|
+
self._cache_misses: int = 0
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def cache_stats(self) -> dict[str, int]:
|
|
88
|
+
"""Return cache hit/miss statistics."""
|
|
89
|
+
return {
|
|
90
|
+
"hits": self._cache_hits,
|
|
91
|
+
"misses": self._cache_misses,
|
|
92
|
+
"entries": len(self._cache),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def clear_cache(self) -> None:
|
|
96
|
+
"""Clear the analysis cache."""
|
|
97
|
+
self._cache.clear()
|
|
98
|
+
self._cache_hits = 0
|
|
99
|
+
self._cache_misses = 0
|
|
100
|
+
|
|
101
|
+
def invalidate(self, file_path: str) -> None:
|
|
102
|
+
"""Remove cached analysis for a specific file.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
file_path: Path of the file to invalidate.
|
|
106
|
+
"""
|
|
107
|
+
self._cache.pop(file_path, None)
|
|
108
|
+
|
|
109
|
+
def update_file(self, file_path: str, content: str, language: str = "") -> FileAnalysis:
|
|
110
|
+
"""Re-analyze a single file with provided content and update cache.
|
|
111
|
+
|
|
112
|
+
Unlike analyze_file() which reads from disk, this accepts content
|
|
113
|
+
directly — useful when the caller already has the new content
|
|
114
|
+
(e.g. after an edit tool produces it).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
file_path: File path (for language detection and cache key).
|
|
118
|
+
content: The new file content to analyze.
|
|
119
|
+
language: Language hint (auto-detected if empty).
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Updated FileAnalysis.
|
|
123
|
+
"""
|
|
124
|
+
# Invalidate old entry
|
|
125
|
+
self.invalidate(file_path)
|
|
126
|
+
|
|
127
|
+
if not language:
|
|
128
|
+
language = _detect_language(Path(file_path).suffix)
|
|
129
|
+
|
|
130
|
+
lines = content.splitlines()
|
|
131
|
+
content_hash = _djb2_hash(content)
|
|
132
|
+
|
|
133
|
+
if _TREE_SITTER_AVAILABLE and language in ("python", "javascript", "typescript"):
|
|
134
|
+
chunks = self._analyze_with_tree_sitter(content, language, file_path)
|
|
135
|
+
else:
|
|
136
|
+
chunks = self._analyze_with_regex(content, language, file_path)
|
|
137
|
+
|
|
138
|
+
imports = _extract_imports(content, language)
|
|
139
|
+
has_main = _has_main_guard(content, language)
|
|
140
|
+
|
|
141
|
+
result = FileAnalysis(
|
|
142
|
+
path=file_path,
|
|
143
|
+
language=language,
|
|
144
|
+
chunks=chunks,
|
|
145
|
+
imports=imports,
|
|
146
|
+
line_count=len(lines),
|
|
147
|
+
has_main=has_main,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self._cache[file_path] = (content_hash, result)
|
|
151
|
+
self._cache_misses += 1
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
def analyze_file(self, path: str, language: str = "") -> FileAnalysis:
|
|
155
|
+
"""Analyze a source file.
|
|
156
|
+
|
|
157
|
+
Uses content-hash caching: if the file content hasn't changed
|
|
158
|
+
since the last analysis, returns the cached result.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
path: File path to analyze.
|
|
162
|
+
language: Language hint (auto-detected from extension if empty).
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
FileAnalysis with extracted chunks.
|
|
166
|
+
"""
|
|
167
|
+
p = Path(path)
|
|
168
|
+
if not p.exists() or not p.is_file():
|
|
169
|
+
return FileAnalysis(path=path, language=language, chunks=[])
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
173
|
+
except OSError:
|
|
174
|
+
return FileAnalysis(path=path, language=language, chunks=[])
|
|
175
|
+
|
|
176
|
+
# Check content-hash cache
|
|
177
|
+
content_hash = _djb2_hash(content)
|
|
178
|
+
cached = self._cache.get(path)
|
|
179
|
+
if cached is not None and cached[0] == content_hash:
|
|
180
|
+
self._cache_hits += 1
|
|
181
|
+
return cached[1]
|
|
182
|
+
|
|
183
|
+
self._cache_misses += 1
|
|
184
|
+
|
|
185
|
+
if not language:
|
|
186
|
+
language = _detect_language(p.suffix)
|
|
187
|
+
|
|
188
|
+
lines = content.splitlines()
|
|
189
|
+
|
|
190
|
+
if _TREE_SITTER_AVAILABLE and language in ("python", "javascript", "typescript"):
|
|
191
|
+
chunks = self._analyze_with_tree_sitter(content, language, path)
|
|
192
|
+
else:
|
|
193
|
+
chunks = self._analyze_with_regex(content, language, path)
|
|
194
|
+
|
|
195
|
+
# Extract imports
|
|
196
|
+
imports = _extract_imports(content, language)
|
|
197
|
+
|
|
198
|
+
# Check for main entry point
|
|
199
|
+
has_main = _has_main_guard(content, language)
|
|
200
|
+
|
|
201
|
+
result = FileAnalysis(
|
|
202
|
+
path=path,
|
|
203
|
+
language=language,
|
|
204
|
+
chunks=chunks,
|
|
205
|
+
imports=imports,
|
|
206
|
+
line_count=len(lines),
|
|
207
|
+
has_main=has_main,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Store in cache
|
|
211
|
+
self._cache[path] = (content_hash, result)
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
def analyze_files(self, paths: list[str]) -> list[FileAnalysis]:
|
|
215
|
+
"""Analyze multiple files.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
paths: List of file paths.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of FileAnalysis results.
|
|
222
|
+
"""
|
|
223
|
+
return [self.analyze_file(p) for p in paths]
|
|
224
|
+
|
|
225
|
+
def _analyze_with_tree_sitter(
|
|
226
|
+
self, content: str, language: str, path: str
|
|
227
|
+
) -> list[CodeChunk]:
|
|
228
|
+
"""Analyze using tree-sitter (when available)."""
|
|
229
|
+
try:
|
|
230
|
+
import tree_sitter # type: ignore[import-untyped]
|
|
231
|
+
|
|
232
|
+
# Get or create parser
|
|
233
|
+
if language not in self._parsers:
|
|
234
|
+
if language == "python":
|
|
235
|
+
import tree_sitter_python as ts_python # type: ignore[import-untyped]
|
|
236
|
+
lang = tree_sitter.Language(ts_python.language())
|
|
237
|
+
elif language in ("javascript", "typescript"):
|
|
238
|
+
import tree_sitter_javascript as ts_js # type: ignore[import-untyped]
|
|
239
|
+
lang = tree_sitter.Language(ts_js.language())
|
|
240
|
+
else:
|
|
241
|
+
return self._analyze_with_regex(content, language, path)
|
|
242
|
+
|
|
243
|
+
parser = tree_sitter.Parser(lang)
|
|
244
|
+
self._parsers[language] = parser
|
|
245
|
+
|
|
246
|
+
parser = self._parsers[language]
|
|
247
|
+
tree = parser.parse(bytes(content, "utf-8"))
|
|
248
|
+
|
|
249
|
+
chunks: list[CodeChunk] = []
|
|
250
|
+
self._walk_tree_sitter_node(tree.root_node, content, language, path, chunks)
|
|
251
|
+
return chunks
|
|
252
|
+
|
|
253
|
+
except Exception:
|
|
254
|
+
# Fall back to regex on any tree-sitter error
|
|
255
|
+
return self._analyze_with_regex(content, language, path)
|
|
256
|
+
|
|
257
|
+
def _walk_tree_sitter_node(
|
|
258
|
+
self,
|
|
259
|
+
node: Any,
|
|
260
|
+
content: str,
|
|
261
|
+
language: str,
|
|
262
|
+
path: str,
|
|
263
|
+
chunks: list[CodeChunk],
|
|
264
|
+
parent_name: str = "",
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Walk tree-sitter AST and extract chunks."""
|
|
267
|
+
lines = content.splitlines()
|
|
268
|
+
|
|
269
|
+
for child in node.children:
|
|
270
|
+
kind = ""
|
|
271
|
+
name = ""
|
|
272
|
+
|
|
273
|
+
if language == "python":
|
|
274
|
+
if child.type == "function_definition":
|
|
275
|
+
kind = "method" if parent_name else "function"
|
|
276
|
+
name_node = child.child_by_field_name("name")
|
|
277
|
+
name = name_node.text.decode() if name_node else ""
|
|
278
|
+
elif child.type == "class_definition":
|
|
279
|
+
kind = "class"
|
|
280
|
+
name_node = child.child_by_field_name("name")
|
|
281
|
+
name = name_node.text.decode() if name_node else ""
|
|
282
|
+
elif language in ("javascript", "typescript"):
|
|
283
|
+
if child.type in ("function_declaration", "arrow_function"):
|
|
284
|
+
kind = "function"
|
|
285
|
+
name_node = child.child_by_field_name("name")
|
|
286
|
+
name = name_node.text.decode() if name_node else ""
|
|
287
|
+
elif child.type == "class_declaration":
|
|
288
|
+
kind = "class"
|
|
289
|
+
name_node = child.child_by_field_name("name")
|
|
290
|
+
name = name_node.text.decode() if name_node else ""
|
|
291
|
+
|
|
292
|
+
if kind and name:
|
|
293
|
+
start_line = child.start_point[0] + 1
|
|
294
|
+
end_line = child.end_point[0] + 1
|
|
295
|
+
chunk_content = "\n".join(lines[start_line - 1:end_line])
|
|
296
|
+
|
|
297
|
+
# Extract signature (first line)
|
|
298
|
+
signature = lines[start_line - 1].strip() if start_line <= len(lines) else ""
|
|
299
|
+
|
|
300
|
+
# Extract docstring
|
|
301
|
+
docstring = _extract_docstring(chunk_content, language)
|
|
302
|
+
|
|
303
|
+
chunks.append(CodeChunk(
|
|
304
|
+
name=name,
|
|
305
|
+
kind=kind,
|
|
306
|
+
start_line=start_line,
|
|
307
|
+
end_line=end_line,
|
|
308
|
+
content=chunk_content,
|
|
309
|
+
file_path=path,
|
|
310
|
+
language=language,
|
|
311
|
+
signature=signature,
|
|
312
|
+
parent=parent_name,
|
|
313
|
+
docstring=docstring,
|
|
314
|
+
))
|
|
315
|
+
|
|
316
|
+
# Recurse into class bodies
|
|
317
|
+
if kind == "class":
|
|
318
|
+
self._walk_tree_sitter_node(
|
|
319
|
+
child, content, language, path, chunks, parent_name=name
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
# Recurse into other nodes
|
|
323
|
+
self._walk_tree_sitter_node(
|
|
324
|
+
child, content, language, path, chunks, parent_name=parent_name
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def _analyze_with_regex(
|
|
328
|
+
self, content: str, language: str, path: str
|
|
329
|
+
) -> list[CodeChunk]:
|
|
330
|
+
"""Fallback regex-based code analysis."""
|
|
331
|
+
if language == "python":
|
|
332
|
+
return _regex_analyze_python(content, path)
|
|
333
|
+
elif language in ("javascript", "typescript"):
|
|
334
|
+
return _regex_analyze_js_ts(content, path, language)
|
|
335
|
+
elif language in ("rust", "go", "java"):
|
|
336
|
+
return _regex_analyze_generic(content, path, language)
|
|
337
|
+
return []
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _regex_analyze_python(content: str, path: str) -> list[CodeChunk]:
|
|
341
|
+
"""Extract Python chunks using regex."""
|
|
342
|
+
chunks: list[CodeChunk] = []
|
|
343
|
+
lines = content.splitlines()
|
|
344
|
+
|
|
345
|
+
# Match function and class definitions
|
|
346
|
+
patterns = [
|
|
347
|
+
(r"^(class)\s+(\w+)", "class"),
|
|
348
|
+
(r"^(def)\s+(\w+)", "function"),
|
|
349
|
+
(r"^(\s+def)\s+(\w+)", "method"),
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
i = 0
|
|
353
|
+
while i < len(lines):
|
|
354
|
+
line = lines[i]
|
|
355
|
+
for pattern, kind in patterns:
|
|
356
|
+
match = re.match(pattern, line)
|
|
357
|
+
if match:
|
|
358
|
+
name = match.group(2)
|
|
359
|
+
start_line = i + 1
|
|
360
|
+
|
|
361
|
+
# Find end of block (next line with same or less indentation)
|
|
362
|
+
indent = len(line) - len(line.lstrip())
|
|
363
|
+
end_line = start_line
|
|
364
|
+
for j in range(i + 1, len(lines)):
|
|
365
|
+
stripped = lines[j].strip()
|
|
366
|
+
if not stripped:
|
|
367
|
+
continue
|
|
368
|
+
line_indent = len(lines[j]) - len(lines[j].lstrip())
|
|
369
|
+
if line_indent <= indent and stripped:
|
|
370
|
+
end_line = j
|
|
371
|
+
break
|
|
372
|
+
else:
|
|
373
|
+
end_line = len(lines)
|
|
374
|
+
|
|
375
|
+
chunk_content = "\n".join(lines[i:end_line])
|
|
376
|
+
signature = line.strip()
|
|
377
|
+
docstring = _extract_docstring(chunk_content, "python")
|
|
378
|
+
|
|
379
|
+
# Determine parent
|
|
380
|
+
parent = ""
|
|
381
|
+
if kind == "method" and indent > 0:
|
|
382
|
+
# Look backwards for class definition
|
|
383
|
+
for k in range(i - 1, -1, -1):
|
|
384
|
+
cls_match = re.match(r"^class\s+(\w+)", lines[k])
|
|
385
|
+
if cls_match:
|
|
386
|
+
parent = cls_match.group(1)
|
|
387
|
+
break
|
|
388
|
+
|
|
389
|
+
chunks.append(CodeChunk(
|
|
390
|
+
name=name,
|
|
391
|
+
kind=kind,
|
|
392
|
+
start_line=start_line,
|
|
393
|
+
end_line=end_line,
|
|
394
|
+
content=chunk_content,
|
|
395
|
+
file_path=path,
|
|
396
|
+
language="python",
|
|
397
|
+
signature=signature,
|
|
398
|
+
parent=parent,
|
|
399
|
+
docstring=docstring,
|
|
400
|
+
))
|
|
401
|
+
break
|
|
402
|
+
i += 1
|
|
403
|
+
|
|
404
|
+
return chunks
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _regex_analyze_js_ts(content: str, path: str, language: str) -> list[CodeChunk]:
|
|
408
|
+
"""Extract JS/TS chunks using regex."""
|
|
409
|
+
chunks: list[CodeChunk] = []
|
|
410
|
+
lines = content.splitlines()
|
|
411
|
+
|
|
412
|
+
patterns = [
|
|
413
|
+
(r"(?:export\s+)?(?:async\s+)?function\s+(\w+)", "function"),
|
|
414
|
+
(r"(?:export\s+)?class\s+(\w+)", "class"),
|
|
415
|
+
(r"(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(", "function"),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
for i, line in enumerate(lines):
|
|
419
|
+
for pattern, kind in patterns:
|
|
420
|
+
match = re.search(pattern, line)
|
|
421
|
+
if match:
|
|
422
|
+
name = match.group(1)
|
|
423
|
+
start_line = i + 1
|
|
424
|
+
# Simple heuristic: find matching brace
|
|
425
|
+
brace_count = line.count("{") - line.count("}")
|
|
426
|
+
end_line = start_line
|
|
427
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
428
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
429
|
+
if brace_count <= 0:
|
|
430
|
+
end_line = j + 1
|
|
431
|
+
break
|
|
432
|
+
else:
|
|
433
|
+
end_line = min(i + 50, len(lines))
|
|
434
|
+
|
|
435
|
+
chunk_content = "\n".join(lines[i:end_line])
|
|
436
|
+
chunks.append(CodeChunk(
|
|
437
|
+
name=name,
|
|
438
|
+
kind=kind,
|
|
439
|
+
start_line=start_line,
|
|
440
|
+
end_line=end_line,
|
|
441
|
+
content=chunk_content,
|
|
442
|
+
file_path=path,
|
|
443
|
+
language=language,
|
|
444
|
+
signature=line.strip(),
|
|
445
|
+
))
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
return chunks
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _regex_analyze_generic(content: str, path: str, language: str) -> list[CodeChunk]:
|
|
452
|
+
"""Basic chunk extraction for other languages."""
|
|
453
|
+
chunks: list[CodeChunk] = []
|
|
454
|
+
lines = content.splitlines()
|
|
455
|
+
|
|
456
|
+
# Generic function pattern
|
|
457
|
+
func_pattern = re.compile(r"(?:pub\s+)?(?:fn|func|function|def)\s+(\w+)")
|
|
458
|
+
|
|
459
|
+
for i, line in enumerate(lines):
|
|
460
|
+
match = func_pattern.search(line)
|
|
461
|
+
if match:
|
|
462
|
+
name = match.group(1)
|
|
463
|
+
start_line = i + 1
|
|
464
|
+
brace_count = line.count("{") - line.count("}")
|
|
465
|
+
end_line = start_line
|
|
466
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
467
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
468
|
+
if brace_count <= 0:
|
|
469
|
+
end_line = j + 1
|
|
470
|
+
break
|
|
471
|
+
else:
|
|
472
|
+
end_line = min(i + 50, len(lines))
|
|
473
|
+
|
|
474
|
+
chunks.append(CodeChunk(
|
|
475
|
+
name=name,
|
|
476
|
+
kind="function",
|
|
477
|
+
start_line=start_line,
|
|
478
|
+
end_line=end_line,
|
|
479
|
+
content="\n".join(lines[i:end_line]),
|
|
480
|
+
file_path=path,
|
|
481
|
+
language=language,
|
|
482
|
+
signature=line.strip(),
|
|
483
|
+
))
|
|
484
|
+
|
|
485
|
+
return chunks
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _detect_language(ext: str) -> str:
|
|
489
|
+
"""Detect language from file extension."""
|
|
490
|
+
from attocode_intel._internal.integrations.context.codebase_ast import LANG_EXTENSIONS
|
|
491
|
+
return LANG_EXTENSIONS.get(ext, "")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _extract_imports(content: str, language: str) -> list[str]:
|
|
495
|
+
"""Extract import statements from source code."""
|
|
496
|
+
imports: list[str] = []
|
|
497
|
+
if language == "python":
|
|
498
|
+
for line in content.splitlines():
|
|
499
|
+
stripped = line.strip()
|
|
500
|
+
if stripped.startswith("import ") or stripped.startswith("from "):
|
|
501
|
+
imports.append(stripped)
|
|
502
|
+
elif language in ("javascript", "typescript"):
|
|
503
|
+
import_re = re.compile(r"^import\s+.+", re.MULTILINE)
|
|
504
|
+
imports = import_re.findall(content)
|
|
505
|
+
return imports[:50] # Limit
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _extract_docstring(content: str, language: str) -> str:
|
|
509
|
+
"""Extract docstring from a code chunk."""
|
|
510
|
+
if language == "python":
|
|
511
|
+
match = re.search(r'"""(.*?)"""', content, re.DOTALL)
|
|
512
|
+
if match:
|
|
513
|
+
return match.group(1).strip()[:200]
|
|
514
|
+
match = re.search(r"'''(.*?)'''", content, re.DOTALL)
|
|
515
|
+
if match:
|
|
516
|
+
return match.group(1).strip()[:200]
|
|
517
|
+
elif language in ("javascript", "typescript"):
|
|
518
|
+
match = re.search(r"/\*\*(.*?)\*/", content, re.DOTALL)
|
|
519
|
+
if match:
|
|
520
|
+
return match.group(1).strip()[:200]
|
|
521
|
+
return ""
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _has_main_guard(content: str, language: str) -> bool:
|
|
525
|
+
"""Check if file has a main guard."""
|
|
526
|
+
if language == "python":
|
|
527
|
+
return 'if __name__' in content
|
|
528
|
+
return False
|