codegraphcontext 0.4.7__py3-none-any.whl → 0.4.8__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.
- codegraphcontext/cli/cli_helpers.py +54 -4
- codegraphcontext/cli/config_manager.py +3 -3
- codegraphcontext/cli/main.py +90 -9
- codegraphcontext/core/__init__.py +3 -3
- codegraphcontext/core/database_kuzu.py +57 -9
- codegraphcontext/tools/code_finder.py +172 -28
- codegraphcontext/tools/graph_builder.py +61 -12
- codegraphcontext/tools/handlers/analysis_handlers.py +2 -2
- codegraphcontext/tools/indexing/discovery.py +26 -2
- codegraphcontext/tools/indexing/persistence/writer.py +154 -154
- codegraphcontext/tools/indexing/pipeline.py +16 -3
- codegraphcontext/tools/indexing/resolution/calls.py +2248 -118
- codegraphcontext/tools/indexing/resolution/inheritance.py +26 -15
- codegraphcontext/tools/indexing/schema.py +22 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +69 -5
- codegraphcontext/tools/languages/java.py +143 -8
- codegraphcontext/tools/languages/kotlin.py +1653 -135
- codegraphcontext/tools/languages/php.py +18 -14
- codegraphcontext/tools/scip_indexer.py +506 -353
- codegraphcontext/tools/type_utils.py +12 -0
- codegraphcontext/utils/tree_sitter_manager.py +86 -35
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/METADATA +49 -87
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/RECORD +27 -26
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.8.dist-info}/top_level.txt +0 -0
|
@@ -2,10 +2,12 @@ import asyncio
|
|
|
2
2
|
import json
|
|
3
3
|
import uuid
|
|
4
4
|
import urllib.parse
|
|
5
|
+
from collections import Counter
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
import time
|
|
7
8
|
import os
|
|
8
9
|
from typing import Optional, List, Dict, Any
|
|
10
|
+
import typer
|
|
9
11
|
from rich.console import Console
|
|
10
12
|
from rich.table import Table
|
|
11
13
|
from rich.progress import (
|
|
@@ -31,6 +33,31 @@ from .config_manager import resolve_context, ResolvedContext, register_repo_in_c
|
|
|
31
33
|
console = Console()
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
def _print_call_resolution_diagnostics(graph_builder: GraphBuilder, limit: int = 5) -> None:
|
|
37
|
+
diagnostics = getattr(graph_builder, "last_call_resolution_diagnostics", [])
|
|
38
|
+
if not diagnostics:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
reason_counts = Counter(d.get("reason", "unknown") for d in diagnostics)
|
|
42
|
+
summary = ", ".join(
|
|
43
|
+
f"{reason}={count}" for reason, count in reason_counts.most_common()
|
|
44
|
+
)
|
|
45
|
+
console.print(
|
|
46
|
+
f"[yellow]Skipped {len(diagnostics)} unresolved call relationship(s): {summary}[/yellow]"
|
|
47
|
+
)
|
|
48
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
49
|
+
table.add_column("Call", style="cyan", overflow="fold")
|
|
50
|
+
table.add_column("Reason", style="yellow")
|
|
51
|
+
table.add_column("Location", style="dim", overflow="fold")
|
|
52
|
+
for diagnostic in diagnostics[:limit]:
|
|
53
|
+
table.add_row(
|
|
54
|
+
str(diagnostic.get("full_call_name") or ""),
|
|
55
|
+
str(diagnostic.get("reason") or ""),
|
|
56
|
+
f"{diagnostic.get('caller_file_path')}:{diagnostic.get('line_number')}",
|
|
57
|
+
)
|
|
58
|
+
console.print(table)
|
|
59
|
+
|
|
60
|
+
|
|
34
61
|
def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, Any, Any, ResolvedContext]:
|
|
35
62
|
"""
|
|
36
63
|
Initializes and returns core service managers based on the resolved context.
|
|
@@ -243,6 +270,7 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
243
270
|
asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
|
|
244
271
|
time_end = time.time()
|
|
245
272
|
elapsed = time_end - time_start
|
|
273
|
+
_print_call_resolution_diagnostics(graph_builder)
|
|
246
274
|
console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
|
|
247
275
|
|
|
248
276
|
# Check if auto-watch is enabled
|
|
@@ -259,6 +287,7 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
259
287
|
|
|
260
288
|
except Exception as e:
|
|
261
289
|
console.print(f"[bold red]An error occurred during indexing:[/bold red] {e}")
|
|
290
|
+
raise typer.Exit(code=1)
|
|
262
291
|
finally:
|
|
263
292
|
db_manager.close_driver()
|
|
264
293
|
|
|
@@ -289,6 +318,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
289
318
|
|
|
290
319
|
try:
|
|
291
320
|
asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True, cgcignore_path=ctx.cgcignore_path))
|
|
321
|
+
_print_call_resolution_diagnostics(graph_builder)
|
|
292
322
|
console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
|
|
293
323
|
except Exception as e:
|
|
294
324
|
console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
|
|
@@ -354,9 +384,11 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
354
384
|
|
|
355
385
|
db_manager, _, _, ctx = services
|
|
356
386
|
|
|
357
|
-
# Replicating safety checks from MCPServer
|
|
387
|
+
# Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
|
|
388
|
+
import re
|
|
358
389
|
forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
|
|
359
|
-
|
|
390
|
+
pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
|
|
391
|
+
if re.search(pattern, query, re.IGNORECASE):
|
|
360
392
|
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
361
393
|
db_manager.close_driver()
|
|
362
394
|
return
|
|
@@ -382,9 +414,11 @@ def cypher_helper_visual(query: str, context: Optional[str] = None):
|
|
|
382
414
|
|
|
383
415
|
db_manager, _, _, ctx = services
|
|
384
416
|
|
|
385
|
-
# Replicating safety checks from MCPServer
|
|
417
|
+
# Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
|
|
418
|
+
import re
|
|
386
419
|
forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
|
|
387
|
-
|
|
420
|
+
pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
|
|
421
|
+
if re.search(pattern, query, re.IGNORECASE):
|
|
388
422
|
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
389
423
|
db_manager.close_driver()
|
|
390
424
|
return
|
|
@@ -540,9 +574,11 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
540
574
|
asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
|
|
541
575
|
time_end = time.time()
|
|
542
576
|
elapsed = time_end - time_start
|
|
577
|
+
_print_call_resolution_diagnostics(graph_builder)
|
|
543
578
|
console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
|
|
544
579
|
except Exception as e:
|
|
545
580
|
console.print(f"[bold red]An error occurred during re-indexing:[/bold red] {e}")
|
|
581
|
+
raise typer.Exit(code=1)
|
|
546
582
|
finally:
|
|
547
583
|
db_manager.close_driver()
|
|
548
584
|
|
|
@@ -669,6 +705,12 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
669
705
|
class_count = session.run("MATCH (c:Class) RETURN count(c) as c").single()["c"]
|
|
670
706
|
module_count = session.run("MATCH (m:Module) RETURN count(m) as c").single()["c"]
|
|
671
707
|
|
|
708
|
+
# Extended node types (PHP, Rust, Go, etc.)
|
|
709
|
+
interface_count = session.run("MATCH (i:Interface) RETURN count(i) as c").single()["c"]
|
|
710
|
+
trait_count = session.run("MATCH (t:Trait) RETURN count(t) as c").single()["c"]
|
|
711
|
+
struct_count = session.run("MATCH (s:Struct) RETURN count(s) as c").single()["c"]
|
|
712
|
+
enum_count = session.run("MATCH (e:Enum) RETURN count(e) as c").single()["c"]
|
|
713
|
+
|
|
672
714
|
table = Table(show_header=True, header_style="bold magenta")
|
|
673
715
|
table.add_column("Metric", style="cyan")
|
|
674
716
|
table.add_column("Count", style="green", justify="right")
|
|
@@ -677,6 +719,14 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
677
719
|
table.add_row("Files", str(file_count))
|
|
678
720
|
table.add_row("Functions", str(func_count))
|
|
679
721
|
table.add_row("Classes", str(class_count))
|
|
722
|
+
if interface_count > 0:
|
|
723
|
+
table.add_row("Interfaces", str(interface_count))
|
|
724
|
+
if trait_count > 0:
|
|
725
|
+
table.add_row("Traits", str(trait_count))
|
|
726
|
+
if struct_count > 0:
|
|
727
|
+
table.add_row("Structs", str(struct_count))
|
|
728
|
+
if enum_count > 0:
|
|
729
|
+
table.add_row("Enums", str(enum_count))
|
|
680
730
|
table.add_row("Modules", str(module_count))
|
|
681
731
|
|
|
682
732
|
console.print(table)
|
|
@@ -48,7 +48,7 @@ DEFAULT_CONFIG = {
|
|
|
48
48
|
"INDEX_SOURCE": "true",
|
|
49
49
|
# SCIP indexer feature flag (default off — existing Tree-sitter behaviour unchanged)
|
|
50
50
|
"SCIP_INDEXER": "false",
|
|
51
|
-
"SCIP_LANGUAGES": "python,typescript,go,rust,java",
|
|
51
|
+
"SCIP_LANGUAGES": "python,typescript,javascript,go,rust,java,dart,cpp,c,csharp",
|
|
52
52
|
"SKIP_EXTERNAL_RESOLUTION": "false",
|
|
53
53
|
# 0 = unlimited; any positive integer caps MCP tool response size.
|
|
54
54
|
"MAX_TOOL_RESPONSE_TOKENS": "0",
|
|
@@ -85,7 +85,7 @@ CONFIG_DESCRIPTIONS = {
|
|
|
85
85
|
"IGNORE_DIRS": "Comma-separated list of directory names to ignore during indexing",
|
|
86
86
|
"INDEX_SOURCE": "Store full source code in graph database (for faster indexing use false, for better performance use true)",
|
|
87
87
|
"SCIP_INDEXER": "Use SCIP-based indexing for higher accuracy call/inheritance resolution (requires scip-<lang> tools installed)",
|
|
88
|
-
"SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,go,rust,java)",
|
|
88
|
+
"SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,javascript,go,rust,java,dart,cpp,c,csharp)",
|
|
89
89
|
"SKIP_EXTERNAL_RESOLUTION": "Skip resolution attempts for external library method calls (recommended for enterprise large Java/Spring codebases)",
|
|
90
90
|
"MAX_TOOL_RESPONSE_TOKENS": "Maximum tokens per MCP tool response (0 = unlimited). Truncates oversized payloads and appends a notice.",
|
|
91
91
|
"TOOL_RESULT_LIMITS": "JSON object mapping tool names to max result counts, e.g. {\"find_code\": 20, \"analyze_code_relationships\": 10}. Missing keys use built-in defaults.",
|
|
@@ -859,7 +859,7 @@ def resolve_context(
|
|
|
859
859
|
)
|
|
860
860
|
|
|
861
861
|
# --- 4. Global fallback ---
|
|
862
|
-
db = load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
862
|
+
db = os.getenv("CGC_RUNTIME_DB_TYPE") or load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
863
863
|
return ResolvedContext(
|
|
864
864
|
mode="global",
|
|
865
865
|
context_name="",
|
codegraphcontext/cli/main.py
CHANGED
|
@@ -300,7 +300,7 @@ def _load_credentials():
|
|
|
300
300
|
Step 2 skips duplicate loading when that file is the same path as the global file.
|
|
301
301
|
Arbitrary repo-root `.env` files are not loaded—only CodeGraphContext config paths.
|
|
302
302
|
"""
|
|
303
|
-
from dotenv import dotenv_values
|
|
303
|
+
from dotenv import dotenv_values, find_dotenv
|
|
304
304
|
from codegraphcontext.cli.config_manager import (
|
|
305
305
|
ensure_config_dir,
|
|
306
306
|
codegraphcontext_dotenv_at_cwd,
|
|
@@ -586,7 +586,7 @@ def bundle_export(
|
|
|
586
586
|
services = _initialize_services(context)
|
|
587
587
|
if not all(services[:3]):
|
|
588
588
|
return
|
|
589
|
-
db_manager,
|
|
589
|
+
db_manager, _, code_finder = services[:3]
|
|
590
590
|
|
|
591
591
|
try:
|
|
592
592
|
output_path = Path(output)
|
|
@@ -753,9 +753,19 @@ def load_shortcut(
|
|
|
753
753
|
# REGISTRY COMMAND GROUP - Browse and Download Bundles
|
|
754
754
|
# ============================================================================
|
|
755
755
|
|
|
756
|
-
registry_app = typer.Typer(
|
|
756
|
+
registry_app = typer.Typer(
|
|
757
|
+
help="Browse and download bundles from the registry",
|
|
758
|
+
invoke_without_command=True,
|
|
759
|
+
)
|
|
757
760
|
app.add_typer(registry_app, name="registry")
|
|
758
761
|
|
|
762
|
+
|
|
763
|
+
@registry_app.callback()
|
|
764
|
+
def registry_callback(ctx: typer.Context):
|
|
765
|
+
"""Browse and download bundles from the registry."""
|
|
766
|
+
if ctx.invoked_subcommand is None:
|
|
767
|
+
typer.echo(ctx.get_help())
|
|
768
|
+
|
|
759
769
|
@registry_app.command("list")
|
|
760
770
|
def registry_list(
|
|
761
771
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information including download URLs"),
|
|
@@ -940,11 +950,11 @@ def doctor():
|
|
|
940
950
|
elif default_db == "kuzudb":
|
|
941
951
|
from importlib.util import find_spec
|
|
942
952
|
|
|
943
|
-
if find_spec("
|
|
953
|
+
if find_spec("kuzu") is not None:
|
|
944
954
|
console.print(f" [green]✓[/green] KuzuDB is installed")
|
|
945
955
|
else:
|
|
946
956
|
console.print(f" [red]✗[/red] KuzuDB is not installed")
|
|
947
|
-
console.print(f" Run: pip install
|
|
957
|
+
console.print(f" Run: pip install kuzu")
|
|
948
958
|
all_checks_passed = False
|
|
949
959
|
else:
|
|
950
960
|
# FalkorDB
|
|
@@ -1199,7 +1209,7 @@ def report(
|
|
|
1199
1209
|
"""
|
|
1200
1210
|
_load_credentials()
|
|
1201
1211
|
output_path = Path(output) if output else Path.cwd() / "CGC_REPORT.md"
|
|
1202
|
-
db_manager, _, _ = _initialize_services(context)
|
|
1212
|
+
db_manager, _, _, _ = _initialize_services(context)
|
|
1203
1213
|
try:
|
|
1204
1214
|
from codegraphcontext.tools.report_generator import generate_report
|
|
1205
1215
|
report_text = generate_report(db_manager, output_path=output_path, include_java=java)
|
|
@@ -1374,6 +1384,18 @@ def find_by_name(
|
|
|
1374
1384
|
results.extend(variables)
|
|
1375
1385
|
results.extend(modules)
|
|
1376
1386
|
results.extend(imports)
|
|
1387
|
+
|
|
1388
|
+
# Also search Interface, Trait, Struct, Enum nodes (PHP, Rust, Go, etc.)
|
|
1389
|
+
with db_manager.get_driver().session() as session:
|
|
1390
|
+
for label in ['Interface', 'Trait', 'Struct', 'Enum']:
|
|
1391
|
+
res = session.run(
|
|
1392
|
+
f"MATCH (n:{label}) WHERE n.name = $name RETURN n.name as name, n.path as path, n.line_number as line_number",
|
|
1393
|
+
name=name
|
|
1394
|
+
)
|
|
1395
|
+
for record in res:
|
|
1396
|
+
row = dict(record)
|
|
1397
|
+
row['type'] = label
|
|
1398
|
+
results.append(row)
|
|
1377
1399
|
|
|
1378
1400
|
elif type.lower() == 'function':
|
|
1379
1401
|
results = code_finder.find_by_function_name(name, fuzzy_search=False)
|
|
@@ -1460,7 +1482,7 @@ def find_by_pattern(
|
|
|
1460
1482
|
if not case_sensitive:
|
|
1461
1483
|
query = """
|
|
1462
1484
|
MATCH (n)
|
|
1463
|
-
WHERE (n:Function OR n:Class OR n:Module OR n:Variable) AND toLower(n.name) CONTAINS toLower($pattern)
|
|
1485
|
+
WHERE (n:Function OR n:Class OR n:Module OR n:Variable OR n:Interface OR n:Trait OR n:Struct OR n:Enum) AND toLower(n.name) CONTAINS toLower($pattern)
|
|
1464
1486
|
RETURN
|
|
1465
1487
|
labels(n)[0] as type,
|
|
1466
1488
|
n.name as name,
|
|
@@ -1473,7 +1495,7 @@ def find_by_pattern(
|
|
|
1473
1495
|
else:
|
|
1474
1496
|
query = """
|
|
1475
1497
|
MATCH (n)
|
|
1476
|
-
WHERE (n:Function OR n:Class OR n:Module OR n:Variable) AND n.name CONTAINS $pattern
|
|
1498
|
+
WHERE (n:Function OR n:Class OR n:Module OR n:Variable OR n:Interface OR n:Trait OR n:Struct OR n:Enum) AND n.name CONTAINS $pattern
|
|
1477
1499
|
RETURN
|
|
1478
1500
|
labels(n)[0] as type,
|
|
1479
1501
|
n.name as name,
|
|
@@ -1978,6 +2000,65 @@ def analyze_chain(
|
|
|
1978
2000
|
finally:
|
|
1979
2001
|
db_manager.close_driver()
|
|
1980
2002
|
|
|
2003
|
+
@analyze_app.command("kotlin-call-audit")
|
|
2004
|
+
def analyze_kotlin_call_audit(
|
|
2005
|
+
repo_path: Optional[str] = typer.Option(None, "--repo-path", "-r", help="Limit audit to paths under this repository root"),
|
|
2006
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Maximum examples/top names to show"),
|
|
2007
|
+
json_output: bool = typer.Option(False, "--json", help="Print machine-readable JSON"),
|
|
2008
|
+
fail_on_ambiguity: bool = typer.Option(False, "--fail-on-ambiguity", help="Exit non-zero if any ambiguous Kotlin call groups are found"),
|
|
2009
|
+
context: Optional[str] = typer.Option(None, "--context", "-c", help="Specific context to use"),
|
|
2010
|
+
):
|
|
2011
|
+
"""
|
|
2012
|
+
Audit Kotlin function call edges for multi-target callsite ambiguity.
|
|
2013
|
+
|
|
2014
|
+
Example:
|
|
2015
|
+
cgc analyze kotlin-call-audit --context elrond-stable --fail-on-ambiguity
|
|
2016
|
+
cgc analyze kotlin-call-audit --json
|
|
2017
|
+
"""
|
|
2018
|
+
_load_credentials()
|
|
2019
|
+
services = _initialize_services(context)
|
|
2020
|
+
if not all(services[:3]):
|
|
2021
|
+
return
|
|
2022
|
+
db_manager, _, code_finder = services[:3]
|
|
2023
|
+
|
|
2024
|
+
try:
|
|
2025
|
+
result = code_finder.audit_kotlin_call_ambiguity(repo_path=repo_path, limit=limit)
|
|
2026
|
+
if json_output:
|
|
2027
|
+
console.print_json(json.dumps(result))
|
|
2028
|
+
else:
|
|
2029
|
+
console.print("\n[bold cyan]Kotlin CALLS ambiguity audit[/bold cyan]")
|
|
2030
|
+
summary = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
|
|
2031
|
+
summary.add_column("Metric", style="cyan")
|
|
2032
|
+
summary.add_column("Value", style="green")
|
|
2033
|
+
summary.add_row("Kotlin fn→fn CALLS edges", str(result["kotlin_fn_to_fn_edges"]))
|
|
2034
|
+
summary.add_row("Ambiguous groups", str(result["ambiguous_groups"]))
|
|
2035
|
+
summary.add_row("Ambiguous edges", str(result["ambiguous_edges"]))
|
|
2036
|
+
console.print(summary)
|
|
2037
|
+
|
|
2038
|
+
if result["examples"]:
|
|
2039
|
+
examples = Table(show_header=True, header_style="bold magenta", box=box.ROUNDED)
|
|
2040
|
+
examples.add_column("Callsite", style="cyan", overflow="fold")
|
|
2041
|
+
examples.add_column("Call", style="yellow", overflow="fold")
|
|
2042
|
+
examples.add_column("Targets", style="green", overflow="fold")
|
|
2043
|
+
for example in result["examples"]:
|
|
2044
|
+
targets = "\n".join(
|
|
2045
|
+
f"{target['context'] or ''}:{target['line_number']} {target['path']}"
|
|
2046
|
+
for target in example["targets"]
|
|
2047
|
+
)
|
|
2048
|
+
examples.add_row(
|
|
2049
|
+
f"{example.get('caller_name')} {example.get('caller_path')}:{example.get('call_line')}",
|
|
2050
|
+
str(example.get("full_call_name") or ""),
|
|
2051
|
+
targets,
|
|
2052
|
+
)
|
|
2053
|
+
console.print(examples)
|
|
2054
|
+
else:
|
|
2055
|
+
console.print("[green]No ambiguous Kotlin call groups found.[/green]")
|
|
2056
|
+
|
|
2057
|
+
if fail_on_ambiguity and result["ambiguous_groups"]:
|
|
2058
|
+
raise typer.Exit(1)
|
|
2059
|
+
finally:
|
|
2060
|
+
db_manager.close_driver()
|
|
2061
|
+
|
|
1981
2062
|
@analyze_app.command("deps")
|
|
1982
2063
|
def analyze_dependencies(
|
|
1983
2064
|
ctx: typer.Context,
|
|
@@ -2654,4 +2735,4 @@ def _write_datasource_graph(ingested: dict) -> None:
|
|
|
2654
2735
|
|
|
2655
2736
|
|
|
2656
2737
|
if __name__ == "__main__":
|
|
2657
|
-
app()
|
|
2738
|
+
app()
|
|
@@ -22,7 +22,7 @@ import importlib.util
|
|
|
22
22
|
def _is_kuzudb_available() -> bool:
|
|
23
23
|
"""Check if KùzuDB is installed."""
|
|
24
24
|
try:
|
|
25
|
-
return importlib.util.find_spec("
|
|
25
|
+
return importlib.util.find_spec("kuzu") is not None
|
|
26
26
|
except ImportError:
|
|
27
27
|
return False
|
|
28
28
|
|
|
@@ -78,7 +78,7 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
|
|
|
78
78
|
db_type = db_type.lower()
|
|
79
79
|
if db_type == 'kuzudb':
|
|
80
80
|
if not _is_kuzudb_available():
|
|
81
|
-
raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install
|
|
81
|
+
raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install kuzu'")
|
|
82
82
|
from .database_kuzu import KuzuDBManager
|
|
83
83
|
info_logger(f"Using KùzuDB (explicit) at {db_path or 'default path'}")
|
|
84
84
|
return KuzuDBManager(db_path=db_path)
|
|
@@ -168,7 +168,7 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
|
|
|
168
168
|
return NornicDBManager()
|
|
169
169
|
|
|
170
170
|
error_msg = "No database backend available.\n"
|
|
171
|
-
error_msg += "Recommended: Install KùzuDB for zero-config ('pip install
|
|
171
|
+
error_msg += "Recommended: Install KùzuDB for zero-config ('pip install kuzu')\n"
|
|
172
172
|
|
|
173
173
|
if platform.system() != "Windows":
|
|
174
174
|
error_msg += "Alternative: Install FalkorDB Lite ('pip install falkordblite')\n"
|
|
@@ -81,7 +81,7 @@ class KuzuDBManager:
|
|
|
81
81
|
info_logger("KùzuDB connection established and schema verified")
|
|
82
82
|
break
|
|
83
83
|
except ImportError:
|
|
84
|
-
error_logger("KùzuDB is not installed. Run 'pip install
|
|
84
|
+
error_logger("KùzuDB is not installed. Run 'pip install kuzu'")
|
|
85
85
|
raise ValueError("KùzuDB missing.")
|
|
86
86
|
except Exception as e:
|
|
87
87
|
if "lock" in str(e).lower() and attempt < max_retries - 1:
|
|
@@ -108,8 +108,8 @@ class KuzuDBManager:
|
|
|
108
108
|
("Directory", "path STRING, name STRING, PRIMARY KEY (path)"),
|
|
109
109
|
("Module", "name STRING, lang STRING, full_import_name STRING, PRIMARY KEY (name)"),
|
|
110
110
|
# For types with composite keys (name, path, line_number), we use a 'uid'
|
|
111
|
-
("Function", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, cyclomatic_complexity INT64, context STRING, context_type STRING, class_context STRING, is_dependency BOOLEAN, decorators STRING[], args STRING[], http_method STRING, http_path STRING, PRIMARY KEY (uid)"),
|
|
112
|
-
("Class", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, decorators STRING[], PRIMARY KEY (uid)"),
|
|
111
|
+
("Function", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, cyclomatic_complexity INT64, context STRING, context_type STRING, class_context STRING, class_context_line INT64, is_dependency BOOLEAN, decorators STRING[], args STRING[], http_method STRING, http_path STRING, PRIMARY KEY (uid)"),
|
|
112
|
+
("Class", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, node_type STRING, is_dependency BOOLEAN, decorators STRING[], PRIMARY KEY (uid)"),
|
|
113
113
|
("Variable", "uid STRING, name STRING, path STRING, line_number INT64, source STRING, docstring STRING, lang STRING, value STRING, context STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
114
114
|
("Trait", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
115
115
|
("Interface", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
@@ -132,9 +132,21 @@ class KuzuDBManager:
|
|
|
132
132
|
# or the rel table creation will fail silently, leading to runtime
|
|
133
133
|
# "Binder exception: Table CONTAINS does not exist".
|
|
134
134
|
("CONTAINS", "FROM File TO Function, FROM File TO Class, FROM File TO Variable, FROM File TO Trait, FROM File TO Interface, FROM `Macro` TO `Macro`, FROM File TO `Macro`, FROM File TO Struct, FROM File TO Enum, FROM File TO `Union`, FROM File TO Annotation, FROM File TO Record, FROM File TO `Property`, FROM Repository TO Directory, FROM Directory TO Directory, FROM Directory TO File, FROM Repository TO File, FROM Class TO Function, FROM Function TO Function", True),
|
|
135
|
-
("CALLS", "
|
|
135
|
+
("CALLS", """
|
|
136
|
+
FROM Function TO Function, FROM Function TO Class, FROM Function TO Interface, FROM Function TO Trait, FROM Function TO Struct, FROM Function TO Enum, FROM Function TO `Record`, FROM Function TO `Union`,
|
|
137
|
+
FROM Class TO Function, FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait, FROM Class TO Struct, FROM Class TO Enum, FROM Class TO `Record`, FROM Class TO `Union`,
|
|
138
|
+
FROM Interface TO Function, FROM Interface TO Class, FROM Interface TO Interface,
|
|
139
|
+
FROM File TO Function, FROM File TO Class, FROM File TO Interface, FROM File TO Trait, FROM File TO Struct, FROM File TO Enum, FROM File TO `Record`, FROM File TO `Union`,
|
|
140
|
+
line_number INT64, args STRING[], full_call_name STRING, confidence DOUBLE, resolution_tier INT64, confidence_label STRING, source STRING, resolution_method STRING, called_name STRING
|
|
141
|
+
""", True),
|
|
136
142
|
("IMPORTS", "FROM File TO Module, alias STRING, full_import_name STRING, imported_name STRING, line_number INT64", False),
|
|
137
|
-
("INHERITS", "
|
|
143
|
+
("INHERITS", """
|
|
144
|
+
FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait,
|
|
145
|
+
FROM `Record` TO `Record`, FROM `Record` TO Interface,
|
|
146
|
+
FROM Interface TO Interface, FROM Interface TO Trait,
|
|
147
|
+
FROM Struct TO Interface, FROM Struct TO Trait,
|
|
148
|
+
confidence_label STRING
|
|
149
|
+
""", True),
|
|
138
150
|
("HAS_PARAMETER", "FROM Function TO Parameter", False),
|
|
139
151
|
("INCLUDES", "FROM Class TO Module", False),
|
|
140
152
|
("IMPLEMENTS", "FROM Class TO Interface, FROM Struct TO Interface, FROM Record TO Interface", True),
|
|
@@ -171,12 +183,15 @@ class KuzuDBManager:
|
|
|
171
183
|
("Module", "full_import_name", "STRING"),
|
|
172
184
|
("IMPORTS", "full_import_name", "STRING"),
|
|
173
185
|
("IMPORTS", "imported_name", "STRING"),
|
|
174
|
-
# Freshness properties added to Repository in 0.4.
|
|
186
|
+
# Freshness properties added to Repository in 0.4.8
|
|
175
187
|
("Repository", "indexed_at", "STRING"),
|
|
176
188
|
("Repository", "commit_hash", "STRING"),
|
|
177
189
|
# Spring endpoint properties on Function
|
|
178
190
|
("Function", "http_method", "STRING"),
|
|
179
191
|
("Function", "http_path", "STRING"),
|
|
192
|
+
# Kotlin/JVM precision improvements
|
|
193
|
+
("Function", "class_context_line", "INT64"),
|
|
194
|
+
("Class", "node_type", "STRING"),
|
|
180
195
|
]
|
|
181
196
|
|
|
182
197
|
# REL TABLE GROUP migrations: KuzuDB creates sub-tables named
|
|
@@ -222,6 +237,7 @@ class KuzuDBManager:
|
|
|
222
237
|
continue
|
|
223
238
|
warning_logger(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
|
|
224
239
|
debug_log(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
|
|
240
|
+
raise RuntimeError("Kuzu Schema Migration Failed") from e
|
|
225
241
|
|
|
226
242
|
def close_driver(self):
|
|
227
243
|
"""Closes the connection."""
|
|
@@ -259,7 +275,7 @@ class KuzuDBManager:
|
|
|
259
275
|
import kuzu
|
|
260
276
|
return True, None
|
|
261
277
|
except ImportError:
|
|
262
|
-
return False, "KùzuDB is not installed. Run 'pip install
|
|
278
|
+
return False, "KùzuDB is not installed. Run 'pip install kuzu'"
|
|
263
279
|
|
|
264
280
|
class KuzuDriverWrapper:
|
|
265
281
|
def __init__(self, conn, query_lock: Optional[threading.RLock] = None):
|
|
@@ -470,8 +486,8 @@ class KuzuSessionWrapper:
|
|
|
470
486
|
'File': {'path', 'name', 'relative_path', 'package_name', 'is_dependency'},
|
|
471
487
|
'Directory': {'path', 'name'},
|
|
472
488
|
'Module': {'name', 'lang', 'full_import_name'},
|
|
473
|
-
'Function': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'cyclomatic_complexity', 'context', 'context_type', 'class_context', 'is_dependency', 'decorators', 'args', 'http_method', 'http_path'},
|
|
474
|
-
'Class': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency', 'decorators'},
|
|
489
|
+
'Function': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'cyclomatic_complexity', 'context', 'context_type', 'class_context', 'class_context_line', 'is_dependency', 'decorators', 'args', 'http_method', 'http_path'},
|
|
490
|
+
'Class': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'node_type', 'is_dependency', 'decorators'},
|
|
475
491
|
'Variable': {'uid', 'name', 'path', 'line_number', 'source', 'docstring', 'lang', 'value', 'context', 'is_dependency'},
|
|
476
492
|
'Trait': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
477
493
|
'Interface': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
@@ -701,6 +717,38 @@ class KuzuSessionWrapper:
|
|
|
701
717
|
for label in labels_to_escape:
|
|
702
718
|
query = re.sub(rf':{label}\b', f':`{label}`', query)
|
|
703
719
|
|
|
720
|
+
# Translate (n:Label1 OR n:Label2 ...) to label(n) IN ['Label1', 'Label2', ...]
|
|
721
|
+
def poly_replacer(match):
|
|
722
|
+
full_match = match.group(0)
|
|
723
|
+
var_name = match.group(1)
|
|
724
|
+
# Find all labels associated with this variable in the OR chain
|
|
725
|
+
labels = re.findall(rf'{var_name}:([a-zA-Z0-9_`]+)', full_match)
|
|
726
|
+
# Strip backticks from labels
|
|
727
|
+
labels = [l.strip('`') for l in labels]
|
|
728
|
+
return f"label({var_name}) IN {json.dumps(labels)}"
|
|
729
|
+
|
|
730
|
+
# Regex to match (n:Label1 OR n:Label2 OR n:Label3)
|
|
731
|
+
query = re.sub(r'\((\w+):[a-zA-Z0-9_`]+(?:\s+OR\s+\1:[a-zA-Z0-9_`]+)+\)', poly_replacer, query)
|
|
732
|
+
|
|
733
|
+
# Translate single WHERE n:Label to label(n) = 'Label'
|
|
734
|
+
# This is more complex because we don't want to match MATCH/MERGE
|
|
735
|
+
# For now, we only target where it appears after WHERE or AND/OR
|
|
736
|
+
def single_label_replacer(match):
|
|
737
|
+
prefix = match.group(1)
|
|
738
|
+
var_name = match.group(2)
|
|
739
|
+
label = match.group(3).strip('`')
|
|
740
|
+
return f"{prefix}label({var_name}) = '{label}'"
|
|
741
|
+
|
|
742
|
+
query = re.sub(r'(WHERE\s+|AND\s+|OR\s+|WHEN\s+)(\w+):([a-zA-Z0-9_`]+)', single_label_replacer, query, flags=re.IGNORECASE)
|
|
743
|
+
|
|
744
|
+
# Handle NOT n:Label → NOT label(n) = 'Label'
|
|
745
|
+
def not_label_replacer(match):
|
|
746
|
+
prefix = match.group(1)
|
|
747
|
+
var_name = match.group(2)
|
|
748
|
+
label_name = match.group(3).strip('`')
|
|
749
|
+
return f"{prefix}NOT label({var_name}) = '{label_name}'"
|
|
750
|
+
query = re.sub(r'(WHERE\s+|AND\s+|OR\s+)NOT\s+(\w+):([a-zA-Z0-9_`]+)', not_label_replacer, query, flags=re.IGNORECASE)
|
|
751
|
+
|
|
704
752
|
# 4. Polymorphic matches and label access
|
|
705
753
|
query = query.replace("labels(n)[0]", "label(n)")
|
|
706
754
|
|