codegraphcontext 0.4.7__py3-none-any.whl → 0.4.9__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 +57 -6
- codegraphcontext/cli/config_manager.py +13 -6
- codegraphcontext/cli/main.py +101 -10
- codegraphcontext/core/__init__.py +3 -3
- codegraphcontext/core/database_falkordb.py +2 -2
- codegraphcontext/core/database_kuzu.py +101 -14
- codegraphcontext/server.py +45 -0
- codegraphcontext/tools/code_finder.py +204 -42
- codegraphcontext/tools/graph_builder.py +65 -12
- codegraphcontext/tools/handlers/analysis_handlers.py +5 -4
- codegraphcontext/tools/indexing/discovery.py +26 -2
- codegraphcontext/tools/indexing/persistence/writer.py +314 -214
- codegraphcontext/tools/indexing/pipeline.py +17 -4
- codegraphcontext/tools/indexing/resolution/calls.py +2291 -118
- codegraphcontext/tools/indexing/resolution/inheritance.py +34 -16
- codegraphcontext/tools/indexing/schema.py +75 -46
- codegraphcontext/tools/indexing/scip_pipeline.py +69 -5
- codegraphcontext/tools/languages/c.py +86 -4
- codegraphcontext/tools/languages/cpp.py +109 -53
- codegraphcontext/tools/languages/csharp.py +23 -4
- codegraphcontext/tools/languages/css.py +2 -3
- codegraphcontext/tools/languages/dart.py +138 -80
- codegraphcontext/tools/languages/elixir.py +54 -9
- codegraphcontext/tools/languages/go.py +63 -2
- codegraphcontext/tools/languages/html.py +8 -8
- codegraphcontext/tools/languages/java.py +178 -19
- codegraphcontext/tools/languages/javascript.py +1 -0
- codegraphcontext/tools/languages/kotlin.py +1694 -141
- codegraphcontext/tools/languages/perl.py +26 -4
- codegraphcontext/tools/languages/php.py +24 -14
- codegraphcontext/tools/languages/python.py +7 -1
- codegraphcontext/tools/languages/ruby.py +20 -1
- codegraphcontext/tools/languages/rust.py +58 -39
- codegraphcontext/tools/languages/scala.py +19 -7
- codegraphcontext/tools/languages/swift.py +57 -53
- 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.9.dist-info}/METADATA +49 -87
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/RECORD +44 -43
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/WHEEL +0 -0
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.4.7.dist-info → codegraphcontext-0.4.9.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.
|
|
@@ -60,8 +87,9 @@ def _initialize_services(cli_context_flag: Optional[str] = None) -> tuple[Any, A
|
|
|
60
87
|
):
|
|
61
88
|
os.environ["DEFAULT_DATABASE"] = ctx.database
|
|
62
89
|
|
|
63
|
-
# Pass the exact DB path resolved from the context
|
|
64
|
-
|
|
90
|
+
# Pass the exact DB path resolved from the context, or the runtime override
|
|
91
|
+
runtime_path = os.getenv("CGC_RUNTIME_DB_PATH")
|
|
92
|
+
db_manager = get_database_manager(db_path=runtime_path or ctx.db_path)
|
|
65
93
|
except ValueError as e:
|
|
66
94
|
console.print(f"[bold red]Database Configuration Error:[/bold red] {e}")
|
|
67
95
|
return None, None, None, ctx
|
|
@@ -243,6 +271,7 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
243
271
|
asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
|
|
244
272
|
time_end = time.time()
|
|
245
273
|
elapsed = time_end - time_start
|
|
274
|
+
_print_call_resolution_diagnostics(graph_builder)
|
|
246
275
|
console.print(f"[green]Successfully finished indexing: {path} in {elapsed:.2f} seconds[/green]")
|
|
247
276
|
|
|
248
277
|
# Check if auto-watch is enabled
|
|
@@ -259,6 +288,7 @@ def index_helper(path: str, context: Optional[str] = None):
|
|
|
259
288
|
|
|
260
289
|
except Exception as e:
|
|
261
290
|
console.print(f"[bold red]An error occurred during indexing:[/bold red] {e}")
|
|
291
|
+
raise typer.Exit(code=1)
|
|
262
292
|
finally:
|
|
263
293
|
db_manager.close_driver()
|
|
264
294
|
|
|
@@ -289,6 +319,7 @@ def add_package_helper(package_name: str, language: str, context: Optional[str]
|
|
|
289
319
|
|
|
290
320
|
try:
|
|
291
321
|
asyncio.run(_run_index_with_progress(graph_builder, package_path, is_dependency=True, cgcignore_path=ctx.cgcignore_path))
|
|
322
|
+
_print_call_resolution_diagnostics(graph_builder)
|
|
292
323
|
console.print(f"[green]Successfully finished indexing package: {package_name}[/green]")
|
|
293
324
|
except Exception as e:
|
|
294
325
|
console.print(f"[bold red]An error occurred during package indexing:[/bold red] {e}")
|
|
@@ -354,9 +385,11 @@ def cypher_helper(query: str, context: Optional[str] = None):
|
|
|
354
385
|
|
|
355
386
|
db_manager, _, _, ctx = services
|
|
356
387
|
|
|
357
|
-
# Replicating safety checks from MCPServer
|
|
388
|
+
# Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
|
|
389
|
+
import re
|
|
358
390
|
forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
|
|
359
|
-
|
|
391
|
+
pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
|
|
392
|
+
if re.search(pattern, query, re.IGNORECASE):
|
|
360
393
|
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
361
394
|
db_manager.close_driver()
|
|
362
395
|
return
|
|
@@ -382,9 +415,11 @@ def cypher_helper_visual(query: str, context: Optional[str] = None):
|
|
|
382
415
|
|
|
383
416
|
db_manager, _, _, ctx = services
|
|
384
417
|
|
|
385
|
-
# Replicating safety checks from MCPServer
|
|
418
|
+
# Replicating safety checks from MCPServer (using word boundaries to avoid false positives like 'createEmail')
|
|
419
|
+
import re
|
|
386
420
|
forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
|
|
387
|
-
|
|
421
|
+
pattern = r'\b(' + '|'.join(forbidden_keywords) + r')\b'
|
|
422
|
+
if re.search(pattern, query, re.IGNORECASE):
|
|
388
423
|
console.print("[bold red]Error: This command only supports read-only queries.[/bold red]")
|
|
389
424
|
db_manager.close_driver()
|
|
390
425
|
return
|
|
@@ -540,9 +575,11 @@ def reindex_helper(path: str, context: Optional[str] = None):
|
|
|
540
575
|
asyncio.run(_run_index_with_progress(graph_builder, path_obj, is_dependency=False, cgcignore_path=ctx.cgcignore_path))
|
|
541
576
|
time_end = time.time()
|
|
542
577
|
elapsed = time_end - time_start
|
|
578
|
+
_print_call_resolution_diagnostics(graph_builder)
|
|
543
579
|
console.print(f"[green]Successfully re-indexed: {path} in {elapsed:.2f} seconds[/green]")
|
|
544
580
|
except Exception as e:
|
|
545
581
|
console.print(f"[bold red]An error occurred during re-indexing:[/bold red] {e}")
|
|
582
|
+
raise typer.Exit(code=1)
|
|
546
583
|
finally:
|
|
547
584
|
db_manager.close_driver()
|
|
548
585
|
|
|
@@ -669,6 +706,12 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
669
706
|
class_count = session.run("MATCH (c:Class) RETURN count(c) as c").single()["c"]
|
|
670
707
|
module_count = session.run("MATCH (m:Module) RETURN count(m) as c").single()["c"]
|
|
671
708
|
|
|
709
|
+
# Extended node types (PHP, Rust, Go, etc.)
|
|
710
|
+
interface_count = session.run("MATCH (i:Interface) RETURN count(i) as c").single()["c"]
|
|
711
|
+
trait_count = session.run("MATCH (t:Trait) RETURN count(t) as c").single()["c"]
|
|
712
|
+
struct_count = session.run("MATCH (s:Struct) RETURN count(s) as c").single()["c"]
|
|
713
|
+
enum_count = session.run("MATCH (e:Enum) RETURN count(e) as c").single()["c"]
|
|
714
|
+
|
|
672
715
|
table = Table(show_header=True, header_style="bold magenta")
|
|
673
716
|
table.add_column("Metric", style="cyan")
|
|
674
717
|
table.add_column("Count", style="green", justify="right")
|
|
@@ -677,6 +720,14 @@ def stats_helper(path: str = None, context: Optional[str] = None):
|
|
|
677
720
|
table.add_row("Files", str(file_count))
|
|
678
721
|
table.add_row("Functions", str(func_count))
|
|
679
722
|
table.add_row("Classes", str(class_count))
|
|
723
|
+
if interface_count > 0:
|
|
724
|
+
table.add_row("Interfaces", str(interface_count))
|
|
725
|
+
if trait_count > 0:
|
|
726
|
+
table.add_row("Traits", str(trait_count))
|
|
727
|
+
if struct_count > 0:
|
|
728
|
+
table.add_row("Structs", str(struct_count))
|
|
729
|
+
if enum_count > 0:
|
|
730
|
+
table.add_row("Enums", str(enum_count))
|
|
680
731
|
table.add_row("Modules", str(module_count))
|
|
681
732
|
|
|
682
733
|
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.",
|
|
@@ -634,10 +634,17 @@ def _default_global_db_path(database: str) -> str:
|
|
|
634
634
|
"""Return the canonical DB path for the global context.
|
|
635
635
|
|
|
636
636
|
New layout: ``~/.codegraphcontext/global/db/<backend>/``
|
|
637
|
-
For backward-compat,
|
|
637
|
+
For backward-compat, we check:
|
|
638
|
+
1. FALKORDB_PATH in config (if database is falkordb)
|
|
639
|
+
2. Legacy flat path
|
|
640
|
+
3. New layout default
|
|
638
641
|
"""
|
|
639
|
-
if database == "falkordb"
|
|
640
|
-
|
|
642
|
+
if database == "falkordb":
|
|
643
|
+
custom_path = load_config().get("FALKORDB_PATH")
|
|
644
|
+
if custom_path:
|
|
645
|
+
return str(Path(custom_path).resolve())
|
|
646
|
+
if _LEGACY_FALKORDB_PATH.exists():
|
|
647
|
+
return str(_LEGACY_FALKORDB_PATH)
|
|
641
648
|
return str(CONFIG_DIR / "global" / "db" / database)
|
|
642
649
|
|
|
643
650
|
|
|
@@ -859,7 +866,7 @@ def resolve_context(
|
|
|
859
866
|
)
|
|
860
867
|
|
|
861
868
|
# --- 4. Global fallback ---
|
|
862
|
-
db = load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
869
|
+
db = os.getenv("CGC_RUNTIME_DB_TYPE") or load_config().get("DEFAULT_DATABASE", "falkordb")
|
|
863
870
|
return ResolvedContext(
|
|
864
871
|
mode="global",
|
|
865
872
|
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,
|
|
@@ -2471,12 +2552,22 @@ def main(
|
|
|
2471
2552
|
"-h",
|
|
2472
2553
|
help="[Root-level only] Show help and exit",
|
|
2473
2554
|
is_eager=True,
|
|
2474
|
-
),
|
|
2555
|
+
),
|
|
2556
|
+
db_path: Optional[str] = typer.Option(
|
|
2557
|
+
None,
|
|
2558
|
+
"--path",
|
|
2559
|
+
"--db-path",
|
|
2560
|
+
help="[Global] Temporarily override database path (for local DBs like KuzuDB)"
|
|
2561
|
+
),
|
|
2475
2562
|
):
|
|
2476
2563
|
"""
|
|
2477
2564
|
Main entry point for the cgc CLI application.
|
|
2478
2565
|
If no subcommand is provided, it displays a welcome message with instructions.
|
|
2479
2566
|
"""
|
|
2567
|
+
if db_path:
|
|
2568
|
+
os.environ["CGC_RUNTIME_DB_PATH"] = db_path
|
|
2569
|
+
if database:
|
|
2570
|
+
os.environ["CGC_RUNTIME_DB_TYPE"] = database
|
|
2480
2571
|
# Initialize context object for sharing state with subcommands
|
|
2481
2572
|
ctx.ensure_object(dict)
|
|
2482
2573
|
|
|
@@ -2654,4 +2745,4 @@ def _write_datasource_graph(ingested: dict) -> None:
|
|
|
2654
2745
|
|
|
2655
2746
|
|
|
2656
2747
|
if __name__ == "__main__":
|
|
2657
|
-
app()
|
|
2748
|
+
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"
|
|
@@ -335,7 +335,7 @@ class FalkorDBDriverWrapper:
|
|
|
335
335
|
def __init__(self, graph):
|
|
336
336
|
self.graph = graph
|
|
337
337
|
|
|
338
|
-
def session(self):
|
|
338
|
+
def session(self, **kwargs):
|
|
339
339
|
"""Returns a session-like object for FalkorDB."""
|
|
340
340
|
return FalkorDBSessionWrapper(self.graph)
|
|
341
341
|
|
|
@@ -377,7 +377,7 @@ class FalkorDBSessionWrapper:
|
|
|
377
377
|
except Exception as e:
|
|
378
378
|
# Ignore errors about existing constraints/indexes
|
|
379
379
|
error_msg = str(e).lower()
|
|
380
|
-
if "already exists" in error_msg or "already created" in error_msg:
|
|
380
|
+
if "already exists" in error_msg or "already created" in error_msg or "already indexed" in error_msg:
|
|
381
381
|
return FalkorDBResultWrapper(None)
|
|
382
382
|
|
|
383
383
|
error_logger(f"FalkorDB query failed: {query[:100]}... Error: {e}")
|
|
@@ -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:
|
|
@@ -106,10 +106,10 @@ class KuzuDBManager:
|
|
|
106
106
|
("Repository", "path STRING, name STRING, is_dependency BOOLEAN, indexed_at STRING, commit_hash STRING, PRIMARY KEY (path)"),
|
|
107
107
|
("File", "path STRING, name STRING, relative_path STRING, package_name STRING, is_dependency BOOLEAN, PRIMARY KEY (path)"),
|
|
108
108
|
("Directory", "path STRING, name STRING, PRIMARY KEY (path)"),
|
|
109
|
-
("Module", "name STRING, lang STRING, full_import_name STRING, PRIMARY KEY (name)"),
|
|
109
|
+
("Module", "name STRING, lang STRING, full_import_name STRING, path STRING, line_number INT64, 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)"),
|
|
@@ -120,7 +120,12 @@ class KuzuDBManager:
|
|
|
120
120
|
("Annotation", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
121
121
|
("Record", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
122
122
|
("Property", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
123
|
-
("Parameter", "uid STRING, name STRING, path STRING, function_line_number INT64, PRIMARY KEY (uid)")
|
|
123
|
+
("Parameter", "uid STRING, name STRING, path STRING, function_line_number INT64, PRIMARY KEY (uid)"),
|
|
124
|
+
("Mixin", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
125
|
+
("Extension", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
126
|
+
("Object", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
127
|
+
("DbTable", "name STRING, fqn STRING, datasource_name STRING, path STRING, PRIMARY KEY (name)"),
|
|
128
|
+
("ExternalClass", "name STRING, path STRING, PRIMARY KEY (name)")
|
|
124
129
|
]
|
|
125
130
|
|
|
126
131
|
# rel_tables: list of (table_name, schema, use_group)
|
|
@@ -131,14 +136,57 @@ class KuzuDBManager:
|
|
|
131
136
|
# keywords in CREATE REL TABLE statements. We must escape them with backticks
|
|
132
137
|
# or the rel table creation will fail silently, leading to runtime
|
|
133
138
|
# "Binder exception: Table CONTAINS does not exist".
|
|
134
|
-
("CONTAINS", "
|
|
135
|
-
|
|
139
|
+
("CONTAINS", """
|
|
140
|
+
FROM File TO Function, FROM File TO Class, FROM File TO Variable, FROM File TO Trait, FROM File TO Interface,
|
|
141
|
+
FROM File TO `Macro`, FROM File TO Struct, FROM File TO Enum, FROM File TO `Union`, FROM File TO Annotation,
|
|
142
|
+
FROM File TO Record, FROM File TO `Property`, FROM File TO Mixin, FROM File TO Extension, FROM File TO Module,
|
|
143
|
+
FROM File TO Object,
|
|
144
|
+
FROM Repository TO Directory, FROM Directory TO Directory, FROM Directory TO File, FROM Repository TO File,
|
|
145
|
+
FROM Class TO Function, FROM Module TO Function, FROM Interface TO Function, FROM Struct TO Function,
|
|
146
|
+
FROM Record TO Function, FROM Trait TO Function, FROM Object TO Function, FROM Mixin TO Function,
|
|
147
|
+
FROM Extension TO Function, FROM Class TO Class, FROM Class TO Interface, FROM Class TO Struct,
|
|
148
|
+
FROM Class TO Variable, FROM Module TO Class, FROM Module TO Module, FROM `Macro` TO `Macro`, FROM Function TO Function
|
|
149
|
+
""", True),
|
|
150
|
+
("CALLS", """
|
|
151
|
+
FROM Function TO Function, FROM Function TO Class, FROM Function TO Interface, FROM Function TO Trait,
|
|
152
|
+
FROM Function TO Struct, FROM Function TO Enum, FROM Function TO Record, FROM Function TO `Union`,
|
|
153
|
+
FROM Function TO Mixin, FROM Function TO Extension, FROM Function TO Object,
|
|
154
|
+
FROM Class TO Function, FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait,
|
|
155
|
+
FROM Class TO Struct, FROM Class TO Enum, FROM Class TO Record, FROM Class TO `Union`,
|
|
156
|
+
FROM Interface TO Function, FROM Interface TO Class, FROM Interface TO Interface,
|
|
157
|
+
FROM Trait TO Function, FROM Trait TO Class, FROM Trait TO Interface,
|
|
158
|
+
FROM Mixin TO Function, FROM Mixin TO Class, FROM Mixin TO Interface,
|
|
159
|
+
FROM Extension TO Function, FROM Extension TO Class, FROM Extension TO Interface,
|
|
160
|
+
FROM Object TO Function, FROM Object TO Class, FROM Object TO Interface,
|
|
161
|
+
FROM `Union` TO Function, FROM `Union` TO Class, FROM `Union` TO Interface,
|
|
162
|
+
FROM `Macro` TO Function, FROM `Macro` TO Class, FROM `Macro` TO Interface,
|
|
163
|
+
FROM File TO Function, FROM File TO Class, FROM File TO Interface, FROM File TO Trait,
|
|
164
|
+
FROM File TO Struct, FROM File TO Enum, FROM File TO Record, FROM File TO `Union`,
|
|
165
|
+
FROM Variable TO Function, FROM Variable TO Class, FROM Variable TO Interface,
|
|
166
|
+
line_number INT64, args STRING[], full_call_name STRING, confidence DOUBLE, resolution_tier INT64,
|
|
167
|
+
confidence_label STRING, source STRING, resolution_method STRING, called_name STRING
|
|
168
|
+
""", True),
|
|
136
169
|
("IMPORTS", "FROM File TO Module, alias STRING, full_import_name STRING, imported_name STRING, line_number INT64", False),
|
|
137
|
-
("INHERITS", "
|
|
170
|
+
("INHERITS", """
|
|
171
|
+
FROM Class TO Class, FROM Class TO Interface, FROM Class TO Trait, FROM Class TO Mixin, FROM Class TO Extension, FROM Class TO ExternalClass, FROM Class TO Struct, FROM Class TO Enum, FROM Class TO `Union`, FROM Class TO Record, FROM Class TO Object,
|
|
172
|
+
FROM Trait TO Trait, FROM Trait TO Interface, FROM Trait TO ExternalClass, FROM Trait TO Class,
|
|
173
|
+
FROM Interface TO Interface, FROM Interface TO Trait, FROM Interface TO ExternalClass, FROM Interface TO Class,
|
|
174
|
+
FROM Struct TO Interface, FROM Struct TO Trait, FROM Struct TO ExternalClass, FROM Struct TO Struct, FROM Struct TO Class,
|
|
175
|
+
FROM Record TO Record, FROM Record TO Interface, FROM Record TO ExternalClass, FROM Record TO Class, FROM Record TO Struct,
|
|
176
|
+
FROM Mixin TO Mixin, FROM Mixin TO Interface, FROM Mixin TO ExternalClass, FROM Mixin TO Class,
|
|
177
|
+
FROM Extension TO Extension, FROM Extension TO Interface, FROM Extension TO ExternalClass, FROM Extension TO Class,
|
|
178
|
+
FROM Enum TO Class, FROM Enum TO Interface, FROM Enum TO ExternalClass, FROM Enum TO Enum,
|
|
179
|
+
FROM `Union` TO Class, FROM `Union` TO Interface, FROM `Union` TO ExternalClass, FROM `Union` TO `Union`,
|
|
180
|
+
FROM Module TO Module, FROM Module TO ExternalClass, FROM Object TO Object, FROM Object TO ExternalClass, FROM Object TO Class,
|
|
181
|
+
confidence_label STRING
|
|
182
|
+
""", True),
|
|
138
183
|
("HAS_PARAMETER", "FROM Function TO Parameter", False),
|
|
139
184
|
("INCLUDES", "FROM Class TO Module", False),
|
|
140
|
-
("IMPLEMENTS", "FROM Class TO Interface, FROM Struct TO Interface, FROM Record TO Interface", True),
|
|
141
|
-
("INJECTS", "FROM Class TO Class, field_name STRING, inject_line INT64, confidence_label STRING", False)
|
|
185
|
+
("IMPLEMENTS", "FROM Class TO Interface, FROM Struct TO Interface, FROM Record TO Interface, FROM Mixin TO Interface, FROM Extension TO Interface, FROM Enum TO Interface, FROM Object TO Interface, FROM `Union` TO Interface, FROM Trait TO Interface", True),
|
|
186
|
+
("INJECTS", "FROM Class TO Class, field_name STRING, inject_line INT64, confidence_label STRING", False),
|
|
187
|
+
("MAPS_TO", "FROM Class TO DbTable, datastore STRING, line_number INT64", False),
|
|
188
|
+
("READS", "FROM Function TO DbTable, line_number INT64", False),
|
|
189
|
+
("WRITES", "FROM Function TO DbTable, line_number INT64", False)
|
|
142
190
|
]
|
|
143
191
|
|
|
144
192
|
for table_name, schema in node_tables:
|
|
@@ -169,14 +217,20 @@ class KuzuDBManager:
|
|
|
169
217
|
simple_migrations = [
|
|
170
218
|
("File", "package_name", "STRING"),
|
|
171
219
|
("Module", "full_import_name", "STRING"),
|
|
220
|
+
("Module", "path", "STRING"),
|
|
221
|
+
("Module", "line_number", "INT64"),
|
|
222
|
+
("DbTable", "path", "STRING"),
|
|
223
|
+
("ExternalClass", "path", "STRING"),
|
|
172
224
|
("IMPORTS", "full_import_name", "STRING"),
|
|
173
225
|
("IMPORTS", "imported_name", "STRING"),
|
|
174
|
-
# Freshness properties added to Repository in 0.4.7
|
|
175
226
|
("Repository", "indexed_at", "STRING"),
|
|
176
227
|
("Repository", "commit_hash", "STRING"),
|
|
177
228
|
# Spring endpoint properties on Function
|
|
178
229
|
("Function", "http_method", "STRING"),
|
|
179
230
|
("Function", "http_path", "STRING"),
|
|
231
|
+
# Kotlin/JVM precision improvements
|
|
232
|
+
("Function", "class_context_line", "INT64"),
|
|
233
|
+
("Class", "node_type", "STRING"),
|
|
180
234
|
]
|
|
181
235
|
|
|
182
236
|
# REL TABLE GROUP migrations: KuzuDB creates sub-tables named
|
|
@@ -222,6 +276,7 @@ class KuzuDBManager:
|
|
|
222
276
|
continue
|
|
223
277
|
warning_logger(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
|
|
224
278
|
debug_log(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
|
|
279
|
+
raise RuntimeError("Kuzu Schema Migration Failed") from e
|
|
225
280
|
|
|
226
281
|
def close_driver(self):
|
|
227
282
|
"""Closes the connection."""
|
|
@@ -259,7 +314,7 @@ class KuzuDBManager:
|
|
|
259
314
|
import kuzu
|
|
260
315
|
return True, None
|
|
261
316
|
except ImportError:
|
|
262
|
-
return False, "KùzuDB is not installed. Run 'pip install
|
|
317
|
+
return False, "KùzuDB is not installed. Run 'pip install kuzu'"
|
|
263
318
|
|
|
264
319
|
class KuzuDriverWrapper:
|
|
265
320
|
def __init__(self, conn, query_lock: Optional[threading.RLock] = None):
|
|
@@ -470,8 +525,8 @@ class KuzuSessionWrapper:
|
|
|
470
525
|
'File': {'path', 'name', 'relative_path', 'package_name', 'is_dependency'},
|
|
471
526
|
'Directory': {'path', 'name'},
|
|
472
527
|
'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'},
|
|
528
|
+
'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'},
|
|
529
|
+
'Class': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'node_type', 'is_dependency', 'decorators'},
|
|
475
530
|
'Variable': {'uid', 'name', 'path', 'line_number', 'source', 'docstring', 'lang', 'value', 'context', 'is_dependency'},
|
|
476
531
|
'Trait': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
477
532
|
'Interface': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
@@ -701,6 +756,38 @@ class KuzuSessionWrapper:
|
|
|
701
756
|
for label in labels_to_escape:
|
|
702
757
|
query = re.sub(rf':{label}\b', f':`{label}`', query)
|
|
703
758
|
|
|
759
|
+
# Translate (n:Label1 OR n:Label2 ...) to label(n) IN ['Label1', 'Label2', ...]
|
|
760
|
+
def poly_replacer(match):
|
|
761
|
+
full_match = match.group(0)
|
|
762
|
+
var_name = match.group(1)
|
|
763
|
+
# Find all labels associated with this variable in the OR chain
|
|
764
|
+
labels = re.findall(rf'{var_name}:([a-zA-Z0-9_`]+)', full_match)
|
|
765
|
+
# Strip backticks from labels
|
|
766
|
+
labels = [l.strip('`') for l in labels]
|
|
767
|
+
return f"label({var_name}) IN {json.dumps(labels)}"
|
|
768
|
+
|
|
769
|
+
# Regex to match (n:Label1 OR n:Label2 OR n:Label3)
|
|
770
|
+
query = re.sub(r'\((\w+):[a-zA-Z0-9_`]+(?:\s+OR\s+\1:[a-zA-Z0-9_`]+)+\)', poly_replacer, query)
|
|
771
|
+
|
|
772
|
+
# Translate single WHERE n:Label to label(n) = 'Label'
|
|
773
|
+
# This is more complex because we don't want to match MATCH/MERGE
|
|
774
|
+
# For now, we only target where it appears after WHERE or AND/OR
|
|
775
|
+
def single_label_replacer(match):
|
|
776
|
+
prefix = match.group(1)
|
|
777
|
+
var_name = match.group(2)
|
|
778
|
+
label = match.group(3).strip('`')
|
|
779
|
+
return f"{prefix}label({var_name}) = '{label}'"
|
|
780
|
+
|
|
781
|
+
query = re.sub(r'(WHERE\s+|AND\s+|OR\s+|WHEN\s+)(\w+):([a-zA-Z0-9_`]+)', single_label_replacer, query, flags=re.IGNORECASE)
|
|
782
|
+
|
|
783
|
+
# Handle NOT n:Label → NOT label(n) = 'Label'
|
|
784
|
+
def not_label_replacer(match):
|
|
785
|
+
prefix = match.group(1)
|
|
786
|
+
var_name = match.group(2)
|
|
787
|
+
label_name = match.group(3).strip('`')
|
|
788
|
+
return f"{prefix}NOT label({var_name}) = '{label_name}'"
|
|
789
|
+
query = re.sub(r'(WHERE\s+|AND\s+|OR\s+)NOT\s+(\w+):([a-zA-Z0-9_`]+)', not_label_replacer, query, flags=re.IGNORECASE)
|
|
790
|
+
|
|
704
791
|
# 4. Polymorphic matches and label access
|
|
705
792
|
query = query.replace("labels(n)[0]", "label(n)")
|
|
706
793
|
|