patchi 0.7.5__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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1955 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Symbol Graph — function/class/route-level dependency graph.
|
|
3
|
+
|
|
4
|
+
Parallel to ImportGraph (file-level). Uses tree-sitter for accurate
|
|
5
|
+
AST-based symbol extraction. Backed by SQLite for incremental updates.
|
|
6
|
+
|
|
7
|
+
SymbolNode types:
|
|
8
|
+
- function, async_function, method, async_method
|
|
9
|
+
- class
|
|
10
|
+
- route (web route handler)
|
|
11
|
+
- variable (module-level exported)
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
import sqlite3
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from enum import StrEnum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from patchi.core.brain.languages import Lang, detect_language, get_parser
|
|
27
|
+
from patchi.core.brain.scanner import FileInfo
|
|
28
|
+
|
|
29
|
+
# ── Constants ───────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
SYMBOL_GRAPH_DB = "symbol_graph.db"
|
|
32
|
+
|
|
33
|
+
# Serializes ensure_built()'s check-then-build so concurrent callers cannot
|
|
34
|
+
# race full rebuilds (DELETE + INSERT) on the same SQLite file.
|
|
35
|
+
_BUILD_LOCK = threading.Lock()
|
|
36
|
+
|
|
37
|
+
SCHEMA_SQL = """
|
|
38
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
39
|
+
key TEXT PRIMARY KEY,
|
|
40
|
+
value TEXT
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
44
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
45
|
+
name TEXT NOT NULL,
|
|
46
|
+
kind TEXT NOT NULL,
|
|
47
|
+
file TEXT NOT NULL,
|
|
48
|
+
line INTEGER NOT NULL,
|
|
49
|
+
end_line INTEGER NOT NULL,
|
|
50
|
+
parent_id INTEGER,
|
|
51
|
+
docstring TEXT,
|
|
52
|
+
language TEXT NOT NULL,
|
|
53
|
+
hash TEXT NOT NULL,
|
|
54
|
+
decorators TEXT DEFAULT '',
|
|
55
|
+
params TEXT DEFAULT '',
|
|
56
|
+
is_exported INTEGER DEFAULT 0,
|
|
57
|
+
UNIQUE(name, file, line)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
61
|
+
source_id INTEGER NOT NULL,
|
|
62
|
+
target_id INTEGER NOT NULL,
|
|
63
|
+
kind TEXT NOT NULL DEFAULT 'calls',
|
|
64
|
+
PRIMARY KEY (source_id, target_id, kind),
|
|
65
|
+
FOREIGN KEY (source_id) REFERENCES symbols(id),
|
|
66
|
+
FOREIGN KEY (target_id) REFERENCES symbols(id)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
_RUST_ROUTE_ATTR_RE = re.compile(
|
|
76
|
+
r"#\[\s*(get|post|put|delete|patch|head|options|connect|trace)\s*\(\s*\"([^\"]+)\"\s*\)",
|
|
77
|
+
re.IGNORECASE,
|
|
78
|
+
)
|
|
79
|
+
_SVELTE_SCRIPT_RE = re.compile(r"<script[^>]*>", re.IGNORECASE)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Symbol kind enum ────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_log = logging.getLogger("patchi.brain.symbol_graph")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SymbolKind(StrEnum):
|
|
89
|
+
FUNCTION = "function"
|
|
90
|
+
ASYNC_FUNCTION = "async_function"
|
|
91
|
+
METHOD = "method"
|
|
92
|
+
ASYNC_METHOD = "async_method"
|
|
93
|
+
CLASS = "class"
|
|
94
|
+
ROUTE = "route"
|
|
95
|
+
VARIABLE = "variable"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── Data types ──────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class SymbolNode:
|
|
103
|
+
id: int = 0
|
|
104
|
+
name: str = ""
|
|
105
|
+
kind: str = ""
|
|
106
|
+
file: str = ""
|
|
107
|
+
line: int = 0
|
|
108
|
+
end_line: int = 0
|
|
109
|
+
parent_id: int | None = None
|
|
110
|
+
docstring: str = ""
|
|
111
|
+
language: str = ""
|
|
112
|
+
hash: str = ""
|
|
113
|
+
decorators: list[str] = field(default_factory=list)
|
|
114
|
+
params: list[str] = field(default_factory=list)
|
|
115
|
+
is_exported: bool = False
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict:
|
|
118
|
+
return {
|
|
119
|
+
"id": self.id,
|
|
120
|
+
"name": self.name,
|
|
121
|
+
"kind": self.kind,
|
|
122
|
+
"file": self.file,
|
|
123
|
+
"line": self.line,
|
|
124
|
+
"end_line": self.end_line,
|
|
125
|
+
"parent_id": self.parent_id,
|
|
126
|
+
"language": self.language,
|
|
127
|
+
"decorators": self.decorators,
|
|
128
|
+
"params": self.params,
|
|
129
|
+
"is_exported": self.is_exported,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def qualified_name(self) -> str:
|
|
134
|
+
"""Dotted module path + symbol name, e.g. 'src.main.handler'."""
|
|
135
|
+
if self.file and self.file != ".":
|
|
136
|
+
mod = self.file[:-3] if self.file.endswith(".py") else self.file
|
|
137
|
+
mod = mod.replace("/", ".").replace("\\", ".")
|
|
138
|
+
return f"{mod}.{self.name}"
|
|
139
|
+
return self.name
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class GraphDiff:
|
|
144
|
+
added: list[SymbolNode] = field(default_factory=list)
|
|
145
|
+
removed: list[SymbolNode] = field(default_factory=list)
|
|
146
|
+
modified: list[SymbolNode] = field(default_factory=list)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def has_changes(self) -> bool:
|
|
150
|
+
return bool(self.added or self.removed or self.modified)
|
|
151
|
+
|
|
152
|
+
def summary(self) -> str:
|
|
153
|
+
parts = []
|
|
154
|
+
if self.added:
|
|
155
|
+
parts.append(f"+{len(self.added)} added")
|
|
156
|
+
if self.removed:
|
|
157
|
+
parts.append(f"-{len(self.removed)} removed")
|
|
158
|
+
if self.modified:
|
|
159
|
+
parts.append(f"~{len(self.modified)} modified")
|
|
160
|
+
return ", ".join(parts) if parts else "no changes"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── SymbolGraph ─────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class SymbolGraph:
|
|
167
|
+
"""Symbol-level dependency graph backed by SQLite."""
|
|
168
|
+
|
|
169
|
+
def __init__(self, root: Path):
|
|
170
|
+
self.root = root
|
|
171
|
+
self.db_path = root / ".patchi" / SYMBOL_GRAPH_DB
|
|
172
|
+
self._conn: sqlite3.Connection | None = None
|
|
173
|
+
self._init_db()
|
|
174
|
+
|
|
175
|
+
def _init_db(self) -> None:
|
|
176
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
conn = self._get_conn()
|
|
178
|
+
conn.executescript(SCHEMA_SQL)
|
|
179
|
+
|
|
180
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
181
|
+
if self._conn is None:
|
|
182
|
+
self._conn = sqlite3.connect(str(self.db_path))
|
|
183
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
184
|
+
self._conn.execute("PRAGMA synchronous=NORMAL")
|
|
185
|
+
return self._conn
|
|
186
|
+
|
|
187
|
+
def close(self) -> None:
|
|
188
|
+
if self._conn is not None:
|
|
189
|
+
self._conn.close()
|
|
190
|
+
self._conn = None
|
|
191
|
+
|
|
192
|
+
# ── Public API ──────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
def build_from_files(self, files: list[FileInfo]) -> int:
|
|
195
|
+
"""Full rebuild from a list of FileInfo objects. Returns symbol count."""
|
|
196
|
+
conn = self._get_conn()
|
|
197
|
+
conn.execute("DELETE FROM edges")
|
|
198
|
+
conn.execute("DELETE FROM symbols")
|
|
199
|
+
conn.execute("DELETE FROM meta")
|
|
200
|
+
|
|
201
|
+
count = 0
|
|
202
|
+
for fi in files:
|
|
203
|
+
symbols = self._extract_symbols(fi)
|
|
204
|
+
for sym in symbols:
|
|
205
|
+
self._insert_symbol(conn, sym, fi.path)
|
|
206
|
+
count += 1
|
|
207
|
+
|
|
208
|
+
self._build_edges(conn, files)
|
|
209
|
+
conn.execute(
|
|
210
|
+
"INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)",
|
|
211
|
+
("last_build", str(time.time())),
|
|
212
|
+
)
|
|
213
|
+
conn.commit()
|
|
214
|
+
return count
|
|
215
|
+
|
|
216
|
+
def build_from_root(self) -> int:
|
|
217
|
+
"""Build by scanning root directory directly."""
|
|
218
|
+
from patchi.core.brain.scanner import FileScanner
|
|
219
|
+
|
|
220
|
+
scanner = FileScanner(self.root)
|
|
221
|
+
files = scanner.scan()
|
|
222
|
+
return self.build_from_files(files)
|
|
223
|
+
|
|
224
|
+
def patch(self, changed_files: list[Path]) -> GraphDiff:
|
|
225
|
+
"""Incremental update: re-parse only changed files. Returns diff."""
|
|
226
|
+
diff = GraphDiff()
|
|
227
|
+
conn = self._get_conn()
|
|
228
|
+
|
|
229
|
+
for file_path in changed_files:
|
|
230
|
+
try:
|
|
231
|
+
rel = file_path.relative_to(self.root).as_posix()
|
|
232
|
+
except ValueError:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
lang = detect_language(file_path)
|
|
236
|
+
if lang == Lang.UNKNOWN:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
old_symbols = self._get_symbols_in_file(conn, rel)
|
|
240
|
+
for sym in old_symbols:
|
|
241
|
+
diff.removed.append(sym)
|
|
242
|
+
conn.execute("DELETE FROM symbols WHERE file = ?", (rel,))
|
|
243
|
+
|
|
244
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
245
|
+
fi = FileInfo(
|
|
246
|
+
path=rel,
|
|
247
|
+
language=lang,
|
|
248
|
+
size_bytes=len(content.encode("utf-8")),
|
|
249
|
+
lines=content.count("\n") + 1,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
new_symbols = self._extract_symbols(fi)
|
|
253
|
+
for sym in new_symbols:
|
|
254
|
+
self._insert_symbol(conn, sym, rel)
|
|
255
|
+
old = next(
|
|
256
|
+
(s for s in old_symbols if s.name == sym.name and s.line == sym.line), None
|
|
257
|
+
)
|
|
258
|
+
if old and old.hash != sym.hash:
|
|
259
|
+
diff.modified.append(sym)
|
|
260
|
+
elif not old:
|
|
261
|
+
diff.added.append(sym)
|
|
262
|
+
|
|
263
|
+
for sym in list(diff.removed):
|
|
264
|
+
still_exists = any(s.name == sym.name and s.line == sym.line for s in new_symbols)
|
|
265
|
+
if still_exists:
|
|
266
|
+
diff.removed.remove(sym)
|
|
267
|
+
|
|
268
|
+
conn.commit()
|
|
269
|
+
return diff
|
|
270
|
+
|
|
271
|
+
def get_symbol(
|
|
272
|
+
self, name: str, file: str | None = None, line: int | None = None
|
|
273
|
+
) -> SymbolNode | None:
|
|
274
|
+
"""Look up a symbol by name (and optionally file+line)."""
|
|
275
|
+
conn = self._get_conn()
|
|
276
|
+
if file and line:
|
|
277
|
+
cur = conn.execute(
|
|
278
|
+
"SELECT * FROM symbols WHERE name = ? AND file = ? AND line = ?",
|
|
279
|
+
(name, file, line),
|
|
280
|
+
)
|
|
281
|
+
elif file:
|
|
282
|
+
cur = conn.execute("SELECT * FROM symbols WHERE name = ? AND file = ?", (name, file))
|
|
283
|
+
else:
|
|
284
|
+
cur = conn.execute("SELECT * FROM symbols WHERE name = ? LIMIT 1", (name,))
|
|
285
|
+
row = cur.fetchone()
|
|
286
|
+
return self._row_to_symbol(row) if row else None
|
|
287
|
+
|
|
288
|
+
def get_symbols_in_file(self, file: str) -> list[SymbolNode]:
|
|
289
|
+
"""All symbols defined in a file."""
|
|
290
|
+
return self._get_symbols_in_file(self._get_conn(), file)
|
|
291
|
+
|
|
292
|
+
def get_symbols_by_kind(self, kind: SymbolKind | str) -> list[SymbolNode]:
|
|
293
|
+
"""All symbols of a given kind."""
|
|
294
|
+
conn = self._get_conn()
|
|
295
|
+
cur = conn.execute(
|
|
296
|
+
"SELECT * FROM symbols WHERE kind = ? ORDER BY file, line",
|
|
297
|
+
(kind.value if isinstance(kind, SymbolKind) else kind,),
|
|
298
|
+
)
|
|
299
|
+
return [self._row_to_symbol(r) for r in cur.fetchall()]
|
|
300
|
+
|
|
301
|
+
def get_all_symbols(self) -> list[SymbolNode]:
|
|
302
|
+
"""Every symbol in the graph, ordered by file and line."""
|
|
303
|
+
conn = self._get_conn()
|
|
304
|
+
cur = conn.execute("SELECT * FROM symbols ORDER BY file, line")
|
|
305
|
+
return [self._row_to_symbol(r) for r in cur.fetchall()]
|
|
306
|
+
|
|
307
|
+
def ensure_built(self) -> int:
|
|
308
|
+
"""Build the graph from root if it is empty. Returns symbol count.
|
|
309
|
+
|
|
310
|
+
Idempotent — a populated DB is left untouched, so this is safe to call
|
|
311
|
+
before every query site (e.g. governor neighborhood builders) without
|
|
312
|
+
re-scanning the whole project on each invocation.
|
|
313
|
+
|
|
314
|
+
Thread-safe: a module-level lock serializes the check-then-build, so
|
|
315
|
+
two concurrent governor paths can never both see count()==0 and race
|
|
316
|
+
DELETE/INSERT on the same SQLite file.
|
|
317
|
+
"""
|
|
318
|
+
count = self.count()
|
|
319
|
+
if count > 0:
|
|
320
|
+
return count
|
|
321
|
+
with _BUILD_LOCK:
|
|
322
|
+
# Re-check inside the lock — another caller may have built it.
|
|
323
|
+
count = self.count()
|
|
324
|
+
if count > 0:
|
|
325
|
+
return count
|
|
326
|
+
return self.build_from_root()
|
|
327
|
+
|
|
328
|
+
def get_dependents(self, symbol_name: str, file: str | None = None) -> list[SymbolNode]:
|
|
329
|
+
"""Symbols that reference (depend on) the given symbol."""
|
|
330
|
+
conn = self._get_conn()
|
|
331
|
+
if file:
|
|
332
|
+
cur = conn.execute(
|
|
333
|
+
"""SELECT s.* FROM symbols s
|
|
334
|
+
JOIN edges e ON s.id = e.source_id
|
|
335
|
+
JOIN symbols t ON t.id = e.target_id
|
|
336
|
+
WHERE t.name = ? AND t.file = ?""",
|
|
337
|
+
(symbol_name, file),
|
|
338
|
+
)
|
|
339
|
+
else:
|
|
340
|
+
cur = conn.execute(
|
|
341
|
+
"""SELECT s.* FROM symbols s
|
|
342
|
+
JOIN edges e ON s.id = e.source_id
|
|
343
|
+
JOIN symbols t ON t.id = e.target_id
|
|
344
|
+
WHERE t.name = ?""",
|
|
345
|
+
(symbol_name,),
|
|
346
|
+
)
|
|
347
|
+
return [self._row_to_symbol(r) for r in cur.fetchall()]
|
|
348
|
+
|
|
349
|
+
def get_dependencies(self, symbol_id: int) -> list[SymbolNode]:
|
|
350
|
+
"""Symbols that the given symbol references."""
|
|
351
|
+
conn = self._get_conn()
|
|
352
|
+
cur = conn.execute(
|
|
353
|
+
"""SELECT s.* FROM symbols s
|
|
354
|
+
JOIN edges e ON s.id = e.target_id
|
|
355
|
+
WHERE e.source_id = ?""",
|
|
356
|
+
(symbol_id,),
|
|
357
|
+
)
|
|
358
|
+
return [self._row_to_symbol(r) for r in cur.fetchall()]
|
|
359
|
+
|
|
360
|
+
def get_transitive_dependents(
|
|
361
|
+
self, symbol_name: str, file: str, max_depth: int = 5
|
|
362
|
+
) -> list[SymbolNode]:
|
|
363
|
+
"""All symbols that transitively depend on the given symbol."""
|
|
364
|
+
visited: set[int] = set()
|
|
365
|
+
result: list[SymbolNode] = []
|
|
366
|
+
queue: list[tuple[int, int]] = []
|
|
367
|
+
symbol = self.get_symbol(symbol_name, file)
|
|
368
|
+
if not symbol:
|
|
369
|
+
return []
|
|
370
|
+
queue.extend((d.id, 1) for d in self.get_dependents(symbol_name, file))
|
|
371
|
+
|
|
372
|
+
while queue:
|
|
373
|
+
sid, depth = queue.pop(0)
|
|
374
|
+
if sid in visited or depth > max_depth:
|
|
375
|
+
continue
|
|
376
|
+
visited.add(sid)
|
|
377
|
+
s = self._get_symbol_by_id(sid)
|
|
378
|
+
if s:
|
|
379
|
+
result.append(s)
|
|
380
|
+
queue.extend((d.id, depth + 1) for d in self.get_dependents(s.name, s.file))
|
|
381
|
+
|
|
382
|
+
return result
|
|
383
|
+
|
|
384
|
+
def get_route_endpoints(self) -> list[SymbolNode]:
|
|
385
|
+
"""All route handler symbols."""
|
|
386
|
+
return self.get_symbols_by_kind(SymbolKind.ROUTE)
|
|
387
|
+
|
|
388
|
+
def count(self) -> int:
|
|
389
|
+
conn = self._get_conn()
|
|
390
|
+
cur = conn.execute("SELECT COUNT(*) FROM symbols")
|
|
391
|
+
return cur.fetchone()[0]
|
|
392
|
+
|
|
393
|
+
def export_json(self) -> dict:
|
|
394
|
+
"""Export entire graph as JSON (for web UI / CLI display)."""
|
|
395
|
+
conn = self._get_conn()
|
|
396
|
+
symbols = [
|
|
397
|
+
self._row_to_symbol(r).to_dict()
|
|
398
|
+
for r in conn.execute("SELECT * FROM symbols ORDER BY file, line").fetchall()
|
|
399
|
+
]
|
|
400
|
+
edges = [
|
|
401
|
+
{"source": r[0], "target": r[1], "kind": r[2]}
|
|
402
|
+
for r in conn.execute("SELECT * FROM edges").fetchall()
|
|
403
|
+
]
|
|
404
|
+
meta = dict(conn.execute("SELECT key, value FROM meta").fetchall())
|
|
405
|
+
return {"symbols": symbols, "edges": edges, "meta": meta, "count": len(symbols)}
|
|
406
|
+
|
|
407
|
+
# ── Internal: Extraction ───────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
def _extract_symbols(self, fi: FileInfo) -> list[SymbolNode]:
|
|
410
|
+
"""Extract SymbolNodes from a single file using tree-sitter or regex."""
|
|
411
|
+
lang = fi.language
|
|
412
|
+
abs_path = self.root / fi.path
|
|
413
|
+
if not abs_path.exists():
|
|
414
|
+
return []
|
|
415
|
+
|
|
416
|
+
# Read as bytes for tree-sitter (byte offsets), decode to str for ast fallback
|
|
417
|
+
raw_bytes = abs_path.read_bytes()
|
|
418
|
+
content_str = raw_bytes.decode("utf-8", errors="replace")
|
|
419
|
+
|
|
420
|
+
if lang == Lang.PYTHON:
|
|
421
|
+
return self._extract_python(raw_bytes, content_str, fi.path)
|
|
422
|
+
elif lang in (Lang.JAVASCRIPT, Lang.TYPESCRIPT):
|
|
423
|
+
return self._extract_js_ts(raw_bytes, content_str, fi.path, lang)
|
|
424
|
+
elif lang == Lang.RUST:
|
|
425
|
+
return self._extract_rust(raw_bytes, content_str, fi.path)
|
|
426
|
+
elif lang == Lang.SVELTE:
|
|
427
|
+
return self._extract_svelte(raw_bytes, content_str, fi.path)
|
|
428
|
+
elif lang == Lang.JAVA:
|
|
429
|
+
return self._extract_java(raw_bytes, content_str, fi.path)
|
|
430
|
+
elif lang == Lang.GO:
|
|
431
|
+
return self._extract_go(raw_bytes, content_str, fi.path)
|
|
432
|
+
elif lang in (Lang.C, Lang.CPP):
|
|
433
|
+
return self._extract_c_cpp(raw_bytes, content_str, fi.path, lang)
|
|
434
|
+
elif lang == Lang.SWIFT:
|
|
435
|
+
return self._extract_swift(raw_bytes, content_str, fi.path)
|
|
436
|
+
elif lang == Lang.RUBY:
|
|
437
|
+
return self._extract_ruby(raw_bytes, content_str, fi.path)
|
|
438
|
+
elif lang == Lang.PHP:
|
|
439
|
+
return self._extract_generic_ts(
|
|
440
|
+
raw_bytes,
|
|
441
|
+
content_str,
|
|
442
|
+
fi.path,
|
|
443
|
+
lang,
|
|
444
|
+
function_types={"function_definition"},
|
|
445
|
+
method_types={"method_declaration"},
|
|
446
|
+
class_types={"class_declaration"},
|
|
447
|
+
)
|
|
448
|
+
elif lang == Lang.C_SHARP:
|
|
449
|
+
return self._extract_generic_ts(
|
|
450
|
+
raw_bytes,
|
|
451
|
+
content_str,
|
|
452
|
+
fi.path,
|
|
453
|
+
lang,
|
|
454
|
+
method_types={"method_declaration"},
|
|
455
|
+
class_types={"class_declaration"},
|
|
456
|
+
)
|
|
457
|
+
elif lang == Lang.KOTLIN:
|
|
458
|
+
return self._extract_generic_ts(
|
|
459
|
+
raw_bytes,
|
|
460
|
+
content_str,
|
|
461
|
+
fi.path,
|
|
462
|
+
lang,
|
|
463
|
+
function_types={"function_declaration"},
|
|
464
|
+
method_types={"function_declaration"},
|
|
465
|
+
class_types={"class_declaration"},
|
|
466
|
+
)
|
|
467
|
+
elif lang == Lang.DART:
|
|
468
|
+
return self._extract_generic_ts(
|
|
469
|
+
raw_bytes,
|
|
470
|
+
content_str,
|
|
471
|
+
fi.path,
|
|
472
|
+
lang,
|
|
473
|
+
function_types={"function_declaration"},
|
|
474
|
+
method_types={"method_declaration"},
|
|
475
|
+
class_types={"class_definition"},
|
|
476
|
+
)
|
|
477
|
+
elif lang == Lang.BASH:
|
|
478
|
+
return self._extract_generic_ts(
|
|
479
|
+
raw_bytes,
|
|
480
|
+
content_str,
|
|
481
|
+
fi.path,
|
|
482
|
+
lang,
|
|
483
|
+
function_types={"function_definition"},
|
|
484
|
+
)
|
|
485
|
+
elif lang in (Lang.CSS, Lang.SQL):
|
|
486
|
+
return []
|
|
487
|
+
else:
|
|
488
|
+
return []
|
|
489
|
+
|
|
490
|
+
def _extract_python(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
|
|
491
|
+
"""Python symbols via tree-sitter or ast."""
|
|
492
|
+
symbols: list[SymbolNode] = []
|
|
493
|
+
|
|
494
|
+
parser = get_parser(Lang.PYTHON)
|
|
495
|
+
if parser:
|
|
496
|
+
try:
|
|
497
|
+
tree = parser.parse(raw_bytes)
|
|
498
|
+
self._walk_python(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
499
|
+
if symbols:
|
|
500
|
+
return symbols
|
|
501
|
+
except Exception as e:
|
|
502
|
+
_log.warning("SymbolGraph._extract_python failed: %s", e)
|
|
503
|
+
|
|
504
|
+
return self._extract_python_ast(content_str, file)
|
|
505
|
+
|
|
506
|
+
def _extract_python_ast(self, content: str, file: str) -> list[SymbolNode]:
|
|
507
|
+
"""Fallback Python symbol extraction using stdlib ast."""
|
|
508
|
+
import ast
|
|
509
|
+
|
|
510
|
+
symbols: list[SymbolNode] = []
|
|
511
|
+
try:
|
|
512
|
+
tree = ast.parse(content)
|
|
513
|
+
except SyntaxError:
|
|
514
|
+
return symbols
|
|
515
|
+
|
|
516
|
+
lines = content.split("\n")
|
|
517
|
+
|
|
518
|
+
for node in ast.walk(tree):
|
|
519
|
+
if isinstance(node, ast.FunctionDef):
|
|
520
|
+
end = getattr(node, "end_lineno", node.lineno) or node.lineno
|
|
521
|
+
sym = SymbolNode(
|
|
522
|
+
name=node.name,
|
|
523
|
+
kind=SymbolKind.ASYNC_FUNCTION
|
|
524
|
+
if isinstance(node, ast.AsyncFunctionDef)
|
|
525
|
+
else SymbolKind.FUNCTION,
|
|
526
|
+
file=file,
|
|
527
|
+
line=node.lineno,
|
|
528
|
+
end_line=end,
|
|
529
|
+
docstring=ast.get_docstring(node) or "",
|
|
530
|
+
language="python",
|
|
531
|
+
decorators=[
|
|
532
|
+
d.id if isinstance(d, ast.Name) else ""
|
|
533
|
+
for d in node.decorator_list
|
|
534
|
+
if isinstance(d, ast.Name)
|
|
535
|
+
],
|
|
536
|
+
params=[a.arg for a in node.args.args] if hasattr(node.args, "args") else [],
|
|
537
|
+
is_exported=not node.name.startswith("_") or file == "__init__.py",
|
|
538
|
+
hash=_content_hash(lines[node.lineno - 1] if node.lineno <= len(lines) else ""),
|
|
539
|
+
)
|
|
540
|
+
symbols.append(sym)
|
|
541
|
+
|
|
542
|
+
elif isinstance(node, ast.ClassDef):
|
|
543
|
+
end = getattr(node, "end_lineno", node.lineno) or node.lineno
|
|
544
|
+
sym = SymbolNode(
|
|
545
|
+
name=node.name,
|
|
546
|
+
kind=SymbolKind.CLASS,
|
|
547
|
+
file=file,
|
|
548
|
+
line=node.lineno,
|
|
549
|
+
end_line=end,
|
|
550
|
+
docstring=ast.get_docstring(node) or "",
|
|
551
|
+
language="python",
|
|
552
|
+
is_exported=True,
|
|
553
|
+
hash=_content_hash(lines[node.lineno - 1] if node.lineno <= len(lines) else ""),
|
|
554
|
+
)
|
|
555
|
+
symbols.append(sym)
|
|
556
|
+
|
|
557
|
+
return symbols
|
|
558
|
+
|
|
559
|
+
def _walk_python(
|
|
560
|
+
self,
|
|
561
|
+
node: Any,
|
|
562
|
+
raw_bytes: bytes,
|
|
563
|
+
content_str: str,
|
|
564
|
+
file: str,
|
|
565
|
+
symbols: list[SymbolNode],
|
|
566
|
+
parent_id: int | None,
|
|
567
|
+
) -> None:
|
|
568
|
+
"""Walk Python tree-sitter CST for function/class/route symbols."""
|
|
569
|
+
buf = raw_bytes # byte buffer for tree-sitter byte-offset lookups
|
|
570
|
+
try:
|
|
571
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
572
|
+
except Exception as e:
|
|
573
|
+
_log.warning("SymbolGraph._walk_python failed: %s", e)
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
578
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
579
|
+
except Exception as e:
|
|
580
|
+
_log.warning("SymbolGraph._walk_python failed: %s", e)
|
|
581
|
+
return
|
|
582
|
+
|
|
583
|
+
if node_type in ("function_definition", "async_function_definition"):
|
|
584
|
+
name_node = self._child_by_field(node, "name")
|
|
585
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
586
|
+
decorators = self._extract_decorators_buf(node, buf)
|
|
587
|
+
params = self._extract_params_buf(node, buf)
|
|
588
|
+
docstring = self._extract_docstring_buf(node, buf)
|
|
589
|
+
kind = (
|
|
590
|
+
SymbolKind.ASYNC_FUNCTION
|
|
591
|
+
if node_type == "async_function_definition"
|
|
592
|
+
else SymbolKind.FUNCTION
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
is_route = any(d for d in decorators if "route" in d or "app." in d or "router." in d)
|
|
596
|
+
sym_kind = SymbolKind.ROUTE if is_route else kind
|
|
597
|
+
|
|
598
|
+
sym = SymbolNode(
|
|
599
|
+
name=name,
|
|
600
|
+
kind=sym_kind,
|
|
601
|
+
file=file,
|
|
602
|
+
line=start_line,
|
|
603
|
+
end_line=end_line,
|
|
604
|
+
parent_id=parent_id,
|
|
605
|
+
docstring=docstring,
|
|
606
|
+
language="python",
|
|
607
|
+
decorators=decorators,
|
|
608
|
+
params=params,
|
|
609
|
+
is_exported=not name.startswith("_") or file == "__init__.py",
|
|
610
|
+
hash=_node_hash_buf(node, buf),
|
|
611
|
+
)
|
|
612
|
+
symbols.append(sym)
|
|
613
|
+
|
|
614
|
+
for child in self._children(node):
|
|
615
|
+
self._walk_python(child, buf, content_str, file, symbols, None)
|
|
616
|
+
|
|
617
|
+
elif node_type == "decorated_definition":
|
|
618
|
+
decorators = self._extract_decorators_buf(node, buf)
|
|
619
|
+
for child in self._children(node):
|
|
620
|
+
child_type = getattr(child, "type", "") if hasattr(child, "type") else ""
|
|
621
|
+
if child_type in (
|
|
622
|
+
"function_definition",
|
|
623
|
+
"async_function_definition",
|
|
624
|
+
"class_definition",
|
|
625
|
+
):
|
|
626
|
+
self._walk_python(child, buf, content_str, file, symbols, parent_id)
|
|
627
|
+
if symbols:
|
|
628
|
+
symbols[-1].decorators = decorators
|
|
629
|
+
else:
|
|
630
|
+
self._walk_python(child, buf, content_str, file, symbols, parent_id)
|
|
631
|
+
|
|
632
|
+
elif node_type == "class_definition":
|
|
633
|
+
name_node = self._child_by_field(node, "name")
|
|
634
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
635
|
+
docstring = self._extract_docstring_buf(node, buf)
|
|
636
|
+
sym = SymbolNode(
|
|
637
|
+
name=name,
|
|
638
|
+
kind=SymbolKind.CLASS,
|
|
639
|
+
file=file,
|
|
640
|
+
line=start_line,
|
|
641
|
+
end_line=end_line,
|
|
642
|
+
parent_id=parent_id,
|
|
643
|
+
docstring=docstring,
|
|
644
|
+
language="python",
|
|
645
|
+
is_exported=True,
|
|
646
|
+
hash=_node_hash_buf(node, buf),
|
|
647
|
+
)
|
|
648
|
+
symbols.append(sym)
|
|
649
|
+
|
|
650
|
+
body = self._child_by_field(node, "body")
|
|
651
|
+
if body:
|
|
652
|
+
for child in self._children(body):
|
|
653
|
+
self._walk_python(child, buf, content_str, file, symbols, None)
|
|
654
|
+
|
|
655
|
+
else:
|
|
656
|
+
for child in self._children(node):
|
|
657
|
+
self._walk_python(child, buf, content_str, file, symbols, parent_id)
|
|
658
|
+
|
|
659
|
+
def _extract_js_ts(
|
|
660
|
+
self, raw_bytes: bytes, content_str: str, file: str, lang: Lang
|
|
661
|
+
) -> list[SymbolNode]:
|
|
662
|
+
"""JS/TS symbols via tree-sitter."""
|
|
663
|
+
symbols: list[SymbolNode] = []
|
|
664
|
+
parser = get_parser(lang)
|
|
665
|
+
if not parser:
|
|
666
|
+
return symbols
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
tree = parser.parse(raw_bytes)
|
|
670
|
+
self._walk_js_ts(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
671
|
+
except Exception as e:
|
|
672
|
+
_log.warning("SymbolGraph._extract_js_ts failed: %s", e)
|
|
673
|
+
|
|
674
|
+
return symbols
|
|
675
|
+
|
|
676
|
+
def _walk_js_ts(
|
|
677
|
+
self,
|
|
678
|
+
node: Any,
|
|
679
|
+
raw_bytes: bytes,
|
|
680
|
+
content_str: str,
|
|
681
|
+
file: str,
|
|
682
|
+
symbols: list[SymbolNode],
|
|
683
|
+
parent_id: int | None,
|
|
684
|
+
) -> None:
|
|
685
|
+
buf = raw_bytes
|
|
686
|
+
try:
|
|
687
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
688
|
+
except Exception as e:
|
|
689
|
+
_log.warning("SymbolGraph._walk_js_ts failed: %s", e)
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
694
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
695
|
+
except Exception as e:
|
|
696
|
+
_log.warning("SymbolGraph._walk_js_ts failed: %s", e)
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
func_types = {
|
|
700
|
+
"function_declaration",
|
|
701
|
+
"function_definition",
|
|
702
|
+
"method_definition",
|
|
703
|
+
"arrow_function",
|
|
704
|
+
"generator_function_declaration",
|
|
705
|
+
}
|
|
706
|
+
class_types = {"class_declaration", "class_definition"}
|
|
707
|
+
export_types = {"export_statement", "export_specifier"}
|
|
708
|
+
|
|
709
|
+
if node_type in func_types:
|
|
710
|
+
name_node = self._child_by_field(node, "name")
|
|
711
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
712
|
+
decorators = self._extract_decorators_buf(node, buf)
|
|
713
|
+
params = self._extract_params_buf(node, buf)
|
|
714
|
+
kind = (
|
|
715
|
+
SymbolKind.ASYNC_FUNCTION if node_type == "arrow_function" else SymbolKind.FUNCTION
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
is_route = any(d for d in decorators if "route" in d or "app." in d or "router." in d)
|
|
719
|
+
sym_kind = SymbolKind.ROUTE if is_route else kind
|
|
720
|
+
|
|
721
|
+
sym = SymbolNode(
|
|
722
|
+
name=name,
|
|
723
|
+
kind=sym_kind,
|
|
724
|
+
file=file,
|
|
725
|
+
line=start_line,
|
|
726
|
+
end_line=end_line,
|
|
727
|
+
parent_id=parent_id,
|
|
728
|
+
language="typescript" if file.endswith((".ts", ".tsx")) else "javascript",
|
|
729
|
+
decorators=decorators,
|
|
730
|
+
params=params,
|
|
731
|
+
is_exported=False,
|
|
732
|
+
hash=_node_hash_buf(node, buf),
|
|
733
|
+
)
|
|
734
|
+
symbols.append(sym)
|
|
735
|
+
for child in self._children(node):
|
|
736
|
+
self._walk_js_ts(child, buf, content_str, file, symbols, None)
|
|
737
|
+
|
|
738
|
+
elif node_type in class_types:
|
|
739
|
+
name_node = self._child_by_field(node, "name")
|
|
740
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
741
|
+
sym = SymbolNode(
|
|
742
|
+
name=name,
|
|
743
|
+
kind=SymbolKind.CLASS,
|
|
744
|
+
file=file,
|
|
745
|
+
line=start_line,
|
|
746
|
+
end_line=end_line,
|
|
747
|
+
parent_id=parent_id,
|
|
748
|
+
language="typescript" if file.endswith((".ts", ".tsx")) else "javascript",
|
|
749
|
+
is_exported=False,
|
|
750
|
+
hash=_node_hash_buf(node, buf),
|
|
751
|
+
)
|
|
752
|
+
symbols.append(sym)
|
|
753
|
+
body = self._child_by_field(node, "body")
|
|
754
|
+
if body:
|
|
755
|
+
for child in self._children(body):
|
|
756
|
+
self._walk_js_ts(child, buf, content_str, file, symbols, None)
|
|
757
|
+
|
|
758
|
+
elif node_type in export_types:
|
|
759
|
+
for child in self._children(node):
|
|
760
|
+
self._walk_js_ts(child, buf, content_str, file, symbols, parent_id)
|
|
761
|
+
|
|
762
|
+
else:
|
|
763
|
+
for child in self._children(node):
|
|
764
|
+
self._walk_js_ts(child, buf, content_str, file, symbols, parent_id)
|
|
765
|
+
|
|
766
|
+
def _extract_rust(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
|
|
767
|
+
symbols: list[SymbolNode] = []
|
|
768
|
+
parser = get_parser(Lang.RUST)
|
|
769
|
+
if not parser:
|
|
770
|
+
return symbols
|
|
771
|
+
try:
|
|
772
|
+
tree = parser.parse(raw_bytes)
|
|
773
|
+
self._walk_rust(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
774
|
+
except Exception as e:
|
|
775
|
+
_log.warning("SymbolGraph._extract_rust failed: %s", e)
|
|
776
|
+
return symbols
|
|
777
|
+
|
|
778
|
+
def _walk_rust(
|
|
779
|
+
self,
|
|
780
|
+
node: Any,
|
|
781
|
+
raw_bytes: bytes,
|
|
782
|
+
content_str: str,
|
|
783
|
+
file: str,
|
|
784
|
+
symbols: list[SymbolNode],
|
|
785
|
+
parent_id: int | None,
|
|
786
|
+
) -> None:
|
|
787
|
+
buf = raw_bytes
|
|
788
|
+
try:
|
|
789
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
790
|
+
except Exception as e:
|
|
791
|
+
_log.warning("SymbolGraph._walk_rust failed: %s", e)
|
|
792
|
+
return
|
|
793
|
+
|
|
794
|
+
try:
|
|
795
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
796
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
797
|
+
except Exception as e:
|
|
798
|
+
_log.warning("SymbolGraph._walk_rust failed: %s", e)
|
|
799
|
+
return
|
|
800
|
+
|
|
801
|
+
# Track route attribute macros on adjacent items
|
|
802
|
+
pending_route: tuple[str, str] | None = None
|
|
803
|
+
if node_type == "attribute_item":
|
|
804
|
+
attr_text = self._node_text_buf(node, buf)
|
|
805
|
+
rm = _RUST_ROUTE_ATTR_RE.search(attr_text)
|
|
806
|
+
if rm:
|
|
807
|
+
pending_route = (rm.group(1).upper(), rm.group(2))
|
|
808
|
+
for child in self._children(node):
|
|
809
|
+
self._walk_rust(child, buf, content_str, file, symbols, parent_id)
|
|
810
|
+
return
|
|
811
|
+
|
|
812
|
+
if node_type == "function_item":
|
|
813
|
+
name_node = self._child_by_field(node, "name")
|
|
814
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
815
|
+
params = self._extract_params_buf(node, buf)
|
|
816
|
+
|
|
817
|
+
is_route = pending_route is not None
|
|
818
|
+
kind = SymbolKind.ROUTE if is_route else SymbolKind.FUNCTION
|
|
819
|
+
|
|
820
|
+
sym = SymbolNode(
|
|
821
|
+
name=name,
|
|
822
|
+
kind=kind,
|
|
823
|
+
file=file,
|
|
824
|
+
line=start_line,
|
|
825
|
+
end_line=end_line,
|
|
826
|
+
parent_id=parent_id,
|
|
827
|
+
language="rust",
|
|
828
|
+
params=params,
|
|
829
|
+
hash=_node_hash_buf(node, buf),
|
|
830
|
+
)
|
|
831
|
+
if is_route and pending_route:
|
|
832
|
+
sym.decorators = [f'#[{pending_route[0].lower()}("{pending_route[1]}")]']
|
|
833
|
+
symbols.append(sym)
|
|
834
|
+
|
|
835
|
+
elif node_type == "struct_item":
|
|
836
|
+
name_node = self._child_by_field(node, "name")
|
|
837
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
838
|
+
sym = SymbolNode(
|
|
839
|
+
name=name,
|
|
840
|
+
kind=SymbolKind.CLASS,
|
|
841
|
+
file=file,
|
|
842
|
+
line=start_line,
|
|
843
|
+
end_line=end_line,
|
|
844
|
+
parent_id=parent_id,
|
|
845
|
+
language="rust",
|
|
846
|
+
hash=_node_hash_buf(node, buf),
|
|
847
|
+
)
|
|
848
|
+
symbols.append(sym)
|
|
849
|
+
|
|
850
|
+
elif node_type == "enum_item":
|
|
851
|
+
name_node = self._child_by_field(node, "name")
|
|
852
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
853
|
+
sym = SymbolNode(
|
|
854
|
+
name=name,
|
|
855
|
+
kind=SymbolKind.CLASS,
|
|
856
|
+
file=file,
|
|
857
|
+
line=start_line,
|
|
858
|
+
end_line=end_line,
|
|
859
|
+
parent_id=parent_id,
|
|
860
|
+
language="rust",
|
|
861
|
+
hash=_node_hash_buf(node, buf),
|
|
862
|
+
)
|
|
863
|
+
symbols.append(sym)
|
|
864
|
+
|
|
865
|
+
elif node_type == "impl_item":
|
|
866
|
+
type_node = self._child_by_field(node, "type")
|
|
867
|
+
trait_node = self._child_by_field(node, "trait")
|
|
868
|
+
name = ""
|
|
869
|
+
if trait_node:
|
|
870
|
+
name = (
|
|
871
|
+
f"impl {self._node_text_buf(trait_node, buf)} for {self._node_text_buf(type_node, buf)}"
|
|
872
|
+
if type_node
|
|
873
|
+
else f"impl {self._node_text_buf(trait_node, buf)}"
|
|
874
|
+
)
|
|
875
|
+
elif type_node:
|
|
876
|
+
name = f"impl {self._node_text_buf(type_node, buf)}"
|
|
877
|
+
if name:
|
|
878
|
+
sym = SymbolNode(
|
|
879
|
+
name=name,
|
|
880
|
+
kind=SymbolKind.CLASS,
|
|
881
|
+
file=file,
|
|
882
|
+
line=start_line,
|
|
883
|
+
end_line=end_line,
|
|
884
|
+
parent_id=parent_id,
|
|
885
|
+
language="rust",
|
|
886
|
+
hash=_node_hash_buf(node, buf),
|
|
887
|
+
)
|
|
888
|
+
symbols.append(sym)
|
|
889
|
+
body = self._child_by_field(node, "body")
|
|
890
|
+
if body:
|
|
891
|
+
for child in self._children(body):
|
|
892
|
+
self._walk_rust(child, buf, content_str, file, symbols, None)
|
|
893
|
+
|
|
894
|
+
elif node_type == "trait_item":
|
|
895
|
+
name_node = self._child_by_field(node, "name")
|
|
896
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
897
|
+
sym = SymbolNode(
|
|
898
|
+
name=f"trait {name}",
|
|
899
|
+
kind=SymbolKind.CLASS,
|
|
900
|
+
file=file,
|
|
901
|
+
line=start_line,
|
|
902
|
+
end_line=end_line,
|
|
903
|
+
parent_id=parent_id,
|
|
904
|
+
language="rust",
|
|
905
|
+
hash=_node_hash_buf(node, buf),
|
|
906
|
+
)
|
|
907
|
+
symbols.append(sym)
|
|
908
|
+
|
|
909
|
+
else:
|
|
910
|
+
for child in self._children(node):
|
|
911
|
+
self._walk_rust(child, buf, content_str, file, symbols, parent_id)
|
|
912
|
+
|
|
913
|
+
def _extract_svelte(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
|
|
914
|
+
symbols: list[SymbolNode] = []
|
|
915
|
+
parser = get_parser(Lang.SVELTE)
|
|
916
|
+
if not parser:
|
|
917
|
+
return symbols
|
|
918
|
+
try:
|
|
919
|
+
tree = parser.parse(raw_bytes)
|
|
920
|
+
self._walk_svelte(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
921
|
+
except Exception as e:
|
|
922
|
+
_log.warning("SymbolGraph._extract_svelte failed: %s", e)
|
|
923
|
+
return symbols
|
|
924
|
+
|
|
925
|
+
def _walk_svelte(
|
|
926
|
+
self,
|
|
927
|
+
node: Any,
|
|
928
|
+
raw_bytes: bytes,
|
|
929
|
+
content_str: str,
|
|
930
|
+
file: str,
|
|
931
|
+
symbols: list[SymbolNode],
|
|
932
|
+
parent_id: int | None,
|
|
933
|
+
) -> None:
|
|
934
|
+
buf = raw_bytes
|
|
935
|
+
try:
|
|
936
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
937
|
+
except Exception as e:
|
|
938
|
+
_log.warning("SymbolGraph._walk_svelte failed: %s", e)
|
|
939
|
+
return
|
|
940
|
+
|
|
941
|
+
try:
|
|
942
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
943
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
944
|
+
except Exception as e:
|
|
945
|
+
_log.warning("SymbolGraph._walk_svelte failed: %s", e)
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
# Svelte <script> tag contains JavaScript/TypeScript — reuse JS walker
|
|
949
|
+
if node_type == "script_element":
|
|
950
|
+
raw_tag = self._node_text_buf(node, buf)
|
|
951
|
+
_SVELTE_SCRIPT_RE.match(raw_tag)
|
|
952
|
+
has_ts = (
|
|
953
|
+
"ts" in raw_tag[:80] or 'lang="ts"' in raw_tag[:80] or "lang='ts'" in raw_tag[:80]
|
|
954
|
+
)
|
|
955
|
+
js_lang = Lang.TYPESCRIPT if has_ts else Lang.JAVASCRIPT
|
|
956
|
+
js_parser = get_parser(js_lang)
|
|
957
|
+
if js_parser:
|
|
958
|
+
inner_text = raw_tag
|
|
959
|
+
inner_bytes = inner_text.encode("utf-8")
|
|
960
|
+
try:
|
|
961
|
+
js_tree = js_parser.parse(inner_bytes)
|
|
962
|
+
self._walk_js_ts(
|
|
963
|
+
js_tree.root_node, inner_bytes, inner_text, file, symbols, parent_id
|
|
964
|
+
)
|
|
965
|
+
except Exception as e:
|
|
966
|
+
_log.warning("SymbolGraph._walk_svelte failed: %s", e)
|
|
967
|
+
return
|
|
968
|
+
|
|
969
|
+
# Component tags (capitalized) → custom components referenced
|
|
970
|
+
if node_type == "element":
|
|
971
|
+
tag_text = (
|
|
972
|
+
self._node_text_buf(node, buf).split()[0] if self._node_text_buf(node, buf) else ""
|
|
973
|
+
)
|
|
974
|
+
if tag_text and tag_text[0].isupper():
|
|
975
|
+
sym = SymbolNode(
|
|
976
|
+
name=tag_text,
|
|
977
|
+
kind=SymbolKind.CLASS,
|
|
978
|
+
file=file,
|
|
979
|
+
line=start_line,
|
|
980
|
+
end_line=end_line,
|
|
981
|
+
parent_id=parent_id,
|
|
982
|
+
language="svelte",
|
|
983
|
+
hash=_node_hash_buf(node, buf),
|
|
984
|
+
)
|
|
985
|
+
symbols.append(sym)
|
|
986
|
+
|
|
987
|
+
for child in self._children(node):
|
|
988
|
+
self._walk_svelte(child, buf, content_str, file, symbols, parent_id)
|
|
989
|
+
|
|
990
|
+
# ── Java extractor ─────────────────────────────────────────────────────
|
|
991
|
+
|
|
992
|
+
def _extract_java(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
|
|
993
|
+
symbols: list[SymbolNode] = []
|
|
994
|
+
parser = get_parser(Lang.JAVA)
|
|
995
|
+
if not parser:
|
|
996
|
+
return symbols
|
|
997
|
+
try:
|
|
998
|
+
tree = parser.parse(raw_bytes)
|
|
999
|
+
self._walk_java(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
1000
|
+
except Exception as e:
|
|
1001
|
+
_log.warning("SymbolGraph._extract_java failed: %s", e)
|
|
1002
|
+
return symbols
|
|
1003
|
+
|
|
1004
|
+
def _walk_java(
|
|
1005
|
+
self,
|
|
1006
|
+
node: Any,
|
|
1007
|
+
raw_bytes: bytes,
|
|
1008
|
+
content_str: str,
|
|
1009
|
+
file: str,
|
|
1010
|
+
symbols: list[SymbolNode],
|
|
1011
|
+
parent_id: int | None,
|
|
1012
|
+
) -> None:
|
|
1013
|
+
buf = raw_bytes
|
|
1014
|
+
try:
|
|
1015
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
_log.warning("SymbolGraph._walk_java failed: %s", e)
|
|
1018
|
+
return
|
|
1019
|
+
try:
|
|
1020
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
1021
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
1022
|
+
except Exception as e:
|
|
1023
|
+
_log.warning("SymbolGraph._walk_java failed: %s", e)
|
|
1024
|
+
return
|
|
1025
|
+
|
|
1026
|
+
if node_type == "class_declaration":
|
|
1027
|
+
name_node = self._child_by_field(node, "name")
|
|
1028
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1029
|
+
sym = SymbolNode(
|
|
1030
|
+
name=name,
|
|
1031
|
+
kind=SymbolKind.CLASS,
|
|
1032
|
+
file=file,
|
|
1033
|
+
line=start_line,
|
|
1034
|
+
end_line=end_line,
|
|
1035
|
+
parent_id=parent_id,
|
|
1036
|
+
language="java",
|
|
1037
|
+
hash=_node_hash_buf(node, buf),
|
|
1038
|
+
)
|
|
1039
|
+
symbols.append(sym)
|
|
1040
|
+
body = self._child_by_field(node, "body")
|
|
1041
|
+
if body:
|
|
1042
|
+
for child in self._children(body):
|
|
1043
|
+
self._walk_java(child, buf, content_str, file, symbols, None)
|
|
1044
|
+
|
|
1045
|
+
elif node_type == "interface_declaration":
|
|
1046
|
+
name_node = self._child_by_field(node, "name")
|
|
1047
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1048
|
+
sym = SymbolNode(
|
|
1049
|
+
name=f"interface {name}",
|
|
1050
|
+
kind=SymbolKind.CLASS,
|
|
1051
|
+
file=file,
|
|
1052
|
+
line=start_line,
|
|
1053
|
+
end_line=end_line,
|
|
1054
|
+
parent_id=parent_id,
|
|
1055
|
+
language="java",
|
|
1056
|
+
hash=_node_hash_buf(node, buf),
|
|
1057
|
+
)
|
|
1058
|
+
symbols.append(sym)
|
|
1059
|
+
|
|
1060
|
+
elif node_type == "annotation_type_declaration":
|
|
1061
|
+
name_node = self._child_by_field(node, "name")
|
|
1062
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1063
|
+
sym = SymbolNode(
|
|
1064
|
+
name=f"@interface {name}",
|
|
1065
|
+
kind=SymbolKind.CLASS,
|
|
1066
|
+
file=file,
|
|
1067
|
+
line=start_line,
|
|
1068
|
+
end_line=end_line,
|
|
1069
|
+
parent_id=parent_id,
|
|
1070
|
+
language="java",
|
|
1071
|
+
hash=_node_hash_buf(node, buf),
|
|
1072
|
+
)
|
|
1073
|
+
symbols.append(sym)
|
|
1074
|
+
|
|
1075
|
+
elif node_type == "method_declaration":
|
|
1076
|
+
name_node = self._child_by_field(node, "name")
|
|
1077
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1078
|
+
decorators = self._extract_decorators_buf(node, buf)
|
|
1079
|
+
is_route = any(
|
|
1080
|
+
"GetMapping" in d
|
|
1081
|
+
or "PostMapping" in d
|
|
1082
|
+
or "RequestMapping" in d
|
|
1083
|
+
or "PutMapping" in d
|
|
1084
|
+
or "DeleteMapping" in d
|
|
1085
|
+
or "PatchMapping" in d
|
|
1086
|
+
for d in decorators
|
|
1087
|
+
)
|
|
1088
|
+
kind = SymbolKind.ROUTE if is_route else SymbolKind.FUNCTION
|
|
1089
|
+
sym = SymbolNode(
|
|
1090
|
+
name=name,
|
|
1091
|
+
kind=kind,
|
|
1092
|
+
file=file,
|
|
1093
|
+
line=start_line,
|
|
1094
|
+
end_line=end_line,
|
|
1095
|
+
parent_id=parent_id,
|
|
1096
|
+
language="java",
|
|
1097
|
+
decorators=decorators,
|
|
1098
|
+
hash=_node_hash_buf(node, buf),
|
|
1099
|
+
)
|
|
1100
|
+
symbols.append(sym)
|
|
1101
|
+
for child in self._children(node):
|
|
1102
|
+
self._walk_java(child, buf, content_str, file, symbols, None)
|
|
1103
|
+
|
|
1104
|
+
elif node_type == "enum_declaration":
|
|
1105
|
+
name_node = self._child_by_field(node, "name")
|
|
1106
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1107
|
+
sym = SymbolNode(
|
|
1108
|
+
name=f"enum {name}",
|
|
1109
|
+
kind=SymbolKind.CLASS,
|
|
1110
|
+
file=file,
|
|
1111
|
+
line=start_line,
|
|
1112
|
+
end_line=end_line,
|
|
1113
|
+
parent_id=parent_id,
|
|
1114
|
+
language="java",
|
|
1115
|
+
hash=_node_hash_buf(node, buf),
|
|
1116
|
+
)
|
|
1117
|
+
symbols.append(sym)
|
|
1118
|
+
|
|
1119
|
+
elif node_type == "record_declaration":
|
|
1120
|
+
name_node = self._child_by_field(node, "name")
|
|
1121
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1122
|
+
sym = SymbolNode(
|
|
1123
|
+
name=f"record {name}",
|
|
1124
|
+
kind=SymbolKind.CLASS,
|
|
1125
|
+
file=file,
|
|
1126
|
+
line=start_line,
|
|
1127
|
+
end_line=end_line,
|
|
1128
|
+
parent_id=parent_id,
|
|
1129
|
+
language="java",
|
|
1130
|
+
hash=_node_hash_buf(node, buf),
|
|
1131
|
+
)
|
|
1132
|
+
symbols.append(sym)
|
|
1133
|
+
|
|
1134
|
+
else:
|
|
1135
|
+
for child in self._children(node):
|
|
1136
|
+
self._walk_java(child, buf, content_str, file, symbols, parent_id)
|
|
1137
|
+
|
|
1138
|
+
# ── Go extractor ───────────────────────────────────────────────────────
|
|
1139
|
+
|
|
1140
|
+
def _extract_go(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
|
|
1141
|
+
symbols: list[SymbolNode] = []
|
|
1142
|
+
parser = get_parser(Lang.GO)
|
|
1143
|
+
if not parser:
|
|
1144
|
+
return symbols
|
|
1145
|
+
try:
|
|
1146
|
+
tree = parser.parse(raw_bytes)
|
|
1147
|
+
self._walk_go(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
1148
|
+
except Exception as e:
|
|
1149
|
+
_log.warning("SymbolGraph._extract_go failed: %s", e)
|
|
1150
|
+
return symbols
|
|
1151
|
+
|
|
1152
|
+
def _walk_go(
|
|
1153
|
+
self,
|
|
1154
|
+
node: Any,
|
|
1155
|
+
raw_bytes: bytes,
|
|
1156
|
+
content_str: str,
|
|
1157
|
+
file: str,
|
|
1158
|
+
symbols: list[SymbolNode],
|
|
1159
|
+
parent_id: int | None,
|
|
1160
|
+
) -> None:
|
|
1161
|
+
buf = raw_bytes
|
|
1162
|
+
try:
|
|
1163
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
1164
|
+
except Exception as e:
|
|
1165
|
+
_log.warning("SymbolGraph._walk_go failed: %s", e)
|
|
1166
|
+
return
|
|
1167
|
+
try:
|
|
1168
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
1169
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
1170
|
+
except Exception as e:
|
|
1171
|
+
_log.warning("SymbolGraph._walk_go failed: %s", e)
|
|
1172
|
+
return
|
|
1173
|
+
|
|
1174
|
+
if node_type == "function_declaration":
|
|
1175
|
+
name_node = self._child_by_field(node, "name")
|
|
1176
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1177
|
+
sym = SymbolNode(
|
|
1178
|
+
name=name,
|
|
1179
|
+
kind=SymbolKind.FUNCTION,
|
|
1180
|
+
file=file,
|
|
1181
|
+
line=start_line,
|
|
1182
|
+
end_line=end_line,
|
|
1183
|
+
parent_id=parent_id,
|
|
1184
|
+
language="go",
|
|
1185
|
+
hash=_node_hash_buf(node, buf),
|
|
1186
|
+
)
|
|
1187
|
+
symbols.append(sym)
|
|
1188
|
+
|
|
1189
|
+
elif node_type == "method_declaration":
|
|
1190
|
+
name_node = self._child_by_field(node, "name")
|
|
1191
|
+
receiver_node = self._child_by_field(node, "receiver")
|
|
1192
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1193
|
+
if receiver_node:
|
|
1194
|
+
recv_text = self._node_text_buf(receiver_node, buf)
|
|
1195
|
+
name = f"({recv_text}).{name}"
|
|
1196
|
+
sym = SymbolNode(
|
|
1197
|
+
name=name,
|
|
1198
|
+
kind=SymbolKind.METHOD,
|
|
1199
|
+
file=file,
|
|
1200
|
+
line=start_line,
|
|
1201
|
+
end_line=end_line,
|
|
1202
|
+
parent_id=parent_id,
|
|
1203
|
+
language="go",
|
|
1204
|
+
hash=_node_hash_buf(node, buf),
|
|
1205
|
+
)
|
|
1206
|
+
symbols.append(sym)
|
|
1207
|
+
|
|
1208
|
+
elif node_type == "type_declaration":
|
|
1209
|
+
for child in self._children(node):
|
|
1210
|
+
self._walk_go(child, buf, content_str, file, symbols, parent_id)
|
|
1211
|
+
|
|
1212
|
+
elif node_type == "type_spec":
|
|
1213
|
+
name_node = self._child_by_field(node, "name")
|
|
1214
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1215
|
+
type_node = self._child_by_field(node, "type")
|
|
1216
|
+
sub_kind = SymbolKind.CLASS
|
|
1217
|
+
if type_node:
|
|
1218
|
+
ttype = getattr(type_node, "type", "")
|
|
1219
|
+
if ttype == "struct_type":
|
|
1220
|
+
sub_kind = SymbolKind.CLASS
|
|
1221
|
+
elif ttype == "interface_type":
|
|
1222
|
+
name = f"interface {name}"
|
|
1223
|
+
sym = SymbolNode(
|
|
1224
|
+
name=name,
|
|
1225
|
+
kind=sub_kind,
|
|
1226
|
+
file=file,
|
|
1227
|
+
line=start_line,
|
|
1228
|
+
end_line=end_line,
|
|
1229
|
+
parent_id=parent_id,
|
|
1230
|
+
language="go",
|
|
1231
|
+
hash=_node_hash_buf(node, buf),
|
|
1232
|
+
)
|
|
1233
|
+
symbols.append(sym)
|
|
1234
|
+
if type_node:
|
|
1235
|
+
for child in self._children(type_node):
|
|
1236
|
+
self._walk_go(child, buf, content_str, file, symbols, None)
|
|
1237
|
+
|
|
1238
|
+
else:
|
|
1239
|
+
for child in self._children(node):
|
|
1240
|
+
self._walk_go(child, buf, content_str, file, symbols, parent_id)
|
|
1241
|
+
|
|
1242
|
+
# ── C/C++ extractor ────────────────────────────────────────────────────
|
|
1243
|
+
|
|
1244
|
+
def _extract_c_cpp(
|
|
1245
|
+
self, raw_bytes: bytes, content_str: str, file: str, lang: Lang
|
|
1246
|
+
) -> list[SymbolNode]:
|
|
1247
|
+
symbols: list[SymbolNode] = []
|
|
1248
|
+
parser = get_parser(lang)
|
|
1249
|
+
if not parser:
|
|
1250
|
+
return symbols
|
|
1251
|
+
try:
|
|
1252
|
+
tree = parser.parse(raw_bytes)
|
|
1253
|
+
self._walk_c_cpp(tree.root_node, raw_bytes, content_str, file, symbols, None, lang)
|
|
1254
|
+
except Exception as e:
|
|
1255
|
+
_log.warning("SymbolGraph._extract_c_cpp failed: %s", e)
|
|
1256
|
+
return symbols
|
|
1257
|
+
|
|
1258
|
+
def _walk_c_cpp(
|
|
1259
|
+
self,
|
|
1260
|
+
node: Any,
|
|
1261
|
+
raw_bytes: bytes,
|
|
1262
|
+
content_str: str,
|
|
1263
|
+
file: str,
|
|
1264
|
+
symbols: list[SymbolNode],
|
|
1265
|
+
parent_id: int | None,
|
|
1266
|
+
lang: Lang,
|
|
1267
|
+
) -> None:
|
|
1268
|
+
buf = raw_bytes
|
|
1269
|
+
try:
|
|
1270
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
1271
|
+
except Exception as e:
|
|
1272
|
+
_log.warning("SymbolGraph._walk_c_cpp failed: %s", e)
|
|
1273
|
+
return
|
|
1274
|
+
try:
|
|
1275
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
1276
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
1277
|
+
except Exception as e:
|
|
1278
|
+
_log.warning("SymbolGraph._walk_c_cpp failed: %s", e)
|
|
1279
|
+
return
|
|
1280
|
+
|
|
1281
|
+
lang_str = "c" if lang == Lang.C else "cpp"
|
|
1282
|
+
|
|
1283
|
+
if node_type == "function_definition":
|
|
1284
|
+
decl = self._child_by_field(node, "declarator")
|
|
1285
|
+
name = ""
|
|
1286
|
+
if decl:
|
|
1287
|
+
name_node = (
|
|
1288
|
+
self._child_by_field(decl, "declarator")
|
|
1289
|
+
if hasattr(decl, "child_by_field_name")
|
|
1290
|
+
else None
|
|
1291
|
+
)
|
|
1292
|
+
if not name_node:
|
|
1293
|
+
name_node = self._child_by_field(decl, "name")
|
|
1294
|
+
if name_node:
|
|
1295
|
+
name = self._node_text_buf(name_node, buf)
|
|
1296
|
+
if not name:
|
|
1297
|
+
name = self._node_text_buf(node, buf)[:30]
|
|
1298
|
+
sym = SymbolNode(
|
|
1299
|
+
name=name,
|
|
1300
|
+
kind=SymbolKind.FUNCTION,
|
|
1301
|
+
file=file,
|
|
1302
|
+
line=start_line,
|
|
1303
|
+
end_line=end_line,
|
|
1304
|
+
parent_id=parent_id,
|
|
1305
|
+
language=lang_str,
|
|
1306
|
+
hash=_node_hash_buf(node, buf),
|
|
1307
|
+
)
|
|
1308
|
+
symbols.append(sym)
|
|
1309
|
+
|
|
1310
|
+
elif node_type == "struct_specifier" and lang == Lang.CPP:
|
|
1311
|
+
name_node = self._child_by_field(node, "name")
|
|
1312
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1313
|
+
sym = SymbolNode(
|
|
1314
|
+
name=name,
|
|
1315
|
+
kind=SymbolKind.CLASS,
|
|
1316
|
+
file=file,
|
|
1317
|
+
line=start_line,
|
|
1318
|
+
end_line=end_line,
|
|
1319
|
+
parent_id=parent_id,
|
|
1320
|
+
language=lang_str,
|
|
1321
|
+
hash=_node_hash_buf(node, buf),
|
|
1322
|
+
)
|
|
1323
|
+
symbols.append(sym)
|
|
1324
|
+
|
|
1325
|
+
elif node_type == "class_specifier":
|
|
1326
|
+
name_node = self._child_by_field(node, "name")
|
|
1327
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1328
|
+
sym = SymbolNode(
|
|
1329
|
+
name=name,
|
|
1330
|
+
kind=SymbolKind.CLASS,
|
|
1331
|
+
file=file,
|
|
1332
|
+
line=start_line,
|
|
1333
|
+
end_line=end_line,
|
|
1334
|
+
parent_id=parent_id,
|
|
1335
|
+
language=lang_str,
|
|
1336
|
+
hash=_node_hash_buf(node, buf),
|
|
1337
|
+
)
|
|
1338
|
+
symbols.append(sym)
|
|
1339
|
+
body = self._child_by_field(node, "body")
|
|
1340
|
+
if body:
|
|
1341
|
+
for child in self._children(body):
|
|
1342
|
+
self._walk_c_cpp(child, buf, content_str, file, symbols, None, lang)
|
|
1343
|
+
|
|
1344
|
+
elif node_type == "namespace_definition":
|
|
1345
|
+
name_node = self._child_by_field(node, "name")
|
|
1346
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1347
|
+
sym = SymbolNode(
|
|
1348
|
+
name=f"namespace {name}",
|
|
1349
|
+
kind=SymbolKind.CLASS,
|
|
1350
|
+
file=file,
|
|
1351
|
+
line=start_line,
|
|
1352
|
+
end_line=end_line,
|
|
1353
|
+
parent_id=parent_id,
|
|
1354
|
+
language=lang_str,
|
|
1355
|
+
hash=_node_hash_buf(node, buf),
|
|
1356
|
+
)
|
|
1357
|
+
symbols.append(sym)
|
|
1358
|
+
|
|
1359
|
+
else:
|
|
1360
|
+
for child in self._children(node):
|
|
1361
|
+
self._walk_c_cpp(child, buf, content_str, file, symbols, parent_id, lang)
|
|
1362
|
+
|
|
1363
|
+
def _extract_swift(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
|
|
1364
|
+
symbols: list[SymbolNode] = []
|
|
1365
|
+
parser = get_parser(Lang.SWIFT)
|
|
1366
|
+
if not parser:
|
|
1367
|
+
return symbols
|
|
1368
|
+
try:
|
|
1369
|
+
tree = parser.parse(raw_bytes)
|
|
1370
|
+
self._walk_swift(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
1371
|
+
except Exception as e:
|
|
1372
|
+
_log.warning("SymbolGraph._extract_swift failed: %s", e)
|
|
1373
|
+
return symbols
|
|
1374
|
+
|
|
1375
|
+
def _walk_swift(
|
|
1376
|
+
self,
|
|
1377
|
+
node: Any,
|
|
1378
|
+
raw_bytes: bytes,
|
|
1379
|
+
content_str: str,
|
|
1380
|
+
file: str,
|
|
1381
|
+
symbols: list[SymbolNode],
|
|
1382
|
+
parent_id: int | None,
|
|
1383
|
+
) -> None:
|
|
1384
|
+
buf = raw_bytes
|
|
1385
|
+
try:
|
|
1386
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
1387
|
+
except Exception as e:
|
|
1388
|
+
_log.warning("SymbolGraph._walk_swift failed: %s", e)
|
|
1389
|
+
return
|
|
1390
|
+
try:
|
|
1391
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
1392
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
1393
|
+
except Exception as e:
|
|
1394
|
+
_log.warning("SymbolGraph._walk_swift failed: %s", e)
|
|
1395
|
+
return
|
|
1396
|
+
|
|
1397
|
+
if node_type == "import_declaration":
|
|
1398
|
+
# Skip — imports are handled by scanner, not needed as symbols
|
|
1399
|
+
pass
|
|
1400
|
+
|
|
1401
|
+
elif node_type == "class_declaration":
|
|
1402
|
+
name_node = self._child_by_field(node, "name")
|
|
1403
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1404
|
+
sym = SymbolNode(
|
|
1405
|
+
name=name,
|
|
1406
|
+
kind=SymbolKind.CLASS,
|
|
1407
|
+
file=file,
|
|
1408
|
+
line=start_line,
|
|
1409
|
+
end_line=end_line,
|
|
1410
|
+
parent_id=parent_id,
|
|
1411
|
+
language="swift",
|
|
1412
|
+
hash=_node_hash_buf(node, buf),
|
|
1413
|
+
)
|
|
1414
|
+
symbols.append(sym)
|
|
1415
|
+
body = self._child_by_field(node, "body")
|
|
1416
|
+
if body:
|
|
1417
|
+
for child in self._children(body):
|
|
1418
|
+
self._walk_swift(child, buf, content_str, file, symbols, sym.id)
|
|
1419
|
+
|
|
1420
|
+
elif node_type == "function_declaration":
|
|
1421
|
+
name_node = self._child_by_field(node, "name")
|
|
1422
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1423
|
+
sym = SymbolNode(
|
|
1424
|
+
name=name,
|
|
1425
|
+
kind=SymbolKind.FUNCTION,
|
|
1426
|
+
file=file,
|
|
1427
|
+
line=start_line,
|
|
1428
|
+
end_line=end_line,
|
|
1429
|
+
parent_id=parent_id,
|
|
1430
|
+
language="swift",
|
|
1431
|
+
hash=_node_hash_buf(node, buf),
|
|
1432
|
+
)
|
|
1433
|
+
symbols.append(sym)
|
|
1434
|
+
for child in self._children(node):
|
|
1435
|
+
self._walk_swift(child, buf, content_str, file, symbols, sym.id)
|
|
1436
|
+
|
|
1437
|
+
elif node_type == "protocol_declaration":
|
|
1438
|
+
name_node = self._child_by_field(node, "name")
|
|
1439
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1440
|
+
symbols.append(
|
|
1441
|
+
SymbolNode(
|
|
1442
|
+
name=f"protocol {name}",
|
|
1443
|
+
kind=SymbolKind.CLASS,
|
|
1444
|
+
file=file,
|
|
1445
|
+
line=start_line,
|
|
1446
|
+
end_line=end_line,
|
|
1447
|
+
parent_id=parent_id,
|
|
1448
|
+
language="swift",
|
|
1449
|
+
hash=_node_hash_buf(node, buf),
|
|
1450
|
+
)
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
else:
|
|
1454
|
+
for child in self._children(node):
|
|
1455
|
+
self._walk_swift(child, buf, content_str, file, symbols, parent_id)
|
|
1456
|
+
|
|
1457
|
+
# ── Ruby extractor ───────────────────────────────────────────────────────
|
|
1458
|
+
|
|
1459
|
+
def _extract_ruby(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
|
|
1460
|
+
symbols: list[SymbolNode] = []
|
|
1461
|
+
parser = get_parser(Lang.RUBY)
|
|
1462
|
+
if not parser:
|
|
1463
|
+
return symbols
|
|
1464
|
+
try:
|
|
1465
|
+
tree = parser.parse(raw_bytes)
|
|
1466
|
+
self._walk_ruby(tree.root_node, raw_bytes, content_str, file, symbols, None)
|
|
1467
|
+
except Exception as e:
|
|
1468
|
+
_log.warning("SymbolGraph._extract_ruby failed: %s", e)
|
|
1469
|
+
return symbols
|
|
1470
|
+
|
|
1471
|
+
def _walk_ruby(
|
|
1472
|
+
self,
|
|
1473
|
+
node: Any,
|
|
1474
|
+
raw_bytes: bytes,
|
|
1475
|
+
content_str: str,
|
|
1476
|
+
file: str,
|
|
1477
|
+
symbols: list[SymbolNode],
|
|
1478
|
+
parent_id: int | None,
|
|
1479
|
+
) -> None:
|
|
1480
|
+
buf = raw_bytes
|
|
1481
|
+
try:
|
|
1482
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
1483
|
+
except Exception as e:
|
|
1484
|
+
_log.warning("SymbolGraph._walk_ruby failed: %s", e)
|
|
1485
|
+
return
|
|
1486
|
+
try:
|
|
1487
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
1488
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
1489
|
+
except Exception as e:
|
|
1490
|
+
_log.warning("SymbolGraph._walk_ruby failed: %s", e)
|
|
1491
|
+
return
|
|
1492
|
+
|
|
1493
|
+
if node_type == "call":
|
|
1494
|
+
name = ""
|
|
1495
|
+
for c in self._children(node):
|
|
1496
|
+
if c.type == "identifier":
|
|
1497
|
+
name = self._node_text_buf(c, buf)
|
|
1498
|
+
break
|
|
1499
|
+
if name in ("require", "require_relative", "load", "include", "extend", "prepend"):
|
|
1500
|
+
arg = ""
|
|
1501
|
+
for c in self._children(node):
|
|
1502
|
+
if c.type == "argument_list":
|
|
1503
|
+
for a in self._children(c):
|
|
1504
|
+
if a.type == "string":
|
|
1505
|
+
arg = self._node_text_buf(a, buf).strip("\"'")
|
|
1506
|
+
break
|
|
1507
|
+
if arg:
|
|
1508
|
+
# Skip — imports handled by scanner
|
|
1509
|
+
pass
|
|
1510
|
+
|
|
1511
|
+
elif node_type == "method":
|
|
1512
|
+
name_node = None
|
|
1513
|
+
for c in self._children(node):
|
|
1514
|
+
if c.type == "identifier":
|
|
1515
|
+
name_node = c
|
|
1516
|
+
break
|
|
1517
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1518
|
+
sym = SymbolNode(
|
|
1519
|
+
name=name,
|
|
1520
|
+
kind=SymbolKind.FUNCTION,
|
|
1521
|
+
file=file,
|
|
1522
|
+
line=start_line,
|
|
1523
|
+
end_line=end_line,
|
|
1524
|
+
parent_id=parent_id,
|
|
1525
|
+
language="ruby",
|
|
1526
|
+
hash=_node_hash_buf(node, buf),
|
|
1527
|
+
)
|
|
1528
|
+
symbols.append(sym)
|
|
1529
|
+
body = None
|
|
1530
|
+
for c in self._children(node):
|
|
1531
|
+
if c.type == "body_statement":
|
|
1532
|
+
body = c
|
|
1533
|
+
break
|
|
1534
|
+
if body:
|
|
1535
|
+
for child in self._children(body):
|
|
1536
|
+
self._walk_ruby(child, buf, content_str, file, symbols, sym.id)
|
|
1537
|
+
|
|
1538
|
+
elif node_type == "class":
|
|
1539
|
+
name_node = None
|
|
1540
|
+
for c in self._children(node):
|
|
1541
|
+
if c.type == "constant":
|
|
1542
|
+
name_node = c
|
|
1543
|
+
break
|
|
1544
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1545
|
+
sym = SymbolNode(
|
|
1546
|
+
name=name,
|
|
1547
|
+
kind=SymbolKind.CLASS,
|
|
1548
|
+
file=file,
|
|
1549
|
+
line=start_line,
|
|
1550
|
+
end_line=end_line,
|
|
1551
|
+
parent_id=parent_id,
|
|
1552
|
+
language="ruby",
|
|
1553
|
+
hash=_node_hash_buf(node, buf),
|
|
1554
|
+
)
|
|
1555
|
+
symbols.append(sym)
|
|
1556
|
+
for c in self._children(node):
|
|
1557
|
+
if c.type == "body_statement":
|
|
1558
|
+
for child in self._children(c):
|
|
1559
|
+
self._walk_ruby(child, buf, content_str, file, symbols, sym.id)
|
|
1560
|
+
|
|
1561
|
+
elif node_type in ("module", "singleton_class"):
|
|
1562
|
+
name_node = None
|
|
1563
|
+
for c in self._children(node):
|
|
1564
|
+
if c.type == "constant":
|
|
1565
|
+
name_node = c
|
|
1566
|
+
break
|
|
1567
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1568
|
+
sym = SymbolNode(
|
|
1569
|
+
name=f"module {name}" if node_type == "module" else name,
|
|
1570
|
+
kind=SymbolKind.CLASS,
|
|
1571
|
+
file=file,
|
|
1572
|
+
line=start_line,
|
|
1573
|
+
end_line=end_line,
|
|
1574
|
+
parent_id=parent_id,
|
|
1575
|
+
language="ruby",
|
|
1576
|
+
hash=_node_hash_buf(node, buf),
|
|
1577
|
+
)
|
|
1578
|
+
symbols.append(sym)
|
|
1579
|
+
for c in self._children(node):
|
|
1580
|
+
if c.type == "body_statement":
|
|
1581
|
+
for child in self._children(c):
|
|
1582
|
+
self._walk_ruby(child, buf, content_str, file, symbols, sym.id)
|
|
1583
|
+
|
|
1584
|
+
else:
|
|
1585
|
+
for child in self._children(node):
|
|
1586
|
+
self._walk_ruby(child, buf, content_str, file, symbols, parent_id)
|
|
1587
|
+
|
|
1588
|
+
def _extract_generic_ts(
|
|
1589
|
+
self,
|
|
1590
|
+
raw_bytes: bytes,
|
|
1591
|
+
content_str: str,
|
|
1592
|
+
file: str,
|
|
1593
|
+
lang: Lang,
|
|
1594
|
+
function_types: set[str] | None = None,
|
|
1595
|
+
method_types: set[str] | None = None,
|
|
1596
|
+
class_types: set[str] | None = None,
|
|
1597
|
+
) -> list[SymbolNode]:
|
|
1598
|
+
"""Generic tree-sitter symbol extractor for languages without custom walkers."""
|
|
1599
|
+
symbols: list[SymbolNode] = []
|
|
1600
|
+
parser = get_parser(lang)
|
|
1601
|
+
if not parser:
|
|
1602
|
+
return []
|
|
1603
|
+
try:
|
|
1604
|
+
tree = parser.parse(raw_bytes)
|
|
1605
|
+
self._walk_generic(
|
|
1606
|
+
tree.root_node,
|
|
1607
|
+
raw_bytes,
|
|
1608
|
+
file,
|
|
1609
|
+
lang,
|
|
1610
|
+
symbols,
|
|
1611
|
+
None,
|
|
1612
|
+
function_types or set(),
|
|
1613
|
+
method_types or set(),
|
|
1614
|
+
class_types or set(),
|
|
1615
|
+
)
|
|
1616
|
+
except Exception as e:
|
|
1617
|
+
_log.warning("SymbolGraph._extract_generic_ts failed: %s", e)
|
|
1618
|
+
return symbols
|
|
1619
|
+
|
|
1620
|
+
def _walk_generic(
|
|
1621
|
+
self,
|
|
1622
|
+
node: Any,
|
|
1623
|
+
buf: bytes,
|
|
1624
|
+
file: str,
|
|
1625
|
+
lang: Lang,
|
|
1626
|
+
symbols: list[SymbolNode],
|
|
1627
|
+
parent_id: int | None,
|
|
1628
|
+
function_types: set[str],
|
|
1629
|
+
method_types: set[str],
|
|
1630
|
+
class_types: set[str],
|
|
1631
|
+
) -> None:
|
|
1632
|
+
try:
|
|
1633
|
+
node_type = node.type if hasattr(node, "type") else ""
|
|
1634
|
+
except Exception as e:
|
|
1635
|
+
_log.warning("SymbolGraph._walk_generic failed: %s", e)
|
|
1636
|
+
return
|
|
1637
|
+
try:
|
|
1638
|
+
start_line = node.start_point[0] + 1 if node.start_point else 0
|
|
1639
|
+
end_line = node.end_point[0] + 1 if node.end_point else 0
|
|
1640
|
+
except Exception as e:
|
|
1641
|
+
_log.warning("SymbolGraph._walk_generic failed: %s", e)
|
|
1642
|
+
return
|
|
1643
|
+
|
|
1644
|
+
kind = None
|
|
1645
|
+
if node_type in function_types:
|
|
1646
|
+
kind = SymbolKind.FUNCTION
|
|
1647
|
+
name_node = self._child_by_field(node, "name")
|
|
1648
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1649
|
+
elif node_type in method_types:
|
|
1650
|
+
kind = SymbolKind.FUNCTION
|
|
1651
|
+
name_node = self._child_by_field(node, "name")
|
|
1652
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1653
|
+
elif node_type in class_types:
|
|
1654
|
+
kind = SymbolKind.CLASS
|
|
1655
|
+
name_node = self._child_by_field(node, "name")
|
|
1656
|
+
name = self._node_text_buf(name_node, buf) if name_node else "anon"
|
|
1657
|
+
|
|
1658
|
+
if kind:
|
|
1659
|
+
lang_str = lang.value if hasattr(lang, "value") else str(lang)
|
|
1660
|
+
sym = SymbolNode(
|
|
1661
|
+
name=name,
|
|
1662
|
+
kind=kind,
|
|
1663
|
+
file=file,
|
|
1664
|
+
line=start_line,
|
|
1665
|
+
end_line=end_line,
|
|
1666
|
+
parent_id=parent_id,
|
|
1667
|
+
language=lang_str,
|
|
1668
|
+
hash=_node_hash_buf(node, buf),
|
|
1669
|
+
)
|
|
1670
|
+
symbols.append(sym)
|
|
1671
|
+
for child in self._children(node):
|
|
1672
|
+
self._walk_generic(
|
|
1673
|
+
child,
|
|
1674
|
+
buf,
|
|
1675
|
+
file,
|
|
1676
|
+
lang,
|
|
1677
|
+
symbols,
|
|
1678
|
+
sym.id,
|
|
1679
|
+
function_types,
|
|
1680
|
+
method_types,
|
|
1681
|
+
class_types,
|
|
1682
|
+
)
|
|
1683
|
+
return
|
|
1684
|
+
|
|
1685
|
+
for child in self._children(node):
|
|
1686
|
+
self._walk_generic(
|
|
1687
|
+
child,
|
|
1688
|
+
buf,
|
|
1689
|
+
file,
|
|
1690
|
+
lang,
|
|
1691
|
+
symbols,
|
|
1692
|
+
parent_id,
|
|
1693
|
+
function_types,
|
|
1694
|
+
method_types,
|
|
1695
|
+
class_types,
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
# ── Internal: Tree-sitter helpers ──────────────────────────────────────
|
|
1699
|
+
|
|
1700
|
+
def _children(self, node: Any) -> list[Any]:
|
|
1701
|
+
try:
|
|
1702
|
+
return list(node.children) if hasattr(node, "children") else []
|
|
1703
|
+
except Exception as e:
|
|
1704
|
+
_log.warning("SymbolGraph._children failed: %s", e)
|
|
1705
|
+
return []
|
|
1706
|
+
|
|
1707
|
+
def _child_by_field(self, node: Any, field_name: str) -> Any | None:
|
|
1708
|
+
try:
|
|
1709
|
+
return (
|
|
1710
|
+
node.child_by_field_name(field_name)
|
|
1711
|
+
if hasattr(node, "child_by_field_name")
|
|
1712
|
+
else None
|
|
1713
|
+
)
|
|
1714
|
+
except Exception as e:
|
|
1715
|
+
_log.warning("SymbolGraph._child_by_field failed: %s", e)
|
|
1716
|
+
return None
|
|
1717
|
+
|
|
1718
|
+
def _node_text_buf(self, node: Any, buf: bytes) -> str:
|
|
1719
|
+
"""Extract text from a tree-sitter node using the raw byte buffer."""
|
|
1720
|
+
try:
|
|
1721
|
+
if hasattr(node, "start_byte") and hasattr(node, "end_byte"):
|
|
1722
|
+
return buf[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
1723
|
+
except Exception as e:
|
|
1724
|
+
_log.warning("SymbolGraph._node_text_buf failed: %s", e)
|
|
1725
|
+
return ""
|
|
1726
|
+
|
|
1727
|
+
def _extract_decorators_buf(self, node: Any, buf: bytes) -> list[str]:
|
|
1728
|
+
decorators: list[str] = []
|
|
1729
|
+
decorator = self._child_by_field(node, "decorator")
|
|
1730
|
+
if decorator:
|
|
1731
|
+
text = self._node_text_buf(decorator, buf)
|
|
1732
|
+
if text:
|
|
1733
|
+
decorators.append(text.strip())
|
|
1734
|
+
for child in self._children(node):
|
|
1735
|
+
try:
|
|
1736
|
+
if getattr(child, "type", "") == "decorator":
|
|
1737
|
+
text = self._node_text_buf(child, buf)
|
|
1738
|
+
if text:
|
|
1739
|
+
decorators.append(text.strip())
|
|
1740
|
+
except Exception as e:
|
|
1741
|
+
_log.warning("SymbolGraph._extract_decorators_buf failed: %s", e)
|
|
1742
|
+
return decorators
|
|
1743
|
+
|
|
1744
|
+
def _extract_params_buf(self, node: Any, buf: bytes) -> list[str]:
|
|
1745
|
+
params_node = self._child_by_field(node, "parameters")
|
|
1746
|
+
if not params_node:
|
|
1747
|
+
return []
|
|
1748
|
+
text = self._node_text_buf(params_node, buf)
|
|
1749
|
+
if not text:
|
|
1750
|
+
return []
|
|
1751
|
+
import re
|
|
1752
|
+
|
|
1753
|
+
return [
|
|
1754
|
+
p.strip()
|
|
1755
|
+
for p in re.split(r"[,:]", text.strip("()"))
|
|
1756
|
+
if p.strip() and not p.strip().startswith("*")
|
|
1757
|
+
]
|
|
1758
|
+
|
|
1759
|
+
def _extract_docstring_buf(self, node: Any, buf: bytes) -> str:
|
|
1760
|
+
body = self._child_by_field(node, "body")
|
|
1761
|
+
if not body:
|
|
1762
|
+
return ""
|
|
1763
|
+
children = self._children(body)
|
|
1764
|
+
if not children:
|
|
1765
|
+
return ""
|
|
1766
|
+
first = children[0]
|
|
1767
|
+
try:
|
|
1768
|
+
if hasattr(first, "type") and first.type in ("expression_statement", "string"):
|
|
1769
|
+
return self._node_text_buf(first, buf)[:200]
|
|
1770
|
+
except Exception as e:
|
|
1771
|
+
_log.warning("SymbolGraph._extract_docstring_buf failed: %s", e)
|
|
1772
|
+
return ""
|
|
1773
|
+
|
|
1774
|
+
# ── Internal: SQLite helpers ───────────────────────────────────────────
|
|
1775
|
+
|
|
1776
|
+
def _insert_symbol(self, conn: sqlite3.Connection, sym: SymbolNode, file: str) -> None:
|
|
1777
|
+
content_hash = (
|
|
1778
|
+
sym.hash or hashlib.md5(f"{sym.name}{sym.line}{sym.kind}".encode()).hexdigest()[:12]
|
|
1779
|
+
)
|
|
1780
|
+
conn.execute(
|
|
1781
|
+
"""INSERT OR IGNORE INTO symbols
|
|
1782
|
+
(name, kind, file, line, end_line, parent_id, docstring, language, hash, decorators, params, is_exported)
|
|
1783
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
1784
|
+
(
|
|
1785
|
+
sym.name,
|
|
1786
|
+
sym.kind,
|
|
1787
|
+
file,
|
|
1788
|
+
sym.line,
|
|
1789
|
+
sym.end_line,
|
|
1790
|
+
sym.parent_id,
|
|
1791
|
+
sym.docstring,
|
|
1792
|
+
sym.language,
|
|
1793
|
+
content_hash,
|
|
1794
|
+
",".join(sym.decorators),
|
|
1795
|
+
",".join(sym.params),
|
|
1796
|
+
1 if sym.is_exported else 0,
|
|
1797
|
+
),
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
def _get_symbols_in_file(self, conn: sqlite3.Connection, file: str) -> list[SymbolNode]:
|
|
1801
|
+
cur = conn.execute("SELECT * FROM symbols WHERE file = ? ORDER BY line", (file,))
|
|
1802
|
+
return [self._row_to_symbol(r) for r in cur.fetchall()]
|
|
1803
|
+
|
|
1804
|
+
def _get_symbol_by_id(self, sid: int) -> SymbolNode | None:
|
|
1805
|
+
conn = self._get_conn()
|
|
1806
|
+
cur = conn.execute("SELECT * FROM symbols WHERE id = ?", (sid,))
|
|
1807
|
+
row = cur.fetchone()
|
|
1808
|
+
return self._row_to_symbol(row) if row else None
|
|
1809
|
+
|
|
1810
|
+
def _row_to_symbol(self, row: sqlite3.Row) -> SymbolNode:
|
|
1811
|
+
return SymbolNode(
|
|
1812
|
+
id=row[0],
|
|
1813
|
+
name=row[1],
|
|
1814
|
+
kind=row[2],
|
|
1815
|
+
file=row[3],
|
|
1816
|
+
line=row[4],
|
|
1817
|
+
end_line=row[5],
|
|
1818
|
+
parent_id=row[6],
|
|
1819
|
+
docstring=row[7] or "",
|
|
1820
|
+
language=row[8],
|
|
1821
|
+
hash=row[9] or "",
|
|
1822
|
+
decorators=row[10].split(",") if row[10] else [],
|
|
1823
|
+
params=row[11].split(",") if row[11] else [],
|
|
1824
|
+
is_exported=bool(row[12]),
|
|
1825
|
+
)
|
|
1826
|
+
|
|
1827
|
+
def _build_edges(self, conn: sqlite3.Connection, files: list[FileInfo]) -> None:
|
|
1828
|
+
"""Build symbol-level call/reference edges by scanning ASTs."""
|
|
1829
|
+
for fi in files:
|
|
1830
|
+
abs_path = self.root / fi.path
|
|
1831
|
+
if not abs_path.exists():
|
|
1832
|
+
continue
|
|
1833
|
+
content = abs_path.read_text(encoding="utf-8", errors="ignore")
|
|
1834
|
+
file_symbols = self._get_symbols_in_file(conn, fi.path)
|
|
1835
|
+
if not file_symbols:
|
|
1836
|
+
continue
|
|
1837
|
+
|
|
1838
|
+
refs = self._find_references(content, fi.language)
|
|
1839
|
+
for sym in file_symbols:
|
|
1840
|
+
for ref_name in refs:
|
|
1841
|
+
target = self._find_local_symbol(conn, ref_name, fi.path, sym.line)
|
|
1842
|
+
if target and target.id != sym.id:
|
|
1843
|
+
try:
|
|
1844
|
+
conn.execute(
|
|
1845
|
+
"INSERT OR IGNORE INTO edges (source_id, target_id, kind) VALUES (?, ?, ?)",
|
|
1846
|
+
(sym.id, target.id, "calls"),
|
|
1847
|
+
)
|
|
1848
|
+
except Exception as e:
|
|
1849
|
+
_log.warning("SymbolGraph._build_edges failed: %s", e)
|
|
1850
|
+
|
|
1851
|
+
def _find_references(self, content: str, lang: Lang) -> set[str]:
|
|
1852
|
+
"""Extract referenced names from source content using AST."""
|
|
1853
|
+
import ast
|
|
1854
|
+
|
|
1855
|
+
refs: set[str] = set()
|
|
1856
|
+
if lang == Lang.PYTHON:
|
|
1857
|
+
try:
|
|
1858
|
+
tree = ast.parse(content)
|
|
1859
|
+
for node in ast.walk(tree):
|
|
1860
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
|
1861
|
+
refs.add(node.func.id)
|
|
1862
|
+
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
|
1863
|
+
refs.add(node.func.attr)
|
|
1864
|
+
elif isinstance(node, ast.Name):
|
|
1865
|
+
refs.add(node.id)
|
|
1866
|
+
except SyntaxError:
|
|
1867
|
+
pass
|
|
1868
|
+
else:
|
|
1869
|
+
parser = get_parser(lang)
|
|
1870
|
+
if parser:
|
|
1871
|
+
try:
|
|
1872
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
1873
|
+
self._walk_calls(tree.root_node, refs)
|
|
1874
|
+
except Exception as e:
|
|
1875
|
+
_log.warning("SymbolGraph._find_references failed: %s", e)
|
|
1876
|
+
|
|
1877
|
+
return refs
|
|
1878
|
+
|
|
1879
|
+
def _walk_calls(self, node: Any, refs: set[str]) -> None:
|
|
1880
|
+
"""Walk a tree-sitter AST extracting call target names."""
|
|
1881
|
+
try:
|
|
1882
|
+
ntype = node.type if hasattr(node, "type") else ""
|
|
1883
|
+
except Exception as e:
|
|
1884
|
+
_log.warning("SymbolGraph._walk_calls failed: %s", e)
|
|
1885
|
+
return
|
|
1886
|
+
|
|
1887
|
+
# Common call-like node types across grammars
|
|
1888
|
+
if ntype in (
|
|
1889
|
+
"call_expression",
|
|
1890
|
+
"method_invocation",
|
|
1891
|
+
"function_call_expression",
|
|
1892
|
+
"invocation_expression",
|
|
1893
|
+
):
|
|
1894
|
+
fn = self._child_by_field(node, "function") or self._child_by_field(node, "name")
|
|
1895
|
+
if fn:
|
|
1896
|
+
try:
|
|
1897
|
+
text = fn.text if hasattr(fn, "text") else b""
|
|
1898
|
+
if isinstance(text, bytes):
|
|
1899
|
+
text = text.decode("utf-8", errors="replace")
|
|
1900
|
+
name = str(text).split(".")[-1].split("::")[-1]
|
|
1901
|
+
if name and name.isidentifier():
|
|
1902
|
+
refs.add(name)
|
|
1903
|
+
except Exception as e:
|
|
1904
|
+
_log.warning("SymbolGraph._walk_calls failed: %s", e)
|
|
1905
|
+
|
|
1906
|
+
# Ruby: call node has method field
|
|
1907
|
+
elif ntype == "call":
|
|
1908
|
+
fn = self._child_by_field(node, "method")
|
|
1909
|
+
if fn:
|
|
1910
|
+
try:
|
|
1911
|
+
text = fn.text if hasattr(fn, "text") else b""
|
|
1912
|
+
if isinstance(text, bytes):
|
|
1913
|
+
text = text.decode("utf-8", errors="replace")
|
|
1914
|
+
name = str(text)
|
|
1915
|
+
if name and name.isidentifier():
|
|
1916
|
+
refs.add(name)
|
|
1917
|
+
except Exception as e:
|
|
1918
|
+
_log.warning("SymbolGraph._walk_calls failed: %s", e)
|
|
1919
|
+
|
|
1920
|
+
for child in node.children if hasattr(node, "children") else []:
|
|
1921
|
+
self._walk_calls(child, refs)
|
|
1922
|
+
|
|
1923
|
+
def _find_local_symbol(
|
|
1924
|
+
self, conn: sqlite3.Connection, name: str, file: str, near_line: int
|
|
1925
|
+
) -> SymbolNode | None:
|
|
1926
|
+
"""Find a symbol by name in the same file, preferring the one closest to near_line."""
|
|
1927
|
+
cur = conn.execute(
|
|
1928
|
+
"SELECT * FROM symbols WHERE file = ? AND name = ? ORDER BY ABS(line - ?) LIMIT 1",
|
|
1929
|
+
(file, name, near_line),
|
|
1930
|
+
)
|
|
1931
|
+
row = cur.fetchone()
|
|
1932
|
+
return self._row_to_symbol(row) if row else None
|
|
1933
|
+
|
|
1934
|
+
def __enter__(self) -> SymbolGraph:
|
|
1935
|
+
return self
|
|
1936
|
+
|
|
1937
|
+
def __exit__(self, *args: Any) -> None:
|
|
1938
|
+
self.close()
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
# ── Utility ─────────────────────────────────────────────────────────────────────
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
def _content_hash(content: str) -> str:
|
|
1945
|
+
return hashlib.md5(content.encode("utf-8")).hexdigest()[:12]
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
def _node_hash_buf(node: Any, buf: bytes) -> str:
|
|
1949
|
+
"""Hash the node's source content from a raw byte buffer."""
|
|
1950
|
+
try:
|
|
1951
|
+
if hasattr(node, "start_byte") and hasattr(node, "end_byte"):
|
|
1952
|
+
return hashlib.md5(buf[node.start_byte : node.end_byte]).hexdigest()[:12]
|
|
1953
|
+
except Exception as e:
|
|
1954
|
+
_log.debug("_node_hash_buf failed: %s", e)
|
|
1955
|
+
return hashlib.md5(str(id(node)).encode()).hexdigest()[:12]
|