codegraph-py 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +12 -0
- codegraph/__main__.py +6 -0
- codegraph/cli.py +911 -0
- codegraph/codegraph.py +618 -0
- codegraph/context/__init__.py +216 -0
- codegraph/db/__init__.py +11 -0
- codegraph/db/connection.py +163 -0
- codegraph/db/queries.py +752 -0
- codegraph/db/schema.sql +151 -0
- codegraph/directory.py +160 -0
- codegraph/errors.py +101 -0
- codegraph/extraction/__init__.py +956 -0
- codegraph/extraction/languages/__init__.py +64 -0
- codegraph/extraction/languages/base.py +132 -0
- codegraph/extraction/languages/c_cfg.py +53 -0
- codegraph/extraction/languages/cpp_cfg.py +60 -0
- codegraph/extraction/languages/dart_cfg.py +53 -0
- codegraph/extraction/languages/go_cfg.py +70 -0
- codegraph/extraction/languages/java_cfg.py +62 -0
- codegraph/extraction/languages/javascript_cfg.py +84 -0
- codegraph/extraction/languages/kotlin_cfg.py +52 -0
- codegraph/extraction/languages/lua_cfg.py +65 -0
- codegraph/extraction/languages/python_cfg.py +75 -0
- codegraph/extraction/languages/ruby_cfg.py +48 -0
- codegraph/extraction/languages/rust_cfg.py +68 -0
- codegraph/extraction/languages/scala_cfg.py +59 -0
- codegraph/extraction/languages/swift_cfg.py +64 -0
- codegraph/extraction/languages/typescript_cfg.py +89 -0
- codegraph/extraction/tree_sitter_extractor.py +689 -0
- codegraph/graph/__init__.py +685 -0
- codegraph/installer/__init__.py +6 -0
- codegraph/mcp/__init__.py +668 -0
- codegraph/project_config.py +191 -0
- codegraph/resolution/__init__.py +337 -0
- codegraph/search/__init__.py +653 -0
- codegraph/sync/__init__.py +204 -0
- codegraph/types.py +334 -0
- codegraph/ui/__init__.py +11 -0
- codegraph/utils.py +218 -0
- codegraph_py-1.0.0.dist-info/METADATA +238 -0
- codegraph_py-1.0.0.dist-info/RECORD +44 -0
- codegraph_py-1.0.0.dist-info/WHEEL +5 -0
- codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
- codegraph_py-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeGraph Project Configuration
|
|
3
|
+
|
|
4
|
+
Manages codegraph.json configuration file reading and writing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from codegraph import errors
|
|
14
|
+
|
|
15
|
+
CONFIG_FILE = 'codegraph.json'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Config Types
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
class ProjectConfig:
|
|
23
|
+
"""Project configuration loaded from codegraph.json."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
project_root: str,
|
|
28
|
+
include_patterns: Optional[List[str]] = None,
|
|
29
|
+
exclude_patterns: Optional[List[str]] = None,
|
|
30
|
+
include_ignored_patterns: Optional[List[str]] = None,
|
|
31
|
+
extension_overrides: Optional[Dict[str, str]] = None,
|
|
32
|
+
):
|
|
33
|
+
self.project_root = project_root
|
|
34
|
+
self.include_patterns = include_patterns or []
|
|
35
|
+
self.exclude_patterns = exclude_patterns or []
|
|
36
|
+
self.include_ignored_patterns = include_ignored_patterns or []
|
|
37
|
+
self.extension_overrides = extension_overrides or {}
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
40
|
+
"""Convert config to dictionary."""
|
|
41
|
+
return {
|
|
42
|
+
'include_patterns': self.include_patterns,
|
|
43
|
+
'exclude_patterns': self.exclude_patterns,
|
|
44
|
+
'include_ignored_patterns': self.include_ignored_patterns,
|
|
45
|
+
'extension_overrides': self.extension_overrides,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_dict(cls, project_root: str, data: Dict[str, Any]) -> ProjectConfig:
|
|
50
|
+
"""Create config from dictionary."""
|
|
51
|
+
return cls(
|
|
52
|
+
project_root=project_root,
|
|
53
|
+
include_patterns=data.get('include_patterns'),
|
|
54
|
+
exclude_patterns=data.get('exclude_patterns'),
|
|
55
|
+
include_ignored_patterns=data.get('include_ignored_patterns'),
|
|
56
|
+
extension_overrides=data.get('extension_overrides'),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# Config File Path
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
def get_config_path(project_root: str) -> str:
|
|
65
|
+
"""Get the path to the codegraph.json config file."""
|
|
66
|
+
return os.path.join(project_root, CONFIG_FILE)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def config_exists(project_root: str) -> bool:
|
|
70
|
+
"""Check if codegraph.json exists in the project root."""
|
|
71
|
+
return os.path.isfile(get_config_path(project_root))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# =============================================================================
|
|
75
|
+
# Load / Save
|
|
76
|
+
# =============================================================================
|
|
77
|
+
|
|
78
|
+
def load_config(project_root: str) -> ProjectConfig:
|
|
79
|
+
"""Load the codegraph.json configuration file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
project_root: Path to the project root directory.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
ProjectConfig object with the loaded configuration.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ConfigError: If the config file cannot be read or parsed.
|
|
89
|
+
"""
|
|
90
|
+
config_path = get_config_path(project_root)
|
|
91
|
+
|
|
92
|
+
if not os.path.isfile(config_path):
|
|
93
|
+
return ProjectConfig(project_root=project_root)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
97
|
+
data = json.load(f)
|
|
98
|
+
except json.JSONDecodeError as e:
|
|
99
|
+
raise errors.ConfigError(
|
|
100
|
+
f"Failed to parse {CONFIG_FILE}: {e}"
|
|
101
|
+
) from e
|
|
102
|
+
except OSError as e:
|
|
103
|
+
raise errors.ConfigError(
|
|
104
|
+
f"Failed to read {CONFIG_FILE}: {e}"
|
|
105
|
+
) from e
|
|
106
|
+
|
|
107
|
+
return ProjectConfig.from_dict(project_root, data)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def save_config(project_root: str, config: ProjectConfig) -> None:
|
|
111
|
+
"""Save the configuration to codegraph.json.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
project_root: Path to the project root directory.
|
|
115
|
+
config: ProjectConfig object to save.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ConfigError: If the config file cannot be written.
|
|
119
|
+
"""
|
|
120
|
+
config_path = get_config_path(project_root)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
124
|
+
json.dump(config.to_dict(), f, indent=2)
|
|
125
|
+
f.write('\n')
|
|
126
|
+
except OSError as e:
|
|
127
|
+
raise errors.ConfigError(
|
|
128
|
+
f"Failed to write {CONFIG_FILE}: {e}"
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# =============================================================================
|
|
133
|
+
# Pattern Loaders
|
|
134
|
+
# =============================================================================
|
|
135
|
+
|
|
136
|
+
def load_exclude_patterns(project_root: str) -> List[str]:
|
|
137
|
+
"""Load exclude patterns from codegraph.json.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
project_root: Path to the project root directory.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of exclude patterns (glob patterns).
|
|
144
|
+
"""
|
|
145
|
+
config = load_config(project_root)
|
|
146
|
+
return config.exclude_patterns
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def load_include_patterns(project_root: str) -> List[str]:
|
|
150
|
+
"""Load include patterns from codegraph.json.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
project_root: Path to the project root directory.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of include patterns (glob patterns).
|
|
157
|
+
"""
|
|
158
|
+
config = load_config(project_root)
|
|
159
|
+
return config.include_patterns
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def load_include_ignored_patterns(project_root: str) -> List[str]:
|
|
163
|
+
"""Load include_ignored_patterns from codegraph.json.
|
|
164
|
+
|
|
165
|
+
These patterns specify files to include even if they would
|
|
166
|
+
normally be ignored (e.g., node_modules, .git files).
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
project_root: Path to the project root directory.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of include_ignored patterns (glob patterns).
|
|
173
|
+
"""
|
|
174
|
+
config = load_config(project_root)
|
|
175
|
+
return config.include_ignored_patterns
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def load_extension_overrides(project_root: str) -> Dict[str, str]:
|
|
179
|
+
"""Load extension overrides from codegraph.json.
|
|
180
|
+
|
|
181
|
+
Extension overrides allow mapping file extensions to specific
|
|
182
|
+
languages (e.g., {".inc": "php"}).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
project_root: Path to the project root directory.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Dictionary mapping file extensions to language names.
|
|
189
|
+
"""
|
|
190
|
+
config = load_config(project_root)
|
|
191
|
+
return config.extension_overrides
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeGraph Resolution Module
|
|
3
|
+
|
|
4
|
+
Reference resolver for connecting symbol usages to their definitions.
|
|
5
|
+
Strategy:
|
|
6
|
+
Phase 1 — Build import map (module name → project files)
|
|
7
|
+
Phase 2 — Resolve import edges (module → file)
|
|
8
|
+
Phase 3 — Resolve call references via import-aware name matching
|
|
9
|
+
Phase 4 — Fallback to global name matching
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, List, Optional, Set, Tuple, Any, Callable
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
from codegraph.types import Node, Edge, UnresolvedReference
|
|
20
|
+
from codegraph.db.queries import QueryBuilder
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ResolutionResult:
|
|
25
|
+
"""Result of a resolution pass."""
|
|
26
|
+
resolved_count: int = 0
|
|
27
|
+
remaining_count: int = 0
|
|
28
|
+
edges_created: int = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ImportEntry:
|
|
33
|
+
"""A single import entry for a file."""
|
|
34
|
+
name: str # imported name (e.g. 'join')
|
|
35
|
+
qualified_name: str # full qualified (e.g. 'os.path.join')
|
|
36
|
+
module: str # module part (e.g. 'os.path')
|
|
37
|
+
node_id: str # import node ID in graph
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ReferenceResolver:
|
|
41
|
+
"""Resolves unresolved references by matching names to defined symbols."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, project_root: str, queries: QueryBuilder):
|
|
44
|
+
self._project_root = project_root
|
|
45
|
+
self._queries = queries
|
|
46
|
+
self._initialized = False
|
|
47
|
+
|
|
48
|
+
# Import map: {file_path: {imported_name: [ImportEntry, ...]}}
|
|
49
|
+
self._import_map: Dict[str, Dict[str, List[ImportEntry]]] = {}
|
|
50
|
+
# Module registry: {module_name: [file_path, ...]}
|
|
51
|
+
self._module_registry: Dict[str, List[str]] = {}
|
|
52
|
+
# Node name cache: {name: [Node, ...]} for fast lookup
|
|
53
|
+
self._name_cache: Dict[str, List[Node]] = {}
|
|
54
|
+
|
|
55
|
+
# =========================================================================
|
|
56
|
+
# Initialization — build import maps & registries
|
|
57
|
+
# =========================================================================
|
|
58
|
+
|
|
59
|
+
def initialize(self) -> None:
|
|
60
|
+
"""Build import maps, module registry, and name cache."""
|
|
61
|
+
self._build_import_map()
|
|
62
|
+
self._build_module_registry()
|
|
63
|
+
self._build_name_cache()
|
|
64
|
+
self._initialized = True
|
|
65
|
+
|
|
66
|
+
def _build_import_map(self) -> None:
|
|
67
|
+
"""Scan all import nodes to build per-file import maps."""
|
|
68
|
+
import_nodes = self._queries.get_nodes_by_kind('import')
|
|
69
|
+
for node in import_nodes:
|
|
70
|
+
fp = node.file_path
|
|
71
|
+
if fp not in self._import_map:
|
|
72
|
+
self._import_map[fp] = {}
|
|
73
|
+
|
|
74
|
+
name = node.name
|
|
75
|
+
qualified = node.qualified_name
|
|
76
|
+
|
|
77
|
+
# Determine the module part from qualified_name
|
|
78
|
+
if '.' in qualified:
|
|
79
|
+
module = qualified.rsplit('.', 1)[0]
|
|
80
|
+
else:
|
|
81
|
+
module = qualified
|
|
82
|
+
|
|
83
|
+
entry = ImportEntry(
|
|
84
|
+
name=name,
|
|
85
|
+
qualified_name=qualified,
|
|
86
|
+
module=module,
|
|
87
|
+
node_id=node.id,
|
|
88
|
+
)
|
|
89
|
+
self._import_map[fp].setdefault(name, []).append(entry)
|
|
90
|
+
|
|
91
|
+
def _build_module_registry(self) -> None:
|
|
92
|
+
"""Build a mapping from module names to project files."""
|
|
93
|
+
all_files = self._queries.get_all_files()
|
|
94
|
+
|
|
95
|
+
for f in all_files:
|
|
96
|
+
path = f.path
|
|
97
|
+
# Convert file path to Python module name
|
|
98
|
+
if path.endswith('.py'):
|
|
99
|
+
if path.endswith('__init__.py'):
|
|
100
|
+
module = path[:-len('__init__.py')].rstrip('/').replace('/', '.')
|
|
101
|
+
else:
|
|
102
|
+
module = path[:-3].replace('/', '.')
|
|
103
|
+
elif path.endswith('.ts') or path.endswith('.tsx'):
|
|
104
|
+
module = path.rsplit('.', 1)[0].replace('/', '.')
|
|
105
|
+
elif path.endswith('.js') or path.endswith('.jsx'):
|
|
106
|
+
module = path.rsplit('.', 1)[0].replace('/', '.')
|
|
107
|
+
else:
|
|
108
|
+
module = path.rsplit('.', 1)[0].replace('/', '.') if '.' in path else path
|
|
109
|
+
|
|
110
|
+
self._module_registry.setdefault(module, []).append(path)
|
|
111
|
+
|
|
112
|
+
# Also register parent modules
|
|
113
|
+
parts = module.split('.')
|
|
114
|
+
for i in range(1, len(parts)):
|
|
115
|
+
parent = '.'.join(parts[:i])
|
|
116
|
+
if parent not in self._module_registry:
|
|
117
|
+
self._module_registry[parent] = []
|
|
118
|
+
|
|
119
|
+
def _build_name_cache(self) -> None:
|
|
120
|
+
"""Build a cache of all nodes by name for fast lookup."""
|
|
121
|
+
# This is too expensive to load all nodes — use DB queries instead
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
# =========================================================================
|
|
125
|
+
# Main resolution pipeline
|
|
126
|
+
# =========================================================================
|
|
127
|
+
|
|
128
|
+
def resolve_all(self, on_progress: Optional[Callable] = None) -> ResolutionResult:
|
|
129
|
+
"""Resolve all pending unresolved references."""
|
|
130
|
+
if not self._initialized:
|
|
131
|
+
self.initialize()
|
|
132
|
+
|
|
133
|
+
refs = self._queries.get_unresolved_refs()
|
|
134
|
+
if not refs:
|
|
135
|
+
return ResolutionResult()
|
|
136
|
+
|
|
137
|
+
resolved = 0
|
|
138
|
+
edges_created = 0
|
|
139
|
+
total = len(refs)
|
|
140
|
+
|
|
141
|
+
for i, ref in enumerate(refs):
|
|
142
|
+
target_node = self._resolve_single(ref)
|
|
143
|
+
if target_node:
|
|
144
|
+
edge_kind = self._ref_kind_to_edge_kind(ref.reference_kind)
|
|
145
|
+
edge = Edge(
|
|
146
|
+
source=ref.from_node_id,
|
|
147
|
+
target=target_node.id,
|
|
148
|
+
kind=edge_kind,
|
|
149
|
+
line=ref.line,
|
|
150
|
+
column=ref.column,
|
|
151
|
+
provenance='resolved',
|
|
152
|
+
)
|
|
153
|
+
self._queries.insert_edge(edge)
|
|
154
|
+
self._queries._db.execute(
|
|
155
|
+
'DELETE FROM unresolved_refs WHERE id = ?', (ref.id,)
|
|
156
|
+
)
|
|
157
|
+
resolved += 1
|
|
158
|
+
edges_created += 1
|
|
159
|
+
|
|
160
|
+
if on_progress and (i % 50 == 0 or i == total - 1):
|
|
161
|
+
on_progress(i + 1, total)
|
|
162
|
+
|
|
163
|
+
return ResolutionResult(
|
|
164
|
+
resolved_count=resolved,
|
|
165
|
+
remaining_count=self._queries.get_unresolved_refs_count(),
|
|
166
|
+
edges_created=edges_created,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _resolve_single(self, ref: UnresolvedReference) -> Optional[Node]:
|
|
170
|
+
"""
|
|
171
|
+
Resolve a single reference using multi-strategy approach.
|
|
172
|
+
|
|
173
|
+
Strategies (in order):
|
|
174
|
+
1. Import-aware: ref's source file imported this name
|
|
175
|
+
2. Same-file: definition exists in same file as ref
|
|
176
|
+
3. Global: name matches a node anywhere in project
|
|
177
|
+
4. Qualified: candidates list has a match
|
|
178
|
+
5. Module-path: reference_name is a module that maps to a file
|
|
179
|
+
"""
|
|
180
|
+
name = ref.reference_name
|
|
181
|
+
source_node = self._queries.get_node_by_id(ref.from_node_id)
|
|
182
|
+
source_file = source_node.file_path if source_node else None
|
|
183
|
+
|
|
184
|
+
# ── Strategy 1: Import-aware resolution ──
|
|
185
|
+
if source_file and source_file in self._import_map:
|
|
186
|
+
target = self._resolve_via_imports(name, source_file)
|
|
187
|
+
if target:
|
|
188
|
+
return target
|
|
189
|
+
|
|
190
|
+
# ── Strategy 2: Same-file resolution ──
|
|
191
|
+
if source_file:
|
|
192
|
+
target = self._resolve_in_file(name, source_file)
|
|
193
|
+
if target:
|
|
194
|
+
return target
|
|
195
|
+
|
|
196
|
+
# ── Strategy 3: Global name matching ──
|
|
197
|
+
target = self._resolve_global(name, source_file)
|
|
198
|
+
if target:
|
|
199
|
+
return target
|
|
200
|
+
|
|
201
|
+
# ── Strategy 4: Qualified name candidates ──
|
|
202
|
+
if ref.candidates:
|
|
203
|
+
for cname in ref.candidates:
|
|
204
|
+
matches = self._queries.get_nodes_by_qualified_name(cname)
|
|
205
|
+
if matches:
|
|
206
|
+
return matches[0]
|
|
207
|
+
|
|
208
|
+
# ── Strategy 5: Module name → file node ──
|
|
209
|
+
# If the reference looks like a module name, try to resolve to file
|
|
210
|
+
if '.' in name or '_' in name:
|
|
211
|
+
file_paths = self._module_registry.get(name, [])
|
|
212
|
+
if file_paths:
|
|
213
|
+
# Find the file node for the first match
|
|
214
|
+
for fp in file_paths:
|
|
215
|
+
file_nodes = self._queries.get_nodes_by_file(fp)
|
|
216
|
+
for n in file_nodes:
|
|
217
|
+
if n.kind == 'file':
|
|
218
|
+
return n
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
# =========================================================================
|
|
223
|
+
# Resolution strategies
|
|
224
|
+
# =========================================================================
|
|
225
|
+
|
|
226
|
+
def _resolve_via_imports(self, name: str, source_file: str) -> Optional[Node]:
|
|
227
|
+
"""Check if the source file imports 'name' from somewhere, and resolve it."""
|
|
228
|
+
file_imports = self._import_map.get(source_file, {})
|
|
229
|
+
entries = file_imports.get(name, [])
|
|
230
|
+
|
|
231
|
+
for entry in entries:
|
|
232
|
+
# Try to find the symbol in the imported module's files
|
|
233
|
+
mod_paths = self._module_registry.get(entry.module, [])
|
|
234
|
+
for fp in mod_paths:
|
|
235
|
+
nodes = self._queries.get_nodes_by_file(fp)
|
|
236
|
+
for n in nodes:
|
|
237
|
+
if n.name == name:
|
|
238
|
+
return n
|
|
239
|
+
|
|
240
|
+
# Also check the import node's qualified name
|
|
241
|
+
qname_candidates = self._queries.get_nodes_by_qualified_name(entry.qualified_name)
|
|
242
|
+
if qname_candidates:
|
|
243
|
+
return qname_candidates[0]
|
|
244
|
+
|
|
245
|
+
# Last resort: look for the module as a file
|
|
246
|
+
for fp in mod_paths:
|
|
247
|
+
file_nodes = self._queries.get_nodes_by_file(fp)
|
|
248
|
+
for n in file_nodes:
|
|
249
|
+
if n.kind == 'file':
|
|
250
|
+
return n
|
|
251
|
+
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
def _resolve_in_file(self, name: str, file_path: str) -> Optional[Node]:
|
|
255
|
+
"""Look for a definition of 'name' in the same file."""
|
|
256
|
+
nodes = self._queries.get_nodes_by_file(file_path)
|
|
257
|
+
for n in nodes:
|
|
258
|
+
if n.name == name and n.kind not in ('file', 'import', 'module'):
|
|
259
|
+
return n
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
def _resolve_global(self, name: str, source_file: Optional[str]) -> Optional[Node]:
|
|
263
|
+
"""Global name matching across all indexed nodes."""
|
|
264
|
+
candidates = self._queries.get_nodes_by_name(name)
|
|
265
|
+
|
|
266
|
+
# Filter out file and import nodes
|
|
267
|
+
real_candidates = [
|
|
268
|
+
c for c in candidates
|
|
269
|
+
if c.kind not in ('file', 'import', 'module')
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
if not real_candidates:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
# Prefer same file results
|
|
276
|
+
if source_file:
|
|
277
|
+
same_file = [c for c in real_candidates if c.file_path == source_file]
|
|
278
|
+
if same_file:
|
|
279
|
+
return same_file[0]
|
|
280
|
+
|
|
281
|
+
# Prefer exported/non-private symbols
|
|
282
|
+
exported = [c for c in real_candidates if c.is_exported or not c.name.startswith('_')]
|
|
283
|
+
if exported:
|
|
284
|
+
return exported[0]
|
|
285
|
+
|
|
286
|
+
return real_candidates[0]
|
|
287
|
+
|
|
288
|
+
# =========================================================================
|
|
289
|
+
# Post-processing passes (stubs for future enhancements)
|
|
290
|
+
# =========================================================================
|
|
291
|
+
|
|
292
|
+
def resolve_chained_calls_via_conformance(self) -> None:
|
|
293
|
+
"""Second pass: chained calls whose method lives on a supertype."""
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
def resolve_deferred_this_member_refs(self) -> None:
|
|
297
|
+
"""Resolve 'this.<member>' callback registrations inherited from supertype."""
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
def run_post_extract(self) -> None:
|
|
301
|
+
"""Run cross-file finalization after extraction."""
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
# =========================================================================
|
|
305
|
+
# Helpers
|
|
306
|
+
# =========================================================================
|
|
307
|
+
|
|
308
|
+
def _ref_kind_to_edge_kind(self, ref_kind: str) -> str:
|
|
309
|
+
"""Convert a reference kind to an edge kind."""
|
|
310
|
+
mapping = {
|
|
311
|
+
'calls': 'calls',
|
|
312
|
+
'call': 'calls',
|
|
313
|
+
'imports': 'imports',
|
|
314
|
+
'exports': 'exports',
|
|
315
|
+
'extends': 'extends',
|
|
316
|
+
'implements': 'implements',
|
|
317
|
+
'type_of': 'type_of',
|
|
318
|
+
'returns': 'returns',
|
|
319
|
+
'instantiates': 'instantiates',
|
|
320
|
+
'overrides': 'overrides',
|
|
321
|
+
'decorates': 'decorates',
|
|
322
|
+
'function_ref': 'references',
|
|
323
|
+
}
|
|
324
|
+
return mapping.get(ref_kind, 'references')
|
|
325
|
+
|
|
326
|
+
def get_import_map(self) -> Dict[str, Dict[str, List[ImportEntry]]]:
|
|
327
|
+
"""Get the import map (for debugging/inspection)."""
|
|
328
|
+
return dict(self._import_map)
|
|
329
|
+
|
|
330
|
+
def get_module_registry(self) -> Dict[str, List[str]]:
|
|
331
|
+
"""Get the module registry (for debugging/inspection)."""
|
|
332
|
+
return dict(self._module_registry)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def create_resolver(project_root: str, queries: QueryBuilder) -> ReferenceResolver:
|
|
336
|
+
"""Factory function to create a ReferenceResolver."""
|
|
337
|
+
return ReferenceResolver(project_root, queries)
|