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,1461 @@
|
|
|
1
|
+
"""Codebase AST analysis using tree-sitter.
|
|
2
|
+
|
|
3
|
+
Provides structural code analysis for Python, JavaScript, and TypeScript
|
|
4
|
+
using tree-sitter parsers. Falls back to regex-based extraction when
|
|
5
|
+
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 Literal
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Parameter and property detail types
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class ParamDef:
|
|
22
|
+
"""Detailed parameter definition for a function/method."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
type_annotation: str = ""
|
|
26
|
+
default_value: str = ""
|
|
27
|
+
is_rest: bool = False # *args
|
|
28
|
+
is_kwonly: bool = False # after *
|
|
29
|
+
is_kwargs: bool = False # **kwargs
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class PropertyDef:
|
|
34
|
+
"""A class property (attribute) definition."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
start_line: int
|
|
38
|
+
type_annotation: str = ""
|
|
39
|
+
has_default: bool = False
|
|
40
|
+
visibility: str = "public" # public / private (_prefix) / name_mangled (__prefix)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Core AST node types
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(slots=True)
|
|
49
|
+
class FunctionDef:
|
|
50
|
+
"""A function definition."""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
start_line: int
|
|
54
|
+
end_line: int
|
|
55
|
+
params: list[str] = field(default_factory=list)
|
|
56
|
+
return_type: str = ""
|
|
57
|
+
decorators: list[str] = field(default_factory=list)
|
|
58
|
+
is_async: bool = False
|
|
59
|
+
is_method: bool = False
|
|
60
|
+
docstring: str = ""
|
|
61
|
+
# --- Extended fields ---
|
|
62
|
+
parameters: list[ParamDef] = field(default_factory=list)
|
|
63
|
+
visibility: str = "public"
|
|
64
|
+
is_generator: bool = False
|
|
65
|
+
is_staticmethod: bool = False
|
|
66
|
+
is_classmethod: bool = False
|
|
67
|
+
is_property: bool = False
|
|
68
|
+
type_params: list[str] = field(default_factory=list)
|
|
69
|
+
qualified_name: str = ""
|
|
70
|
+
is_nested: bool = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class ClassDef:
|
|
75
|
+
"""A class definition."""
|
|
76
|
+
|
|
77
|
+
name: str
|
|
78
|
+
start_line: int
|
|
79
|
+
end_line: int
|
|
80
|
+
bases: list[str] = field(default_factory=list)
|
|
81
|
+
methods: list[FunctionDef] = field(default_factory=list)
|
|
82
|
+
decorators: list[str] = field(default_factory=list)
|
|
83
|
+
docstring: str = ""
|
|
84
|
+
# --- Extended fields ---
|
|
85
|
+
properties: list[PropertyDef] = field(default_factory=list)
|
|
86
|
+
is_abstract: bool = False
|
|
87
|
+
metaclass: str = ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(slots=True)
|
|
91
|
+
class ImportDef:
|
|
92
|
+
"""An import statement."""
|
|
93
|
+
|
|
94
|
+
module: str
|
|
95
|
+
names: list[str] = field(default_factory=list)
|
|
96
|
+
alias: str = ""
|
|
97
|
+
is_from: bool = False
|
|
98
|
+
is_reexport: bool = False
|
|
99
|
+
line: int = 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(slots=True)
|
|
103
|
+
class FileAST:
|
|
104
|
+
"""AST summary for a single file."""
|
|
105
|
+
|
|
106
|
+
path: str
|
|
107
|
+
language: str
|
|
108
|
+
functions: list[FunctionDef] = field(default_factory=list)
|
|
109
|
+
classes: list[ClassDef] = field(default_factory=list)
|
|
110
|
+
imports: list[ImportDef] = field(default_factory=list)
|
|
111
|
+
top_level_vars: list[str] = field(default_factory=list)
|
|
112
|
+
line_count: int = 0
|
|
113
|
+
parsing_tier: str = "" # "tree_sitter", "regex", "generic_ts", "ctags", "minimal"
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def symbol_count(self) -> int:
|
|
117
|
+
return len(self.functions) + len(self.classes) + len(self.top_level_vars)
|
|
118
|
+
|
|
119
|
+
def get_symbols(self) -> list[str]:
|
|
120
|
+
"""Get all top-level symbol names (functions, classes, and constants)."""
|
|
121
|
+
symbols = [f.name for f in self.functions]
|
|
122
|
+
symbols.extend(c.name for c in self.classes)
|
|
123
|
+
symbols.extend(self.top_level_vars)
|
|
124
|
+
return symbols
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Change tracking types (for incremental updates)
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(slots=True)
|
|
133
|
+
class DependencyChanges:
|
|
134
|
+
"""Changes to a file's import dependencies."""
|
|
135
|
+
|
|
136
|
+
added: list[ImportDef] = field(default_factory=list)
|
|
137
|
+
removed: list[ImportDef] = field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(slots=True)
|
|
141
|
+
class SymbolChange:
|
|
142
|
+
"""A change to a symbol detected by AST diffing."""
|
|
143
|
+
|
|
144
|
+
kind: Literal["added", "removed", "modified"]
|
|
145
|
+
symbol_name: str
|
|
146
|
+
symbol_kind: str # 'function', 'class', 'method'
|
|
147
|
+
file_path: str
|
|
148
|
+
previous: FunctionDef | ClassDef | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(slots=True)
|
|
152
|
+
class FileChangeResult:
|
|
153
|
+
"""Result of incrementally updating a single file's AST."""
|
|
154
|
+
|
|
155
|
+
file_path: str
|
|
156
|
+
symbol_changes: list[SymbolChange] = field(default_factory=list)
|
|
157
|
+
dependency_changes: DependencyChanges = field(default_factory=DependencyChanges)
|
|
158
|
+
was_incremental: bool = True
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# Language detection
|
|
162
|
+
LANG_EXTENSIONS: dict[str, str] = {
|
|
163
|
+
".py": "python",
|
|
164
|
+
".pyi": "python",
|
|
165
|
+
".js": "javascript",
|
|
166
|
+
".jsx": "javascript",
|
|
167
|
+
".mjs": "javascript",
|
|
168
|
+
".cjs": "javascript",
|
|
169
|
+
".ts": "typescript",
|
|
170
|
+
".tsx": "typescript",
|
|
171
|
+
".mts": "typescript",
|
|
172
|
+
".cts": "typescript",
|
|
173
|
+
".go": "go",
|
|
174
|
+
".rs": "rust",
|
|
175
|
+
".java": "java",
|
|
176
|
+
".rb": "ruby",
|
|
177
|
+
".c": "c",
|
|
178
|
+
".h": "c",
|
|
179
|
+
".cpp": "cpp",
|
|
180
|
+
".hpp": "cpp",
|
|
181
|
+
".cc": "cpp",
|
|
182
|
+
".cxx": "cpp",
|
|
183
|
+
".hxx": "cpp",
|
|
184
|
+
".hh": "cpp",
|
|
185
|
+
".metal": "cpp",
|
|
186
|
+
".cs": "csharp",
|
|
187
|
+
".php": "php",
|
|
188
|
+
".swift": "swift",
|
|
189
|
+
".kt": "kotlin",
|
|
190
|
+
".kts": "kotlin",
|
|
191
|
+
".scala": "scala",
|
|
192
|
+
".sc": "scala",
|
|
193
|
+
".lua": "lua",
|
|
194
|
+
".ex": "elixir",
|
|
195
|
+
".exs": "elixir",
|
|
196
|
+
".hs": "haskell",
|
|
197
|
+
".lhs": "haskell",
|
|
198
|
+
".sh": "bash",
|
|
199
|
+
".bash": "bash",
|
|
200
|
+
".zsh": "bash",
|
|
201
|
+
".tf": "hcl",
|
|
202
|
+
".hcl": "hcl",
|
|
203
|
+
".tfvars": "hcl",
|
|
204
|
+
".zig": "zig",
|
|
205
|
+
".nim": "nim",
|
|
206
|
+
".fs": "fsharp",
|
|
207
|
+
".fsi": "fsharp",
|
|
208
|
+
".fsx": "fsharp",
|
|
209
|
+
".cr": "crystal",
|
|
210
|
+
".m": "objc",
|
|
211
|
+
".mm": "objc",
|
|
212
|
+
# Data/config languages
|
|
213
|
+
".yaml": "yaml",
|
|
214
|
+
".yml": "yaml",
|
|
215
|
+
".toml": "toml",
|
|
216
|
+
".json": "json",
|
|
217
|
+
".jsonc": "json",
|
|
218
|
+
".html": "html",
|
|
219
|
+
".htm": "html",
|
|
220
|
+
".css": "css",
|
|
221
|
+
".scss": "css",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def detect_language(file_path: str) -> str:
|
|
226
|
+
"""Detect language from file extension."""
|
|
227
|
+
ext = Path(file_path).suffix.lower()
|
|
228
|
+
return LANG_EXTENSIONS.get(ext, "unknown")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# Regex-based fallback parsers (no tree-sitter dependency)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _parse_python_params(params_str: str) -> tuple[list[str], list[ParamDef]]:
|
|
235
|
+
"""Parse a Python function parameter string into both simple names and detailed ParamDefs.
|
|
236
|
+
|
|
237
|
+
Handles type annotations, default values, *args, **kwargs, and keyword-only params.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Tuple of (simple name list for backward compat, detailed ParamDef list).
|
|
241
|
+
"""
|
|
242
|
+
simple: list[str] = []
|
|
243
|
+
detailed: list[ParamDef] = []
|
|
244
|
+
|
|
245
|
+
if not params_str.strip():
|
|
246
|
+
return simple, detailed
|
|
247
|
+
|
|
248
|
+
# Split parameters respecting brackets and string literals
|
|
249
|
+
params_raw: list[str] = []
|
|
250
|
+
depth = 0
|
|
251
|
+
current: list[str] = []
|
|
252
|
+
i = 0
|
|
253
|
+
while i < len(params_str):
|
|
254
|
+
ch = params_str[i]
|
|
255
|
+
if ch in ('"', "'"):
|
|
256
|
+
# Skip string literals
|
|
257
|
+
quote = ch
|
|
258
|
+
current.append(ch)
|
|
259
|
+
i += 1
|
|
260
|
+
while i < len(params_str) and params_str[i] != quote:
|
|
261
|
+
if params_str[i] == "\\":
|
|
262
|
+
current.append(params_str[i])
|
|
263
|
+
i += 1
|
|
264
|
+
if i < len(params_str):
|
|
265
|
+
current.append(params_str[i])
|
|
266
|
+
i += 1
|
|
267
|
+
continue
|
|
268
|
+
current.append(params_str[i])
|
|
269
|
+
i += 1
|
|
270
|
+
if i < len(params_str):
|
|
271
|
+
current.append(params_str[i]) # closing quote
|
|
272
|
+
i += 1
|
|
273
|
+
continue
|
|
274
|
+
if ch in ("(", "[", "{"):
|
|
275
|
+
depth += 1
|
|
276
|
+
current.append(ch)
|
|
277
|
+
elif ch in (")", "]", "}"):
|
|
278
|
+
depth -= 1
|
|
279
|
+
current.append(ch)
|
|
280
|
+
elif ch == "," and depth == 0:
|
|
281
|
+
params_raw.append("".join(current).strip())
|
|
282
|
+
current = []
|
|
283
|
+
else:
|
|
284
|
+
current.append(ch)
|
|
285
|
+
i += 1
|
|
286
|
+
if current:
|
|
287
|
+
params_raw.append("".join(current).strip())
|
|
288
|
+
|
|
289
|
+
seen_star = False
|
|
290
|
+
for raw in params_raw:
|
|
291
|
+
raw = raw.strip()
|
|
292
|
+
if not raw:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
# Bare * separator
|
|
296
|
+
if raw == "*":
|
|
297
|
+
seen_star = True
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
is_rest = raw.startswith("*") and not raw.startswith("**")
|
|
301
|
+
is_kwargs = raw.startswith("**")
|
|
302
|
+
|
|
303
|
+
# Strip * / **
|
|
304
|
+
cleaned = raw.lstrip("*")
|
|
305
|
+
|
|
306
|
+
# Split name : type = default
|
|
307
|
+
name = cleaned
|
|
308
|
+
type_ann = ""
|
|
309
|
+
default_val = ""
|
|
310
|
+
|
|
311
|
+
if "=" in cleaned:
|
|
312
|
+
before_eq, default_val = cleaned.split("=", 1)
|
|
313
|
+
default_val = default_val.strip()
|
|
314
|
+
cleaned = before_eq.strip()
|
|
315
|
+
|
|
316
|
+
if ":" in cleaned:
|
|
317
|
+
name, type_ann = cleaned.split(":", 1)
|
|
318
|
+
name = name.strip()
|
|
319
|
+
type_ann = type_ann.strip()
|
|
320
|
+
else:
|
|
321
|
+
name = cleaned.strip()
|
|
322
|
+
|
|
323
|
+
simple.append(name)
|
|
324
|
+
detailed.append(ParamDef(
|
|
325
|
+
name=name,
|
|
326
|
+
type_annotation=type_ann,
|
|
327
|
+
default_value=default_val,
|
|
328
|
+
is_rest=is_rest,
|
|
329
|
+
is_kwonly=seen_star and not is_rest and not is_kwargs,
|
|
330
|
+
is_kwargs=is_kwargs,
|
|
331
|
+
))
|
|
332
|
+
|
|
333
|
+
if is_rest:
|
|
334
|
+
seen_star = True
|
|
335
|
+
|
|
336
|
+
return simple, detailed
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _detect_visibility(name: str) -> str:
|
|
340
|
+
"""Detect Python visibility from naming convention."""
|
|
341
|
+
if name.startswith("__") and not name.endswith("__"):
|
|
342
|
+
return "name_mangled"
|
|
343
|
+
if name.startswith("_") and not name.startswith("__"):
|
|
344
|
+
return "private"
|
|
345
|
+
return "public"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _check_generator(lines: list[str], start: int, end: int, indent: int) -> bool:
|
|
349
|
+
"""Check if a function body contains yield/yield from statements."""
|
|
350
|
+
for j in range(start + 1, min(end, len(lines))):
|
|
351
|
+
stripped = lines[j].strip()
|
|
352
|
+
if stripped.startswith(("yield ", "yield\n", "yield\r")) or "yield " in stripped:
|
|
353
|
+
# Verify it's at the right indentation level (not in a nested def)
|
|
354
|
+
line_indent = len(lines[j]) - len(lines[j].lstrip())
|
|
355
|
+
if line_indent > indent:
|
|
356
|
+
return True
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _extract_class_properties(
|
|
361
|
+
lines: list[str], class_start: int, class_end: int,
|
|
362
|
+
) -> list[PropertyDef]:
|
|
363
|
+
"""Extract class properties from __init__ body and class-level annotations."""
|
|
364
|
+
props: list[PropertyDef] = []
|
|
365
|
+
seen_names: set[str] = set()
|
|
366
|
+
|
|
367
|
+
for j in range(class_start, min(class_end, len(lines))):
|
|
368
|
+
stripped = lines[j].strip()
|
|
369
|
+
|
|
370
|
+
# self.x = ... or self.x: type = ...
|
|
371
|
+
init_match = re.match(r"self\.(\w+)\s*(?::\s*(\S+))?\s*=", stripped)
|
|
372
|
+
if init_match:
|
|
373
|
+
attr_name = init_match.group(1)
|
|
374
|
+
if attr_name not in seen_names:
|
|
375
|
+
seen_names.add(attr_name)
|
|
376
|
+
props.append(PropertyDef(
|
|
377
|
+
name=attr_name,
|
|
378
|
+
start_line=j + 1,
|
|
379
|
+
type_annotation=init_match.group(2) or "",
|
|
380
|
+
has_default=True,
|
|
381
|
+
visibility=_detect_visibility(attr_name),
|
|
382
|
+
))
|
|
383
|
+
|
|
384
|
+
# Class-level annotation: name: type (no self.)
|
|
385
|
+
# Must be at one indent level inside class
|
|
386
|
+
if not stripped.startswith("self.") and not stripped.startswith("def "):
|
|
387
|
+
ann_match = re.match(r"^(\w+)\s*:\s*(\S+)(?:\s*=\s*(.+))?$", stripped)
|
|
388
|
+
if ann_match:
|
|
389
|
+
line_indent = len(lines[j]) - len(lines[j].lstrip())
|
|
390
|
+
# Must be indented (inside class body)
|
|
391
|
+
if line_indent > 0:
|
|
392
|
+
attr_name = ann_match.group(1)
|
|
393
|
+
if attr_name not in seen_names and attr_name not in ("class", "def", "return"):
|
|
394
|
+
seen_names.add(attr_name)
|
|
395
|
+
props.append(PropertyDef(
|
|
396
|
+
name=attr_name,
|
|
397
|
+
start_line=j + 1,
|
|
398
|
+
type_annotation=ann_match.group(2) or "",
|
|
399
|
+
has_default=ann_match.group(3) is not None,
|
|
400
|
+
visibility=_detect_visibility(attr_name),
|
|
401
|
+
))
|
|
402
|
+
|
|
403
|
+
return props
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
_STRINGS_AND_COMMENTS_RE = re.compile(
|
|
407
|
+
r'#.*$|"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'', re.MULTILINE,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _count_parens(line: str) -> int:
|
|
412
|
+
"""Count unbalanced parentheses, ignoring those inside strings and comments."""
|
|
413
|
+
stripped = _STRINGS_AND_COMMENTS_RE.sub("", line)
|
|
414
|
+
return stripped.count("(") - stripped.count(")")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _join_multiline_defs(lines: list[str]) -> list[tuple[int, int, str]]:
|
|
418
|
+
"""Preprocess lines to join multi-line ``def`` and ``class`` signatures.
|
|
419
|
+
|
|
420
|
+
Detects lines that start a ``def`` or ``class`` statement but have
|
|
421
|
+
unbalanced parentheses, then accumulates subsequent lines until
|
|
422
|
+
parentheses balance and the line ends with ``:``.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
A list of ``(start_line_index, end_line_index, joined_line)`` tuples.
|
|
426
|
+
*start_line_index* is the original line where the statement begins.
|
|
427
|
+
*end_line_index* is the last raw line consumed (equal to start for
|
|
428
|
+
single-line statements). Lines that were folded into a preceding
|
|
429
|
+
signature are omitted (their content is merged into the signature
|
|
430
|
+
line).
|
|
431
|
+
"""
|
|
432
|
+
result: list[tuple[int, int, str]] = []
|
|
433
|
+
i = 0
|
|
434
|
+
n = len(lines)
|
|
435
|
+
|
|
436
|
+
while i < n:
|
|
437
|
+
line = lines[i]
|
|
438
|
+
stripped = line.strip()
|
|
439
|
+
|
|
440
|
+
# Quick check: does this line start a def or class?
|
|
441
|
+
is_def = bool(re.match(r"\s*(async\s+)?def\s+\w+", line))
|
|
442
|
+
is_class = bool(re.match(r"\s*class\s+\w+", stripped))
|
|
443
|
+
|
|
444
|
+
if (is_def or is_class) and "(" in line:
|
|
445
|
+
# Count parentheses balance on this line (ignoring strings/comments)
|
|
446
|
+
depth = _count_parens(line)
|
|
447
|
+
if depth > 0:
|
|
448
|
+
# Multi-line signature — accumulate until balanced
|
|
449
|
+
# Strip comments from each line before joining so embedded
|
|
450
|
+
# comments don't confuse downstream parsing.
|
|
451
|
+
start_idx = i
|
|
452
|
+
parts = [_STRINGS_AND_COMMENTS_RE.sub(
|
|
453
|
+
lambda m: m.group() if m.group()[0] != "#" else "", line,
|
|
454
|
+
).rstrip()]
|
|
455
|
+
i += 1
|
|
456
|
+
while i < n and depth > 0:
|
|
457
|
+
continuation = lines[i]
|
|
458
|
+
depth += _count_parens(continuation)
|
|
459
|
+
cleaned = _STRINGS_AND_COMMENTS_RE.sub(
|
|
460
|
+
lambda m: m.group() if m.group()[0] != "#" else "", continuation,
|
|
461
|
+
).strip()
|
|
462
|
+
parts.append(cleaned)
|
|
463
|
+
i += 1
|
|
464
|
+
# i now points to the line AFTER the last continuation line
|
|
465
|
+
end_idx = i - 1
|
|
466
|
+
joined = " ".join(parts)
|
|
467
|
+
result.append((start_idx, end_idx, joined))
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# Regular line — pass through
|
|
471
|
+
result.append((i, i, line))
|
|
472
|
+
i += 1
|
|
473
|
+
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _match_function_def(line: str) -> re.Match[str] | None:
|
|
478
|
+
"""Match a function definition with balanced-paren-aware parameter extraction.
|
|
479
|
+
|
|
480
|
+
Handles nested parens in default values like ``def foo(x: int = max(0, 1))``.
|
|
481
|
+
Returns a match-like object with groups:
|
|
482
|
+
1: indent, 2: async prefix or None, 3: name,
|
|
483
|
+
4: type params or "", 5: params string, 6: return type or ""
|
|
484
|
+
"""
|
|
485
|
+
m = re.match(r"^(\s*)(async\s+)?def\s+(\w+)\s*(?:\[([^\]]*)\])?\s*\(", line)
|
|
486
|
+
if not m:
|
|
487
|
+
return None
|
|
488
|
+
# Find the matching closing paren by counting depth
|
|
489
|
+
paren_start = m.end() - 1 # index of the opening '('
|
|
490
|
+
depth = 1
|
|
491
|
+
i = paren_start + 1
|
|
492
|
+
while i < len(line) and depth > 0:
|
|
493
|
+
ch = line[i]
|
|
494
|
+
if ch == "(":
|
|
495
|
+
depth += 1
|
|
496
|
+
elif ch == ")":
|
|
497
|
+
depth -= 1
|
|
498
|
+
elif ch in ('"', "'"):
|
|
499
|
+
# Skip string literals
|
|
500
|
+
quote = ch
|
|
501
|
+
i += 1
|
|
502
|
+
while i < len(line) and line[i] != quote:
|
|
503
|
+
if line[i] == "\\":
|
|
504
|
+
i += 1
|
|
505
|
+
i += 1
|
|
506
|
+
i += 1
|
|
507
|
+
if depth != 0:
|
|
508
|
+
return None
|
|
509
|
+
paren_end = i - 1 # index of closing ')'
|
|
510
|
+
params_str = line[paren_start + 1:paren_end]
|
|
511
|
+
rest = line[paren_end + 1:]
|
|
512
|
+
# Match optional return type and colon
|
|
513
|
+
ret_match = re.match(r"\s*(?:->\s*(.+?))?\s*:\s*$", rest)
|
|
514
|
+
if not ret_match:
|
|
515
|
+
return None
|
|
516
|
+
return_type = ret_match.group(1) or ""
|
|
517
|
+
# Build a synthetic match via a simple namespace
|
|
518
|
+
class _FuncMatch:
|
|
519
|
+
def group(self, n: int) -> str | None:
|
|
520
|
+
return [
|
|
521
|
+
line, # 0: full match
|
|
522
|
+
m.group(1), # 1: indent
|
|
523
|
+
m.group(2), # 2: async prefix
|
|
524
|
+
m.group(3), # 3: name
|
|
525
|
+
m.group(4) or "", # 4: type params
|
|
526
|
+
params_str, # 5: params
|
|
527
|
+
return_type, # 6: return type
|
|
528
|
+
][n]
|
|
529
|
+
return _FuncMatch()
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def parse_python(content: str, path: str = "") -> FileAST:
|
|
533
|
+
"""Parse Python source using regex patterns.
|
|
534
|
+
|
|
535
|
+
Extracts functions, classes, imports, and top-level variables.
|
|
536
|
+
Populates extended fields: ParamDef details, visibility, decorator flags,
|
|
537
|
+
generator detection, class properties, abstract detection, metaclass.
|
|
538
|
+
"""
|
|
539
|
+
lines = content.split("\n")
|
|
540
|
+
ast = FileAST(path=path, language="python", line_count=len(lines))
|
|
541
|
+
|
|
542
|
+
# Parse imports
|
|
543
|
+
for i, line in enumerate(lines):
|
|
544
|
+
stripped = line.strip()
|
|
545
|
+
if stripped.startswith("from "):
|
|
546
|
+
match = re.match(r"from\s+([\w.]+)\s+import\s+(.+)", stripped)
|
|
547
|
+
if match:
|
|
548
|
+
module = match.group(1)
|
|
549
|
+
names = [n.strip().split(" as ")[0].strip() for n in match.group(2).split(",")]
|
|
550
|
+
ast.imports.append(ImportDef(
|
|
551
|
+
module=module, names=names, is_from=True, line=i + 1,
|
|
552
|
+
))
|
|
553
|
+
elif stripped.startswith("import "):
|
|
554
|
+
match = re.match(r"import\s+(.+)", stripped)
|
|
555
|
+
if match:
|
|
556
|
+
for part in match.group(1).split(","):
|
|
557
|
+
parts = part.strip().split(" as ")
|
|
558
|
+
ast.imports.append(ImportDef(
|
|
559
|
+
module=parts[0].strip(),
|
|
560
|
+
alias=parts[1].strip() if len(parts) > 1 else "",
|
|
561
|
+
line=i + 1,
|
|
562
|
+
))
|
|
563
|
+
|
|
564
|
+
# Preprocess lines to join multi-line def/class signatures
|
|
565
|
+
preprocessed = _join_multiline_defs(lines)
|
|
566
|
+
|
|
567
|
+
# Parse classes and functions
|
|
568
|
+
current_class: ClassDef | None = None
|
|
569
|
+
decorators: list[str] = []
|
|
570
|
+
|
|
571
|
+
for orig_line_idx, sig_end_idx, line in preprocessed:
|
|
572
|
+
i = orig_line_idx
|
|
573
|
+
stripped = line.strip()
|
|
574
|
+
|
|
575
|
+
# Decorators
|
|
576
|
+
if stripped.startswith("@"):
|
|
577
|
+
decorators.append(stripped[1:].split("(")[0])
|
|
578
|
+
continue
|
|
579
|
+
|
|
580
|
+
# Class definition
|
|
581
|
+
class_match = re.match(r"^class\s+(\w+)\s*(?:\(([^)]*)\))?\s*:", stripped)
|
|
582
|
+
if class_match and not line.startswith((" ", "\t")):
|
|
583
|
+
class_name = class_match.group(1)
|
|
584
|
+
bases_str = class_match.group(2) or ""
|
|
585
|
+
bases = [b.strip() for b in bases_str.split(",") if b.strip()] if bases_str else []
|
|
586
|
+
|
|
587
|
+
# Extract metaclass from bases (metaclass=ABCMeta)
|
|
588
|
+
metaclass = ""
|
|
589
|
+
filtered_bases: list[str] = []
|
|
590
|
+
for b in bases:
|
|
591
|
+
mc_match = re.match(r"metaclass\s*=\s*(\w+)", b)
|
|
592
|
+
if mc_match:
|
|
593
|
+
metaclass = mc_match.group(1)
|
|
594
|
+
else:
|
|
595
|
+
filtered_bases.append(b)
|
|
596
|
+
|
|
597
|
+
# Find end of class (next non-indented line)
|
|
598
|
+
# Start scanning from after the last line of the signature
|
|
599
|
+
body_start = sig_end_idx + 1
|
|
600
|
+
end_line = body_start
|
|
601
|
+
for j in range(body_start, len(lines)):
|
|
602
|
+
if lines[j].strip() and not lines[j].startswith((" ", "\t")):
|
|
603
|
+
end_line = j
|
|
604
|
+
break
|
|
605
|
+
else:
|
|
606
|
+
end_line = len(lines)
|
|
607
|
+
|
|
608
|
+
# Extract docstring (body starts after the signature)
|
|
609
|
+
docstring = _extract_docstring(lines, body_start)
|
|
610
|
+
|
|
611
|
+
# Detect abstract class
|
|
612
|
+
is_abstract = (
|
|
613
|
+
"ABC" in filtered_bases
|
|
614
|
+
or "ABCMeta" in metaclass
|
|
615
|
+
or "abstractmethod" in decorators
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# Extract properties
|
|
619
|
+
properties = _extract_class_properties(lines, body_start, end_line)
|
|
620
|
+
|
|
621
|
+
current_class = ClassDef(
|
|
622
|
+
name=class_name,
|
|
623
|
+
start_line=i + 1,
|
|
624
|
+
end_line=end_line,
|
|
625
|
+
bases=filtered_bases,
|
|
626
|
+
decorators=decorators,
|
|
627
|
+
docstring=docstring,
|
|
628
|
+
properties=properties,
|
|
629
|
+
is_abstract=is_abstract,
|
|
630
|
+
metaclass=metaclass,
|
|
631
|
+
)
|
|
632
|
+
ast.classes.append(current_class)
|
|
633
|
+
decorators = []
|
|
634
|
+
continue
|
|
635
|
+
|
|
636
|
+
# Function definition (with optional PEP 695 type params)
|
|
637
|
+
# Use balanced-paren-aware extraction instead of [^)]*
|
|
638
|
+
func_match = _match_function_def(line)
|
|
639
|
+
if func_match:
|
|
640
|
+
indent = func_match.group(1)
|
|
641
|
+
is_async = func_match.group(2) is not None
|
|
642
|
+
func_name = func_match.group(3)
|
|
643
|
+
type_params_str = func_match.group(4) or ""
|
|
644
|
+
params_str = func_match.group(5)
|
|
645
|
+
return_type = (func_match.group(6) or "").strip().rstrip(":")
|
|
646
|
+
|
|
647
|
+
# Parse type params (PEP 695)
|
|
648
|
+
type_params = [t.strip() for t in type_params_str.split(",") if t.strip()] if type_params_str else []
|
|
649
|
+
|
|
650
|
+
# Parse parameters (both simple and detailed)
|
|
651
|
+
params, parameters = _parse_python_params(params_str)
|
|
652
|
+
|
|
653
|
+
# Find end of function
|
|
654
|
+
# Start scanning from after the last line of the signature
|
|
655
|
+
func_indent = len(indent)
|
|
656
|
+
func_body_start = sig_end_idx + 1
|
|
657
|
+
end_line = func_body_start
|
|
658
|
+
for j in range(func_body_start, len(lines)):
|
|
659
|
+
line = lines[j]
|
|
660
|
+
if line.strip() and not line.startswith(" " * (func_indent + 1)) and not line.startswith("\t" * (func_indent // 4 + 1)) and (len(line) - len(line.lstrip())) <= func_indent:
|
|
661
|
+
end_line = j
|
|
662
|
+
break
|
|
663
|
+
else:
|
|
664
|
+
end_line = len(lines)
|
|
665
|
+
|
|
666
|
+
docstring = _extract_docstring(lines, func_body_start)
|
|
667
|
+
|
|
668
|
+
# Detect decorator-based flags
|
|
669
|
+
is_staticmethod = "staticmethod" in decorators
|
|
670
|
+
is_classmethod = "classmethod" in decorators
|
|
671
|
+
is_property_flag = "property" in decorators
|
|
672
|
+
is_abstract_method = "abstractmethod" in decorators
|
|
673
|
+
|
|
674
|
+
# Detect visibility
|
|
675
|
+
visibility = _detect_visibility(func_name)
|
|
676
|
+
|
|
677
|
+
# Detect generator
|
|
678
|
+
is_generator = _check_generator(lines, i, end_line, func_indent)
|
|
679
|
+
|
|
680
|
+
# If abstract method found on a function inside a class, mark class abstract
|
|
681
|
+
if is_abstract_method and current_class and not current_class.is_abstract:
|
|
682
|
+
current_class.is_abstract = True
|
|
683
|
+
|
|
684
|
+
func = FunctionDef(
|
|
685
|
+
name=func_name,
|
|
686
|
+
start_line=i + 1,
|
|
687
|
+
end_line=end_line,
|
|
688
|
+
params=params,
|
|
689
|
+
return_type=return_type,
|
|
690
|
+
decorators=decorators,
|
|
691
|
+
is_async=is_async,
|
|
692
|
+
is_method=func_indent > 0,
|
|
693
|
+
docstring=docstring,
|
|
694
|
+
parameters=parameters,
|
|
695
|
+
visibility=visibility,
|
|
696
|
+
is_generator=is_generator,
|
|
697
|
+
is_staticmethod=is_staticmethod,
|
|
698
|
+
is_classmethod=is_classmethod,
|
|
699
|
+
is_property=is_property_flag,
|
|
700
|
+
type_params=type_params,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
if current_class and func_indent > 0:
|
|
704
|
+
current_class.methods.append(func)
|
|
705
|
+
else:
|
|
706
|
+
ast.functions.append(func)
|
|
707
|
+
current_class = None
|
|
708
|
+
|
|
709
|
+
decorators = []
|
|
710
|
+
continue
|
|
711
|
+
|
|
712
|
+
# Top-level variable
|
|
713
|
+
if not line.startswith((" ", "\t")) and "=" in stripped and not stripped.startswith(("#", "def ", "class ", "@", "if ", "for ", "while ", "return ")):
|
|
714
|
+
var_match = re.match(r"^([A-Z][A-Za-z_0-9]*|[A-Z_][A-Z_0-9]*)\s*[=:]", stripped)
|
|
715
|
+
if var_match:
|
|
716
|
+
ast.top_level_vars.append(var_match.group(1))
|
|
717
|
+
|
|
718
|
+
if stripped and not stripped.startswith(("#", "@")):
|
|
719
|
+
decorators = []
|
|
720
|
+
|
|
721
|
+
return ast
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _parse_js_params(params_str: str) -> tuple[list[str], list[ParamDef]]:
|
|
725
|
+
"""Parse a JS/TS parameter string into param names and ParamDef list."""
|
|
726
|
+
params: list[str] = []
|
|
727
|
+
parameters: list[ParamDef] = []
|
|
728
|
+
if not params_str.strip():
|
|
729
|
+
return params, parameters
|
|
730
|
+
for p in params_str.split(","):
|
|
731
|
+
p = p.strip()
|
|
732
|
+
if not p:
|
|
733
|
+
continue
|
|
734
|
+
is_rest = p.startswith("...")
|
|
735
|
+
p_cleaned = p.lstrip(".")
|
|
736
|
+
name = p_cleaned
|
|
737
|
+
type_ann = ""
|
|
738
|
+
default_val = ""
|
|
739
|
+
if "=" in p_cleaned:
|
|
740
|
+
name, default_val = p_cleaned.split("=", 1)
|
|
741
|
+
name = name.strip()
|
|
742
|
+
default_val = default_val.strip()
|
|
743
|
+
if ":" in name:
|
|
744
|
+
name, type_ann = name.split(":", 1)
|
|
745
|
+
name = name.strip()
|
|
746
|
+
type_ann = type_ann.strip()
|
|
747
|
+
# Strip optional ? from name
|
|
748
|
+
name = name.rstrip("?")
|
|
749
|
+
params.append(name)
|
|
750
|
+
parameters.append(ParamDef(
|
|
751
|
+
name=name,
|
|
752
|
+
type_annotation=type_ann,
|
|
753
|
+
default_value=default_val,
|
|
754
|
+
is_rest=is_rest,
|
|
755
|
+
))
|
|
756
|
+
return params, parameters
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def parse_javascript(content: str, path: str = "") -> FileAST:
|
|
760
|
+
"""Parse JavaScript/TypeScript source using regex patterns.
|
|
761
|
+
|
|
762
|
+
Extracts functions with parameter details, classes with methods and end
|
|
763
|
+
lines, and TypeScript visibility modifiers.
|
|
764
|
+
"""
|
|
765
|
+
lines = content.split("\n")
|
|
766
|
+
lang = "typescript" if path.endswith((".ts", ".tsx")) else "javascript"
|
|
767
|
+
ast = FileAST(path=path, language=lang, line_count=len(lines))
|
|
768
|
+
|
|
769
|
+
decorators: list[str] = []
|
|
770
|
+
# Track class body for method extraction
|
|
771
|
+
current_class: ClassDef | None = None
|
|
772
|
+
class_brace_depth: int = 0
|
|
773
|
+
|
|
774
|
+
# Method pattern: matches class methods like `async foo(x: number): void {`
|
|
775
|
+
# including optional visibility, static, abstract, get/set modifiers.
|
|
776
|
+
_method_re = re.compile(
|
|
777
|
+
r"^(?:(?:public|private|protected|readonly)\s+)*"
|
|
778
|
+
r"(?:static\s+)?(?:abstract\s+)?(?:async\s+)?"
|
|
779
|
+
r"(?:get\s+|set\s+)?"
|
|
780
|
+
r"(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)"
|
|
781
|
+
r"(?:\s*:\s*([^{]+?))?\s*\{"
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
for i, line in enumerate(lines):
|
|
785
|
+
stripped = line.strip()
|
|
786
|
+
|
|
787
|
+
# Track brace depth inside a class body
|
|
788
|
+
if current_class is not None:
|
|
789
|
+
class_brace_depth += stripped.count("{") - stripped.count("}")
|
|
790
|
+
if class_brace_depth <= 0:
|
|
791
|
+
current_class = None
|
|
792
|
+
class_brace_depth = 0
|
|
793
|
+
|
|
794
|
+
# Decorators (TypeScript)
|
|
795
|
+
if stripped.startswith("@"):
|
|
796
|
+
decorators.append(stripped[1:].split("(")[0])
|
|
797
|
+
continue
|
|
798
|
+
|
|
799
|
+
# Import statements
|
|
800
|
+
import_match = re.match(r"import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+))\s+from\s+['\"]([^'\"]+)['\"]", stripped)
|
|
801
|
+
if import_match:
|
|
802
|
+
names = []
|
|
803
|
+
if import_match.group(1):
|
|
804
|
+
names = [n.strip().split(" as ")[0].strip() for n in import_match.group(1).split(",")]
|
|
805
|
+
elif import_match.group(2):
|
|
806
|
+
names = [import_match.group(2)]
|
|
807
|
+
ast.imports.append(ImportDef(
|
|
808
|
+
module=import_match.group(3),
|
|
809
|
+
names=names,
|
|
810
|
+
is_from=True,
|
|
811
|
+
line=i + 1,
|
|
812
|
+
))
|
|
813
|
+
|
|
814
|
+
# Function definitions (top-level `function` keyword)
|
|
815
|
+
func_match = re.match(
|
|
816
|
+
r"^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)"
|
|
817
|
+
r"(?:\s*:\s*([^{]+?))?\s*\{",
|
|
818
|
+
stripped,
|
|
819
|
+
)
|
|
820
|
+
if func_match:
|
|
821
|
+
func_name = func_match.group(1)
|
|
822
|
+
params, parameters = _parse_js_params(func_match.group(2))
|
|
823
|
+
return_type = (func_match.group(3) or "").strip()
|
|
824
|
+
|
|
825
|
+
# Find end line via brace matching
|
|
826
|
+
end_line = i + 1
|
|
827
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
828
|
+
if brace_count > 0:
|
|
829
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
830
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
831
|
+
if brace_count <= 0:
|
|
832
|
+
end_line = j + 1
|
|
833
|
+
break
|
|
834
|
+
|
|
835
|
+
ast.functions.append(FunctionDef(
|
|
836
|
+
name=func_name,
|
|
837
|
+
start_line=i + 1,
|
|
838
|
+
end_line=end_line,
|
|
839
|
+
params=params,
|
|
840
|
+
parameters=parameters,
|
|
841
|
+
return_type=return_type,
|
|
842
|
+
is_async="async " in stripped,
|
|
843
|
+
decorators=decorators,
|
|
844
|
+
))
|
|
845
|
+
decorators = []
|
|
846
|
+
continue
|
|
847
|
+
|
|
848
|
+
# Class definitions
|
|
849
|
+
class_match = re.match(
|
|
850
|
+
r"^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([^{]+))?\s*\{",
|
|
851
|
+
stripped,
|
|
852
|
+
)
|
|
853
|
+
if class_match:
|
|
854
|
+
bases = [class_match.group(2)] if class_match.group(2) else []
|
|
855
|
+
is_abstract = "abstract " in stripped.split("class")[0]
|
|
856
|
+
|
|
857
|
+
# Find end line via brace matching
|
|
858
|
+
end_line = i + 1
|
|
859
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
860
|
+
if brace_count > 0:
|
|
861
|
+
for j in range(i + 1, min(i + 1000, len(lines))):
|
|
862
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
863
|
+
if brace_count <= 0:
|
|
864
|
+
end_line = j + 1
|
|
865
|
+
break
|
|
866
|
+
|
|
867
|
+
current_class = ClassDef(
|
|
868
|
+
name=class_match.group(1),
|
|
869
|
+
start_line=i + 1,
|
|
870
|
+
end_line=end_line,
|
|
871
|
+
bases=bases,
|
|
872
|
+
decorators=decorators,
|
|
873
|
+
is_abstract=is_abstract,
|
|
874
|
+
)
|
|
875
|
+
ast.classes.append(current_class)
|
|
876
|
+
# Track brace depth: start at 1 (the opening brace of the class)
|
|
877
|
+
class_brace_depth = stripped.count("{") - stripped.count("}")
|
|
878
|
+
decorators = []
|
|
879
|
+
continue
|
|
880
|
+
|
|
881
|
+
# Method definitions inside a class body
|
|
882
|
+
if current_class is not None and class_brace_depth > 0:
|
|
883
|
+
method_match = _method_re.match(stripped)
|
|
884
|
+
if method_match:
|
|
885
|
+
method_name = method_match.group(1)
|
|
886
|
+
# Skip constructor as a special case — still include it
|
|
887
|
+
params, parameters = _parse_js_params(method_match.group(2))
|
|
888
|
+
return_type = (method_match.group(3) or "").strip()
|
|
889
|
+
|
|
890
|
+
# Find method end line via brace matching
|
|
891
|
+
end_line = i + 1
|
|
892
|
+
m_brace = stripped.count("{") - stripped.count("}")
|
|
893
|
+
if m_brace > 0:
|
|
894
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
895
|
+
m_brace += lines[j].count("{") - lines[j].count("}")
|
|
896
|
+
if m_brace <= 0:
|
|
897
|
+
end_line = j + 1
|
|
898
|
+
break
|
|
899
|
+
|
|
900
|
+
current_class.methods.append(FunctionDef(
|
|
901
|
+
name=method_name,
|
|
902
|
+
start_line=i + 1,
|
|
903
|
+
end_line=end_line,
|
|
904
|
+
params=params,
|
|
905
|
+
parameters=parameters,
|
|
906
|
+
return_type=return_type,
|
|
907
|
+
is_async="async " in stripped.split(method_name)[0],
|
|
908
|
+
decorators=decorators,
|
|
909
|
+
))
|
|
910
|
+
decorators = []
|
|
911
|
+
continue
|
|
912
|
+
|
|
913
|
+
if stripped and not stripped.startswith("@"):
|
|
914
|
+
decorators = []
|
|
915
|
+
|
|
916
|
+
return ast
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def parse_rust(content: str, path: str = "") -> FileAST:
|
|
920
|
+
"""Parse Rust source using regex patterns.
|
|
921
|
+
|
|
922
|
+
Extracts functions, structs, enums, traits, impl blocks, imports (use/mod).
|
|
923
|
+
"""
|
|
924
|
+
lines = content.split("\n")
|
|
925
|
+
ast = FileAST(path=path, language="rust", line_count=len(lines))
|
|
926
|
+
|
|
927
|
+
for i, line in enumerate(lines):
|
|
928
|
+
stripped = line.strip()
|
|
929
|
+
|
|
930
|
+
# Imports: use ... ;
|
|
931
|
+
use_match = re.match(r"^(?:pub\s+)?use\s+(.+);", stripped)
|
|
932
|
+
if use_match:
|
|
933
|
+
module = use_match.group(1).strip()
|
|
934
|
+
# Extract the base path (before :: { ... })
|
|
935
|
+
base = re.sub(r"\s*::\s*\{[^}]*\}", "", module)
|
|
936
|
+
base = re.sub(r"\s*::\s*\w+$", "", base)
|
|
937
|
+
ast.imports.append(ImportDef(module=module, is_from=True, line=i + 1))
|
|
938
|
+
continue
|
|
939
|
+
|
|
940
|
+
# mod declarations: mod foo;
|
|
941
|
+
mod_match = re.match(r"^(?:pub\s+)?mod\s+(\w+)\s*;", stripped)
|
|
942
|
+
if mod_match:
|
|
943
|
+
ast.imports.append(ImportDef(
|
|
944
|
+
module=mod_match.group(1), is_from=False, line=i + 1,
|
|
945
|
+
))
|
|
946
|
+
continue
|
|
947
|
+
|
|
948
|
+
# Functions: (pub)? (async)? fn name(...)
|
|
949
|
+
fn_match = re.match(
|
|
950
|
+
r"^(?:pub(?:\([\w:]+\))?\s+)?(?:async\s+)?(?:unsafe\s+)?(?:extern\s+\"[^\"]*\"\s+)?fn\s+(\w+)",
|
|
951
|
+
stripped,
|
|
952
|
+
)
|
|
953
|
+
if fn_match:
|
|
954
|
+
func_name = fn_match.group(1)
|
|
955
|
+
# Find end via brace matching
|
|
956
|
+
end_line = i + 1
|
|
957
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
958
|
+
if brace_count > 0:
|
|
959
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
960
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
961
|
+
if brace_count <= 0:
|
|
962
|
+
end_line = j + 1
|
|
963
|
+
break
|
|
964
|
+
is_async = "async " in stripped.split("fn")[0]
|
|
965
|
+
ast.functions.append(FunctionDef(
|
|
966
|
+
name=func_name,
|
|
967
|
+
start_line=i + 1,
|
|
968
|
+
end_line=end_line,
|
|
969
|
+
is_async=is_async,
|
|
970
|
+
))
|
|
971
|
+
continue
|
|
972
|
+
|
|
973
|
+
# Structs: (pub)? struct Name
|
|
974
|
+
struct_match = re.match(r"^(?:pub(?:\([\w:]+\))?\s+)?struct\s+(\w+)", stripped)
|
|
975
|
+
if struct_match:
|
|
976
|
+
end_line = i + 1
|
|
977
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
978
|
+
if brace_count > 0:
|
|
979
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
980
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
981
|
+
if brace_count <= 0:
|
|
982
|
+
end_line = j + 1
|
|
983
|
+
break
|
|
984
|
+
ast.classes.append(ClassDef(
|
|
985
|
+
name=struct_match.group(1),
|
|
986
|
+
start_line=i + 1,
|
|
987
|
+
end_line=end_line,
|
|
988
|
+
))
|
|
989
|
+
continue
|
|
990
|
+
|
|
991
|
+
# Enums: (pub)? enum Name
|
|
992
|
+
enum_match = re.match(r"^(?:pub(?:\([\w:]+\))?\s+)?enum\s+(\w+)", stripped)
|
|
993
|
+
if enum_match:
|
|
994
|
+
end_line = i + 1
|
|
995
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
996
|
+
if brace_count > 0:
|
|
997
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
998
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
999
|
+
if brace_count <= 0:
|
|
1000
|
+
end_line = j + 1
|
|
1001
|
+
break
|
|
1002
|
+
ast.classes.append(ClassDef(
|
|
1003
|
+
name=enum_match.group(1),
|
|
1004
|
+
start_line=i + 1,
|
|
1005
|
+
end_line=end_line,
|
|
1006
|
+
))
|
|
1007
|
+
continue
|
|
1008
|
+
|
|
1009
|
+
# Traits: (pub)? trait Name
|
|
1010
|
+
trait_match = re.match(r"^(?:pub(?:\([\w:]+\))?\s+)?(?:unsafe\s+)?trait\s+(\w+)", stripped)
|
|
1011
|
+
if trait_match:
|
|
1012
|
+
end_line = i + 1
|
|
1013
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
1014
|
+
if brace_count > 0:
|
|
1015
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
1016
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
1017
|
+
if brace_count <= 0:
|
|
1018
|
+
end_line = j + 1
|
|
1019
|
+
break
|
|
1020
|
+
ast.classes.append(ClassDef(
|
|
1021
|
+
name=trait_match.group(1),
|
|
1022
|
+
start_line=i + 1,
|
|
1023
|
+
end_line=end_line,
|
|
1024
|
+
))
|
|
1025
|
+
continue
|
|
1026
|
+
|
|
1027
|
+
# Top-level constants: (pub)? const NAME: ...
|
|
1028
|
+
const_match = re.match(r"^(?:pub\s+)?(?:const|static)\s+(\w+)\s*:", stripped)
|
|
1029
|
+
if const_match:
|
|
1030
|
+
ast.top_level_vars.append(const_match.group(1))
|
|
1031
|
+
|
|
1032
|
+
return ast
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def parse_go(content: str, path: str = "") -> FileAST:
|
|
1036
|
+
"""Parse Go source using regex patterns.
|
|
1037
|
+
|
|
1038
|
+
Extracts functions, methods, structs, interfaces, imports.
|
|
1039
|
+
"""
|
|
1040
|
+
lines = content.split("\n")
|
|
1041
|
+
ast = FileAST(path=path, language="go", line_count=len(lines))
|
|
1042
|
+
|
|
1043
|
+
i = 0
|
|
1044
|
+
while i < len(lines):
|
|
1045
|
+
stripped = lines[i].strip()
|
|
1046
|
+
|
|
1047
|
+
# Import block: import ( ... )
|
|
1048
|
+
if stripped == "import (" or stripped.startswith("import ("):
|
|
1049
|
+
i += 1
|
|
1050
|
+
while i < len(lines):
|
|
1051
|
+
imp_line = lines[i].strip()
|
|
1052
|
+
if imp_line == ")":
|
|
1053
|
+
break
|
|
1054
|
+
# Parse "pkg" or alias "pkg"
|
|
1055
|
+
imp_match = re.match(r'^(?:\w+\s+)?"([^"]+)"', imp_line)
|
|
1056
|
+
if imp_match:
|
|
1057
|
+
ast.imports.append(ImportDef(
|
|
1058
|
+
module=imp_match.group(1), is_from=False, line=i + 1,
|
|
1059
|
+
))
|
|
1060
|
+
i += 1
|
|
1061
|
+
i += 1
|
|
1062
|
+
continue
|
|
1063
|
+
|
|
1064
|
+
# Single import: import "pkg"
|
|
1065
|
+
single_imp = re.match(r'^import\s+(?:\w+\s+)?"([^"]+)"', stripped)
|
|
1066
|
+
if single_imp:
|
|
1067
|
+
ast.imports.append(ImportDef(
|
|
1068
|
+
module=single_imp.group(1), is_from=False, line=i + 1,
|
|
1069
|
+
))
|
|
1070
|
+
i += 1
|
|
1071
|
+
continue
|
|
1072
|
+
|
|
1073
|
+
# Method: func (receiver) Name(...)
|
|
1074
|
+
method_match = re.match(r"^func\s+\([^)]+\)\s+(\w+)\s*\(", stripped)
|
|
1075
|
+
if method_match:
|
|
1076
|
+
func_name = method_match.group(1)
|
|
1077
|
+
end_line = i + 1
|
|
1078
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
1079
|
+
if brace_count > 0:
|
|
1080
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
1081
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
1082
|
+
if brace_count <= 0:
|
|
1083
|
+
end_line = j + 1
|
|
1084
|
+
break
|
|
1085
|
+
ast.functions.append(FunctionDef(
|
|
1086
|
+
name=func_name,
|
|
1087
|
+
start_line=i + 1,
|
|
1088
|
+
end_line=end_line,
|
|
1089
|
+
is_method=True,
|
|
1090
|
+
))
|
|
1091
|
+
i += 1
|
|
1092
|
+
continue
|
|
1093
|
+
|
|
1094
|
+
# Function: func Name(...)
|
|
1095
|
+
fn_match = re.match(r"^func\s+(\w+)\s*\(", stripped)
|
|
1096
|
+
if fn_match:
|
|
1097
|
+
func_name = fn_match.group(1)
|
|
1098
|
+
end_line = i + 1
|
|
1099
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
1100
|
+
if brace_count > 0:
|
|
1101
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
1102
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
1103
|
+
if brace_count <= 0:
|
|
1104
|
+
end_line = j + 1
|
|
1105
|
+
break
|
|
1106
|
+
ast.functions.append(FunctionDef(
|
|
1107
|
+
name=func_name,
|
|
1108
|
+
start_line=i + 1,
|
|
1109
|
+
end_line=end_line,
|
|
1110
|
+
))
|
|
1111
|
+
i += 1
|
|
1112
|
+
continue
|
|
1113
|
+
|
|
1114
|
+
# Struct: type Name struct {
|
|
1115
|
+
struct_match = re.match(r"^type\s+(\w+)\s+struct\s*\{", stripped)
|
|
1116
|
+
if struct_match:
|
|
1117
|
+
end_line = i + 1
|
|
1118
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
1119
|
+
if brace_count > 0:
|
|
1120
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
1121
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
1122
|
+
if brace_count <= 0:
|
|
1123
|
+
end_line = j + 1
|
|
1124
|
+
break
|
|
1125
|
+
ast.classes.append(ClassDef(
|
|
1126
|
+
name=struct_match.group(1),
|
|
1127
|
+
start_line=i + 1,
|
|
1128
|
+
end_line=end_line,
|
|
1129
|
+
))
|
|
1130
|
+
i += 1
|
|
1131
|
+
continue
|
|
1132
|
+
|
|
1133
|
+
# Interface: type Name interface {
|
|
1134
|
+
iface_match = re.match(r"^type\s+(\w+)\s+interface\s*\{", stripped)
|
|
1135
|
+
if iface_match:
|
|
1136
|
+
end_line = i + 1
|
|
1137
|
+
brace_count = stripped.count("{") - stripped.count("}")
|
|
1138
|
+
if brace_count > 0:
|
|
1139
|
+
for j in range(i + 1, min(i + 500, len(lines))):
|
|
1140
|
+
brace_count += lines[j].count("{") - lines[j].count("}")
|
|
1141
|
+
if brace_count <= 0:
|
|
1142
|
+
end_line = j + 1
|
|
1143
|
+
break
|
|
1144
|
+
ast.classes.append(ClassDef(
|
|
1145
|
+
name=iface_match.group(1),
|
|
1146
|
+
start_line=i + 1,
|
|
1147
|
+
end_line=end_line,
|
|
1148
|
+
))
|
|
1149
|
+
i += 1
|
|
1150
|
+
continue
|
|
1151
|
+
|
|
1152
|
+
# Top-level constants
|
|
1153
|
+
const_match = re.match(r"^(?:var|const)\s+(\w+)\s+", stripped)
|
|
1154
|
+
if const_match:
|
|
1155
|
+
ast.top_level_vars.append(const_match.group(1))
|
|
1156
|
+
|
|
1157
|
+
i += 1
|
|
1158
|
+
|
|
1159
|
+
return ast
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _ts_result_to_file_ast(result: dict, file_path: str) -> FileAST:
|
|
1163
|
+
"""Convert tree-sitter parse result dict into a FileAST dataclass."""
|
|
1164
|
+
language = result.get("language", "unknown")
|
|
1165
|
+
|
|
1166
|
+
# Convert function dicts to FunctionDef
|
|
1167
|
+
functions: list[FunctionDef] = []
|
|
1168
|
+
for fn in result.get("functions", []):
|
|
1169
|
+
params = fn.get("parameters", [])
|
|
1170
|
+
param_defs = [
|
|
1171
|
+
ParamDef(name=p) if isinstance(p, str) else p
|
|
1172
|
+
for p in params
|
|
1173
|
+
]
|
|
1174
|
+
functions.append(FunctionDef(
|
|
1175
|
+
name=fn["name"],
|
|
1176
|
+
qualified_name=fn.get("qualified_name", ""),
|
|
1177
|
+
is_nested=fn.get("is_nested", False),
|
|
1178
|
+
is_method=fn.get("is_method", False),
|
|
1179
|
+
start_line=fn.get("start_line", 0),
|
|
1180
|
+
end_line=fn.get("end_line", 0),
|
|
1181
|
+
parameters=param_defs,
|
|
1182
|
+
return_type=fn.get("return_type", ""),
|
|
1183
|
+
decorators=fn.get("decorators", []),
|
|
1184
|
+
is_async=fn.get("is_async", False),
|
|
1185
|
+
visibility=fn.get("visibility", "public"),
|
|
1186
|
+
docstring=fn.get("docstring", ""),
|
|
1187
|
+
))
|
|
1188
|
+
|
|
1189
|
+
# Convert class dicts to ClassDef
|
|
1190
|
+
classes: list[ClassDef] = []
|
|
1191
|
+
for cls in result.get("classes", []):
|
|
1192
|
+
methods: list[FunctionDef] = []
|
|
1193
|
+
for m in cls.get("methods", []):
|
|
1194
|
+
m_params = m.get("parameters", [])
|
|
1195
|
+
m_param_defs = [
|
|
1196
|
+
ParamDef(name=p) if isinstance(p, str) else p
|
|
1197
|
+
for p in m_params
|
|
1198
|
+
]
|
|
1199
|
+
methods.append(FunctionDef(
|
|
1200
|
+
name=m["name"],
|
|
1201
|
+
start_line=m.get("start_line", 0),
|
|
1202
|
+
end_line=m.get("end_line", 0),
|
|
1203
|
+
parameters=m_param_defs,
|
|
1204
|
+
return_type=m.get("return_type", ""),
|
|
1205
|
+
decorators=m.get("decorators", []),
|
|
1206
|
+
is_async=m.get("is_async", False),
|
|
1207
|
+
visibility=m.get("visibility", "public"),
|
|
1208
|
+
docstring=m.get("docstring", ""),
|
|
1209
|
+
))
|
|
1210
|
+
classes.append(ClassDef(
|
|
1211
|
+
name=cls["name"],
|
|
1212
|
+
start_line=cls.get("start_line", 0),
|
|
1213
|
+
end_line=cls.get("end_line", 0),
|
|
1214
|
+
bases=cls.get("bases", []),
|
|
1215
|
+
methods=methods,
|
|
1216
|
+
decorators=cls.get("decorators", []),
|
|
1217
|
+
docstring=cls.get("docstring", ""),
|
|
1218
|
+
))
|
|
1219
|
+
|
|
1220
|
+
# Convert import dicts to ImportDef
|
|
1221
|
+
imports: list[ImportDef] = []
|
|
1222
|
+
for imp in result.get("imports", []):
|
|
1223
|
+
imports.append(ImportDef(
|
|
1224
|
+
module=imp.get("module", ""),
|
|
1225
|
+
is_from=imp.get("is_from", False),
|
|
1226
|
+
names=imp.get("names", []),
|
|
1227
|
+
alias=imp.get("alias", ""),
|
|
1228
|
+
is_reexport=imp.get("is_reexport", False),
|
|
1229
|
+
line=imp.get("start_line", 0),
|
|
1230
|
+
))
|
|
1231
|
+
|
|
1232
|
+
return FileAST(
|
|
1233
|
+
path=file_path,
|
|
1234
|
+
language=language,
|
|
1235
|
+
functions=functions,
|
|
1236
|
+
classes=classes,
|
|
1237
|
+
imports=imports,
|
|
1238
|
+
top_level_vars=result.get("top_level_vars", []),
|
|
1239
|
+
line_count=result.get("line_count", 0),
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def parse_file(file_path: str, content: str | None = None) -> FileAST:
|
|
1244
|
+
"""Parse a source file and return its AST.
|
|
1245
|
+
|
|
1246
|
+
Uses a tiered fallback strategy:
|
|
1247
|
+
1. Tree-sitter with per-language config (25 languages)
|
|
1248
|
+
2. Generic tree-sitter heuristics (30+ additional languages)
|
|
1249
|
+
3. Regex-based parsers for Python/JS/TS/Rust/Go
|
|
1250
|
+
4. Universal-ctags subprocess (150+ languages)
|
|
1251
|
+
5. Minimal FileAST for truly unknown languages
|
|
1252
|
+
|
|
1253
|
+
Returns a FileAST for any input file.
|
|
1254
|
+
"""
|
|
1255
|
+
if content is None:
|
|
1256
|
+
try:
|
|
1257
|
+
content = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
1258
|
+
except OSError:
|
|
1259
|
+
return FileAST(path=file_path, language="unknown")
|
|
1260
|
+
|
|
1261
|
+
lang = detect_language(file_path)
|
|
1262
|
+
|
|
1263
|
+
# Tier 1: Tree-sitter with per-language config (25 languages)
|
|
1264
|
+
try:
|
|
1265
|
+
from attocode_intel._internal.integrations.context.ts_parser import ts_parse_file
|
|
1266
|
+
|
|
1267
|
+
ts_result = ts_parse_file(file_path, content=content, language=lang)
|
|
1268
|
+
if ts_result is not None:
|
|
1269
|
+
result = _ts_result_to_file_ast(ts_result, file_path)
|
|
1270
|
+
result.parsing_tier = "tree_sitter"
|
|
1271
|
+
return result
|
|
1272
|
+
except ImportError:
|
|
1273
|
+
pass # tree-sitter not installed
|
|
1274
|
+
|
|
1275
|
+
# Tier 2: Regex fallback for Python, JavaScript/TypeScript, Rust, Go
|
|
1276
|
+
regex_result: FileAST | None = None
|
|
1277
|
+
if lang == "python":
|
|
1278
|
+
regex_result = parse_python(content, file_path)
|
|
1279
|
+
elif lang in ("javascript", "typescript"):
|
|
1280
|
+
regex_result = parse_javascript(content, file_path)
|
|
1281
|
+
elif lang == "rust":
|
|
1282
|
+
regex_result = parse_rust(content, file_path)
|
|
1283
|
+
elif lang == "go":
|
|
1284
|
+
regex_result = parse_go(content, file_path)
|
|
1285
|
+
if regex_result is not None:
|
|
1286
|
+
regex_result.parsing_tier = "regex"
|
|
1287
|
+
return regex_result
|
|
1288
|
+
|
|
1289
|
+
# Tier 3: Generic tree-sitter heuristics (30+ additional languages)
|
|
1290
|
+
try:
|
|
1291
|
+
from attocode_intel._internal.integrations.context.ts_parser import ts_parse_file_generic
|
|
1292
|
+
|
|
1293
|
+
generic_result = ts_parse_file_generic(file_path, content=content, language=lang)
|
|
1294
|
+
if generic_result is not None:
|
|
1295
|
+
result = _ts_result_to_file_ast(generic_result, file_path)
|
|
1296
|
+
result.parsing_tier = "generic_ts"
|
|
1297
|
+
return result
|
|
1298
|
+
except ImportError:
|
|
1299
|
+
pass
|
|
1300
|
+
|
|
1301
|
+
# Tier 4: Universal-ctags fallback (150+ languages)
|
|
1302
|
+
try:
|
|
1303
|
+
from attocode_intel._internal.integrations.context.ts_parser import ctags_parse_file
|
|
1304
|
+
|
|
1305
|
+
ctags_result = ctags_parse_file(file_path, content=content)
|
|
1306
|
+
if ctags_result is not None:
|
|
1307
|
+
if lang:
|
|
1308
|
+
ctags_result["language"] = lang
|
|
1309
|
+
result = _ts_result_to_file_ast(ctags_result, file_path)
|
|
1310
|
+
result.parsing_tier = "ctags"
|
|
1311
|
+
return result
|
|
1312
|
+
except Exception:
|
|
1313
|
+
pass # ctags not installed or other error
|
|
1314
|
+
|
|
1315
|
+
# Tier 5: Minimal parsing for unknown languages
|
|
1316
|
+
return FileAST(
|
|
1317
|
+
path=file_path,
|
|
1318
|
+
language=lang if lang != "unknown" else lang,
|
|
1319
|
+
line_count=content.count("\n") + 1,
|
|
1320
|
+
parsing_tier="minimal",
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
def diff_file_ast(old: FileAST, new: FileAST) -> list[SymbolChange]:
|
|
1325
|
+
"""Compute symbol-level diff between two versions of a file's AST.
|
|
1326
|
+
|
|
1327
|
+
Compares functions and classes by name and detects additions,
|
|
1328
|
+
removals, and modifications (signature or line range changes).
|
|
1329
|
+
|
|
1330
|
+
Args:
|
|
1331
|
+
old: Previous AST for the file.
|
|
1332
|
+
new: Current AST for the file.
|
|
1333
|
+
|
|
1334
|
+
Returns:
|
|
1335
|
+
List of SymbolChange objects describing what changed.
|
|
1336
|
+
"""
|
|
1337
|
+
changes: list[SymbolChange] = []
|
|
1338
|
+
file_path = new.path or old.path
|
|
1339
|
+
|
|
1340
|
+
# --- Functions ---
|
|
1341
|
+
old_funcs = {f.name: f for f in old.functions}
|
|
1342
|
+
new_funcs = {f.name: f for f in new.functions}
|
|
1343
|
+
|
|
1344
|
+
for name in new_funcs:
|
|
1345
|
+
if name not in old_funcs:
|
|
1346
|
+
changes.append(SymbolChange(
|
|
1347
|
+
kind="added", symbol_name=name, symbol_kind="function",
|
|
1348
|
+
file_path=file_path,
|
|
1349
|
+
))
|
|
1350
|
+
else:
|
|
1351
|
+
of, nf = old_funcs[name], new_funcs[name]
|
|
1352
|
+
if (of.params != nf.params or of.return_type != nf.return_type
|
|
1353
|
+
or of.decorators != nf.decorators or of.is_async != nf.is_async
|
|
1354
|
+
or of.start_line != nf.start_line or of.end_line != nf.end_line):
|
|
1355
|
+
changes.append(SymbolChange(
|
|
1356
|
+
kind="modified", symbol_name=name, symbol_kind="function",
|
|
1357
|
+
file_path=file_path, previous=of,
|
|
1358
|
+
))
|
|
1359
|
+
|
|
1360
|
+
for name in old_funcs:
|
|
1361
|
+
if name not in new_funcs:
|
|
1362
|
+
changes.append(SymbolChange(
|
|
1363
|
+
kind="removed", symbol_name=name, symbol_kind="function",
|
|
1364
|
+
file_path=file_path, previous=old_funcs[name],
|
|
1365
|
+
))
|
|
1366
|
+
|
|
1367
|
+
# --- Classes ---
|
|
1368
|
+
old_classes = {c.name: c for c in old.classes}
|
|
1369
|
+
new_classes = {c.name: c for c in new.classes}
|
|
1370
|
+
|
|
1371
|
+
for name in new_classes:
|
|
1372
|
+
if name not in old_classes:
|
|
1373
|
+
changes.append(SymbolChange(
|
|
1374
|
+
kind="added", symbol_name=name, symbol_kind="class",
|
|
1375
|
+
file_path=file_path,
|
|
1376
|
+
))
|
|
1377
|
+
else:
|
|
1378
|
+
oc, nc = old_classes[name], new_classes[name]
|
|
1379
|
+
old_method_names = {m.name for m in oc.methods}
|
|
1380
|
+
new_method_names = {m.name for m in nc.methods}
|
|
1381
|
+
if (oc.bases != nc.bases or oc.decorators != nc.decorators
|
|
1382
|
+
or old_method_names != new_method_names
|
|
1383
|
+
or oc.start_line != nc.start_line or oc.end_line != nc.end_line):
|
|
1384
|
+
changes.append(SymbolChange(
|
|
1385
|
+
kind="modified", symbol_name=name, symbol_kind="class",
|
|
1386
|
+
file_path=file_path, previous=oc,
|
|
1387
|
+
))
|
|
1388
|
+
# Check individual method changes
|
|
1389
|
+
old_methods = {m.name: m for m in oc.methods}
|
|
1390
|
+
new_methods = {m.name: m for m in nc.methods}
|
|
1391
|
+
for mname in new_methods:
|
|
1392
|
+
if mname not in old_methods:
|
|
1393
|
+
changes.append(SymbolChange(
|
|
1394
|
+
kind="added", symbol_name=f"{name}.{mname}",
|
|
1395
|
+
symbol_kind="method", file_path=file_path,
|
|
1396
|
+
))
|
|
1397
|
+
else:
|
|
1398
|
+
om, nm = old_methods[mname], new_methods[mname]
|
|
1399
|
+
if (om.params != nm.params or om.return_type != nm.return_type
|
|
1400
|
+
or om.start_line != nm.start_line):
|
|
1401
|
+
changes.append(SymbolChange(
|
|
1402
|
+
kind="modified", symbol_name=f"{name}.{mname}",
|
|
1403
|
+
symbol_kind="method", file_path=file_path, previous=om,
|
|
1404
|
+
))
|
|
1405
|
+
for mname in old_methods:
|
|
1406
|
+
if mname not in new_methods:
|
|
1407
|
+
changes.append(SymbolChange(
|
|
1408
|
+
kind="removed", symbol_name=f"{name}.{mname}",
|
|
1409
|
+
symbol_kind="method", file_path=file_path, previous=old_methods[mname],
|
|
1410
|
+
))
|
|
1411
|
+
|
|
1412
|
+
return changes
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
def diff_imports(old: FileAST, new: FileAST) -> DependencyChanges:
|
|
1416
|
+
"""Compute import-level diff between two versions of a file's AST.
|
|
1417
|
+
|
|
1418
|
+
Args:
|
|
1419
|
+
old: Previous AST for the file.
|
|
1420
|
+
new: Current AST for the file.
|
|
1421
|
+
|
|
1422
|
+
Returns:
|
|
1423
|
+
DependencyChanges with added and removed imports.
|
|
1424
|
+
"""
|
|
1425
|
+
old_set = {(i.module, tuple(i.names), i.is_from) for i in old.imports}
|
|
1426
|
+
new_set = {(i.module, tuple(i.names), i.is_from) for i in new.imports}
|
|
1427
|
+
|
|
1428
|
+
added = [
|
|
1429
|
+
ImportDef(module=m, names=list(n), is_from=f)
|
|
1430
|
+
for m, n, f in (new_set - old_set)
|
|
1431
|
+
]
|
|
1432
|
+
removed = [
|
|
1433
|
+
ImportDef(module=m, names=list(n), is_from=f)
|
|
1434
|
+
for m, n, f in (old_set - new_set)
|
|
1435
|
+
]
|
|
1436
|
+
|
|
1437
|
+
return DependencyChanges(added=added, removed=removed)
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _extract_docstring(lines: list[str], start_idx: int) -> str:
|
|
1441
|
+
"""Extract a Python docstring starting at the given line index."""
|
|
1442
|
+
if start_idx >= len(lines):
|
|
1443
|
+
return ""
|
|
1444
|
+
|
|
1445
|
+
# Look for triple-quoted string
|
|
1446
|
+
for i in range(start_idx, min(start_idx + 3, len(lines))):
|
|
1447
|
+
stripped = lines[i].strip()
|
|
1448
|
+
if stripped.startswith('"""') or stripped.startswith("'''"):
|
|
1449
|
+
quote = stripped[:3]
|
|
1450
|
+
if stripped.endswith(quote) and len(stripped) > 6:
|
|
1451
|
+
return stripped[3:-3].strip()
|
|
1452
|
+
# Multi-line docstring
|
|
1453
|
+
doc_lines = [stripped[3:]]
|
|
1454
|
+
for j in range(i + 1, len(lines)):
|
|
1455
|
+
line = lines[j].strip()
|
|
1456
|
+
if quote in line:
|
|
1457
|
+
doc_lines.append(line.split(quote)[0])
|
|
1458
|
+
return "\n".join(doc_lines).strip()
|
|
1459
|
+
doc_lines.append(line)
|
|
1460
|
+
break
|
|
1461
|
+
return ""
|