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,2199 @@
|
|
|
1
|
+
"""Unified tree-sitter parser for multi-language AST extraction.
|
|
2
|
+
|
|
3
|
+
Provides tree-sitter-based parsing for 25+ languages including Python,
|
|
4
|
+
JavaScript, TypeScript, Go, Rust, Java, Ruby, C/C++, C#, PHP, Swift,
|
|
5
|
+
Kotlin, Scala, Lua, Elixir, Haskell, Bash, HCL, Zig, and data formats
|
|
6
|
+
(YAML, TOML, JSON, HTML, CSS). Falls back gracefully when grammars are
|
|
7
|
+
not installed.
|
|
8
|
+
|
|
9
|
+
Also includes a ``GenericTreeSitterExtractor`` that can parse any language
|
|
10
|
+
with an installed tree-sitter grammar using heuristic node-type matching,
|
|
11
|
+
extending coverage to 50+ languages without per-language config.
|
|
12
|
+
|
|
13
|
+
The main entry point is ``ts_parse_file()`` which returns a ``FileAST``
|
|
14
|
+
(the same dataclass used by the regex parsers in ``codebase_ast.py``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Language config: maps language name to tree-sitter grammar module name,
|
|
26
|
+
# and provides node type queries for each language.
|
|
27
|
+
|
|
28
|
+
_TS_AVAILABLE = False
|
|
29
|
+
_PARSERS: dict[str, object] = {} # lang -> tree_sitter.Parser
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class _LangConfig:
|
|
34
|
+
"""Configuration for parsing a specific language via tree-sitter."""
|
|
35
|
+
|
|
36
|
+
grammar_module: str # e.g. "tree_sitter_python"
|
|
37
|
+
function_types: tuple[str, ...] # node types that represent functions
|
|
38
|
+
class_types: tuple[str, ...] # node types that represent classes
|
|
39
|
+
import_types: tuple[str, ...] # node types that represent imports
|
|
40
|
+
method_types: tuple[str, ...] = () # method declarations inside classes
|
|
41
|
+
var_types: tuple[str, ...] = () # top-level variable/constant declarations
|
|
42
|
+
name_field: str = "name" # tree-sitter field for the identifier
|
|
43
|
+
language_func: str = "language" # function name to call on grammar module
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
LANGUAGE_CONFIGS: dict[str, _LangConfig] = {
|
|
47
|
+
# ---------------------------------------------------------------
|
|
48
|
+
# Tier 1: Original 9 languages (production-ready)
|
|
49
|
+
# ---------------------------------------------------------------
|
|
50
|
+
"python": _LangConfig(
|
|
51
|
+
grammar_module="tree_sitter_python",
|
|
52
|
+
function_types=("function_definition",),
|
|
53
|
+
class_types=("class_definition",),
|
|
54
|
+
import_types=("import_statement", "import_from_statement"),
|
|
55
|
+
),
|
|
56
|
+
"javascript": _LangConfig(
|
|
57
|
+
grammar_module="tree_sitter_javascript",
|
|
58
|
+
function_types=("function_declaration", "arrow_function", "function_expression", "generator_function_declaration"),
|
|
59
|
+
class_types=("class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"),
|
|
60
|
+
import_types=("import_statement", "export_statement"),
|
|
61
|
+
method_types=("method_definition",),
|
|
62
|
+
),
|
|
63
|
+
"typescript": _LangConfig(
|
|
64
|
+
grammar_module="tree_sitter_typescript",
|
|
65
|
+
language_func="language_typescript",
|
|
66
|
+
function_types=("function_declaration", "arrow_function", "function_expression", "generator_function_declaration"),
|
|
67
|
+
class_types=("class_declaration", "interface_declaration", "type_alias_declaration", "enum_declaration"),
|
|
68
|
+
import_types=("import_statement", "export_statement"),
|
|
69
|
+
method_types=("method_definition",),
|
|
70
|
+
),
|
|
71
|
+
"go": _LangConfig(
|
|
72
|
+
grammar_module="tree_sitter_go",
|
|
73
|
+
function_types=("function_declaration", "method_declaration"),
|
|
74
|
+
class_types=("type_declaration",),
|
|
75
|
+
import_types=("import_declaration",),
|
|
76
|
+
var_types=("const_declaration", "var_declaration"),
|
|
77
|
+
),
|
|
78
|
+
"rust": _LangConfig(
|
|
79
|
+
grammar_module="tree_sitter_rust",
|
|
80
|
+
function_types=("function_item",),
|
|
81
|
+
class_types=("struct_item", "enum_item", "impl_item", "trait_item"),
|
|
82
|
+
import_types=("use_declaration", "mod_item"),
|
|
83
|
+
),
|
|
84
|
+
"java": _LangConfig(
|
|
85
|
+
grammar_module="tree_sitter_java",
|
|
86
|
+
function_types=("method_declaration", "constructor_declaration"),
|
|
87
|
+
class_types=("class_declaration", "interface_declaration", "enum_declaration"),
|
|
88
|
+
import_types=("import_declaration",),
|
|
89
|
+
),
|
|
90
|
+
"ruby": _LangConfig(
|
|
91
|
+
grammar_module="tree_sitter_ruby",
|
|
92
|
+
function_types=("method",),
|
|
93
|
+
class_types=("class", "module"),
|
|
94
|
+
import_types=("call",), # require/require_relative
|
|
95
|
+
),
|
|
96
|
+
"c": _LangConfig(
|
|
97
|
+
grammar_module="tree_sitter_c",
|
|
98
|
+
function_types=("function_definition",),
|
|
99
|
+
class_types=("struct_specifier", "enum_specifier", "union_specifier"),
|
|
100
|
+
import_types=("preproc_include",),
|
|
101
|
+
),
|
|
102
|
+
"cpp": _LangConfig(
|
|
103
|
+
grammar_module="tree_sitter_cpp",
|
|
104
|
+
function_types=("function_definition",),
|
|
105
|
+
class_types=(
|
|
106
|
+
"class_specifier", "struct_specifier", "enum_specifier",
|
|
107
|
+
"namespace_definition",
|
|
108
|
+
),
|
|
109
|
+
import_types=("preproc_include",),
|
|
110
|
+
),
|
|
111
|
+
"csharp": _LangConfig(
|
|
112
|
+
grammar_module="tree_sitter_c_sharp",
|
|
113
|
+
function_types=("method_declaration", "constructor_declaration", "local_function_statement"),
|
|
114
|
+
class_types=(
|
|
115
|
+
"class_declaration", "struct_declaration", "interface_declaration",
|
|
116
|
+
"enum_declaration", "record_declaration", "namespace_declaration",
|
|
117
|
+
),
|
|
118
|
+
import_types=("using_directive",),
|
|
119
|
+
),
|
|
120
|
+
"php": _LangConfig(
|
|
121
|
+
grammar_module="tree_sitter_php",
|
|
122
|
+
function_types=("function_definition", "method_declaration"),
|
|
123
|
+
class_types=(
|
|
124
|
+
"class_declaration", "interface_declaration", "trait_declaration",
|
|
125
|
+
"enum_declaration",
|
|
126
|
+
),
|
|
127
|
+
import_types=("namespace_use_declaration",),
|
|
128
|
+
method_types=("method_declaration",),
|
|
129
|
+
language_func="language_php",
|
|
130
|
+
),
|
|
131
|
+
"swift": _LangConfig(
|
|
132
|
+
grammar_module="tree_sitter_swift",
|
|
133
|
+
function_types=("function_declaration", "init_declaration"),
|
|
134
|
+
class_types=(
|
|
135
|
+
"class_declaration", "struct_declaration", "protocol_declaration",
|
|
136
|
+
"enum_declaration", "extension_declaration",
|
|
137
|
+
),
|
|
138
|
+
import_types=("import_declaration",),
|
|
139
|
+
),
|
|
140
|
+
"kotlin": _LangConfig(
|
|
141
|
+
grammar_module="tree_sitter_kotlin",
|
|
142
|
+
function_types=("function_declaration",),
|
|
143
|
+
class_types=(
|
|
144
|
+
"class_declaration", "object_declaration", "interface_declaration",
|
|
145
|
+
"enum_class_body",
|
|
146
|
+
),
|
|
147
|
+
import_types=("import_header", "import"),
|
|
148
|
+
),
|
|
149
|
+
"scala": _LangConfig(
|
|
150
|
+
grammar_module="tree_sitter_scala",
|
|
151
|
+
function_types=("function_definition",),
|
|
152
|
+
class_types=(
|
|
153
|
+
"class_definition", "object_definition", "trait_definition",
|
|
154
|
+
"enum_definition",
|
|
155
|
+
),
|
|
156
|
+
import_types=("import_declaration",),
|
|
157
|
+
),
|
|
158
|
+
"lua": _LangConfig(
|
|
159
|
+
grammar_module="tree_sitter_lua",
|
|
160
|
+
function_types=("function_declaration", "function_definition"),
|
|
161
|
+
class_types=(), # Lua has no native classes
|
|
162
|
+
import_types=(), # require() is a regular function call
|
|
163
|
+
),
|
|
164
|
+
"elixir": _LangConfig(
|
|
165
|
+
grammar_module="tree_sitter_elixir",
|
|
166
|
+
# Elixir uses macro calls for everything — handled specially in _process_node
|
|
167
|
+
function_types=(),
|
|
168
|
+
class_types=(),
|
|
169
|
+
import_types=(),
|
|
170
|
+
),
|
|
171
|
+
"haskell": _LangConfig(
|
|
172
|
+
grammar_module="tree_sitter_haskell",
|
|
173
|
+
function_types=("function", "bind"),
|
|
174
|
+
class_types=("data_type", "newtype", "type_synonym", "class", "instance"),
|
|
175
|
+
import_types=("import",),
|
|
176
|
+
),
|
|
177
|
+
"bash": _LangConfig(
|
|
178
|
+
grammar_module="tree_sitter_bash",
|
|
179
|
+
function_types=("function_definition",),
|
|
180
|
+
class_types=(), # No classes in bash
|
|
181
|
+
import_types=("command",), # source/. commands
|
|
182
|
+
),
|
|
183
|
+
"hcl": _LangConfig(
|
|
184
|
+
grammar_module="tree_sitter_hcl",
|
|
185
|
+
function_types=(), # HCL doesn't have functions
|
|
186
|
+
class_types=("block",), # resource, data, module blocks
|
|
187
|
+
import_types=(), # No imports in HCL
|
|
188
|
+
),
|
|
189
|
+
"zig": _LangConfig(
|
|
190
|
+
grammar_module="tree_sitter_zig",
|
|
191
|
+
function_types=("function_declaration",),
|
|
192
|
+
class_types=("struct_declaration", "enum_declaration", "union_declaration"),
|
|
193
|
+
import_types=(), # @import is a builtin call
|
|
194
|
+
var_types=("variable_declaration",),
|
|
195
|
+
),
|
|
196
|
+
# Data/config languages
|
|
197
|
+
"yaml": _LangConfig(
|
|
198
|
+
grammar_module="tree_sitter_yaml",
|
|
199
|
+
function_types=(),
|
|
200
|
+
class_types=(),
|
|
201
|
+
import_types=(),
|
|
202
|
+
),
|
|
203
|
+
"toml": _LangConfig(
|
|
204
|
+
grammar_module="tree_sitter_toml",
|
|
205
|
+
function_types=(),
|
|
206
|
+
class_types=("table",), # [section] headers
|
|
207
|
+
import_types=(),
|
|
208
|
+
),
|
|
209
|
+
"json": _LangConfig(
|
|
210
|
+
grammar_module="tree_sitter_json",
|
|
211
|
+
function_types=(),
|
|
212
|
+
class_types=(),
|
|
213
|
+
import_types=(),
|
|
214
|
+
),
|
|
215
|
+
"html": _LangConfig(
|
|
216
|
+
grammar_module="tree_sitter_html",
|
|
217
|
+
function_types=(),
|
|
218
|
+
class_types=("element",), # HTML elements
|
|
219
|
+
import_types=("script_element", "style_element"), # External resources
|
|
220
|
+
),
|
|
221
|
+
"css": _LangConfig(
|
|
222
|
+
grammar_module="tree_sitter_css",
|
|
223
|
+
function_types=(),
|
|
224
|
+
class_types=("rule_set",), # CSS selectors
|
|
225
|
+
import_types=("import_statement",), # @import
|
|
226
|
+
),
|
|
227
|
+
"erlang": _LangConfig(
|
|
228
|
+
grammar_module="tree_sitter_erlang",
|
|
229
|
+
function_types=("function_clause",),
|
|
230
|
+
class_types=("module_attribute",), # -module(name). attribute
|
|
231
|
+
import_types=("attribute",), # -import(Module, [Functions]).
|
|
232
|
+
),
|
|
233
|
+
"clojure": _LangConfig(
|
|
234
|
+
grammar_module="tree_sitter_clojure",
|
|
235
|
+
# Clojure uses macro calls for everything — handled specially in _process_node
|
|
236
|
+
function_types=(),
|
|
237
|
+
class_types=(),
|
|
238
|
+
import_types=(),
|
|
239
|
+
),
|
|
240
|
+
"perl": _LangConfig(
|
|
241
|
+
grammar_module="tree_sitter_perl",
|
|
242
|
+
function_types=("subroutine_declaration_statement", "function_definition"),
|
|
243
|
+
class_types=("package_statement",), # package Foo;
|
|
244
|
+
import_types=("use_statement", "require_statement"),
|
|
245
|
+
),
|
|
246
|
+
"crystal": _LangConfig(
|
|
247
|
+
grammar_module="tree_sitter_crystal",
|
|
248
|
+
function_types=("method_def", "fun_def"),
|
|
249
|
+
class_types=("class_def", "module_def", "struct_def", "lib_def"),
|
|
250
|
+
import_types=("require",),
|
|
251
|
+
),
|
|
252
|
+
"dart": _LangConfig(
|
|
253
|
+
grammar_module="tree_sitter_dart",
|
|
254
|
+
function_types=("function_signature", "method_signature", "function_body"),
|
|
255
|
+
class_types=(
|
|
256
|
+
"class_definition", "enum_declaration",
|
|
257
|
+
"mixin_declaration", "extension_declaration",
|
|
258
|
+
),
|
|
259
|
+
import_types=("import_or_export",),
|
|
260
|
+
),
|
|
261
|
+
"ocaml": _LangConfig(
|
|
262
|
+
grammar_module="tree_sitter_ocaml",
|
|
263
|
+
function_types=("value_definition", "let_binding"),
|
|
264
|
+
class_types=("type_definition", "module_definition", "class_definition"),
|
|
265
|
+
import_types=("open_statement",),
|
|
266
|
+
language_func="language_ocaml",
|
|
267
|
+
),
|
|
268
|
+
"fsharp": _LangConfig(
|
|
269
|
+
grammar_module="tree_sitter_fsharp",
|
|
270
|
+
function_types=("function_or_value_defn", "member_defn"),
|
|
271
|
+
class_types=("type_definition", "module_defn", "namespace_defn"),
|
|
272
|
+
import_types=("open_declaration", "module_abbrev"),
|
|
273
|
+
),
|
|
274
|
+
"julia": _LangConfig(
|
|
275
|
+
grammar_module="tree_sitter_julia",
|
|
276
|
+
function_types=("function_definition", "short_function_definition", "macro_definition"),
|
|
277
|
+
class_types=("struct_definition", "abstract_definition", "module_definition"),
|
|
278
|
+
import_types=("import_statement", "using_statement"),
|
|
279
|
+
),
|
|
280
|
+
"nim": _LangConfig(
|
|
281
|
+
grammar_module="tree_sitter_nim",
|
|
282
|
+
function_types=(
|
|
283
|
+
"proc_declaration", "func_declaration", "method_declaration",
|
|
284
|
+
"template_declaration", "macro_declaration",
|
|
285
|
+
),
|
|
286
|
+
class_types=("type_section", "object_declaration"),
|
|
287
|
+
import_types=("import_statement", "from_statement"),
|
|
288
|
+
),
|
|
289
|
+
"r": _LangConfig(
|
|
290
|
+
grammar_module="tree_sitter_r",
|
|
291
|
+
# R functions are assignments like `f <- function() {}` — handled specially
|
|
292
|
+
function_types=(),
|
|
293
|
+
class_types=(), # R uses S4/R6 classes via function calls
|
|
294
|
+
import_types=("call",), # library() and require() are function calls
|
|
295
|
+
),
|
|
296
|
+
"objc": _LangConfig(
|
|
297
|
+
grammar_module="tree_sitter_objc",
|
|
298
|
+
function_types=("function_definition",), # C-style functions
|
|
299
|
+
class_types=(
|
|
300
|
+
"class_interface", "class_implementation",
|
|
301
|
+
"protocol_declaration", "category_interface",
|
|
302
|
+
),
|
|
303
|
+
import_types=("preproc_import", "preproc_include"),
|
|
304
|
+
method_types=("method_declaration",),
|
|
305
|
+
),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Aliases
|
|
309
|
+
LANGUAGE_CONFIGS["c++"] = LANGUAGE_CONFIGS["cpp"]
|
|
310
|
+
LANGUAGE_CONFIGS["c#"] = LANGUAGE_CONFIGS["csharp"]
|
|
311
|
+
LANGUAGE_CONFIGS["shell"] = LANGUAGE_CONFIGS["bash"]
|
|
312
|
+
LANGUAGE_CONFIGS["sh"] = LANGUAGE_CONFIGS["bash"]
|
|
313
|
+
LANGUAGE_CONFIGS["terraform"] = LANGUAGE_CONFIGS["hcl"]
|
|
314
|
+
LANGUAGE_CONFIGS["scss"] = LANGUAGE_CONFIGS["css"]
|
|
315
|
+
LANGUAGE_CONFIGS["objective-c"] = LANGUAGE_CONFIGS["objc"]
|
|
316
|
+
LANGUAGE_CONFIGS["objective_c"] = LANGUAGE_CONFIGS["objc"]
|
|
317
|
+
LANGUAGE_CONFIGS["f#"] = LANGUAGE_CONFIGS["fsharp"]
|
|
318
|
+
LANGUAGE_CONFIGS["metal"] = LANGUAGE_CONFIGS["cpp"]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _try_init_tree_sitter() -> bool:
|
|
322
|
+
"""Try to import tree-sitter. Returns True if available."""
|
|
323
|
+
global _TS_AVAILABLE
|
|
324
|
+
try:
|
|
325
|
+
import tree_sitter # noqa: F401
|
|
326
|
+
|
|
327
|
+
_TS_AVAILABLE = True
|
|
328
|
+
except ImportError:
|
|
329
|
+
_TS_AVAILABLE = False
|
|
330
|
+
return _TS_AVAILABLE
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _get_parser(language: str):
|
|
334
|
+
"""Get or create a tree-sitter Parser for the given language.
|
|
335
|
+
|
|
336
|
+
Returns None if the grammar is not installed.
|
|
337
|
+
"""
|
|
338
|
+
if language in _PARSERS:
|
|
339
|
+
return _PARSERS[language]
|
|
340
|
+
|
|
341
|
+
if not _TS_AVAILABLE and not _try_init_tree_sitter():
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
config = LANGUAGE_CONFIGS.get("typescript" if language == "tsx" else language)
|
|
345
|
+
if config is None:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
import importlib
|
|
350
|
+
|
|
351
|
+
import tree_sitter as ts
|
|
352
|
+
|
|
353
|
+
grammar_mod = importlib.import_module(config.grammar_module)
|
|
354
|
+
lang_fn = getattr(grammar_mod, "language_tsx" if language == "tsx" else config.language_func)
|
|
355
|
+
lang = ts.Language(lang_fn())
|
|
356
|
+
parser = ts.Parser(lang)
|
|
357
|
+
_PARSERS[language] = parser
|
|
358
|
+
return parser
|
|
359
|
+
except (ImportError, AttributeError, Exception) as e:
|
|
360
|
+
logger.debug("tree-sitter grammar for %s not available via module: %s", language, e)
|
|
361
|
+
|
|
362
|
+
# Fallback: try tree-sitter-language-pack
|
|
363
|
+
try:
|
|
364
|
+
import tree_sitter as ts
|
|
365
|
+
from tree_sitter_language_pack import get_language as _pack_get_language
|
|
366
|
+
|
|
367
|
+
lang = _pack_get_language(language)
|
|
368
|
+
parser = ts.Parser(lang)
|
|
369
|
+
_PARSERS[language] = parser
|
|
370
|
+
return parser
|
|
371
|
+
except Exception as e2:
|
|
372
|
+
logger.debug("tree-sitter grammar for %s not in language-pack: %s", language, e2)
|
|
373
|
+
_PARSERS[language] = None # Cache the failure
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def is_available(language: str = "") -> bool:
|
|
378
|
+
"""Check if tree-sitter parsing is available for a language (or any language)."""
|
|
379
|
+
if not _TS_AVAILABLE and not _try_init_tree_sitter():
|
|
380
|
+
return False
|
|
381
|
+
if language:
|
|
382
|
+
return _get_parser(language) is not None
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def supported_languages() -> list[str]:
|
|
387
|
+
"""Return list of languages with tree-sitter configs defined."""
|
|
388
|
+
return list(LANGUAGE_CONFIGS.keys())
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
# AST extraction helpers
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _node_text(node, source_bytes: bytes) -> str:
|
|
397
|
+
"""Extract the text content of a tree-sitter node."""
|
|
398
|
+
return source_bytes[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _find_name(node, source_bytes: bytes) -> str:
|
|
402
|
+
"""Extract the name identifier from a node (function/class/etc)."""
|
|
403
|
+
# Try the standard 'name' field first
|
|
404
|
+
name_node = node.child_by_field_name("name")
|
|
405
|
+
if name_node:
|
|
406
|
+
return _node_text(name_node, source_bytes)
|
|
407
|
+
|
|
408
|
+
# C/C++: name is inside declarator → function_declarator → identifier
|
|
409
|
+
declarator = node.child_by_field_name("declarator")
|
|
410
|
+
if declarator:
|
|
411
|
+
if declarator.type == "function_declarator":
|
|
412
|
+
for child in declarator.children:
|
|
413
|
+
if child.type in ("identifier", "field_identifier", "qualified_identifier"):
|
|
414
|
+
return _node_text(child, source_bytes)
|
|
415
|
+
# pointer_declarator or other wrappers around function_declarator
|
|
416
|
+
for sub in declarator.children:
|
|
417
|
+
if sub.type == "function_declarator":
|
|
418
|
+
for child in sub.children:
|
|
419
|
+
if child.type == "identifier":
|
|
420
|
+
return _node_text(child, source_bytes)
|
|
421
|
+
|
|
422
|
+
# Fallback: look for identifier child
|
|
423
|
+
for child in node.children:
|
|
424
|
+
if child.type == "identifier":
|
|
425
|
+
return _node_text(child, source_bytes)
|
|
426
|
+
if child.type == "type_identifier":
|
|
427
|
+
return _node_text(child, source_bytes)
|
|
428
|
+
|
|
429
|
+
# Go: type_declaration → type_spec → type_identifier
|
|
430
|
+
for child in node.children:
|
|
431
|
+
if child.type == "type_spec":
|
|
432
|
+
for grandchild in child.children:
|
|
433
|
+
if grandchild.type == "type_identifier":
|
|
434
|
+
return _node_text(grandchild, source_bytes)
|
|
435
|
+
|
|
436
|
+
return ""
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _find_parameters(node, source_bytes: bytes) -> list[str]:
|
|
440
|
+
"""Extract parameter names from a function node."""
|
|
441
|
+
params_node = node.child_by_field_name("parameters")
|
|
442
|
+
if params_node is None:
|
|
443
|
+
# Some languages use "formal_parameters"
|
|
444
|
+
for child in node.children:
|
|
445
|
+
if child.type in ("parameters", "formal_parameters", "parameter_list"):
|
|
446
|
+
params_node = child
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
if params_node is None:
|
|
450
|
+
return []
|
|
451
|
+
|
|
452
|
+
param_names: list[str] = []
|
|
453
|
+
for child in params_node.children:
|
|
454
|
+
if child.type in (
|
|
455
|
+
"identifier", "typed_parameter", "default_parameter",
|
|
456
|
+
"typed_default_parameter", "list_splat_pattern",
|
|
457
|
+
"dictionary_splat_pattern", "formal_parameter",
|
|
458
|
+
"required_parameter", "optional_parameter",
|
|
459
|
+
"rest_parameter", "spread_element",
|
|
460
|
+
):
|
|
461
|
+
name = _find_name(child, source_bytes)
|
|
462
|
+
if not name:
|
|
463
|
+
name = _node_text(child, source_bytes).split(":")[0].split("=")[0].strip()
|
|
464
|
+
name = name.lstrip("*").lstrip("&")
|
|
465
|
+
if name and name not in ("self", "cls", ",", "(", ")"):
|
|
466
|
+
param_names.append(name)
|
|
467
|
+
return param_names
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _find_return_type(node, source_bytes: bytes) -> str:
|
|
471
|
+
"""Extract return type annotation from a function node."""
|
|
472
|
+
ret = node.child_by_field_name("return_type")
|
|
473
|
+
if ret:
|
|
474
|
+
text = _node_text(ret, source_bytes)
|
|
475
|
+
return text.lstrip("->").lstrip(":").strip()
|
|
476
|
+
return ""
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _is_async(node, source_bytes: bytes) -> bool:
|
|
480
|
+
"""Check if a function node is async."""
|
|
481
|
+
text = _node_text(node, source_bytes)
|
|
482
|
+
return text.lstrip().startswith("async ")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _find_decorators(node, source_bytes: bytes) -> list[str]:
|
|
486
|
+
"""Extract decorators from a function/class node."""
|
|
487
|
+
decorators: list[str] = []
|
|
488
|
+
# Check previous siblings for decorator nodes
|
|
489
|
+
prev = node.prev_named_sibling
|
|
490
|
+
while prev and prev.type == "decorator":
|
|
491
|
+
text = _node_text(prev, source_bytes).lstrip("@").strip()
|
|
492
|
+
decorators.insert(0, text)
|
|
493
|
+
prev = prev.prev_named_sibling
|
|
494
|
+
return decorators
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _find_go_doc_comment(node, source_bytes: bytes) -> str:
|
|
498
|
+
"""Extract Go-style doc comment from comment lines immediately above a node.
|
|
499
|
+
|
|
500
|
+
Go doc comments are consecutive ``// Comment`` lines directly preceding a
|
|
501
|
+
declaration with no blank lines in between. This walks backward through
|
|
502
|
+
previous named siblings collecting ``comment`` nodes whose end line is
|
|
503
|
+
adjacent to (or the same as) the start line of the next element.
|
|
504
|
+
"""
|
|
505
|
+
comment_lines: list[str] = []
|
|
506
|
+
expected_line = node.start_point[0] # 0-based line of the declaration
|
|
507
|
+
|
|
508
|
+
prev = node.prev_named_sibling
|
|
509
|
+
while prev is not None and prev.type == "comment":
|
|
510
|
+
# The comment must end exactly one line before the expected line
|
|
511
|
+
if prev.end_point[0] + 1 != expected_line:
|
|
512
|
+
break
|
|
513
|
+
text = _node_text(prev, source_bytes).strip()
|
|
514
|
+
# Strip leading "//" (single-line) or "/* ... */" (block)
|
|
515
|
+
if text.startswith("//"):
|
|
516
|
+
text = text[2:].strip()
|
|
517
|
+
elif text.startswith("/*") and text.endswith("*/"):
|
|
518
|
+
text = text[2:-2].strip()
|
|
519
|
+
comment_lines.insert(0, text)
|
|
520
|
+
expected_line = prev.start_point[0]
|
|
521
|
+
prev = prev.prev_named_sibling
|
|
522
|
+
|
|
523
|
+
return "\n".join(comment_lines) if comment_lines else ""
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _extract_go_receiver(node, source_bytes: bytes) -> str:
|
|
527
|
+
"""Extract receiver type name from a Go method_declaration node.
|
|
528
|
+
|
|
529
|
+
Go methods look like ``func (r *Receiver) MethodName(...) ...``.
|
|
530
|
+
The tree-sitter Go grammar exposes a ``receiver`` field containing a
|
|
531
|
+
``parameter_list`` with one ``parameter_declaration`` whose ``type``
|
|
532
|
+
child holds the receiver type (possibly wrapped in ``pointer_type``).
|
|
533
|
+
"""
|
|
534
|
+
receiver_node = node.child_by_field_name("receiver")
|
|
535
|
+
if receiver_node is None:
|
|
536
|
+
return ""
|
|
537
|
+
|
|
538
|
+
for child in receiver_node.children:
|
|
539
|
+
if child.type == "parameter_declaration":
|
|
540
|
+
type_node = child.child_by_field_name("type")
|
|
541
|
+
if type_node is None:
|
|
542
|
+
# Fallback: look for type_identifier or pointer_type child
|
|
543
|
+
for sub in child.children:
|
|
544
|
+
if sub.type in ("type_identifier", "pointer_type"):
|
|
545
|
+
type_node = sub
|
|
546
|
+
break
|
|
547
|
+
if type_node is not None:
|
|
548
|
+
# pointer_type wraps the actual type_identifier
|
|
549
|
+
if type_node.type == "pointer_type":
|
|
550
|
+
for sub in type_node.children:
|
|
551
|
+
if sub.type == "type_identifier":
|
|
552
|
+
return _node_text(sub, source_bytes)
|
|
553
|
+
return _node_text(type_node, source_bytes)
|
|
554
|
+
return ""
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _get_visibility(name: str, language: str) -> str:
|
|
558
|
+
"""Determine visibility from name conventions."""
|
|
559
|
+
if language == "python":
|
|
560
|
+
if name.startswith("__") and not name.endswith("__"):
|
|
561
|
+
return "private"
|
|
562
|
+
if name.startswith("_"):
|
|
563
|
+
return "private"
|
|
564
|
+
return "public"
|
|
565
|
+
elif language == "go":
|
|
566
|
+
# Go convention: uppercase first letter = exported (public)
|
|
567
|
+
if name and name[0].isupper():
|
|
568
|
+
return "public"
|
|
569
|
+
return "private"
|
|
570
|
+
elif language in ("java", "typescript", "c", "cpp", "csharp", "kotlin", "swift", "scala",
|
|
571
|
+
"dart", "objc", "crystal", "fsharp"):
|
|
572
|
+
# Would need modifier parsing; default to public
|
|
573
|
+
return "public"
|
|
574
|
+
elif language == "php":
|
|
575
|
+
# PHP uses explicit modifiers; convention: __ prefix is magic
|
|
576
|
+
if name.startswith("__"):
|
|
577
|
+
return "public" # magic methods
|
|
578
|
+
return "public"
|
|
579
|
+
elif language == "ruby":
|
|
580
|
+
return "public"
|
|
581
|
+
elif language == "lua":
|
|
582
|
+
# Lua convention: _prefix means private
|
|
583
|
+
if name.startswith("_"):
|
|
584
|
+
return "private"
|
|
585
|
+
return "public"
|
|
586
|
+
elif language == "erlang":
|
|
587
|
+
# Erlang: all exported functions are public; default to public
|
|
588
|
+
return "public"
|
|
589
|
+
elif language == "clojure":
|
|
590
|
+
# Handled by _process_clojure_call which checks defn vs defn-
|
|
591
|
+
return "public"
|
|
592
|
+
elif language == "perl":
|
|
593
|
+
# Perl convention: _ prefix means private
|
|
594
|
+
if name.startswith("_"):
|
|
595
|
+
return "private"
|
|
596
|
+
return "public"
|
|
597
|
+
elif language == "ocaml":
|
|
598
|
+
# OCaml: visibility determined by .mli files; default to public
|
|
599
|
+
return "public"
|
|
600
|
+
elif language == "julia":
|
|
601
|
+
# Julia convention: _ prefix means private/internal
|
|
602
|
+
if name.startswith("_"):
|
|
603
|
+
return "private"
|
|
604
|
+
return "public"
|
|
605
|
+
elif language == "nim":
|
|
606
|
+
# Nim: exported procs use * suffix, but that's in the type, not the name
|
|
607
|
+
# Convention: default to public
|
|
608
|
+
return "public"
|
|
609
|
+
elif language == "r":
|
|
610
|
+
# R: . prefix means hidden/private
|
|
611
|
+
if name.startswith("."):
|
|
612
|
+
return "private"
|
|
613
|
+
return "public"
|
|
614
|
+
return "public"
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _extract_import_module(node, source_bytes: bytes, language: str) -> str:
|
|
618
|
+
"""Extract the module name from an import node."""
|
|
619
|
+
text = _node_text(node, source_bytes).strip()
|
|
620
|
+
|
|
621
|
+
if language == "python":
|
|
622
|
+
# "from X import Y" or "import X"
|
|
623
|
+
if text.startswith("from "):
|
|
624
|
+
parts = text.split()
|
|
625
|
+
return parts[1] if len(parts) > 1 else ""
|
|
626
|
+
if text.startswith("import "):
|
|
627
|
+
return text[7:].split(",")[0].strip().split(" as ")[0].strip()
|
|
628
|
+
elif language in ("javascript", "typescript"):
|
|
629
|
+
# "import X from 'Y'" or "import 'Y'"
|
|
630
|
+
if "from " in text:
|
|
631
|
+
parts = text.split("from ")
|
|
632
|
+
return parts[-1].strip().strip("'\"`;")
|
|
633
|
+
if text.startswith("import "):
|
|
634
|
+
return text[7:].strip().strip("'\"`;")
|
|
635
|
+
elif language == "go":
|
|
636
|
+
# import "fmt" or import ( "fmt" "os" )
|
|
637
|
+
# Tree-sitter Go grammar: import_declaration → import_spec → path
|
|
638
|
+
# or import_declaration → import_spec_list → import_spec* → path
|
|
639
|
+
for child in node.children:
|
|
640
|
+
if child.type == "import_spec":
|
|
641
|
+
path_node = child.child_by_field_name("path")
|
|
642
|
+
if path_node:
|
|
643
|
+
return _node_text(path_node, source_bytes).strip('"')
|
|
644
|
+
elif child.type == "import_spec_list":
|
|
645
|
+
# Grouped imports: return first one
|
|
646
|
+
for spec in child.children:
|
|
647
|
+
if spec.type == "import_spec":
|
|
648
|
+
path_node = spec.child_by_field_name("path")
|
|
649
|
+
if path_node:
|
|
650
|
+
return _node_text(path_node, source_bytes).strip('"')
|
|
651
|
+
# Fallback: try direct path field (some grammar versions)
|
|
652
|
+
mod_node = node.child_by_field_name("path")
|
|
653
|
+
if mod_node:
|
|
654
|
+
return _node_text(mod_node, source_bytes).strip('"')
|
|
655
|
+
elif language == "rust":
|
|
656
|
+
# use std::collections::HashMap;
|
|
657
|
+
arg_node = node.child_by_field_name("argument")
|
|
658
|
+
if arg_node:
|
|
659
|
+
return _node_text(arg_node, source_bytes).rstrip(";")
|
|
660
|
+
elif language == "java":
|
|
661
|
+
# import java.util.List;
|
|
662
|
+
return text.replace("import ", "").rstrip(";").strip()
|
|
663
|
+
elif language == "ruby":
|
|
664
|
+
# require 'foo' or require_relative 'foo'
|
|
665
|
+
if "require" in text:
|
|
666
|
+
parts = text.split("'")
|
|
667
|
+
if len(parts) >= 2:
|
|
668
|
+
return parts[1]
|
|
669
|
+
parts = text.split('"')
|
|
670
|
+
if len(parts) >= 2:
|
|
671
|
+
return parts[1]
|
|
672
|
+
elif language in ("c", "cpp"):
|
|
673
|
+
# #include <stdio.h> or #include "header.h"
|
|
674
|
+
for ch in ('<', '"'):
|
|
675
|
+
if ch in text:
|
|
676
|
+
return text.split(ch)[1].split('>' if ch == '<' else '"')[0]
|
|
677
|
+
elif language == "csharp":
|
|
678
|
+
# using System.Collections.Generic;
|
|
679
|
+
text = text.replace("using ", "").rstrip(";").strip()
|
|
680
|
+
# Remove "static" keyword for static usings
|
|
681
|
+
if text.startswith("static "):
|
|
682
|
+
text = text[7:]
|
|
683
|
+
return text
|
|
684
|
+
elif language == "php":
|
|
685
|
+
# use App\Models\User;
|
|
686
|
+
text = text.replace("use ", "").rstrip(";").strip()
|
|
687
|
+
return text
|
|
688
|
+
elif language == "swift":
|
|
689
|
+
# import Foundation
|
|
690
|
+
return text.replace("import ", "").strip()
|
|
691
|
+
elif language == "kotlin":
|
|
692
|
+
# import kotlin.collections.mutableListOf
|
|
693
|
+
return text.replace("import ", "").strip()
|
|
694
|
+
elif language == "scala":
|
|
695
|
+
# import scala.collection.mutable._
|
|
696
|
+
return text.replace("import ", "").strip()
|
|
697
|
+
elif language == "haskell":
|
|
698
|
+
# import Data.Map (Map, fromList)
|
|
699
|
+
parts = text.split()
|
|
700
|
+
if len(parts) >= 2:
|
|
701
|
+
# Handle "import qualified X as Y" or "import X"
|
|
702
|
+
idx = 1
|
|
703
|
+
if idx < len(parts) and parts[idx] == "qualified":
|
|
704
|
+
idx += 1
|
|
705
|
+
if idx < len(parts):
|
|
706
|
+
return parts[idx]
|
|
707
|
+
elif language == "erlang":
|
|
708
|
+
# -import(module, [func/arity, ...]). or -module(name).
|
|
709
|
+
# Attribute text looks like: -import(lists, [map/2]).
|
|
710
|
+
if text.startswith("-import("):
|
|
711
|
+
inner = text[8:].rstrip(").").strip()
|
|
712
|
+
return inner.split(",")[0].strip()
|
|
713
|
+
if text.startswith("-module("):
|
|
714
|
+
inner = text[8:].rstrip(").").strip()
|
|
715
|
+
return inner
|
|
716
|
+
# Generic attribute: extract attribute name
|
|
717
|
+
if text.startswith("-"):
|
|
718
|
+
return text.split("(")[0].lstrip("-").strip()
|
|
719
|
+
elif language == "clojure":
|
|
720
|
+
# Handled by _process_clojure_call; fallback for raw import text
|
|
721
|
+
return text
|
|
722
|
+
elif language == "perl":
|
|
723
|
+
# use Module::Name; or use Module::Name qw(...);
|
|
724
|
+
# require Module::Name;
|
|
725
|
+
if text.startswith("use "):
|
|
726
|
+
mod = text[4:].rstrip(";").strip()
|
|
727
|
+
return mod.split()[0].split("(")[0]
|
|
728
|
+
if text.startswith("require "):
|
|
729
|
+
mod = text[8:].rstrip(";").strip()
|
|
730
|
+
return mod.split()[0].strip("'\"")
|
|
731
|
+
elif language == "crystal":
|
|
732
|
+
# require "module_name"
|
|
733
|
+
if "require" in text:
|
|
734
|
+
parts = text.split('"')
|
|
735
|
+
if len(parts) >= 2:
|
|
736
|
+
return parts[1]
|
|
737
|
+
elif language == "dart":
|
|
738
|
+
# import 'package:flutter/material.dart'; or export '...';
|
|
739
|
+
for quote in ("'", '"'):
|
|
740
|
+
if quote in text:
|
|
741
|
+
parts = text.split(quote)
|
|
742
|
+
if len(parts) >= 2:
|
|
743
|
+
return parts[1]
|
|
744
|
+
elif language == "ocaml":
|
|
745
|
+
# open Module_name
|
|
746
|
+
if text.startswith("open "):
|
|
747
|
+
return text[5:].strip()
|
|
748
|
+
elif language == "fsharp":
|
|
749
|
+
# open System.Collections.Generic or module M = Module.Path
|
|
750
|
+
if text.startswith("open "):
|
|
751
|
+
return text[5:].strip()
|
|
752
|
+
if text.startswith("module ") and "=" in text:
|
|
753
|
+
return text.split("=")[1].strip()
|
|
754
|
+
elif language == "julia":
|
|
755
|
+
# import Module or import Module: func1, func2
|
|
756
|
+
# using Module or using Module: func1, func2
|
|
757
|
+
if text.startswith(("import ", "using ")):
|
|
758
|
+
mod = text.split(None, 1)[1] if len(text.split(None, 1)) > 1 else ""
|
|
759
|
+
return mod.split(":")[0].strip()
|
|
760
|
+
elif language == "nim":
|
|
761
|
+
# import module or from module import symbol
|
|
762
|
+
if text.startswith("from "):
|
|
763
|
+
parts = text.split()
|
|
764
|
+
if len(parts) >= 2:
|
|
765
|
+
return parts[1]
|
|
766
|
+
if text.startswith("import "):
|
|
767
|
+
return text[7:].strip().split(",")[0].strip()
|
|
768
|
+
elif language == "r":
|
|
769
|
+
# library(pkg) or require(pkg)
|
|
770
|
+
if "library(" in text or "require(" in text:
|
|
771
|
+
inner = text.split("(")[1].split(")")[0].strip() if "(" in text else ""
|
|
772
|
+
return inner.strip('"').strip("'")
|
|
773
|
+
elif language == "objc":
|
|
774
|
+
# #import <Foundation/Foundation.h> or #import "header.h"
|
|
775
|
+
# #include <stdio.h> or #include "header.h"
|
|
776
|
+
for ch in ('<', '"'):
|
|
777
|
+
if ch in text:
|
|
778
|
+
return text.split(ch)[1].split('>' if ch == '<' else '"')[0]
|
|
779
|
+
|
|
780
|
+
return text
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _extract_bases(node, source_bytes: bytes, language: str) -> list[str]:
|
|
784
|
+
"""Extract base classes/interfaces from a class node."""
|
|
785
|
+
bases: list[str] = []
|
|
786
|
+
|
|
787
|
+
if language == "python":
|
|
788
|
+
args_node = node.child_by_field_name("superclasses")
|
|
789
|
+
if args_node:
|
|
790
|
+
for child in args_node.children:
|
|
791
|
+
if child.type in ("identifier", "attribute"):
|
|
792
|
+
bases.append(_node_text(child, source_bytes))
|
|
793
|
+
elif language in ("javascript", "typescript", "java"):
|
|
794
|
+
# Look for extends/implements
|
|
795
|
+
for child in node.children:
|
|
796
|
+
if child.type in ("class_heritage", "superclass", "super_interfaces"):
|
|
797
|
+
for ident in child.children:
|
|
798
|
+
if ident.type in ("identifier", "type_identifier", "generic_type"):
|
|
799
|
+
bases.append(_node_text(ident, source_bytes))
|
|
800
|
+
elif language == "rust":
|
|
801
|
+
# trait bounds for impl blocks
|
|
802
|
+
for child in node.children:
|
|
803
|
+
if child.type in ("trait_type", "type_identifier"):
|
|
804
|
+
bases.append(_node_text(child, source_bytes))
|
|
805
|
+
elif language == "csharp":
|
|
806
|
+
for child in node.children:
|
|
807
|
+
if child.type == "base_list":
|
|
808
|
+
for ident in child.children:
|
|
809
|
+
if ident.type in ("identifier", "generic_name", "qualified_name"):
|
|
810
|
+
bases.append(_node_text(ident, source_bytes))
|
|
811
|
+
elif language in ("kotlin", "swift"):
|
|
812
|
+
for child in node.children:
|
|
813
|
+
if child.type in ("delegation_specifier", "type_identifier", "user_type",
|
|
814
|
+
"inheritance_specifier"):
|
|
815
|
+
bases.append(_node_text(child, source_bytes))
|
|
816
|
+
elif language == "scala":
|
|
817
|
+
for child in node.children:
|
|
818
|
+
if child.type in ("extends_clause",):
|
|
819
|
+
for ident in child.children:
|
|
820
|
+
if ident.type in ("type_identifier", "generic_type"):
|
|
821
|
+
bases.append(_node_text(ident, source_bytes))
|
|
822
|
+
elif language == "dart":
|
|
823
|
+
# class Foo extends Bar implements Baz, Qux
|
|
824
|
+
for child in node.children:
|
|
825
|
+
if child.type in ("superclass", "interfaces", "mixins"):
|
|
826
|
+
for ident in child.children:
|
|
827
|
+
if ident.type in ("type_identifier", "identifier", "generic_type"):
|
|
828
|
+
bases.append(_node_text(ident, source_bytes))
|
|
829
|
+
elif language == "crystal":
|
|
830
|
+
# class Foo < Bar
|
|
831
|
+
for child in node.children:
|
|
832
|
+
if child.type in ("superclass", "type_identifier"):
|
|
833
|
+
text = _node_text(child, source_bytes).strip()
|
|
834
|
+
if text and text != "<":
|
|
835
|
+
bases.append(text)
|
|
836
|
+
elif language == "objc":
|
|
837
|
+
# @interface Foo : Bar <Protocol1, Protocol2>
|
|
838
|
+
for child in node.children:
|
|
839
|
+
if child.type in ("superclass_reference", "type_identifier",
|
|
840
|
+
"protocol_qualifiers", "parameterized_class_type_arguments"):
|
|
841
|
+
text = _node_text(child, source_bytes).strip()
|
|
842
|
+
if text:
|
|
843
|
+
bases.append(text)
|
|
844
|
+
elif language == "nim":
|
|
845
|
+
# type Foo = ref object of Bar
|
|
846
|
+
for child in node.children:
|
|
847
|
+
if child.type in ("of_clause", "type_identifier"):
|
|
848
|
+
text = _node_text(child, source_bytes).strip()
|
|
849
|
+
if text.startswith("of "):
|
|
850
|
+
text = text[3:].strip()
|
|
851
|
+
if text:
|
|
852
|
+
bases.append(text)
|
|
853
|
+
|
|
854
|
+
return bases
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
# ---------------------------------------------------------------------------
|
|
858
|
+
# Elixir-specific helpers (macro-based definitions)
|
|
859
|
+
# ---------------------------------------------------------------------------
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
_ELIXIR_FN_MACROS = frozenset({"def", "defp", "defmacro", "defmacrop", "defguard", "defdelegate"})
|
|
863
|
+
_ELIXIR_MOD_MACROS = frozenset({"defmodule", "defprotocol", "defimpl"})
|
|
864
|
+
_ELIXIR_IMPORT_MACROS = frozenset({"import", "use", "require", "alias"})
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _process_elixir_call(
|
|
868
|
+
node, source_bytes: bytes,
|
|
869
|
+
functions: list[dict], classes: list[dict], imports: list[dict],
|
|
870
|
+
parent_class: str,
|
|
871
|
+
) -> str:
|
|
872
|
+
"""Handle Elixir call nodes that represent def/defmodule/import macros.
|
|
873
|
+
|
|
874
|
+
Returns the module name if a defmodule was found (for use as parent_class
|
|
875
|
+
when recursing into the do_block), empty string otherwise.
|
|
876
|
+
"""
|
|
877
|
+
# Get the macro name (first identifier child — Elixir AST has no "target" field)
|
|
878
|
+
target = node.child_by_field_name("target")
|
|
879
|
+
if not target:
|
|
880
|
+
for child in node.children:
|
|
881
|
+
if child.type == "identifier":
|
|
882
|
+
target = child
|
|
883
|
+
break
|
|
884
|
+
if not target:
|
|
885
|
+
return ""
|
|
886
|
+
macro_name = _node_text(target, source_bytes).strip()
|
|
887
|
+
|
|
888
|
+
# Get the arguments (first "arguments" child — may not be a named field)
|
|
889
|
+
args = node.child_by_field_name("arguments")
|
|
890
|
+
if not args:
|
|
891
|
+
for child in node.children:
|
|
892
|
+
if child.type == "arguments":
|
|
893
|
+
args = child
|
|
894
|
+
break
|
|
895
|
+
|
|
896
|
+
if macro_name in _ELIXIR_FN_MACROS:
|
|
897
|
+
# def/defp: first arg is the function head (a call node with the fn name)
|
|
898
|
+
if args and args.children:
|
|
899
|
+
head = args.children[0]
|
|
900
|
+
name = ""
|
|
901
|
+
if head.type == "call":
|
|
902
|
+
# Inner call also uses identifier children, not "target" field
|
|
903
|
+
head_target = head.child_by_field_name("target")
|
|
904
|
+
if not head_target:
|
|
905
|
+
for child in head.children:
|
|
906
|
+
if child.type == "identifier":
|
|
907
|
+
head_target = child
|
|
908
|
+
break
|
|
909
|
+
if head_target:
|
|
910
|
+
name = _node_text(head_target, source_bytes).strip()
|
|
911
|
+
elif head.type == "identifier":
|
|
912
|
+
name = _node_text(head, source_bytes).strip()
|
|
913
|
+
if name:
|
|
914
|
+
functions.append({
|
|
915
|
+
"name": name,
|
|
916
|
+
"parameters": [],
|
|
917
|
+
"return_type": "",
|
|
918
|
+
"start_line": node.start_point[0] + 1,
|
|
919
|
+
"end_line": node.end_point[0] + 1,
|
|
920
|
+
"is_async": False,
|
|
921
|
+
"decorators": [macro_name] if macro_name != "def" else [],
|
|
922
|
+
"visibility": "public" if macro_name in ("def", "defmacro") else "private",
|
|
923
|
+
"parent_class": parent_class,
|
|
924
|
+
})
|
|
925
|
+
return ""
|
|
926
|
+
|
|
927
|
+
elif macro_name in _ELIXIR_MOD_MACROS:
|
|
928
|
+
# defmodule: first arg is the module name (an alias like MyApp.Repo)
|
|
929
|
+
if args and args.children:
|
|
930
|
+
name_node = args.children[0]
|
|
931
|
+
name = _node_text(name_node, source_bytes).strip()
|
|
932
|
+
if name:
|
|
933
|
+
classes.append({
|
|
934
|
+
"name": name,
|
|
935
|
+
"bases": [],
|
|
936
|
+
"methods": [],
|
|
937
|
+
"decorators": [macro_name] if macro_name != "defmodule" else [],
|
|
938
|
+
"start_line": node.start_point[0] + 1,
|
|
939
|
+
"end_line": node.end_point[0] + 1,
|
|
940
|
+
})
|
|
941
|
+
return name
|
|
942
|
+
return ""
|
|
943
|
+
|
|
944
|
+
elif macro_name in _ELIXIR_IMPORT_MACROS:
|
|
945
|
+
# import/use/require/alias: first arg is the module reference
|
|
946
|
+
if args and args.children:
|
|
947
|
+
mod_node = args.children[0]
|
|
948
|
+
module = _node_text(mod_node, source_bytes).strip()
|
|
949
|
+
if module:
|
|
950
|
+
imports.append({
|
|
951
|
+
"module": module,
|
|
952
|
+
"is_from": macro_name == "alias",
|
|
953
|
+
"start_line": node.start_point[0] + 1,
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
return ""
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
# ---------------------------------------------------------------------------
|
|
960
|
+
# Clojure-specific helpers (macro-based definitions)
|
|
961
|
+
# ---------------------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
_CLOJURE_FN_MACROS = frozenset({"defn", "defn-", "defmacro", "defmulti", "defmethod", "defonce"})
|
|
964
|
+
_CLOJURE_CLASS_MACROS = frozenset({"defprotocol", "defrecord", "deftype"})
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _process_clojure_call(
|
|
968
|
+
node, source_bytes: bytes,
|
|
969
|
+
functions: list[dict], classes: list[dict], imports: list[dict],
|
|
970
|
+
parent_class: str,
|
|
971
|
+
) -> str:
|
|
972
|
+
"""Handle Clojure list nodes that represent defn/defprotocol/ns forms.
|
|
973
|
+
|
|
974
|
+
Returns the name if a namespace/protocol was found (for use as parent_class
|
|
975
|
+
when recursing into children), empty string otherwise.
|
|
976
|
+
"""
|
|
977
|
+
# Clojure forms are lists: (defn name [args] body)
|
|
978
|
+
# The first child is the macro symbol
|
|
979
|
+
children = [c for c in node.children if c.type not in ("(", ")", "meta_lit", "metadata")]
|
|
980
|
+
if not children:
|
|
981
|
+
return ""
|
|
982
|
+
|
|
983
|
+
head = children[0]
|
|
984
|
+
macro_name = _node_text(head, source_bytes).strip()
|
|
985
|
+
|
|
986
|
+
if macro_name in _CLOJURE_FN_MACROS:
|
|
987
|
+
# (defn name [params] ...) or (defn- name [params] ...)
|
|
988
|
+
if len(children) >= 2:
|
|
989
|
+
name = _node_text(children[1], source_bytes).strip()
|
|
990
|
+
if name:
|
|
991
|
+
decorators = [macro_name] if macro_name != "defn" else []
|
|
992
|
+
visibility = "public" if macro_name != "defn-" else "private"
|
|
993
|
+
functions.append({
|
|
994
|
+
"name": name,
|
|
995
|
+
"parameters": [],
|
|
996
|
+
"return_type": "",
|
|
997
|
+
"start_line": node.start_point[0] + 1,
|
|
998
|
+
"end_line": node.end_point[0] + 1,
|
|
999
|
+
"is_async": False,
|
|
1000
|
+
"decorators": decorators,
|
|
1001
|
+
"visibility": visibility,
|
|
1002
|
+
"parent_class": parent_class,
|
|
1003
|
+
})
|
|
1004
|
+
return ""
|
|
1005
|
+
|
|
1006
|
+
elif macro_name in _CLOJURE_CLASS_MACROS:
|
|
1007
|
+
# (defprotocol Name ...) / (defrecord Name [...] ...) / (deftype Name [...] ...)
|
|
1008
|
+
if len(children) >= 2:
|
|
1009
|
+
name = _node_text(children[1], source_bytes).strip()
|
|
1010
|
+
if name:
|
|
1011
|
+
decorators = [macro_name] if macro_name != "defprotocol" else []
|
|
1012
|
+
classes.append({
|
|
1013
|
+
"name": name,
|
|
1014
|
+
"bases": [],
|
|
1015
|
+
"methods": [],
|
|
1016
|
+
"decorators": decorators,
|
|
1017
|
+
"start_line": node.start_point[0] + 1,
|
|
1018
|
+
"end_line": node.end_point[0] + 1,
|
|
1019
|
+
})
|
|
1020
|
+
return name
|
|
1021
|
+
return ""
|
|
1022
|
+
|
|
1023
|
+
elif macro_name == "ns":
|
|
1024
|
+
# (ns my.namespace (:require [dep1] [dep2]))
|
|
1025
|
+
if len(children) >= 2:
|
|
1026
|
+
name = _node_text(children[1], source_bytes).strip()
|
|
1027
|
+
if name:
|
|
1028
|
+
classes.append({
|
|
1029
|
+
"name": name,
|
|
1030
|
+
"bases": [],
|
|
1031
|
+
"methods": [],
|
|
1032
|
+
"decorators": [],
|
|
1033
|
+
"start_line": node.start_point[0] + 1,
|
|
1034
|
+
"end_line": node.end_point[0] + 1,
|
|
1035
|
+
})
|
|
1036
|
+
# Extract :require dependencies
|
|
1037
|
+
for child in children[2:]:
|
|
1038
|
+
child_text = _node_text(child, source_bytes).strip()
|
|
1039
|
+
if ":require" in child_text:
|
|
1040
|
+
# Extract module names from require vector
|
|
1041
|
+
for sub in child.children:
|
|
1042
|
+
sub_text = _node_text(sub, source_bytes).strip()
|
|
1043
|
+
if sub_text.startswith("["):
|
|
1044
|
+
mod = sub_text.strip("[]").split()[0] if sub_text.strip("[]") else ""
|
|
1045
|
+
if mod and mod != ":require":
|
|
1046
|
+
imports.append({
|
|
1047
|
+
"module": mod,
|
|
1048
|
+
"is_from": False,
|
|
1049
|
+
"start_line": sub.start_point[0] + 1,
|
|
1050
|
+
})
|
|
1051
|
+
elif sub.type == "sym_lit" and sub_text != ":require":
|
|
1052
|
+
imports.append({
|
|
1053
|
+
"module": sub_text,
|
|
1054
|
+
"is_from": False,
|
|
1055
|
+
"start_line": sub.start_point[0] + 1,
|
|
1056
|
+
})
|
|
1057
|
+
return name
|
|
1058
|
+
return ""
|
|
1059
|
+
|
|
1060
|
+
return ""
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
# ---------------------------------------------------------------------------
|
|
1064
|
+
# R-specific helpers (function assignment detection)
|
|
1065
|
+
# ---------------------------------------------------------------------------
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _process_r_assignment(
|
|
1069
|
+
node, source_bytes: bytes,
|
|
1070
|
+
functions: list[dict], parent_class: str,
|
|
1071
|
+
) -> bool:
|
|
1072
|
+
"""Handle R assignment nodes where RHS is a function_definition.
|
|
1073
|
+
|
|
1074
|
+
R defines functions via `f <- function(x) { ... }` or `f = function(x) { ... }`.
|
|
1075
|
+
Returns True if a function was extracted.
|
|
1076
|
+
"""
|
|
1077
|
+
# Left side: variable name
|
|
1078
|
+
left = node.child_by_field_name("left")
|
|
1079
|
+
if not left:
|
|
1080
|
+
# Try first child
|
|
1081
|
+
children = [c for c in node.children if c.type not in ("<-", "=", "<<-")]
|
|
1082
|
+
if len(children) >= 2:
|
|
1083
|
+
left = children[0]
|
|
1084
|
+
else:
|
|
1085
|
+
return False
|
|
1086
|
+
|
|
1087
|
+
# Right side: check for function_definition
|
|
1088
|
+
right = node.child_by_field_name("right")
|
|
1089
|
+
if not right:
|
|
1090
|
+
children = [c for c in node.children if c.type not in ("<-", "=", "<<-")]
|
|
1091
|
+
if len(children) >= 2:
|
|
1092
|
+
right = children[-1]
|
|
1093
|
+
else:
|
|
1094
|
+
return False
|
|
1095
|
+
|
|
1096
|
+
if right.type != "function_definition":
|
|
1097
|
+
return False
|
|
1098
|
+
|
|
1099
|
+
name = _node_text(left, source_bytes).strip()
|
|
1100
|
+
if not name:
|
|
1101
|
+
return False
|
|
1102
|
+
|
|
1103
|
+
params = _find_parameters(right, source_bytes)
|
|
1104
|
+
functions.append({
|
|
1105
|
+
"name": name,
|
|
1106
|
+
"parameters": params,
|
|
1107
|
+
"return_type": "",
|
|
1108
|
+
"start_line": node.start_point[0] + 1,
|
|
1109
|
+
"end_line": node.end_point[0] + 1,
|
|
1110
|
+
"is_async": False,
|
|
1111
|
+
"decorators": [],
|
|
1112
|
+
"visibility": "public",
|
|
1113
|
+
"parent_class": parent_class,
|
|
1114
|
+
})
|
|
1115
|
+
return True
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
# ---------------------------------------------------------------------------
|
|
1119
|
+
# Main parsing function
|
|
1120
|
+
# ---------------------------------------------------------------------------
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def ts_parse_file(file_path: str, content: str | None = None, language: str = "") -> dict | None:
|
|
1124
|
+
"""Parse a file using tree-sitter and return structured AST data.
|
|
1125
|
+
|
|
1126
|
+
Returns a dict with keys: functions, classes, imports, top_level_vars,
|
|
1127
|
+
line_count — matching the fields of ``FileAST`` from ``codebase_ast.py``.
|
|
1128
|
+
|
|
1129
|
+
Returns None if tree-sitter is not available or the language is unsupported.
|
|
1130
|
+
|
|
1131
|
+
Args:
|
|
1132
|
+
file_path: Path to the source file.
|
|
1133
|
+
content: Optional file content (read from disk if not provided).
|
|
1134
|
+
language: Language name (auto-detected from extension if empty).
|
|
1135
|
+
"""
|
|
1136
|
+
if not language:
|
|
1137
|
+
from attocode_intel._internal.integrations.context.codebase_ast import detect_language
|
|
1138
|
+
language = detect_language(file_path)
|
|
1139
|
+
|
|
1140
|
+
if language not in LANGUAGE_CONFIGS:
|
|
1141
|
+
return None
|
|
1142
|
+
|
|
1143
|
+
parser = _get_parser("tsx" if Path(file_path).suffix == ".tsx" else language)
|
|
1144
|
+
if parser is None:
|
|
1145
|
+
return None
|
|
1146
|
+
|
|
1147
|
+
if content is None:
|
|
1148
|
+
try:
|
|
1149
|
+
content = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
1150
|
+
except OSError:
|
|
1151
|
+
return None
|
|
1152
|
+
|
|
1153
|
+
source_bytes = content.encode("utf-8")
|
|
1154
|
+
|
|
1155
|
+
try:
|
|
1156
|
+
tree = parser.parse(source_bytes)
|
|
1157
|
+
except Exception as e:
|
|
1158
|
+
logger.debug("tree-sitter parse error for %s: %s", file_path, e)
|
|
1159
|
+
return None
|
|
1160
|
+
|
|
1161
|
+
config = LANGUAGE_CONFIGS[language]
|
|
1162
|
+
root = tree.root_node
|
|
1163
|
+
|
|
1164
|
+
functions: list[dict] = []
|
|
1165
|
+
classes: list[dict] = []
|
|
1166
|
+
imports: list[dict] = []
|
|
1167
|
+
top_level_vars: list[str] = []
|
|
1168
|
+
|
|
1169
|
+
def _process_node(node, parent_class: str = "", parent_function: str = "") -> None:
|
|
1170
|
+
"""Recursively process tree-sitter nodes."""
|
|
1171
|
+
ntype = node.type
|
|
1172
|
+
|
|
1173
|
+
# A CommonJS assignment names its function through the left-hand side.
|
|
1174
|
+
if language in ("javascript", "typescript") and ntype == "call_expression":
|
|
1175
|
+
fn = node.child_by_field_name("function")
|
|
1176
|
+
args = node.child_by_field_name("arguments")
|
|
1177
|
+
if fn and _node_text(fn, source_bytes) == "require" and args:
|
|
1178
|
+
values = args.named_children
|
|
1179
|
+
if len(values) == 1 and values[0].type == "string":
|
|
1180
|
+
imports.append({"module": _node_text(values[0], source_bytes)[1:-1],
|
|
1181
|
+
"start_line": node.start_point[0] + 1})
|
|
1182
|
+
# Functions
|
|
1183
|
+
if ntype in config.function_types:
|
|
1184
|
+
name = _find_name(node, source_bytes)
|
|
1185
|
+
definition_start = node.start_point[0] + 1
|
|
1186
|
+
assigned_method = False
|
|
1187
|
+
qualified_name = parent_function + "." + name if parent_function else name
|
|
1188
|
+
if language in ("javascript", "typescript") and node.parent:
|
|
1189
|
+
parent = node.parent
|
|
1190
|
+
target = (parent.child_by_field_name("left") if parent.type == "assignment_expression"
|
|
1191
|
+
else parent.child_by_field_name("name") if parent.type == "variable_declarator"
|
|
1192
|
+
else parent.child_by_field_name("key") if parent.type == "pair" else None)
|
|
1193
|
+
if target:
|
|
1194
|
+
assigned_method = target.type == "member_expression"
|
|
1195
|
+
definition_start = target.start_point[0] + 1
|
|
1196
|
+
qualified_name = _node_text(target, source_bytes)
|
|
1197
|
+
name = qualified_name.rsplit(".", 1)[-1]
|
|
1198
|
+
if not name:
|
|
1199
|
+
return
|
|
1200
|
+
|
|
1201
|
+
params = _find_parameters(node, source_bytes)
|
|
1202
|
+
ret_type = _find_return_type(node, source_bytes)
|
|
1203
|
+
decorators = _find_decorators(node, source_bytes)
|
|
1204
|
+
is_async_fn = _is_async(node, source_bytes)
|
|
1205
|
+
visibility = _get_visibility(name, language)
|
|
1206
|
+
|
|
1207
|
+
# Go: extract receiver type for method_declaration → parent_class
|
|
1208
|
+
effective_parent = parent_class
|
|
1209
|
+
if language == "go" and ntype == "method_declaration":
|
|
1210
|
+
receiver_type = _extract_go_receiver(node, source_bytes)
|
|
1211
|
+
if receiver_type:
|
|
1212
|
+
effective_parent = receiver_type
|
|
1213
|
+
|
|
1214
|
+
# Go: extract doc comment from preceding // comment lines
|
|
1215
|
+
docstring = ""
|
|
1216
|
+
if language == "go":
|
|
1217
|
+
docstring = _find_go_doc_comment(node, source_bytes)
|
|
1218
|
+
|
|
1219
|
+
fn_data = {
|
|
1220
|
+
"name": name,
|
|
1221
|
+
"parameters": params,
|
|
1222
|
+
"return_type": ret_type,
|
|
1223
|
+
"start_line": definition_start,
|
|
1224
|
+
"end_line": node.end_point[0] + 1,
|
|
1225
|
+
"is_async": is_async_fn,
|
|
1226
|
+
"decorators": decorators,
|
|
1227
|
+
"visibility": visibility,
|
|
1228
|
+
"parent_class": effective_parent,
|
|
1229
|
+
"qualified_name": qualified_name,
|
|
1230
|
+
"is_nested": bool(parent_function),
|
|
1231
|
+
"is_method": assigned_method,
|
|
1232
|
+
}
|
|
1233
|
+
if docstring:
|
|
1234
|
+
fn_data["docstring"] = docstring
|
|
1235
|
+
|
|
1236
|
+
if effective_parent:
|
|
1237
|
+
# Will be added as method to the class
|
|
1238
|
+
for cls in classes:
|
|
1239
|
+
if cls["name"] == effective_parent:
|
|
1240
|
+
cls["methods"].append(fn_data)
|
|
1241
|
+
break
|
|
1242
|
+
else:
|
|
1243
|
+
# Go: receiver type may not have a matching type_declaration
|
|
1244
|
+
# in the same file; record as a standalone function with parent_class set
|
|
1245
|
+
functions.append(fn_data)
|
|
1246
|
+
else:
|
|
1247
|
+
functions.append(fn_data)
|
|
1248
|
+
body = node.child_by_field_name("body")
|
|
1249
|
+
if body:
|
|
1250
|
+
for child in body.named_children:
|
|
1251
|
+
_process_node(child, parent_function=(effective_parent + "." + name if effective_parent else qualified_name))
|
|
1252
|
+
return
|
|
1253
|
+
|
|
1254
|
+
# Methods inside classes (some languages have specific method types)
|
|
1255
|
+
if ntype in config.method_types and parent_class:
|
|
1256
|
+
name = _find_name(node, source_bytes)
|
|
1257
|
+
if name:
|
|
1258
|
+
params = _find_parameters(node, source_bytes)
|
|
1259
|
+
ret_type = _find_return_type(node, source_bytes)
|
|
1260
|
+
decorators = _find_decorators(node, source_bytes)
|
|
1261
|
+
is_async_fn = _is_async(node, source_bytes)
|
|
1262
|
+
visibility = _get_visibility(name, language)
|
|
1263
|
+
|
|
1264
|
+
method_data = {
|
|
1265
|
+
"name": name,
|
|
1266
|
+
"parameters": params,
|
|
1267
|
+
"return_type": ret_type,
|
|
1268
|
+
"start_line": node.start_point[0] + 1,
|
|
1269
|
+
"end_line": node.end_point[0] + 1,
|
|
1270
|
+
"is_async": is_async_fn,
|
|
1271
|
+
"decorators": decorators,
|
|
1272
|
+
"visibility": visibility,
|
|
1273
|
+
"parent_class": parent_class,
|
|
1274
|
+
}
|
|
1275
|
+
for cls in classes:
|
|
1276
|
+
if cls["name"] == parent_class:
|
|
1277
|
+
cls["methods"].append(method_data)
|
|
1278
|
+
break
|
|
1279
|
+
return
|
|
1280
|
+
|
|
1281
|
+
# Classes
|
|
1282
|
+
if ntype in config.class_types:
|
|
1283
|
+
name = _find_name(node, source_bytes)
|
|
1284
|
+
if language == "rust" and ntype == "impl_item":
|
|
1285
|
+
target_type = node.child_by_field_name("type")
|
|
1286
|
+
if target_type:
|
|
1287
|
+
name = _node_text(target_type, source_bytes)
|
|
1288
|
+
if not name:
|
|
1289
|
+
# For impl blocks in Rust, try type field
|
|
1290
|
+
type_node = node.child_by_field_name("type")
|
|
1291
|
+
if type_node:
|
|
1292
|
+
name = _node_text(type_node, source_bytes)
|
|
1293
|
+
|
|
1294
|
+
# HCL blocks: resource "aws_eks_cluster" "this" { ... }
|
|
1295
|
+
# Extract resource type as a top-level var in addition to
|
|
1296
|
+
# using the block keyword (resource/data/module) as class name
|
|
1297
|
+
if language == "hcl" and ntype == "block":
|
|
1298
|
+
string_lits = [
|
|
1299
|
+
c for c in node.children if c.type == "string_lit"
|
|
1300
|
+
]
|
|
1301
|
+
for sl in string_lits:
|
|
1302
|
+
for sc in sl.children:
|
|
1303
|
+
if sc.type == "template_literal":
|
|
1304
|
+
resource_name = _node_text(sc, source_bytes)
|
|
1305
|
+
if resource_name and resource_name not in top_level_vars:
|
|
1306
|
+
top_level_vars.append(resource_name)
|
|
1307
|
+
|
|
1308
|
+
if name:
|
|
1309
|
+
decorators = _find_decorators(node, source_bytes)
|
|
1310
|
+
bases = _extract_bases(node, source_bytes, language)
|
|
1311
|
+
|
|
1312
|
+
cls_data = {
|
|
1313
|
+
"name": name,
|
|
1314
|
+
"bases": bases,
|
|
1315
|
+
"methods": [],
|
|
1316
|
+
"decorators": decorators,
|
|
1317
|
+
"start_line": node.start_point[0] + 1,
|
|
1318
|
+
"end_line": node.end_point[0] + 1,
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
# Go: extract doc comment for type declarations
|
|
1322
|
+
if language == "go":
|
|
1323
|
+
docstring = _find_go_doc_comment(node, source_bytes)
|
|
1324
|
+
if docstring:
|
|
1325
|
+
cls_data["docstring"] = docstring
|
|
1326
|
+
|
|
1327
|
+
classes.append(cls_data)
|
|
1328
|
+
|
|
1329
|
+
# Process children to find methods
|
|
1330
|
+
body = node.child_by_field_name("body")
|
|
1331
|
+
if body:
|
|
1332
|
+
for child in body.children:
|
|
1333
|
+
_process_node(child, parent_class=name)
|
|
1334
|
+
else:
|
|
1335
|
+
for child in node.children:
|
|
1336
|
+
_process_node(child, parent_class=name)
|
|
1337
|
+
|
|
1338
|
+
# Go interface methods: type_declaration → type_spec → interface_type → method_elem
|
|
1339
|
+
if language == "go":
|
|
1340
|
+
for child in node.children:
|
|
1341
|
+
if child.type == "type_spec":
|
|
1342
|
+
for gchild in child.children:
|
|
1343
|
+
if gchild.type == "interface_type":
|
|
1344
|
+
for method in gchild.children:
|
|
1345
|
+
if method.type == "method_elem":
|
|
1346
|
+
# Name is in field_identifier child
|
|
1347
|
+
mname = ""
|
|
1348
|
+
for mc in method.children:
|
|
1349
|
+
if mc.type == "field_identifier":
|
|
1350
|
+
mname = _node_text(mc, source_bytes)
|
|
1351
|
+
break
|
|
1352
|
+
if not mname:
|
|
1353
|
+
mname = _find_name(method, source_bytes)
|
|
1354
|
+
if mname:
|
|
1355
|
+
params = _find_parameters(method, source_bytes)
|
|
1356
|
+
fn_data = {
|
|
1357
|
+
"name": mname, "parameters": params,
|
|
1358
|
+
"return_type": "",
|
|
1359
|
+
"start_line": method.start_point[0] + 1,
|
|
1360
|
+
"end_line": method.end_point[0] + 1,
|
|
1361
|
+
"is_async": False, "decorators": [],
|
|
1362
|
+
"visibility": "public",
|
|
1363
|
+
"parent_class": name,
|
|
1364
|
+
}
|
|
1365
|
+
cls_data["methods"].append(fn_data)
|
|
1366
|
+
return
|
|
1367
|
+
|
|
1368
|
+
# Imports
|
|
1369
|
+
if ntype in config.import_types:
|
|
1370
|
+
if language == "rust":
|
|
1371
|
+
from .syntax_evidence import rust_imports
|
|
1372
|
+
imports.extend(rust_imports(node, source_bytes))
|
|
1373
|
+
if ntype == "mod_item" and node.child_by_field_name("body"):
|
|
1374
|
+
for child in node.child_by_field_name("body").named_children:
|
|
1375
|
+
_process_node(child)
|
|
1376
|
+
return
|
|
1377
|
+
if language in ("javascript", "typescript"):
|
|
1378
|
+
source = node.child_by_field_name("source")
|
|
1379
|
+
if source:
|
|
1380
|
+
module = _node_text(source, source_bytes)[1:-1]
|
|
1381
|
+
bindings = []
|
|
1382
|
+
stack = list(node.named_children)
|
|
1383
|
+
while stack:
|
|
1384
|
+
child = stack.pop()
|
|
1385
|
+
if child.type in {"import_specifier", "export_specifier"}:
|
|
1386
|
+
identifiers = [item for item in child.named_children
|
|
1387
|
+
if item.type in {"identifier", "type_identifier"}]
|
|
1388
|
+
if identifiers:
|
|
1389
|
+
imported = _node_text(identifiers[0], source_bytes)
|
|
1390
|
+
local = (_node_text(identifiers[1], source_bytes)
|
|
1391
|
+
if len(identifiers) > 1 else imported)
|
|
1392
|
+
bindings.append({"module": module, "names": [imported],
|
|
1393
|
+
"alias": local if local != imported else "",
|
|
1394
|
+
"is_from": True,
|
|
1395
|
+
"is_reexport": ntype == "export_statement",
|
|
1396
|
+
"start_line": node.start_point[0] + 1})
|
|
1397
|
+
continue
|
|
1398
|
+
if child.type in {"namespace_import", "namespace_export"}:
|
|
1399
|
+
identifier = next((item for item in child.named_children
|
|
1400
|
+
if item.type == "identifier"), None)
|
|
1401
|
+
if identifier:
|
|
1402
|
+
bindings.append({"module": module, "names": ["*"],
|
|
1403
|
+
"alias": _node_text(identifier, source_bytes),
|
|
1404
|
+
"is_from": True,
|
|
1405
|
+
"is_reexport": ntype == "export_statement",
|
|
1406
|
+
"start_line": node.start_point[0] + 1})
|
|
1407
|
+
continue
|
|
1408
|
+
stack.extend(child.named_children)
|
|
1409
|
+
imports.extend(bindings or [{"module": module,
|
|
1410
|
+
"is_reexport": ntype == "export_statement",
|
|
1411
|
+
"start_line": node.start_point[0] + 1}])
|
|
1412
|
+
# Exported declarations still need their definitions extracted.
|
|
1413
|
+
for child in node.named_children:
|
|
1414
|
+
_process_node(child, parent_class)
|
|
1415
|
+
return
|
|
1416
|
+
if language == "python":
|
|
1417
|
+
import ast as python_ast
|
|
1418
|
+
try:
|
|
1419
|
+
statement = python_ast.parse(_node_text(node, source_bytes)).body[0]
|
|
1420
|
+
if isinstance(statement, python_ast.ImportFrom):
|
|
1421
|
+
for binding in statement.names:
|
|
1422
|
+
imports.append({"module": "." * statement.level + (statement.module or ""),
|
|
1423
|
+
"names": [binding.name], "alias": binding.asname or "",
|
|
1424
|
+
"is_from": True, "start_line": node.start_point[0] + 1})
|
|
1425
|
+
elif isinstance(statement, python_ast.Import):
|
|
1426
|
+
for binding in statement.names:
|
|
1427
|
+
imports.append({"module": binding.name, "names": [binding.name],
|
|
1428
|
+
"alias": binding.asname or "", "start_line": node.start_point[0] + 1})
|
|
1429
|
+
return
|
|
1430
|
+
except (SyntaxError, ValueError):
|
|
1431
|
+
pass
|
|
1432
|
+
if language == "go":
|
|
1433
|
+
stack = [node]
|
|
1434
|
+
while stack:
|
|
1435
|
+
item = stack.pop()
|
|
1436
|
+
if item.type == "import_spec":
|
|
1437
|
+
path = item.child_by_field_name("path")
|
|
1438
|
+
if path:
|
|
1439
|
+
imports.append({"module": _node_text(path, source_bytes).strip('"'),
|
|
1440
|
+
"start_line": item.start_point[0] + 1})
|
|
1441
|
+
else:
|
|
1442
|
+
stack.extend(reversed(item.named_children))
|
|
1443
|
+
return
|
|
1444
|
+
module = _extract_import_module(node, source_bytes, language)
|
|
1445
|
+
if module:
|
|
1446
|
+
is_from = language == "python" and ntype == "import_from_statement"
|
|
1447
|
+
imports.append({
|
|
1448
|
+
"module": module,
|
|
1449
|
+
"is_from": is_from,
|
|
1450
|
+
"start_line": node.start_point[0] + 1,
|
|
1451
|
+
})
|
|
1452
|
+
return
|
|
1453
|
+
|
|
1454
|
+
# Top-level variable assignments (Python-specific)
|
|
1455
|
+
if language == "python" and ntype == "expression_statement" and not parent_class:
|
|
1456
|
+
child = node.children[0] if node.children else None
|
|
1457
|
+
if child and child.type == "assignment":
|
|
1458
|
+
left = child.child_by_field_name("left")
|
|
1459
|
+
if left and left.type == "identifier":
|
|
1460
|
+
var_name = _node_text(left, source_bytes)
|
|
1461
|
+
if var_name.isupper() or var_name == "__all__":
|
|
1462
|
+
top_level_vars.append(var_name)
|
|
1463
|
+
return
|
|
1464
|
+
|
|
1465
|
+
# Config-driven top-level variable/constant declarations (Go, Zig, etc.)
|
|
1466
|
+
if config.var_types and ntype in config.var_types and not parent_class:
|
|
1467
|
+
if language == "go":
|
|
1468
|
+
# Go const/var blocks: const ( X = 1; Y = 2 )
|
|
1469
|
+
for child in node.children:
|
|
1470
|
+
if child.type in ("const_spec", "var_spec"):
|
|
1471
|
+
var_name = _find_name(child, source_bytes)
|
|
1472
|
+
if var_name:
|
|
1473
|
+
top_level_vars.append(var_name)
|
|
1474
|
+
else:
|
|
1475
|
+
var_name = _find_name(node, source_bytes)
|
|
1476
|
+
if var_name:
|
|
1477
|
+
top_level_vars.append(var_name)
|
|
1478
|
+
return
|
|
1479
|
+
|
|
1480
|
+
# Elixir: macro calls (def, defmodule, import, etc.)
|
|
1481
|
+
if language == "elixir" and ntype == "call":
|
|
1482
|
+
mod_name = _process_elixir_call(
|
|
1483
|
+
node, source_bytes, functions, classes, imports, parent_class,
|
|
1484
|
+
)
|
|
1485
|
+
# Recurse into do_block for nested defs inside defmodule
|
|
1486
|
+
inner_parent = mod_name or parent_class
|
|
1487
|
+
for child in node.children:
|
|
1488
|
+
if child.type == "do_block":
|
|
1489
|
+
for sub in child.children:
|
|
1490
|
+
_process_node(sub, parent_class=inner_parent)
|
|
1491
|
+
return
|
|
1492
|
+
|
|
1493
|
+
# Clojure: list nodes represent forms like (defn ...), (ns ...), etc.
|
|
1494
|
+
if language == "clojure" and ntype in ("list_lit", "list"):
|
|
1495
|
+
mod_name = _process_clojure_call(
|
|
1496
|
+
node, source_bytes, functions, classes, imports, parent_class,
|
|
1497
|
+
)
|
|
1498
|
+
# Recurse into children for nested definitions
|
|
1499
|
+
if mod_name:
|
|
1500
|
+
for child in node.children:
|
|
1501
|
+
_process_node(child, parent_class=mod_name)
|
|
1502
|
+
return
|
|
1503
|
+
|
|
1504
|
+
# R: detect function assignments (f <- function(...) { ... })
|
|
1505
|
+
if language == "r" and ntype in ("left_assignment", "equals_assignment", "binary_operator"):
|
|
1506
|
+
if _process_r_assignment(node, source_bytes, functions, parent_class):
|
|
1507
|
+
return
|
|
1508
|
+
|
|
1509
|
+
# R: filter import calls to only library() and require()
|
|
1510
|
+
if language == "r" and ntype == "call":
|
|
1511
|
+
# Only treat library() and require() as imports
|
|
1512
|
+
fn_node = node.child_by_field_name("function")
|
|
1513
|
+
if not fn_node:
|
|
1514
|
+
for child in node.children:
|
|
1515
|
+
if child.type == "identifier":
|
|
1516
|
+
fn_node = child
|
|
1517
|
+
break
|
|
1518
|
+
if fn_node:
|
|
1519
|
+
fn_name = _node_text(fn_node, source_bytes).strip()
|
|
1520
|
+
if fn_name in ("library", "require"):
|
|
1521
|
+
module = _extract_import_module(node, source_bytes, language)
|
|
1522
|
+
if module:
|
|
1523
|
+
imports.append({
|
|
1524
|
+
"module": module,
|
|
1525
|
+
"is_from": False,
|
|
1526
|
+
"start_line": node.start_point[0] + 1,
|
|
1527
|
+
})
|
|
1528
|
+
return
|
|
1529
|
+
|
|
1530
|
+
# Recurse into children
|
|
1531
|
+
for child in node.children:
|
|
1532
|
+
_process_node(child, parent_class=parent_class, parent_function=parent_function)
|
|
1533
|
+
|
|
1534
|
+
# Process top-level nodes
|
|
1535
|
+
for child in root.children:
|
|
1536
|
+
_process_node(child)
|
|
1537
|
+
|
|
1538
|
+
# Data/config language extraction: extract top-level keys as vars
|
|
1539
|
+
if language in ("yaml", "toml", "json"):
|
|
1540
|
+
_extract_data_symbols(root, source_bytes, language, top_level_vars, classes)
|
|
1541
|
+
|
|
1542
|
+
# HTML: extract meaningful elements (IDs, component tags)
|
|
1543
|
+
if language == "html":
|
|
1544
|
+
_extract_html_symbols(root, source_bytes, classes, imports)
|
|
1545
|
+
|
|
1546
|
+
# CSS: extract selectors as class names
|
|
1547
|
+
if language == "css":
|
|
1548
|
+
_extract_css_symbols(root, source_bytes, classes)
|
|
1549
|
+
|
|
1550
|
+
return {
|
|
1551
|
+
"functions": functions,
|
|
1552
|
+
"classes": classes,
|
|
1553
|
+
"imports": imports,
|
|
1554
|
+
"top_level_vars": top_level_vars,
|
|
1555
|
+
"line_count": content.count("\n") + 1,
|
|
1556
|
+
"language": language,
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
# ---------------------------------------------------------------------------
|
|
1561
|
+
# Data/config language helpers (Phase 2)
|
|
1562
|
+
# ---------------------------------------------------------------------------
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _extract_data_symbols(
|
|
1566
|
+
root, source_bytes: bytes, language: str,
|
|
1567
|
+
top_level_vars: list[str], classes: list[dict],
|
|
1568
|
+
) -> None:
|
|
1569
|
+
"""Extract structural symbols from data/config files."""
|
|
1570
|
+
if language == "yaml":
|
|
1571
|
+
# Extract top-level mapping keys
|
|
1572
|
+
# Tree-sitter YAML: root → document → block_node → block_mapping → block_mapping_pair
|
|
1573
|
+
def _find_yaml_pairs(node, depth: int = 0):
|
|
1574
|
+
if depth > 5:
|
|
1575
|
+
return
|
|
1576
|
+
if node.type == "block_mapping_pair":
|
|
1577
|
+
key_node = node.child_by_field_name("key")
|
|
1578
|
+
if key_node:
|
|
1579
|
+
key = _node_text(key_node, source_bytes).strip()
|
|
1580
|
+
if key:
|
|
1581
|
+
top_level_vars.append(key)
|
|
1582
|
+
return # don't recurse into values
|
|
1583
|
+
for child in node.children:
|
|
1584
|
+
_find_yaml_pairs(child, depth + 1)
|
|
1585
|
+
_find_yaml_pairs(root)
|
|
1586
|
+
elif language == "toml":
|
|
1587
|
+
# Extract table names and top-level keys
|
|
1588
|
+
for child in root.children:
|
|
1589
|
+
if child.type == "table":
|
|
1590
|
+
# [section] headers become class-like entries
|
|
1591
|
+
for sub in child.children:
|
|
1592
|
+
if sub.type in ("bare_key", "dotted_key", "quoted_key"):
|
|
1593
|
+
name = _node_text(sub, source_bytes).strip("[]\"'")
|
|
1594
|
+
if name:
|
|
1595
|
+
classes.append({
|
|
1596
|
+
"name": name,
|
|
1597
|
+
"bases": [],
|
|
1598
|
+
"methods": [],
|
|
1599
|
+
"decorators": [],
|
|
1600
|
+
"start_line": sub.start_point[0] + 1,
|
|
1601
|
+
"end_line": sub.end_point[0] + 1,
|
|
1602
|
+
})
|
|
1603
|
+
elif child.type == "pair":
|
|
1604
|
+
key_node = child.child_by_field_name("key") or (
|
|
1605
|
+
child.children[0] if child.children else None
|
|
1606
|
+
)
|
|
1607
|
+
if key_node:
|
|
1608
|
+
key = _node_text(key_node, source_bytes).strip()
|
|
1609
|
+
if key:
|
|
1610
|
+
top_level_vars.append(key)
|
|
1611
|
+
elif language == "json":
|
|
1612
|
+
# Extract top-level object keys
|
|
1613
|
+
obj = root.children[0] if root.children else None
|
|
1614
|
+
if obj and obj.type == "object":
|
|
1615
|
+
for child in obj.children:
|
|
1616
|
+
if child.type == "pair":
|
|
1617
|
+
key_node = child.child_by_field_name("key")
|
|
1618
|
+
if key_node:
|
|
1619
|
+
key = _node_text(key_node, source_bytes).strip('"')
|
|
1620
|
+
if key:
|
|
1621
|
+
top_level_vars.append(key)
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def _extract_html_symbols(
|
|
1625
|
+
root, source_bytes: bytes, classes: list[dict], imports: list[dict],
|
|
1626
|
+
) -> None:
|
|
1627
|
+
"""Extract meaningful symbols from HTML."""
|
|
1628
|
+
def _walk_html(node) -> None:
|
|
1629
|
+
if node.type == "element":
|
|
1630
|
+
tag_node = node.child_by_field_name("tag_name")
|
|
1631
|
+
if not tag_node:
|
|
1632
|
+
# Try first child for self-closing
|
|
1633
|
+
for child in node.children:
|
|
1634
|
+
if child.type == "start_tag":
|
|
1635
|
+
for sub in child.children:
|
|
1636
|
+
if sub.type == "tag_name":
|
|
1637
|
+
tag_node = sub
|
|
1638
|
+
break
|
|
1639
|
+
break
|
|
1640
|
+
|
|
1641
|
+
if tag_node:
|
|
1642
|
+
tag = _node_text(tag_node, source_bytes)
|
|
1643
|
+
# Track script/link imports
|
|
1644
|
+
if tag in ("script", "link"):
|
|
1645
|
+
_extract_html_resource(node, source_bytes, imports)
|
|
1646
|
+
# Track elements with IDs as named symbols
|
|
1647
|
+
_extract_html_id(node, source_bytes, tag, classes)
|
|
1648
|
+
|
|
1649
|
+
for child in node.children:
|
|
1650
|
+
_walk_html(child)
|
|
1651
|
+
|
|
1652
|
+
_walk_html(root)
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def _extract_html_resource(node, source_bytes: bytes, imports: list[dict]) -> None:
|
|
1656
|
+
"""Extract src/href from script/link tags."""
|
|
1657
|
+
for child in node.children:
|
|
1658
|
+
if child.type in ("start_tag", "self_closing_tag"):
|
|
1659
|
+
for attr in child.children:
|
|
1660
|
+
if attr.type == "attribute":
|
|
1661
|
+
name_node = attr.child_by_field_name("name")
|
|
1662
|
+
val_node = attr.child_by_field_name("value")
|
|
1663
|
+
if name_node and val_node:
|
|
1664
|
+
attr_name = _node_text(name_node, source_bytes)
|
|
1665
|
+
if attr_name in ("src", "href"):
|
|
1666
|
+
val = _node_text(val_node, source_bytes).strip('"\'')
|
|
1667
|
+
if val:
|
|
1668
|
+
imports.append({
|
|
1669
|
+
"module": val,
|
|
1670
|
+
"is_from": False,
|
|
1671
|
+
"start_line": node.start_point[0] + 1,
|
|
1672
|
+
})
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def _extract_html_id(
|
|
1676
|
+
node, source_bytes: bytes, tag: str, classes: list[dict],
|
|
1677
|
+
) -> None:
|
|
1678
|
+
"""Extract elements with id attributes as named symbols."""
|
|
1679
|
+
for child in node.children:
|
|
1680
|
+
if child.type in ("start_tag", "self_closing_tag"):
|
|
1681
|
+
for attr in child.children:
|
|
1682
|
+
if attr.type == "attribute":
|
|
1683
|
+
name_node = attr.child_by_field_name("name")
|
|
1684
|
+
val_node = attr.child_by_field_name("value")
|
|
1685
|
+
if name_node and val_node and _node_text(name_node, source_bytes) == "id":
|
|
1686
|
+
id_val = _node_text(val_node, source_bytes).strip('"\'')
|
|
1687
|
+
if id_val:
|
|
1688
|
+
classes.append({
|
|
1689
|
+
"name": f"{tag}#{id_val}",
|
|
1690
|
+
"bases": [],
|
|
1691
|
+
"methods": [],
|
|
1692
|
+
"decorators": [],
|
|
1693
|
+
"start_line": node.start_point[0] + 1,
|
|
1694
|
+
"end_line": node.end_point[0] + 1,
|
|
1695
|
+
})
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def _extract_css_symbols(root, source_bytes: bytes, classes: list[dict]) -> None:
|
|
1699
|
+
"""Extract CSS selectors as symbols."""
|
|
1700
|
+
for child in root.children:
|
|
1701
|
+
if child.type == "rule_set":
|
|
1702
|
+
# Get the selector
|
|
1703
|
+
for sub in child.children:
|
|
1704
|
+
if sub.type == "selectors":
|
|
1705
|
+
selector = _node_text(sub, source_bytes).strip()
|
|
1706
|
+
if selector:
|
|
1707
|
+
classes.append({
|
|
1708
|
+
"name": selector,
|
|
1709
|
+
"bases": [],
|
|
1710
|
+
"methods": [],
|
|
1711
|
+
"decorators": [],
|
|
1712
|
+
"start_line": child.start_point[0] + 1,
|
|
1713
|
+
"end_line": child.end_point[0] + 1,
|
|
1714
|
+
})
|
|
1715
|
+
break
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
# ---------------------------------------------------------------------------
|
|
1719
|
+
# Generic tree-sitter extractor (any grammar, no config needed)
|
|
1720
|
+
# ---------------------------------------------------------------------------
|
|
1721
|
+
|
|
1722
|
+
# Heuristic node types that commonly represent definitions across languages.
|
|
1723
|
+
# Used by the generic extractor to identify symbols without per-language config.
|
|
1724
|
+
|
|
1725
|
+
# Node types that indicate a "function-like" definition
|
|
1726
|
+
_GENERIC_FUNCTION_TYPES = frozenset({
|
|
1727
|
+
"function_definition", "function_declaration", "function_item",
|
|
1728
|
+
"method_definition", "method_declaration", "method",
|
|
1729
|
+
"local_function_statement", "constructor_declaration",
|
|
1730
|
+
"init_declaration", "arrow_function", "generator_function_declaration",
|
|
1731
|
+
"function", # Haskell
|
|
1732
|
+
"bind", # Haskell
|
|
1733
|
+
"proc_declaration", "template_declaration", # Nim
|
|
1734
|
+
"fun_def", "val_def", "let_binding", # OCaml, F#
|
|
1735
|
+
"defn", "defn-", "defmacro", # Clojure
|
|
1736
|
+
"function_clause", # Erlang
|
|
1737
|
+
"macro_definition", # Clojure, Elixir
|
|
1738
|
+
"subroutine_definition", "subroutine", # Fortran
|
|
1739
|
+
"class_method_definition", "instance_method_definition", # Obj-C
|
|
1740
|
+
"fun_declaration", # Crystal, Dart
|
|
1741
|
+
"abstract_method_signature", # Dart
|
|
1742
|
+
"short_function_definition", # Julia
|
|
1743
|
+
"function_assignment", # R
|
|
1744
|
+
})
|
|
1745
|
+
|
|
1746
|
+
# Node types that indicate a "class-like" definition
|
|
1747
|
+
_GENERIC_CLASS_TYPES = frozenset({
|
|
1748
|
+
"class_definition", "class_declaration",
|
|
1749
|
+
"struct_definition", "struct_declaration", "struct_specifier", "struct_item",
|
|
1750
|
+
"enum_definition", "enum_declaration", "enum_specifier", "enum_item",
|
|
1751
|
+
"interface_declaration", "trait_definition", "trait_item", "protocol_declaration",
|
|
1752
|
+
"type_declaration", "type_definition", "type_alias",
|
|
1753
|
+
"module_definition", "module_declaration", "namespace_definition",
|
|
1754
|
+
"object_declaration", "record_declaration",
|
|
1755
|
+
"data_type", "newtype", "type_synonym", # Haskell
|
|
1756
|
+
"union_declaration", # Zig
|
|
1757
|
+
"type_section", "object_type", # Nim
|
|
1758
|
+
"deftype", "defrecord", "defprotocol", # Clojure
|
|
1759
|
+
"record_definition", "type_spec", # Erlang
|
|
1760
|
+
"category_interface", "category_implementation", # Obj-C
|
|
1761
|
+
"class_interface", "class_implementation", # Obj-C
|
|
1762
|
+
"abstract_type_definition", # Julia
|
|
1763
|
+
"mixin_declaration", # Dart
|
|
1764
|
+
"lib_declaration", # Crystal
|
|
1765
|
+
})
|
|
1766
|
+
|
|
1767
|
+
# Node types that indicate imports
|
|
1768
|
+
_GENERIC_IMPORT_TYPES = frozenset({
|
|
1769
|
+
"import_statement", "import_declaration", "import_from_statement",
|
|
1770
|
+
"use_declaration", "preproc_include", "using_directive",
|
|
1771
|
+
"namespace_use_declaration", "import_header",
|
|
1772
|
+
"import", # Haskell
|
|
1773
|
+
"using_namespace_declaration", # Zig
|
|
1774
|
+
"open_directive", "open_statement", # OCaml, F#
|
|
1775
|
+
"require_expression", "require", # Clojure, Crystal
|
|
1776
|
+
"include_statement", "include_directive", # Nim, C
|
|
1777
|
+
"import_attribute", "attribute", # Erlang
|
|
1778
|
+
"import_header", # Obj-C
|
|
1779
|
+
"using_statement", # Julia
|
|
1780
|
+
"library_import", # R
|
|
1781
|
+
})
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
class GenericTreeSitterExtractor:
|
|
1785
|
+
"""Parse any tree-sitter grammar using heuristic node-type matching.
|
|
1786
|
+
|
|
1787
|
+
This provides "good enough" symbol extraction for any language with a
|
|
1788
|
+
tree-sitter grammar installed, without needing per-language config.
|
|
1789
|
+
"""
|
|
1790
|
+
|
|
1791
|
+
def __init__(self) -> None:
|
|
1792
|
+
self._grammar_cache: dict[str, object | None] = {}
|
|
1793
|
+
|
|
1794
|
+
def _get_language_obj(self, grammar_module: str, language: str = ""):
|
|
1795
|
+
"""Try to load a tree-sitter Language from a grammar module.
|
|
1796
|
+
|
|
1797
|
+
Attempts in order:
|
|
1798
|
+
1. Individual PyPI grammar package (e.g. tree_sitter_perl)
|
|
1799
|
+
2. tree-sitter-language-pack (170+ bundled languages)
|
|
1800
|
+
"""
|
|
1801
|
+
cache_key = grammar_module or language
|
|
1802
|
+
if cache_key in self._grammar_cache:
|
|
1803
|
+
return self._grammar_cache[cache_key]
|
|
1804
|
+
|
|
1805
|
+
if not _TS_AVAILABLE and not _try_init_tree_sitter():
|
|
1806
|
+
self._grammar_cache[cache_key] = None
|
|
1807
|
+
return None
|
|
1808
|
+
|
|
1809
|
+
# Strategy 1: Individual grammar package
|
|
1810
|
+
if grammar_module:
|
|
1811
|
+
try:
|
|
1812
|
+
import importlib
|
|
1813
|
+
|
|
1814
|
+
import tree_sitter as ts
|
|
1815
|
+
|
|
1816
|
+
mod = importlib.import_module(grammar_module)
|
|
1817
|
+
# Try standard language(), fall back to language_<name>() for
|
|
1818
|
+
# grammars like tree_sitter_php that export language_php()
|
|
1819
|
+
lang_fn = getattr(mod, "language", None)
|
|
1820
|
+
if lang_fn is None:
|
|
1821
|
+
# Search for language_* functions
|
|
1822
|
+
for attr in dir(mod):
|
|
1823
|
+
if attr.startswith("language_") and callable(getattr(mod, attr)):
|
|
1824
|
+
lang_fn = getattr(mod, attr)
|
|
1825
|
+
break
|
|
1826
|
+
if lang_fn is not None:
|
|
1827
|
+
lang = ts.Language(lang_fn())
|
|
1828
|
+
self._grammar_cache[cache_key] = lang
|
|
1829
|
+
return lang
|
|
1830
|
+
except (ImportError, AttributeError, Exception) as e:
|
|
1831
|
+
logger.debug("Generic extractor: grammar %s not available: %s", grammar_module, e)
|
|
1832
|
+
|
|
1833
|
+
# Strategy 2: tree-sitter-language-pack fallback
|
|
1834
|
+
if language:
|
|
1835
|
+
try:
|
|
1836
|
+
from tree_sitter_language_pack import get_language
|
|
1837
|
+
lang_obj = get_language(language)
|
|
1838
|
+
self._grammar_cache[cache_key] = lang_obj
|
|
1839
|
+
logger.debug("Loaded %s from tree-sitter-language-pack", language)
|
|
1840
|
+
return lang_obj
|
|
1841
|
+
except (ImportError, KeyError, Exception) as e:
|
|
1842
|
+
logger.debug("language-pack fallback for %s not available: %s", language, e)
|
|
1843
|
+
|
|
1844
|
+
self._grammar_cache[cache_key] = None
|
|
1845
|
+
return None
|
|
1846
|
+
|
|
1847
|
+
def parse(
|
|
1848
|
+
self, file_path: str, content: str, grammar_module: str, language: str = "",
|
|
1849
|
+
) -> dict | None:
|
|
1850
|
+
"""Parse a file using generic heuristics.
|
|
1851
|
+
|
|
1852
|
+
Args:
|
|
1853
|
+
file_path: Path for error messages.
|
|
1854
|
+
content: Source code content.
|
|
1855
|
+
grammar_module: Python module name for the grammar (e.g. "tree_sitter_dart").
|
|
1856
|
+
language: Optional language label.
|
|
1857
|
+
|
|
1858
|
+
Returns:
|
|
1859
|
+
Dict matching ``ts_parse_file`` output format, or None on failure.
|
|
1860
|
+
"""
|
|
1861
|
+
lang_obj = self._get_language_obj(grammar_module, language=language)
|
|
1862
|
+
if lang_obj is None:
|
|
1863
|
+
return None
|
|
1864
|
+
|
|
1865
|
+
try:
|
|
1866
|
+
import tree_sitter as ts
|
|
1867
|
+
parser = ts.Parser(lang_obj)
|
|
1868
|
+
except Exception:
|
|
1869
|
+
return None
|
|
1870
|
+
|
|
1871
|
+
source_bytes = content.encode("utf-8")
|
|
1872
|
+
try:
|
|
1873
|
+
tree = parser.parse(source_bytes)
|
|
1874
|
+
except Exception as e:
|
|
1875
|
+
logger.debug("Generic parse error for %s: %s", file_path, e)
|
|
1876
|
+
return None
|
|
1877
|
+
|
|
1878
|
+
functions: list[dict] = []
|
|
1879
|
+
classes: list[dict] = []
|
|
1880
|
+
imports: list[dict] = []
|
|
1881
|
+
top_level_vars: list[str] = []
|
|
1882
|
+
|
|
1883
|
+
def _walk(node, depth: int = 0) -> None:
|
|
1884
|
+
ntype = node.type
|
|
1885
|
+
|
|
1886
|
+
if ntype in _GENERIC_FUNCTION_TYPES:
|
|
1887
|
+
name = _find_name(node, source_bytes)
|
|
1888
|
+
if name:
|
|
1889
|
+
functions.append({
|
|
1890
|
+
"name": name,
|
|
1891
|
+
"parameters": _find_parameters(node, source_bytes),
|
|
1892
|
+
"return_type": _find_return_type(node, source_bytes),
|
|
1893
|
+
"start_line": node.start_point[0] + 1,
|
|
1894
|
+
"end_line": node.end_point[0] + 1,
|
|
1895
|
+
"is_async": _is_async(node, source_bytes),
|
|
1896
|
+
"decorators": [],
|
|
1897
|
+
"visibility": "public",
|
|
1898
|
+
"parent_class": "",
|
|
1899
|
+
})
|
|
1900
|
+
return
|
|
1901
|
+
|
|
1902
|
+
if ntype in _GENERIC_CLASS_TYPES:
|
|
1903
|
+
name = _find_name(node, source_bytes)
|
|
1904
|
+
if name:
|
|
1905
|
+
classes.append({
|
|
1906
|
+
"name": name,
|
|
1907
|
+
"bases": [],
|
|
1908
|
+
"methods": [],
|
|
1909
|
+
"decorators": [],
|
|
1910
|
+
"start_line": node.start_point[0] + 1,
|
|
1911
|
+
"end_line": node.end_point[0] + 1,
|
|
1912
|
+
})
|
|
1913
|
+
# Don't return — recurse into children for methods
|
|
1914
|
+
|
|
1915
|
+
if ntype in _GENERIC_IMPORT_TYPES:
|
|
1916
|
+
text = _node_text(node, source_bytes).strip()
|
|
1917
|
+
if text:
|
|
1918
|
+
imports.append({
|
|
1919
|
+
"module": text,
|
|
1920
|
+
"is_from": False,
|
|
1921
|
+
"start_line": node.start_point[0] + 1,
|
|
1922
|
+
})
|
|
1923
|
+
return
|
|
1924
|
+
|
|
1925
|
+
# Only recurse a few levels deep to avoid noise
|
|
1926
|
+
if depth < 5:
|
|
1927
|
+
for child in node.children:
|
|
1928
|
+
_walk(child, depth + 1)
|
|
1929
|
+
|
|
1930
|
+
for child in tree.root_node.children:
|
|
1931
|
+
_walk(child)
|
|
1932
|
+
|
|
1933
|
+
return {
|
|
1934
|
+
"functions": functions,
|
|
1935
|
+
"classes": classes,
|
|
1936
|
+
"imports": imports,
|
|
1937
|
+
"top_level_vars": top_level_vars,
|
|
1938
|
+
"line_count": content.count("\n") + 1,
|
|
1939
|
+
"language": language or "unknown",
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
|
|
1943
|
+
# Singleton for use by ts_parse_file_generic
|
|
1944
|
+
_generic_extractor = GenericTreeSitterExtractor()
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
# Additional grammar modules for languages beyond the 25 configured ones.
|
|
1948
|
+
# Maps language name → grammar module name. Users can extend this.
|
|
1949
|
+
EXTRA_GRAMMAR_MODULES: dict[str, str] = {
|
|
1950
|
+
"dart": "tree_sitter_dart",
|
|
1951
|
+
"r": "tree_sitter_r",
|
|
1952
|
+
"julia": "tree_sitter_julia",
|
|
1953
|
+
"perl": "tree_sitter_perl",
|
|
1954
|
+
"ocaml": "tree_sitter_ocaml",
|
|
1955
|
+
"clojure": "tree_sitter_clojure",
|
|
1956
|
+
"elm": "tree_sitter_elm",
|
|
1957
|
+
"erlang": "tree_sitter_erlang",
|
|
1958
|
+
"nix": "tree_sitter_nix",
|
|
1959
|
+
"solidity": "tree_sitter_solidity",
|
|
1960
|
+
"protobuf": "tree_sitter_proto",
|
|
1961
|
+
"graphql": "tree_sitter_graphql",
|
|
1962
|
+
"sql": "tree_sitter_sql",
|
|
1963
|
+
"latex": "tree_sitter_latex",
|
|
1964
|
+
"make": "tree_sitter_make",
|
|
1965
|
+
"cmake": "tree_sitter_cmake",
|
|
1966
|
+
"dockerfile": "tree_sitter_dockerfile",
|
|
1967
|
+
"verilog": "tree_sitter_verilog",
|
|
1968
|
+
"vhdl": "tree_sitter_vhdl",
|
|
1969
|
+
"wasm": "tree_sitter_wasm",
|
|
1970
|
+
"awk": "tree_sitter_awk",
|
|
1971
|
+
"fish": "tree_sitter_fish",
|
|
1972
|
+
"powershell": "tree_sitter_powershell",
|
|
1973
|
+
"groovy": "tree_sitter_groovy",
|
|
1974
|
+
"gleam": "tree_sitter_gleam",
|
|
1975
|
+
"odin": "tree_sitter_odin",
|
|
1976
|
+
"v": "tree_sitter_v",
|
|
1977
|
+
"pascal": "tree_sitter_pascal",
|
|
1978
|
+
"fortran": "tree_sitter_fortran",
|
|
1979
|
+
"ada": "tree_sitter_ada",
|
|
1980
|
+
"d": "tree_sitter_d",
|
|
1981
|
+
"objc": "tree_sitter_objc",
|
|
1982
|
+
"nim": "tree_sitter_nim",
|
|
1983
|
+
"fsharp": "tree_sitter_fsharp",
|
|
1984
|
+
"crystal": "tree_sitter_crystal",
|
|
1985
|
+
"haskell": "tree_sitter_haskell",
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
# Extension mapping for extra grammars
|
|
1989
|
+
EXTRA_LANG_EXTENSIONS: dict[str, str] = {
|
|
1990
|
+
".dart": "dart",
|
|
1991
|
+
".r": "r",
|
|
1992
|
+
".R": "r",
|
|
1993
|
+
".jl": "julia",
|
|
1994
|
+
".pl": "perl",
|
|
1995
|
+
".pm": "perl",
|
|
1996
|
+
".ml": "ocaml",
|
|
1997
|
+
".mli": "ocaml",
|
|
1998
|
+
".clj": "clojure",
|
|
1999
|
+
".cljs": "clojure",
|
|
2000
|
+
".cljc": "clojure",
|
|
2001
|
+
".elm": "elm",
|
|
2002
|
+
".erl": "erlang",
|
|
2003
|
+
".hrl": "erlang",
|
|
2004
|
+
".nix": "nix",
|
|
2005
|
+
".sol": "solidity",
|
|
2006
|
+
".proto": "protobuf",
|
|
2007
|
+
".graphql": "graphql",
|
|
2008
|
+
".gql": "graphql",
|
|
2009
|
+
".sql": "sql",
|
|
2010
|
+
".tex": "latex",
|
|
2011
|
+
".mk": "make",
|
|
2012
|
+
".cmake": "cmake",
|
|
2013
|
+
".v": "verilog",
|
|
2014
|
+
".sv": "verilog",
|
|
2015
|
+
".vhd": "vhdl",
|
|
2016
|
+
".vhdl": "vhdl",
|
|
2017
|
+
".wat": "wasm",
|
|
2018
|
+
".wast": "wasm",
|
|
2019
|
+
".awk": "awk",
|
|
2020
|
+
".fish": "fish",
|
|
2021
|
+
".ps1": "powershell",
|
|
2022
|
+
".psm1": "powershell",
|
|
2023
|
+
".groovy": "groovy",
|
|
2024
|
+
".gradle": "groovy",
|
|
2025
|
+
".gleam": "gleam",
|
|
2026
|
+
".odin": "odin",
|
|
2027
|
+
".pas": "pascal",
|
|
2028
|
+
".pp": "pascal",
|
|
2029
|
+
".f90": "fortran",
|
|
2030
|
+
".f95": "fortran",
|
|
2031
|
+
".f03": "fortran",
|
|
2032
|
+
".adb": "ada",
|
|
2033
|
+
".ads": "ada",
|
|
2034
|
+
".d": "d",
|
|
2035
|
+
".nim": "nim",
|
|
2036
|
+
".nimble": "nim",
|
|
2037
|
+
".fs": "fsharp",
|
|
2038
|
+
".fsi": "fsharp",
|
|
2039
|
+
".fsx": "fsharp",
|
|
2040
|
+
".cr": "crystal",
|
|
2041
|
+
".m": "objc",
|
|
2042
|
+
".mm": "objc",
|
|
2043
|
+
".hs": "haskell",
|
|
2044
|
+
".lhs": "haskell",
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def ts_parse_file_generic(
|
|
2049
|
+
file_path: str, content: str | None = None, language: str = "",
|
|
2050
|
+
) -> dict | None:
|
|
2051
|
+
"""Parse a file using the generic tree-sitter extractor.
|
|
2052
|
+
|
|
2053
|
+
This handles languages beyond the 25 configured ones, using heuristic
|
|
2054
|
+
node-type matching. Falls back gracefully when grammars aren't installed.
|
|
2055
|
+
|
|
2056
|
+
Args:
|
|
2057
|
+
file_path: Path to the source file.
|
|
2058
|
+
content: Optional content (read from disk if not provided).
|
|
2059
|
+
language: Language name (auto-detected from extension if empty).
|
|
2060
|
+
|
|
2061
|
+
Returns:
|
|
2062
|
+
Dict matching ``ts_parse_file`` format, or None if grammar not available.
|
|
2063
|
+
"""
|
|
2064
|
+
if not language:
|
|
2065
|
+
ext = Path(file_path).suffix
|
|
2066
|
+
language = EXTRA_LANG_EXTENSIONS.get(ext, "")
|
|
2067
|
+
if not language:
|
|
2068
|
+
return None
|
|
2069
|
+
|
|
2070
|
+
grammar_module = EXTRA_GRAMMAR_MODULES.get(language)
|
|
2071
|
+
if not grammar_module:
|
|
2072
|
+
return None
|
|
2073
|
+
|
|
2074
|
+
if content is None:
|
|
2075
|
+
try:
|
|
2076
|
+
content = Path(file_path).read_text(encoding="utf-8", errors="replace")
|
|
2077
|
+
except OSError:
|
|
2078
|
+
return None
|
|
2079
|
+
|
|
2080
|
+
return _generic_extractor.parse(file_path, content, grammar_module, language)
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
# ---------------------------------------------------------------------------
|
|
2084
|
+
# Universal-ctags integration (fallback for languages without grammars)
|
|
2085
|
+
# ---------------------------------------------------------------------------
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
def ctags_parse_file(file_path: str, content: str | None = None) -> dict | None:
|
|
2089
|
+
"""Parse a file using universal-ctags as a subprocess fallback.
|
|
2090
|
+
|
|
2091
|
+
Provides symbol extraction for 150+ languages when ctags is installed.
|
|
2092
|
+
Returns None if ctags is not available or produces no output.
|
|
2093
|
+
"""
|
|
2094
|
+
import json as json_mod
|
|
2095
|
+
import subprocess
|
|
2096
|
+
|
|
2097
|
+
# Write content to a temp file if provided (ctags needs a file on disk)
|
|
2098
|
+
target_path = file_path
|
|
2099
|
+
if content is not None:
|
|
2100
|
+
import tempfile
|
|
2101
|
+
suffix = Path(file_path).suffix
|
|
2102
|
+
try:
|
|
2103
|
+
with tempfile.NamedTemporaryFile(
|
|
2104
|
+
mode="w", suffix=suffix, delete=False, encoding="utf-8",
|
|
2105
|
+
) as temp_file:
|
|
2106
|
+
temp_file.write(content)
|
|
2107
|
+
temp_file.flush()
|
|
2108
|
+
target_path = temp_file.name
|
|
2109
|
+
except OSError:
|
|
2110
|
+
return None
|
|
2111
|
+
|
|
2112
|
+
try:
|
|
2113
|
+
result = subprocess.run(
|
|
2114
|
+
[
|
|
2115
|
+
"ctags", "--output-format=json", "--fields=+neKS",
|
|
2116
|
+
"--kinds-all=*", "-f", "-", target_path,
|
|
2117
|
+
],
|
|
2118
|
+
capture_output=True, text=True, timeout=10,
|
|
2119
|
+
)
|
|
2120
|
+
if result.returncode != 0:
|
|
2121
|
+
return None
|
|
2122
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
2123
|
+
return None
|
|
2124
|
+
finally:
|
|
2125
|
+
if temp_file:
|
|
2126
|
+
Path(temp_file.name).unlink(missing_ok=True)
|
|
2127
|
+
|
|
2128
|
+
functions: list[dict] = []
|
|
2129
|
+
classes: list[dict] = []
|
|
2130
|
+
imports: list[dict] = []
|
|
2131
|
+
top_level_vars: list[str] = []
|
|
2132
|
+
|
|
2133
|
+
for line in result.stdout.strip().splitlines():
|
|
2134
|
+
if not line.strip():
|
|
2135
|
+
continue
|
|
2136
|
+
try:
|
|
2137
|
+
tag = json_mod.loads(line)
|
|
2138
|
+
except json_mod.JSONDecodeError:
|
|
2139
|
+
continue
|
|
2140
|
+
|
|
2141
|
+
name = tag.get("name", "")
|
|
2142
|
+
kind = tag.get("kind", "")
|
|
2143
|
+
line_no = tag.get("line", 0)
|
|
2144
|
+
end_line = tag.get("end", line_no)
|
|
2145
|
+
|
|
2146
|
+
if not name:
|
|
2147
|
+
continue
|
|
2148
|
+
|
|
2149
|
+
if kind in ("function", "method", "subroutine", "procedure", "def"):
|
|
2150
|
+
functions.append({
|
|
2151
|
+
"name": name,
|
|
2152
|
+
"parameters": [],
|
|
2153
|
+
"return_type": tag.get("typeref", ""),
|
|
2154
|
+
"start_line": line_no,
|
|
2155
|
+
"end_line": end_line,
|
|
2156
|
+
"is_async": False,
|
|
2157
|
+
"decorators": [],
|
|
2158
|
+
"visibility": "public",
|
|
2159
|
+
"parent_class": tag.get("scope", ""),
|
|
2160
|
+
})
|
|
2161
|
+
elif kind in ("class", "struct", "interface", "enum", "trait",
|
|
2162
|
+
"module", "namespace", "type", "union"):
|
|
2163
|
+
classes.append({
|
|
2164
|
+
"name": name,
|
|
2165
|
+
"bases": [],
|
|
2166
|
+
"methods": [],
|
|
2167
|
+
"decorators": [],
|
|
2168
|
+
"start_line": line_no,
|
|
2169
|
+
"end_line": end_line,
|
|
2170
|
+
})
|
|
2171
|
+
elif kind in ("import", "include", "using"):
|
|
2172
|
+
imports.append({
|
|
2173
|
+
"module": name,
|
|
2174
|
+
"is_from": False,
|
|
2175
|
+
"start_line": line_no,
|
|
2176
|
+
})
|
|
2177
|
+
elif kind in ("variable", "constant", "define"):
|
|
2178
|
+
top_level_vars.append(name)
|
|
2179
|
+
|
|
2180
|
+
if not functions and not classes and not imports and not top_level_vars:
|
|
2181
|
+
return None
|
|
2182
|
+
|
|
2183
|
+
# Read line count
|
|
2184
|
+
if content is not None:
|
|
2185
|
+
line_count = content.count("\n") + 1
|
|
2186
|
+
else:
|
|
2187
|
+
try:
|
|
2188
|
+
line_count = Path(file_path).read_text(encoding="utf-8", errors="replace").count("\n") + 1
|
|
2189
|
+
except OSError:
|
|
2190
|
+
line_count = 0
|
|
2191
|
+
|
|
2192
|
+
return {
|
|
2193
|
+
"functions": functions,
|
|
2194
|
+
"classes": classes,
|
|
2195
|
+
"imports": imports,
|
|
2196
|
+
"top_level_vars": top_level_vars,
|
|
2197
|
+
"line_count": line_count,
|
|
2198
|
+
"language": "unknown",
|
|
2199
|
+
}
|