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.
Files changed (44) hide show
  1. codegraph/__init__.py +12 -0
  2. codegraph/__main__.py +6 -0
  3. codegraph/cli.py +911 -0
  4. codegraph/codegraph.py +618 -0
  5. codegraph/context/__init__.py +216 -0
  6. codegraph/db/__init__.py +11 -0
  7. codegraph/db/connection.py +163 -0
  8. codegraph/db/queries.py +752 -0
  9. codegraph/db/schema.sql +151 -0
  10. codegraph/directory.py +160 -0
  11. codegraph/errors.py +101 -0
  12. codegraph/extraction/__init__.py +956 -0
  13. codegraph/extraction/languages/__init__.py +64 -0
  14. codegraph/extraction/languages/base.py +132 -0
  15. codegraph/extraction/languages/c_cfg.py +53 -0
  16. codegraph/extraction/languages/cpp_cfg.py +60 -0
  17. codegraph/extraction/languages/dart_cfg.py +53 -0
  18. codegraph/extraction/languages/go_cfg.py +70 -0
  19. codegraph/extraction/languages/java_cfg.py +62 -0
  20. codegraph/extraction/languages/javascript_cfg.py +84 -0
  21. codegraph/extraction/languages/kotlin_cfg.py +52 -0
  22. codegraph/extraction/languages/lua_cfg.py +65 -0
  23. codegraph/extraction/languages/python_cfg.py +75 -0
  24. codegraph/extraction/languages/ruby_cfg.py +48 -0
  25. codegraph/extraction/languages/rust_cfg.py +68 -0
  26. codegraph/extraction/languages/scala_cfg.py +59 -0
  27. codegraph/extraction/languages/swift_cfg.py +64 -0
  28. codegraph/extraction/languages/typescript_cfg.py +89 -0
  29. codegraph/extraction/tree_sitter_extractor.py +689 -0
  30. codegraph/graph/__init__.py +685 -0
  31. codegraph/installer/__init__.py +6 -0
  32. codegraph/mcp/__init__.py +668 -0
  33. codegraph/project_config.py +191 -0
  34. codegraph/resolution/__init__.py +337 -0
  35. codegraph/search/__init__.py +653 -0
  36. codegraph/sync/__init__.py +204 -0
  37. codegraph/types.py +334 -0
  38. codegraph/ui/__init__.py +11 -0
  39. codegraph/utils.py +218 -0
  40. codegraph_py-1.0.0.dist-info/METADATA +238 -0
  41. codegraph_py-1.0.0.dist-info/RECORD +44 -0
  42. codegraph_py-1.0.0.dist-info/WHEEL +5 -0
  43. codegraph_py-1.0.0.dist-info/entry_points.txt +2 -0
  44. 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)