codegraph-nav 0.1.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_nav/__init__.py +194 -0
- codegraph_nav/ast_grep_analyzer.py +448 -0
- codegraph_nav/cli.py +223 -0
- codegraph_nav/code_navigator.py +1328 -0
- codegraph_nav/code_search.py +1009 -0
- codegraph_nav/colors.py +209 -0
- codegraph_nav/completions.py +354 -0
- codegraph_nav/dart_analyzer.py +301 -0
- codegraph_nav/dependency_graph.py +814 -0
- codegraph_nav/domain/__init__.py +20 -0
- codegraph_nav/domain/routes.py +337 -0
- codegraph_nav/domain/schemas.py +229 -0
- codegraph_nav/domain/tags.py +87 -0
- codegraph_nav/exporters.py +563 -0
- codegraph_nav/go_analyzer.py +273 -0
- codegraph_nav/graph/__init__.py +72 -0
- codegraph_nav/graph/builder.py +409 -0
- codegraph_nav/graph/communities.py +402 -0
- codegraph_nav/graph/flows.py +311 -0
- codegraph_nav/graph/query.py +380 -0
- codegraph_nav/graph/schema.py +266 -0
- codegraph_nav/graph/search.py +257 -0
- codegraph_nav/graph/store.py +517 -0
- codegraph_nav/hints.py +195 -0
- codegraph_nav/import_resolver.py +891 -0
- codegraph_nav/js_ts_analyzer.py +564 -0
- codegraph_nav/line_reader.py +664 -0
- codegraph_nav/mcp/__init__.py +39 -0
- codegraph_nav/mcp/__main__.py +5 -0
- codegraph_nav/mcp/server.py +2228 -0
- codegraph_nav/py.typed +2 -0
- codegraph_nav/ruby_analyzer.py +259 -0
- codegraph_nav/rust_analyzer.py +379 -0
- codegraph_nav/token_efficient_renderer.py +743 -0
- codegraph_nav/watcher.py +382 -0
- codegraph_nav-0.1.0.dist-info/METADATA +487 -0
- codegraph_nav-0.1.0.dist-info/RECORD +41 -0
- codegraph_nav-0.1.0.dist-info/WHEEL +5 -0
- codegraph_nav-0.1.0.dist-info/entry_points.txt +4 -0
- codegraph_nav-0.1.0.dist-info/licenses/LICENSE +21 -0
- codegraph_nav-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Code Navigator - Token-efficient code navigation for large codebases.
|
|
2
|
+
|
|
3
|
+
This package provides tools for creating structural maps of codebases and
|
|
4
|
+
navigating them efficiently, reducing token usage by up to 97% when working
|
|
5
|
+
with AI coding assistants.
|
|
6
|
+
|
|
7
|
+
Components:
|
|
8
|
+
- CodeNavigator: Generates JSON index of codebase structure
|
|
9
|
+
- CodeSearcher: Searches the pre-built index for symbols
|
|
10
|
+
- LineReader: Reads specific lines from files efficiently
|
|
11
|
+
|
|
12
|
+
Quick Start:
|
|
13
|
+
1. Generate a code map:
|
|
14
|
+
>>> from codegraph_nav import CodeNavigator
|
|
15
|
+
>>> mapper = CodeNavigator('/path/to/project')
|
|
16
|
+
>>> code_map = mapper.scan()
|
|
17
|
+
|
|
18
|
+
2. Search for symbols:
|
|
19
|
+
>>> from codegraph_nav import CodeSearcher
|
|
20
|
+
>>> searcher = CodeSearcher('.codegraph.json')
|
|
21
|
+
>>> results = searcher.search_symbol('process_payment')
|
|
22
|
+
|
|
23
|
+
3. Read specific lines:
|
|
24
|
+
>>> from codegraph_nav import LineReader
|
|
25
|
+
>>> reader = LineReader()
|
|
26
|
+
>>> content = reader.read_lines('src/api.py', 45, 60)
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> # Full workflow
|
|
30
|
+
>>> from codegraph_nav import CodeNavigator, CodeSearcher, LineReader
|
|
31
|
+
>>>
|
|
32
|
+
>>> # Step 1: Map the codebase (one-time)
|
|
33
|
+
>>> mapper = CodeNavigator('/my/project')
|
|
34
|
+
>>> mapper.scan()
|
|
35
|
+
>>>
|
|
36
|
+
>>> # Step 2: Search for a symbol
|
|
37
|
+
>>> searcher = CodeSearcher('/my/project/.codegraph.json')
|
|
38
|
+
>>> results = searcher.search_symbol('authenticate', symbol_type='function')
|
|
39
|
+
>>> print(results[0].file, results[0].lines)
|
|
40
|
+
'src/auth.py' [45, 89]
|
|
41
|
+
>>>
|
|
42
|
+
>>> # Step 3: Read only those lines
|
|
43
|
+
>>> reader = LineReader('/my/project')
|
|
44
|
+
>>> content = reader.read_symbol('src/auth.py', 45, 89)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import hashlib
|
|
48
|
+
|
|
49
|
+
from .code_navigator import CodeNavigator, GenericAnalyzer, GitIntegration, PythonAnalyzer, Symbol
|
|
50
|
+
from .code_search import CodeSearcher, SearchResult
|
|
51
|
+
from .completions import generate_bash_completion, generate_zsh_completion
|
|
52
|
+
from .dart_analyzer import DartAnalyzer
|
|
53
|
+
from .exporters import GraphVizExporter, HTMLExporter, MarkdownExporter, get_exporter
|
|
54
|
+
from .go_analyzer import GoAnalyzer
|
|
55
|
+
from .js_ts_analyzer import (
|
|
56
|
+
TREE_SITTER_AVAILABLE,
|
|
57
|
+
JavaScriptAnalyzer,
|
|
58
|
+
TypeScriptAnalyzer,
|
|
59
|
+
)
|
|
60
|
+
from .line_reader import LineReader
|
|
61
|
+
from .ruby_analyzer import RubyAnalyzer
|
|
62
|
+
from .rust_analyzer import RustAnalyzer
|
|
63
|
+
from .watcher import CodegraphWatcher
|
|
64
|
+
|
|
65
|
+
# Optional dependency: networkx for DependencyGraph
|
|
66
|
+
try:
|
|
67
|
+
from .dependency_graph import DependencyGraph, FileNode, analyze_repository
|
|
68
|
+
|
|
69
|
+
HAS_NETWORKX = True
|
|
70
|
+
except ImportError:
|
|
71
|
+
DependencyGraph = None # type: ignore
|
|
72
|
+
FileNode = None # type: ignore
|
|
73
|
+
analyze_repository = None # type: ignore
|
|
74
|
+
HAS_NETWORKX = False
|
|
75
|
+
|
|
76
|
+
# Import resolver (always available - no external dependencies)
|
|
77
|
+
from .import_resolver import (
|
|
78
|
+
AliasConfig,
|
|
79
|
+
ImportResolver,
|
|
80
|
+
ResolveResult,
|
|
81
|
+
ResolveStrategy,
|
|
82
|
+
resolve_import_path,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Token-efficient rendering (always available - no external dependencies)
|
|
86
|
+
from .token_efficient_renderer import (
|
|
87
|
+
FileMicroMeta,
|
|
88
|
+
HubLevel,
|
|
89
|
+
TokenEfficientRenderer,
|
|
90
|
+
render_skeleton_tree,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Optional: ast-grep high-performance analyzer
|
|
94
|
+
try:
|
|
95
|
+
from .ast_grep_analyzer import (
|
|
96
|
+
AstGrepAnalyzer,
|
|
97
|
+
AstGrepSymbol,
|
|
98
|
+
analyze_with_ast_grep,
|
|
99
|
+
is_ast_grep_available,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
HAS_AST_GREP = is_ast_grep_available()
|
|
103
|
+
except ImportError:
|
|
104
|
+
AstGrepAnalyzer = None # type: ignore
|
|
105
|
+
AstGrepSymbol = None # type: ignore
|
|
106
|
+
analyze_with_ast_grep = None # type: ignore
|
|
107
|
+
|
|
108
|
+
def is_ast_grep_available() -> bool:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
HAS_AST_GREP = False
|
|
112
|
+
|
|
113
|
+
__version__ = "0.1.0"
|
|
114
|
+
__author__ = "Efren"
|
|
115
|
+
__license__ = "MIT"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def compute_content_hash(content: str) -> str:
|
|
119
|
+
"""Compute a short hash of content for change detection.
|
|
120
|
+
|
|
121
|
+
This is the canonical hash function used across all modules for
|
|
122
|
+
consistent file change detection.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
content: The text content to hash.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A 12-character MD5 hash string.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> compute_content_hash("def foo(): pass")
|
|
132
|
+
'a1b2c3d4e5f6'
|
|
133
|
+
"""
|
|
134
|
+
return hashlib.md5(content.encode()).hexdigest()[:12]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = [
|
|
138
|
+
# Version info
|
|
139
|
+
"__version__",
|
|
140
|
+
"__author__",
|
|
141
|
+
"__license__",
|
|
142
|
+
# Main classes
|
|
143
|
+
"CodeNavigator",
|
|
144
|
+
"CodeSearcher",
|
|
145
|
+
"LineReader",
|
|
146
|
+
"CodegraphWatcher",
|
|
147
|
+
# Dependency Graph (requires networkx)
|
|
148
|
+
"DependencyGraph",
|
|
149
|
+
"FileNode",
|
|
150
|
+
"analyze_repository",
|
|
151
|
+
# Import Resolution
|
|
152
|
+
"ImportResolver",
|
|
153
|
+
"ResolveResult",
|
|
154
|
+
"ResolveStrategy",
|
|
155
|
+
"AliasConfig",
|
|
156
|
+
"resolve_import_path",
|
|
157
|
+
# Token-Efficient Rendering
|
|
158
|
+
"TokenEfficientRenderer",
|
|
159
|
+
"FileMicroMeta",
|
|
160
|
+
"HubLevel",
|
|
161
|
+
"render_skeleton_tree",
|
|
162
|
+
# AST-Grep Analyzer (optional, requires ast-grep-py)
|
|
163
|
+
"AstGrepAnalyzer",
|
|
164
|
+
"AstGrepSymbol",
|
|
165
|
+
"analyze_with_ast_grep",
|
|
166
|
+
"is_ast_grep_available",
|
|
167
|
+
"HAS_AST_GREP",
|
|
168
|
+
# Analyzers
|
|
169
|
+
"PythonAnalyzer",
|
|
170
|
+
"GenericAnalyzer",
|
|
171
|
+
"JavaScriptAnalyzer",
|
|
172
|
+
"TypeScriptAnalyzer",
|
|
173
|
+
"RubyAnalyzer",
|
|
174
|
+
"GoAnalyzer",
|
|
175
|
+
"RustAnalyzer",
|
|
176
|
+
"DartAnalyzer",
|
|
177
|
+
# Exporters
|
|
178
|
+
"MarkdownExporter",
|
|
179
|
+
"HTMLExporter",
|
|
180
|
+
"GraphVizExporter",
|
|
181
|
+
"get_exporter",
|
|
182
|
+
# Supporting classes
|
|
183
|
+
"GitIntegration",
|
|
184
|
+
"Symbol",
|
|
185
|
+
"SearchResult",
|
|
186
|
+
# Completions
|
|
187
|
+
"generate_bash_completion",
|
|
188
|
+
"generate_zsh_completion",
|
|
189
|
+
# Feature flags
|
|
190
|
+
"TREE_SITTER_AVAILABLE",
|
|
191
|
+
"HAS_NETWORKX",
|
|
192
|
+
# Utilities
|
|
193
|
+
"compute_content_hash",
|
|
194
|
+
]
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""AST-Grep Analyzer - High-performance multi-language code analysis.
|
|
3
|
+
|
|
4
|
+
This module provides an optional high-performance analyzer using ast-grep's
|
|
5
|
+
native Python bindings. It offers superior accuracy compared to regex-based
|
|
6
|
+
analysis and supports 20+ programming languages through tree-sitter.
|
|
7
|
+
|
|
8
|
+
Requirements:
|
|
9
|
+
pip install ast-grep-py
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from codegraph_nav.ast_grep_analyzer import AstGrepAnalyzer
|
|
13
|
+
>>> analyzer = AstGrepAnalyzer("example.py", source_code, "python")
|
|
14
|
+
>>> symbols = analyzer.analyze()
|
|
15
|
+
>>> imports = analyzer.find_imports()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
# Check for ast-grep availability
|
|
22
|
+
try:
|
|
23
|
+
from ast_grep_py import SgRoot
|
|
24
|
+
|
|
25
|
+
HAS_AST_GREP = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
HAS_AST_GREP = False
|
|
28
|
+
if not TYPE_CHECKING:
|
|
29
|
+
SgRoot = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class AstGrepSymbol:
|
|
34
|
+
"""Symbol extracted by ast-grep.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name: Symbol name (function, class, variable name).
|
|
38
|
+
type: Symbol type (function, class, method, interface, etc.).
|
|
39
|
+
file_path: Relative file path.
|
|
40
|
+
line_start: Starting line (1-indexed).
|
|
41
|
+
line_end: Ending line (1-indexed).
|
|
42
|
+
signature: Full signature text (truncated).
|
|
43
|
+
parent: Parent class name for methods.
|
|
44
|
+
meta_vars: Captured meta-variables from pattern.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
type: str
|
|
49
|
+
file_path: str
|
|
50
|
+
line_start: int
|
|
51
|
+
line_end: int
|
|
52
|
+
signature: str | None = None
|
|
53
|
+
parent: str | None = None
|
|
54
|
+
meta_vars: dict[str, str] = field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AstGrepAnalyzer:
|
|
58
|
+
"""Multi-language analyzer using ast-grep native Python bindings.
|
|
59
|
+
|
|
60
|
+
This analyzer provides accurate AST-based code analysis for multiple
|
|
61
|
+
languages without the overhead of subprocess calls. It uses declarative
|
|
62
|
+
patterns that are easy to maintain and extend.
|
|
63
|
+
|
|
64
|
+
Supported Languages:
|
|
65
|
+
python, javascript, typescript, go, rust, java, ruby, swift,
|
|
66
|
+
kotlin, c, cpp, csharp, php, lua, scala, elixir
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
file_path: Path to the file being analyzed.
|
|
70
|
+
source: Source code content.
|
|
71
|
+
language: Programming language identifier.
|
|
72
|
+
root: SgRoot instance for AST operations.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> source = '''
|
|
76
|
+
... def greet(name: str) -> str:
|
|
77
|
+
... return f"Hello, {name}"
|
|
78
|
+
... '''
|
|
79
|
+
>>> analyzer = AstGrepAnalyzer("greet.py", source, "python")
|
|
80
|
+
>>> symbols = analyzer.analyze()
|
|
81
|
+
>>> print(symbols[0].name)
|
|
82
|
+
'greet'
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
# Pattern definitions for each language
|
|
86
|
+
# Format: { "symbol_type": "pattern" } or { "symbol_type": {"kind": "ast_node_kind"} }
|
|
87
|
+
PATTERNS: dict[str, dict[str, str | dict[str, str]]] = {
|
|
88
|
+
"python": {
|
|
89
|
+
"function": {"kind": "function_definition"},
|
|
90
|
+
"class": {"kind": "class_definition"},
|
|
91
|
+
"import": "import $MODULE",
|
|
92
|
+
"import_from": "from $MODULE import $$$NAMES",
|
|
93
|
+
"async_function": {"kind": "function_definition", "pattern": "async def $NAME"},
|
|
94
|
+
},
|
|
95
|
+
"javascript": {
|
|
96
|
+
"function": {"kind": "function_declaration"},
|
|
97
|
+
"arrow": "const $NAME = ($$$ARGS) => $$$BODY",
|
|
98
|
+
"arrow_async": "const $NAME = async ($$$ARGS) => $$$BODY",
|
|
99
|
+
"class": {"kind": "class_declaration"},
|
|
100
|
+
"method": {"kind": "method_definition"},
|
|
101
|
+
"import": 'import $$$ITEMS from "$PATH"',
|
|
102
|
+
"import_default": 'import $NAME from "$PATH"',
|
|
103
|
+
"require": 'require("$PATH")',
|
|
104
|
+
},
|
|
105
|
+
"typescript": {
|
|
106
|
+
"function": {"kind": "function_declaration"},
|
|
107
|
+
"arrow": "const $NAME = ($$$ARGS): $RETURN => $$$BODY",
|
|
108
|
+
"class": {"kind": "class_declaration"},
|
|
109
|
+
"interface": {"kind": "interface_declaration"},
|
|
110
|
+
"type_alias": {"kind": "type_alias_declaration"},
|
|
111
|
+
"method": {"kind": "method_definition"},
|
|
112
|
+
"import": 'import $$$ITEMS from "$PATH"',
|
|
113
|
+
"import_type": 'import type $$$ITEMS from "$PATH"',
|
|
114
|
+
},
|
|
115
|
+
"go": {
|
|
116
|
+
"function": "func $NAME($$$ARGS)",
|
|
117
|
+
"method": "func ($RECEIVER) $NAME($$$ARGS)",
|
|
118
|
+
"struct": "type $NAME struct",
|
|
119
|
+
"interface": "type $NAME interface",
|
|
120
|
+
"import": 'import "$PATH"',
|
|
121
|
+
},
|
|
122
|
+
"rust": {
|
|
123
|
+
"function": {"kind": "function_item"},
|
|
124
|
+
"struct": {"kind": "struct_item"},
|
|
125
|
+
"enum": {"kind": "enum_item"},
|
|
126
|
+
"impl": {"kind": "impl_item"},
|
|
127
|
+
"trait": {"kind": "trait_item"},
|
|
128
|
+
"use": "use $PATH",
|
|
129
|
+
},
|
|
130
|
+
"java": {
|
|
131
|
+
"class": {"kind": "class_declaration"},
|
|
132
|
+
"interface": {"kind": "interface_declaration"},
|
|
133
|
+
"method": {"kind": "method_declaration"},
|
|
134
|
+
"import": "import $PATH;",
|
|
135
|
+
},
|
|
136
|
+
"ruby": {
|
|
137
|
+
"method": {"kind": "method"},
|
|
138
|
+
"class": {"kind": "class"},
|
|
139
|
+
"module": {"kind": "module"},
|
|
140
|
+
"require": 'require "$PATH"',
|
|
141
|
+
"require_relative": 'require_relative "$PATH"',
|
|
142
|
+
},
|
|
143
|
+
"swift": {
|
|
144
|
+
"function": "func $NAME($$$ARGS)",
|
|
145
|
+
"class": "class $NAME",
|
|
146
|
+
"struct": "struct $NAME",
|
|
147
|
+
"protocol": "protocol $NAME",
|
|
148
|
+
"import": "import $MODULE",
|
|
149
|
+
},
|
|
150
|
+
"kotlin": {
|
|
151
|
+
"function": "fun $NAME($$$ARGS)",
|
|
152
|
+
"class": "class $NAME",
|
|
153
|
+
"interface": "interface $NAME",
|
|
154
|
+
"import": "import $PATH",
|
|
155
|
+
},
|
|
156
|
+
"c": {
|
|
157
|
+
"function": {"kind": "function_definition"},
|
|
158
|
+
"struct": "struct $NAME",
|
|
159
|
+
"include": '#include "$PATH"',
|
|
160
|
+
"include_system": "#include <$PATH>",
|
|
161
|
+
},
|
|
162
|
+
"cpp": {
|
|
163
|
+
"function": {"kind": "function_definition"},
|
|
164
|
+
"class": {"kind": "class_specifier"},
|
|
165
|
+
"struct": "struct $NAME",
|
|
166
|
+
"namespace": "namespace $NAME",
|
|
167
|
+
"include": '#include "$PATH"',
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Language aliases
|
|
172
|
+
LANGUAGE_ALIASES = {
|
|
173
|
+
"js": "javascript",
|
|
174
|
+
"ts": "typescript",
|
|
175
|
+
"tsx": "typescript",
|
|
176
|
+
"jsx": "javascript",
|
|
177
|
+
"rs": "rust",
|
|
178
|
+
"rb": "ruby",
|
|
179
|
+
"kt": "kotlin",
|
|
180
|
+
"cs": "csharp",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def __init__(self, file_path: str, source: str, language: str):
|
|
184
|
+
"""Initialize the analyzer.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
file_path: Relative path to the file.
|
|
188
|
+
source: Source code content.
|
|
189
|
+
language: Programming language identifier.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ImportError: If ast-grep-py is not installed.
|
|
193
|
+
"""
|
|
194
|
+
if not HAS_AST_GREP:
|
|
195
|
+
raise ImportError(
|
|
196
|
+
"ast-grep-py is required for AstGrepAnalyzer. "
|
|
197
|
+
"Install with: pip install ast-grep-py"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
self.file_path = file_path
|
|
201
|
+
self.source = source
|
|
202
|
+
self.language = self.LANGUAGE_ALIASES.get(language.lower(), language.lower())
|
|
203
|
+
|
|
204
|
+
# Parse source code
|
|
205
|
+
self.root: SgRoot | None = None
|
|
206
|
+
try:
|
|
207
|
+
self.root = SgRoot(source, self.language)
|
|
208
|
+
self._available = True
|
|
209
|
+
except Exception:
|
|
210
|
+
self._available = False
|
|
211
|
+
self.root = None
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def available(self) -> bool:
|
|
215
|
+
"""Check if analysis is available for this language."""
|
|
216
|
+
return self._available and self.language in self.PATTERNS
|
|
217
|
+
|
|
218
|
+
def analyze(self) -> list[AstGrepSymbol]:
|
|
219
|
+
"""Extract all symbols from the source code.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of AstGrepSymbol objects found in the file.
|
|
223
|
+
"""
|
|
224
|
+
if not self.available:
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
symbols = []
|
|
228
|
+
patterns = self.PATTERNS.get(self.language, {})
|
|
229
|
+
assert self.root is not None # guaranteed by self.available check
|
|
230
|
+
root_node = self.root.root()
|
|
231
|
+
|
|
232
|
+
for symbol_type, pattern_config in patterns.items():
|
|
233
|
+
# Skip import patterns (handled separately)
|
|
234
|
+
if "import" in symbol_type or symbol_type in ("require", "use", "include"):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
matches = self._find_matches(root_node, pattern_config)
|
|
238
|
+
|
|
239
|
+
for match in matches:
|
|
240
|
+
symbol = self._extract_symbol(match, symbol_type)
|
|
241
|
+
if symbol:
|
|
242
|
+
symbols.append(symbol)
|
|
243
|
+
|
|
244
|
+
return symbols
|
|
245
|
+
|
|
246
|
+
def _find_matches(self, node: Any, pattern_config) -> list[Any]:
|
|
247
|
+
"""Find matches using pattern or kind."""
|
|
248
|
+
if isinstance(pattern_config, str):
|
|
249
|
+
# Direct pattern
|
|
250
|
+
return list(node.find_all(pattern=pattern_config))
|
|
251
|
+
elif isinstance(pattern_config, dict):
|
|
252
|
+
# Kind-based or combined
|
|
253
|
+
if "kind" in pattern_config and "pattern" in pattern_config:
|
|
254
|
+
# Both kind and pattern
|
|
255
|
+
kind_matches = list(node.find_all(kind=pattern_config["kind"]))
|
|
256
|
+
return [m for m in kind_matches if m.matches(pattern=pattern_config["pattern"])]
|
|
257
|
+
elif "kind" in pattern_config:
|
|
258
|
+
return list(node.find_all(kind=pattern_config["kind"]))
|
|
259
|
+
elif "pattern" in pattern_config:
|
|
260
|
+
return list(node.find_all(pattern=pattern_config["pattern"]))
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
def _extract_symbol(self, match: Any, symbol_type: str) -> AstGrepSymbol | None:
|
|
264
|
+
"""Extract symbol information from a match."""
|
|
265
|
+
try:
|
|
266
|
+
# Try to get name from meta-variable
|
|
267
|
+
name_node = match.get_match("NAME")
|
|
268
|
+
if name_node:
|
|
269
|
+
name = name_node.text()
|
|
270
|
+
else:
|
|
271
|
+
# Try field-based extraction
|
|
272
|
+
name_field = match.field("name")
|
|
273
|
+
if name_field:
|
|
274
|
+
name = name_field.text()
|
|
275
|
+
else:
|
|
276
|
+
# Fallback: extract first identifier
|
|
277
|
+
name = self._extract_name_fallback(match, symbol_type)
|
|
278
|
+
if not name:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
# Get range
|
|
282
|
+
rng = match.range()
|
|
283
|
+
|
|
284
|
+
# Get signature (truncated)
|
|
285
|
+
full_text = match.text()
|
|
286
|
+
signature = full_text[:150] + "..." if len(full_text) > 150 else full_text
|
|
287
|
+
# Clean up signature (first line only for readability)
|
|
288
|
+
signature = signature.split("\n")[0].strip()
|
|
289
|
+
|
|
290
|
+
return AstGrepSymbol(
|
|
291
|
+
name=name,
|
|
292
|
+
type=symbol_type,
|
|
293
|
+
file_path=self.file_path,
|
|
294
|
+
line_start=rng.start.line + 1, # Convert to 1-indexed
|
|
295
|
+
line_end=rng.end.line + 1,
|
|
296
|
+
signature=signature,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
except Exception:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
def _extract_name_fallback(self, match: Any, symbol_type: str) -> str | None:
|
|
303
|
+
"""Fallback method to extract name from AST node."""
|
|
304
|
+
text: str = match.text()
|
|
305
|
+
|
|
306
|
+
# Language-specific extraction
|
|
307
|
+
if self.language == "python":
|
|
308
|
+
if symbol_type in ("function", "async_function"):
|
|
309
|
+
# def name(...) or async def name(...)
|
|
310
|
+
if "def " in text:
|
|
311
|
+
start = text.index("def ") + 4
|
|
312
|
+
end = text.index("(", start)
|
|
313
|
+
return text[start:end].strip()
|
|
314
|
+
elif symbol_type == "class":
|
|
315
|
+
if "class " in text:
|
|
316
|
+
start = text.index("class ") + 6
|
|
317
|
+
end = text.find("(", start)
|
|
318
|
+
if end == -1:
|
|
319
|
+
end = text.find(":", start)
|
|
320
|
+
return text[start:end].strip()
|
|
321
|
+
|
|
322
|
+
elif self.language in ("javascript", "typescript"):
|
|
323
|
+
if symbol_type == "function":
|
|
324
|
+
if "function " in text:
|
|
325
|
+
start = text.index("function ") + 9
|
|
326
|
+
end = text.index("(", start)
|
|
327
|
+
return text[start:end].strip()
|
|
328
|
+
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
def find_imports(self) -> list[str]:
|
|
332
|
+
"""Extract all import statements.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
List of imported module/path strings.
|
|
336
|
+
"""
|
|
337
|
+
if not self.available:
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
imports = []
|
|
341
|
+
patterns = self.PATTERNS.get(self.language, {})
|
|
342
|
+
assert self.root is not None # guaranteed by self.available check
|
|
343
|
+
root_node = self.root.root()
|
|
344
|
+
|
|
345
|
+
# Collect import-related patterns
|
|
346
|
+
import_patterns = {
|
|
347
|
+
k: v
|
|
348
|
+
for k, v in patterns.items()
|
|
349
|
+
if "import" in k or k in ("require", "use", "include", "include_system")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for _pattern_type, pattern in import_patterns.items():
|
|
353
|
+
if isinstance(pattern, str):
|
|
354
|
+
matches = root_node.find_all(pattern=pattern)
|
|
355
|
+
for match in matches:
|
|
356
|
+
# Try common meta-variables
|
|
357
|
+
for var in ["MODULE", "PATH", "ITEMS"]:
|
|
358
|
+
node = match.get_match(var)
|
|
359
|
+
if node:
|
|
360
|
+
text = node.text().strip("\"'")
|
|
361
|
+
if text and text not in imports:
|
|
362
|
+
imports.append(text)
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
return imports
|
|
366
|
+
|
|
367
|
+
def find_classes(self) -> list[tuple[str, list[str]]]:
|
|
368
|
+
"""Find classes with their methods.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
List of (class_name, [method_names]) tuples.
|
|
372
|
+
"""
|
|
373
|
+
if not self.available:
|
|
374
|
+
return []
|
|
375
|
+
|
|
376
|
+
results = []
|
|
377
|
+
assert self.root is not None # guaranteed by self.available check
|
|
378
|
+
root_node = self.root.root()
|
|
379
|
+
|
|
380
|
+
# Find class patterns based on language
|
|
381
|
+
class_kind = {
|
|
382
|
+
"python": "class_definition",
|
|
383
|
+
"javascript": "class_declaration",
|
|
384
|
+
"typescript": "class_declaration",
|
|
385
|
+
"java": "class_declaration",
|
|
386
|
+
"ruby": "class",
|
|
387
|
+
"cpp": "class_specifier",
|
|
388
|
+
}.get(self.language)
|
|
389
|
+
|
|
390
|
+
if not class_kind:
|
|
391
|
+
return []
|
|
392
|
+
|
|
393
|
+
for class_match in root_node.find_all(kind=class_kind):
|
|
394
|
+
# Get class name
|
|
395
|
+
name_field = class_match.field("name")
|
|
396
|
+
class_name = name_field.text() if name_field else "Unknown"
|
|
397
|
+
|
|
398
|
+
# Find methods within this class
|
|
399
|
+
methods = []
|
|
400
|
+
method_kind = {
|
|
401
|
+
"python": "function_definition",
|
|
402
|
+
"javascript": "method_definition",
|
|
403
|
+
"typescript": "method_definition",
|
|
404
|
+
"java": "method_declaration",
|
|
405
|
+
"ruby": "method",
|
|
406
|
+
}.get(self.language)
|
|
407
|
+
|
|
408
|
+
if method_kind:
|
|
409
|
+
for method in class_match.find_all(kind=method_kind):
|
|
410
|
+
method_name_field = method.field("name")
|
|
411
|
+
if method_name_field:
|
|
412
|
+
methods.append(method_name_field.text())
|
|
413
|
+
|
|
414
|
+
results.append((class_name, methods))
|
|
415
|
+
|
|
416
|
+
return results
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def analyze_with_ast_grep(
|
|
420
|
+
file_path: str,
|
|
421
|
+
source: str,
|
|
422
|
+
language: str,
|
|
423
|
+
) -> list[AstGrepSymbol]:
|
|
424
|
+
"""Convenience function to analyze a file with ast-grep.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
file_path: Relative file path.
|
|
428
|
+
source: Source code content.
|
|
429
|
+
language: Programming language.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
List of extracted symbols.
|
|
433
|
+
|
|
434
|
+
Example:
|
|
435
|
+
>>> symbols = analyze_with_ast_grep("app.py", source, "python")
|
|
436
|
+
>>> for sym in symbols:
|
|
437
|
+
... print(f"{sym.type}: {sym.name}")
|
|
438
|
+
"""
|
|
439
|
+
if not HAS_AST_GREP:
|
|
440
|
+
return []
|
|
441
|
+
|
|
442
|
+
analyzer = AstGrepAnalyzer(file_path, source, language)
|
|
443
|
+
return analyzer.analyze()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def is_ast_grep_available() -> bool:
|
|
447
|
+
"""Check if ast-grep-py is installed and available."""
|
|
448
|
+
return HAS_AST_GREP
|