cortex-local 2.2.1__tar.gz → 2.3.0__tar.gz
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.
- {cortex_local-2.2.1 → cortex_local-2.3.0}/PKG-INFO +1 -1
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/chunker.py +108 -7
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/code_graph.py +40 -0
- cortex_local-2.3.0/cortex/core/roslyn_analyzer.py +35 -0
- cortex_local-2.3.0/cortex/core/roslyn_tool/CortexAnalyzer.csproj +13 -0
- cortex_local-2.3.0/cortex/core/roslyn_tool/Program.cs +118 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/indexer.py +12 -3
- {cortex_local-2.2.1 → cortex_local-2.3.0}/README.md +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/__init__.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/cli.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/config.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/__init__.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/cache.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/formatters.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/global_graph.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/graph.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/notes_graph.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/embedder.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/mcp_server.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/state.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/watcher.py +0 -0
- {cortex_local-2.2.1 → cortex_local-2.3.0}/pyproject.toml +0 -0
|
@@ -3,18 +3,44 @@
|
|
|
3
3
|
|
|
4
4
|
import fnmatch
|
|
5
5
|
import os
|
|
6
|
+
import re as _re
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
9
|
+
_RAZOR_BLOCK_RE = _re.compile(r'@(?:code|functions)\s*\{', _re.IGNORECASE)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _extract_razor_cs_blocks(source: str) -> list:
|
|
13
|
+
"""Extract @code{} and @functions{} C# blocks from Razor markup using brace counting."""
|
|
14
|
+
blocks = []
|
|
15
|
+
for match in _RAZOR_BLOCK_RE.finditer(source):
|
|
16
|
+
open_pos = source.index('{', match.start())
|
|
17
|
+
depth = 0
|
|
18
|
+
j = open_pos
|
|
19
|
+
while j < len(source):
|
|
20
|
+
if source[j] == '{':
|
|
21
|
+
depth += 1
|
|
22
|
+
elif source[j] == '}':
|
|
23
|
+
depth -= 1
|
|
24
|
+
if depth == 0:
|
|
25
|
+
start_line = source[:open_pos].count('\n') + 2 # line after opening {
|
|
26
|
+
end_line = source[:j].count('\n') + 1 # line of closing }
|
|
27
|
+
if end_line > start_line:
|
|
28
|
+
blocks.append({"start_line": start_line, "end_line": end_line})
|
|
29
|
+
break
|
|
30
|
+
j += 1
|
|
31
|
+
return blocks
|
|
32
|
+
|
|
8
33
|
REPOS_DIR = Path(os.environ.get("REPOS_DIR", "/tmp/repos"))
|
|
9
34
|
|
|
10
|
-
CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml"}
|
|
11
|
-
SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target"}
|
|
35
|
+
CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml", ".cs", ".razor", ".cshtml"}
|
|
36
|
+
SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target", "bin", "obj"}
|
|
12
37
|
LANG_MAP = {
|
|
13
38
|
".js": "javascript", ".ts": "typescript", ".tsx": "typescript",
|
|
14
39
|
".jsx": "javascript", ".svelte": "svelte", ".py": "python",
|
|
15
40
|
".java": "java", ".go": "go", ".rs": "rust", ".css": "css", ".html": "html",
|
|
16
41
|
".kt": "kotlin", ".kts": "kotlin", ".gd": "gdscript",
|
|
17
|
-
".yml": "yaml", ".yaml": "yaml",
|
|
42
|
+
".yml": "yaml", ".yaml": "yaml", ".cs": "csharp",
|
|
43
|
+
".razor": "razor", ".cshtml": "cshtml",
|
|
18
44
|
}
|
|
19
45
|
|
|
20
46
|
def _is_excluded(rel: str, patterns: list) -> bool:
|
|
@@ -97,6 +123,15 @@ _FRAMEWORK_IMPORTS = [
|
|
|
97
123
|
("use actix", "actix"),
|
|
98
124
|
("use axum", "axum"),
|
|
99
125
|
("use tokio", "tokio"),
|
|
126
|
+
# C#
|
|
127
|
+
("using Microsoft.AspNetCore", "aspnetcore"),
|
|
128
|
+
("using Microsoft.Extensions", "aspnetcore"),
|
|
129
|
+
("using Microsoft.EntityFrameworkCore", "entityframework"),
|
|
130
|
+
("using Newtonsoft.Json", "newtonsoft"),
|
|
131
|
+
("using System.Text.Json", "system-text-json"),
|
|
132
|
+
("using Xunit", "xunit"),
|
|
133
|
+
("using NUnit.Framework", "nunit"),
|
|
134
|
+
("using Microsoft.Azure", "azure-sdk"),
|
|
100
135
|
]
|
|
101
136
|
|
|
102
137
|
try:
|
|
@@ -124,6 +159,15 @@ try:
|
|
|
124
159
|
".kt": {"function_declaration", "class_declaration", "object_declaration"},
|
|
125
160
|
".kts": {"function_declaration", "class_declaration", "object_declaration"},
|
|
126
161
|
}
|
|
162
|
+
try:
|
|
163
|
+
import tree_sitter_c_sharp as _tscs
|
|
164
|
+
_TS_LANGUAGES[".cs"] = Language(_tscs.language())
|
|
165
|
+
_TS_SEMANTIC[".cs"] = {
|
|
166
|
+
"method_declaration", "class_declaration", "interface_declaration",
|
|
167
|
+
"constructor_declaration", "struct_declaration", "enum_declaration",
|
|
168
|
+
}
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
127
171
|
_TS_AVAILABLE = True
|
|
128
172
|
except Exception:
|
|
129
173
|
_TS_AVAILABLE = False
|
|
@@ -198,15 +242,17 @@ def _collect_semantic_nodes(node, target_types: set, depth: int = 0, max_depth:
|
|
|
198
242
|
return result
|
|
199
243
|
|
|
200
244
|
|
|
201
|
-
def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_url_base: str = "") -> list:
|
|
245
|
+
def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_url_base: str = "", roslyn_data: dict | None = None) -> list:
|
|
202
246
|
"""Chunk a source file into indexable segments.
|
|
203
247
|
|
|
204
248
|
base_dir: directory to compute relative paths from (defaults to path.parent).
|
|
205
249
|
github_url_base: e.g. "https://github.com/owner/repo/blob/master"
|
|
250
|
+
roslyn_data: per-file Roslyn analysis dict with "symbols" and "typeRefs" keys.
|
|
206
251
|
"""
|
|
207
252
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
253
|
+
raw = path.read_bytes()
|
|
254
|
+
source_text = raw.decode("utf-8", errors="replace")
|
|
255
|
+
lines = source_text.splitlines()
|
|
210
256
|
except Exception:
|
|
211
257
|
return []
|
|
212
258
|
if not lines:
|
|
@@ -225,6 +271,35 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
|
|
|
225
271
|
h = f"# {repo_name}/{rel} (lines {s}-{e})\n# {tokens}"
|
|
226
272
|
return h + f"\n# {extra}" if extra else h
|
|
227
273
|
|
|
274
|
+
# Razor/Blazor: extract @code blocks as csharp + whole file as razor context
|
|
275
|
+
if ext in (".razor", ".cshtml"):
|
|
276
|
+
chunks = []
|
|
277
|
+
blocks = _extract_razor_cs_blocks(source_text)
|
|
278
|
+
for block in blocks:
|
|
279
|
+
s, e = block["start_line"], block["end_line"]
|
|
280
|
+
body = "\n".join(lines[s - 1:e]).strip()
|
|
281
|
+
if body:
|
|
282
|
+
url = f"{github_url_base}/{rel}#L{s}-L{e}" if github_url_base else ""
|
|
283
|
+
tokens = _path_tokens(repo_name, rel)
|
|
284
|
+
chunks.append({
|
|
285
|
+
"repo": repo_name, "file": rel, "language": "csharp",
|
|
286
|
+
"start_line": s, "end_line": e,
|
|
287
|
+
"text": f"{_header(rel, s, e, tokens)}\n\n{body}",
|
|
288
|
+
"github_url": url,
|
|
289
|
+
})
|
|
290
|
+
# Always include a full-file razor chunk for markup context
|
|
291
|
+
body = "\n".join(lines).strip()
|
|
292
|
+
if body:
|
|
293
|
+
url = f"{github_url_base}/{rel}#L1-L{len(lines)}" if github_url_base else ""
|
|
294
|
+
tokens = _path_tokens(repo_name, rel)
|
|
295
|
+
chunks.append({
|
|
296
|
+
"repo": repo_name, "file": rel, "language": language,
|
|
297
|
+
"start_line": 1, "end_line": len(lines),
|
|
298
|
+
"text": f"{_header(rel, 1, len(lines), tokens)}\n\n{body}",
|
|
299
|
+
"github_url": url,
|
|
300
|
+
})
|
|
301
|
+
return chunks
|
|
302
|
+
|
|
228
303
|
# Small files: keep as single chunk to avoid diluting path/filename signal
|
|
229
304
|
if len(lines) <= MAX_CHUNK_LINES:
|
|
230
305
|
body = "\n".join(lines).strip()
|
|
@@ -239,10 +314,36 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
|
|
|
239
314
|
}]
|
|
240
315
|
return []
|
|
241
316
|
|
|
317
|
+
# Roslyn semantic chunking for .cs (preferred over tree-sitter when available)
|
|
318
|
+
if roslyn_data and ext == ".cs":
|
|
319
|
+
top_level_kinds = {"class", "interface", "struct", "enum", "record"}
|
|
320
|
+
all_syms = roslyn_data.get("symbols", [])
|
|
321
|
+
syms = [s for s in all_syms if s.get("kind") in top_level_kinds] or all_syms
|
|
322
|
+
if syms:
|
|
323
|
+
chunks = []
|
|
324
|
+
for sym in syms:
|
|
325
|
+
s = sym["startLine"] - 1 # 0-indexed
|
|
326
|
+
e = sym["endLine"]
|
|
327
|
+
if e - s > MAX_CHUNK_LINES:
|
|
328
|
+
chunks.extend(_sliding_window(lines, s, e, repo_name, rel, language, github_url_base, extra))
|
|
329
|
+
else:
|
|
330
|
+
body = "\n".join(lines[s:e]).strip()
|
|
331
|
+
if body:
|
|
332
|
+
url = f"{github_url_base}/{rel}#L{s+1}-L{e}" if github_url_base else ""
|
|
333
|
+
tokens = _path_tokens(repo_name, rel)
|
|
334
|
+
chunks.append({
|
|
335
|
+
"repo": repo_name, "file": rel, "language": language,
|
|
336
|
+
"start_line": s + 1, "end_line": e,
|
|
337
|
+
"text": f"{_header(rel, s+1, e, tokens)}\n\n{body}",
|
|
338
|
+
"github_url": url,
|
|
339
|
+
})
|
|
340
|
+
if chunks:
|
|
341
|
+
return chunks
|
|
342
|
+
|
|
242
343
|
if _TS_AVAILABLE and ext in _TS_LANGUAGES:
|
|
243
344
|
try:
|
|
244
345
|
parser = _TSParser(_TS_LANGUAGES[ext])
|
|
245
|
-
tree = parser.parse(
|
|
346
|
+
tree = parser.parse(raw)
|
|
246
347
|
nodes = _collect_semantic_nodes(tree.root_node, _TS_SEMANTIC[ext])
|
|
247
348
|
nodes = [n for n in nodes if n.type != "lexical_declaration" or _has_function_value(n)]
|
|
248
349
|
if nodes:
|
|
@@ -17,6 +17,7 @@ _IMPORT_RE: dict = {
|
|
|
17
17
|
"js": re.compile(r'''(?:import|from|require)\s*\(?['"](\.[^'"]+|\$lib/[^'"]+)['"]'''),
|
|
18
18
|
"ts": re.compile(r'''(?:import|from|require)\s*\(?['"](\.[^'"]+|\$lib/[^'"]+)['"]'''),
|
|
19
19
|
"kt": re.compile(r'^import\s+([\w.]+)', re.MULTILINE),
|
|
20
|
+
"cs": re.compile(r'^using\s+(?!static\b)(?:\w+\s*=\s*)?(\w+(?:\.\w+)+)\s*;', re.MULTILINE),
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
_NAMED_IMPORT_RE = re.compile(
|
|
@@ -31,6 +32,7 @@ _TOP_LEVEL_DEF_RE: dict = {
|
|
|
31
32
|
"py": re.compile(r'^(?:class|def)\s+(\w+)', re.MULTILINE),
|
|
32
33
|
"ts": re.compile(r'(?:export\s+)?(?:class|function|const|interface|type)\s+(\w+)'),
|
|
33
34
|
"kt": re.compile(r'(?:class|fun|object)\s+(\w+)'),
|
|
35
|
+
"cs": re.compile(r'(?m)(?:class|interface|struct|enum|record)\s+(\w+)'),
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
_INHERIT_RE: dict = {
|
|
@@ -42,12 +44,17 @@ _INHERIT_RE: dict = {
|
|
|
42
44
|
r'(?:class|interface|object)\s+\w+[^:{]*:\s*([\w,\s()]+?)(?:\{|$)',
|
|
43
45
|
re.MULTILINE,
|
|
44
46
|
),
|
|
47
|
+
"cs": re.compile(
|
|
48
|
+
r'(?:class|struct|record)\s+\w+(?:<[^>]*>)?\s*:\s*([\w,\s<>.?[\]]+?)(?=\s*(?:where|\{))',
|
|
49
|
+
re.MULTILINE,
|
|
50
|
+
),
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
_LANG_KEY: dict = {
|
|
48
54
|
".py": "py", ".js": "js", ".jsx": "js",
|
|
49
55
|
".ts": "ts", ".tsx": "ts", ".svelte": "js",
|
|
50
56
|
".kt": "kt", ".kts": "kt",
|
|
57
|
+
".cs": "cs", ".razor": "cs", ".cshtml": "cs",
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
_EDGE_PRIORITY: dict = {"import": 0, "call": 1, "inherits": 2}
|
|
@@ -62,6 +69,12 @@ _PY_SKIP_BASES = frozenset({
|
|
|
62
69
|
"list", "dict", "set", "tuple", "Enum", "IntEnum",
|
|
63
70
|
})
|
|
64
71
|
_TS_SKIP_BASES = frozenset({"Error", "EventEmitter", "Component", "React"})
|
|
72
|
+
_CS_SKIP_BASES = frozenset({
|
|
73
|
+
"object", "Object", "Exception", "SystemException", "ApplicationException",
|
|
74
|
+
"Attribute", "Controller", "ControllerBase", "ApiController", "PageModel",
|
|
75
|
+
"BackgroundService", "IHostedService", "IDisposable", "ValueType",
|
|
76
|
+
"IEnumerable", "IList", "ICollection", "IQueryable", "IComparable",
|
|
77
|
+
})
|
|
65
78
|
|
|
66
79
|
|
|
67
80
|
def _extract_imports(path: Path, ext: str) -> list:
|
|
@@ -217,6 +230,14 @@ def _extract_inheritance_targets(
|
|
|
217
230
|
t = symbol_to_file.get(base)
|
|
218
231
|
if t and t != rel:
|
|
219
232
|
targets.add(t)
|
|
233
|
+
elif key == "cs":
|
|
234
|
+
for m in _INHERIT_RE["cs"].finditer(src):
|
|
235
|
+
for base in m.group(1).split(","):
|
|
236
|
+
base = base.strip().split("<")[0].strip()
|
|
237
|
+
if base and base not in _CS_SKIP_BASES:
|
|
238
|
+
t = symbol_to_file.get(base)
|
|
239
|
+
if t and t != rel:
|
|
240
|
+
targets.add(t)
|
|
220
241
|
|
|
221
242
|
return list(targets)
|
|
222
243
|
|
|
@@ -230,6 +251,25 @@ def _add_edge(G, src: str, dst: str, edge_type: str) -> None:
|
|
|
230
251
|
G.add_edge(src, dst, type=edge_type)
|
|
231
252
|
|
|
232
253
|
|
|
254
|
+
def inject_roslyn_edges(G, roslyn_data: dict, all_files: set) -> None:
|
|
255
|
+
"""Add cross-file type reference edges from Roslyn analysis output."""
|
|
256
|
+
if not _NX_AVAILABLE or G is None or not roslyn_data:
|
|
257
|
+
return
|
|
258
|
+
symbol_to_file: dict = {}
|
|
259
|
+
for rel, data in roslyn_data.items():
|
|
260
|
+
for sym in data.get("symbols", []):
|
|
261
|
+
name = sym.get("name", "")
|
|
262
|
+
if name and name not in symbol_to_file:
|
|
263
|
+
symbol_to_file[name] = rel
|
|
264
|
+
for rel, data in roslyn_data.items():
|
|
265
|
+
if rel not in all_files or not G.has_node(rel):
|
|
266
|
+
continue
|
|
267
|
+
for type_ref in data.get("typeRefs", []):
|
|
268
|
+
target = symbol_to_file.get(type_ref)
|
|
269
|
+
if target and target != rel and target in all_files and G.has_node(target):
|
|
270
|
+
_add_edge(G, rel, target, "import")
|
|
271
|
+
|
|
272
|
+
|
|
233
273
|
def build_code_graph(repo_path: Path, all_files: set):
|
|
234
274
|
if not _NX_AVAILABLE:
|
|
235
275
|
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Python wrapper for the Roslyn C# syntax analyzer subprocess."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ROSLYN_BINARY = Path(os.environ.get(
|
|
10
|
+
"ROSLYN_ANALYZER",
|
|
11
|
+
str(Path(__file__).parent / "roslyn_tool" / "CortexAnalyzer")
|
|
12
|
+
))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_available() -> bool:
|
|
16
|
+
return ROSLYN_BINARY.exists() and os.access(ROSLYN_BINARY, os.X_OK)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def analyze_dir(dir_path: Path) -> dict:
|
|
20
|
+
"""Run Roslyn analyzer on a directory. Returns per-file symbol + type_ref data.
|
|
21
|
+
|
|
22
|
+
Keys are relative paths (forward slashes). Returns {} on any failure.
|
|
23
|
+
"""
|
|
24
|
+
if not is_available():
|
|
25
|
+
return {}
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
[str(ROSLYN_BINARY), str(dir_path)],
|
|
29
|
+
capture_output=True, text=True, timeout=60,
|
|
30
|
+
)
|
|
31
|
+
if result.returncode != 0:
|
|
32
|
+
return {}
|
|
33
|
+
return json.loads(result.stdout)
|
|
34
|
+
except Exception:
|
|
35
|
+
return {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
<PropertyGroup>
|
|
3
|
+
<OutputType>Exe</OutputType>
|
|
4
|
+
<TargetFramework>net8.0</TargetFramework>
|
|
5
|
+
<Nullable>enable</Nullable>
|
|
6
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
7
|
+
<AssemblyName>CortexAnalyzer</AssemblyName>
|
|
8
|
+
<RootNamespace>CortexAnalyzer</RootNamespace>
|
|
9
|
+
</PropertyGroup>
|
|
10
|
+
<ItemGroup>
|
|
11
|
+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
|
|
12
|
+
</ItemGroup>
|
|
13
|
+
</Project>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis.CSharp;
|
|
2
|
+
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
3
|
+
using System.Text.Json;
|
|
4
|
+
|
|
5
|
+
var dir = args.Length > 0 ? args[0] : Directory.GetCurrentDirectory();
|
|
6
|
+
var jOpts = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
|
7
|
+
var results = new Dictionary<string, FileResult>();
|
|
8
|
+
|
|
9
|
+
foreach (var file in Directory.EnumerateFiles(dir, "*.cs", SearchOption.AllDirectories))
|
|
10
|
+
{
|
|
11
|
+
var rel = Path.GetRelativePath(dir, file).Replace('\\', '/');
|
|
12
|
+
try
|
|
13
|
+
{
|
|
14
|
+
var source = File.ReadAllText(file);
|
|
15
|
+
var tree = CSharpSyntaxTree.ParseText(source);
|
|
16
|
+
var root = tree.GetRoot();
|
|
17
|
+
|
|
18
|
+
var symbols = new List<SymbolInfo>();
|
|
19
|
+
var typeRefs = new HashSet<string>();
|
|
20
|
+
|
|
21
|
+
foreach (var node in root.DescendantNodes())
|
|
22
|
+
{
|
|
23
|
+
var span = tree.GetLineSpan(node.Span);
|
|
24
|
+
int s = span.StartLinePosition.Line + 1;
|
|
25
|
+
int e = span.EndLinePosition.Line + 1;
|
|
26
|
+
|
|
27
|
+
switch (node)
|
|
28
|
+
{
|
|
29
|
+
case ClassDeclarationSyntax c:
|
|
30
|
+
symbols.Add(new(c.Identifier.Text, "class", s, e));
|
|
31
|
+
AddBaseTypes(typeRefs, c.BaseList);
|
|
32
|
+
break;
|
|
33
|
+
case InterfaceDeclarationSyntax i:
|
|
34
|
+
symbols.Add(new(i.Identifier.Text, "interface", s, e));
|
|
35
|
+
AddBaseTypes(typeRefs, i.BaseList);
|
|
36
|
+
break;
|
|
37
|
+
case RecordDeclarationSyntax r:
|
|
38
|
+
symbols.Add(new(r.Identifier.Text, "record", s, e));
|
|
39
|
+
AddBaseTypes(typeRefs, r.BaseList);
|
|
40
|
+
break;
|
|
41
|
+
case StructDeclarationSyntax st:
|
|
42
|
+
symbols.Add(new(st.Identifier.Text, "struct", s, e));
|
|
43
|
+
AddBaseTypes(typeRefs, st.BaseList);
|
|
44
|
+
break;
|
|
45
|
+
case EnumDeclarationSyntax en:
|
|
46
|
+
symbols.Add(new(en.Identifier.Text, "enum", s, e));
|
|
47
|
+
break;
|
|
48
|
+
case MethodDeclarationSyntax m:
|
|
49
|
+
symbols.Add(new(m.Identifier.Text, "method", s, e));
|
|
50
|
+
AddTypeName(typeRefs, m.ReturnType);
|
|
51
|
+
foreach (var p in m.ParameterList.Parameters)
|
|
52
|
+
if (p.Type is not null) AddTypeName(typeRefs, p.Type);
|
|
53
|
+
break;
|
|
54
|
+
case ConstructorDeclarationSyntax ctor:
|
|
55
|
+
symbols.Add(new(ctor.Identifier.Text, "constructor", s, e));
|
|
56
|
+
foreach (var p in ctor.ParameterList.Parameters)
|
|
57
|
+
if (p.Type is not null) AddTypeName(typeRefs, p.Type);
|
|
58
|
+
break;
|
|
59
|
+
case PropertyDeclarationSyntax prop:
|
|
60
|
+
symbols.Add(new(prop.Identifier.Text, "property", s, e));
|
|
61
|
+
AddTypeName(typeRefs, prop.Type);
|
|
62
|
+
break;
|
|
63
|
+
case FieldDeclarationSyntax f:
|
|
64
|
+
AddTypeName(typeRefs, f.Declaration.Type);
|
|
65
|
+
break;
|
|
66
|
+
case ObjectCreationExpressionSyntax obj:
|
|
67
|
+
AddTypeName(typeRefs, obj.Type);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
results[rel] = new FileResult(symbols, typeRefs.ToList());
|
|
73
|
+
}
|
|
74
|
+
catch { /* skip unparseable files */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Console.WriteLine(JsonSerializer.Serialize(results, jOpts));
|
|
78
|
+
|
|
79
|
+
void AddBaseTypes(HashSet<string> refs, BaseListSyntax? baseList)
|
|
80
|
+
{
|
|
81
|
+
if (baseList is null) return;
|
|
82
|
+
foreach (var bt in baseList.Types)
|
|
83
|
+
AddTypeName(refs, bt.Type);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
void AddTypeName(HashSet<string> refs, TypeSyntax type)
|
|
87
|
+
{
|
|
88
|
+
switch (type)
|
|
89
|
+
{
|
|
90
|
+
case IdentifierNameSyntax id when !IsPrimitive(id.Identifier.Text):
|
|
91
|
+
refs.Add(id.Identifier.Text);
|
|
92
|
+
break;
|
|
93
|
+
case GenericNameSyntax gen:
|
|
94
|
+
if (!IsPrimitive(gen.Identifier.Text)) refs.Add(gen.Identifier.Text);
|
|
95
|
+
foreach (var arg in gen.TypeArgumentList.Arguments) AddTypeName(refs, arg);
|
|
96
|
+
break;
|
|
97
|
+
case QualifiedNameSyntax q:
|
|
98
|
+
AddTypeName(refs, q.Right);
|
|
99
|
+
break;
|
|
100
|
+
case NullableTypeSyntax n:
|
|
101
|
+
AddTypeName(refs, n.ElementType);
|
|
102
|
+
break;
|
|
103
|
+
case ArrayTypeSyntax a:
|
|
104
|
+
AddTypeName(refs, a.ElementType);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
bool IsPrimitive(string n) => n is
|
|
110
|
+
"void" or "string" or "int" or "long" or "double" or "float" or "bool"
|
|
111
|
+
or "byte" or "char" or "object" or "decimal" or "short" or "uint" or "ulong"
|
|
112
|
+
or "ushort" or "sbyte" or "nint" or "nuint" or "var" or "dynamic"
|
|
113
|
+
or "String" or "Int32" or "Int64" or "Boolean" or "Double" or "Object" or "Void"
|
|
114
|
+
or "Byte" or "Char" or "Decimal" or "Single" or "DateTime" or "DateTimeOffset"
|
|
115
|
+
or "Guid" or "TimeSpan" or "Uri";
|
|
116
|
+
|
|
117
|
+
record SymbolInfo(string Name, string Kind, int StartLine, int EndLine);
|
|
118
|
+
record FileResult(List<SymbolInfo> Symbols, List<string> TypeRefs);
|
|
@@ -17,6 +17,7 @@ from .embedder import embed, sparse_embed
|
|
|
17
17
|
from .core.chunker import chunk_file, CODE_EXTS, SKIP_DIRS, _is_excluded
|
|
18
18
|
from .core.cache import load_cache, save_cache
|
|
19
19
|
from .core import graph as _graph
|
|
20
|
+
from .core import roslyn_analyzer as _roslyn
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _qdrant() -> QdrantClient:
|
|
@@ -199,8 +200,13 @@ def index_code(code_dir: Path, show_progress: bool = False) -> int:
|
|
|
199
200
|
cached = 0
|
|
200
201
|
file_point_ids: dict = {}
|
|
201
202
|
|
|
203
|
+
roslyn_data: dict = {}
|
|
204
|
+
if _roslyn.is_available() and any(p.suffix == ".cs" for p in code_files):
|
|
205
|
+
roslyn_data = _roslyn.analyze_dir(code_dir)
|
|
206
|
+
|
|
202
207
|
for path in tqdm(code_files, desc=repo_name, disable=not show_progress):
|
|
203
|
-
|
|
208
|
+
rel_path = str(path.relative_to(code_dir)).replace("\\", "/")
|
|
209
|
+
cache_key = f"{repo_name}/{rel_path}"
|
|
204
210
|
mtime = path.stat().st_mtime
|
|
205
211
|
entry = cache.get(cache_key, {})
|
|
206
212
|
if entry.get("mtime") == mtime:
|
|
@@ -213,7 +219,7 @@ def index_code(code_dir: Path, show_progress: bool = False) -> int:
|
|
|
213
219
|
continue
|
|
214
220
|
|
|
215
221
|
points = []
|
|
216
|
-
for chunk in chunk_file(path, repo_name, base_dir=code_dir):
|
|
222
|
+
for chunk in chunk_file(path, repo_name, base_dir=code_dir, roslyn_data=roslyn_data.get(rel_path)):
|
|
217
223
|
pid = _code_chunk_id(repo_name, chunk["file"], chunk["start_line"])
|
|
218
224
|
points.append(_make_point(chunk["text"], pid, chunk, use_sparse))
|
|
219
225
|
file_point_ids.setdefault(chunk["file"], []).append(pid)
|
|
@@ -226,10 +232,13 @@ def index_code(code_dir: Path, show_progress: bool = False) -> int:
|
|
|
226
232
|
print(f" {repo_name}: {total} chunks from {len(code_files) - cached} files ({cached} cached)", flush=True)
|
|
227
233
|
|
|
228
234
|
all_files = {
|
|
229
|
-
str(p.relative_to(code_dir)) for p in code_files
|
|
235
|
+
str(p.relative_to(code_dir)).replace("\\", "/") for p in code_files
|
|
230
236
|
}
|
|
231
237
|
try:
|
|
232
238
|
G = _graph.build_code_graph(code_dir, all_files)
|
|
239
|
+
if roslyn_data:
|
|
240
|
+
from .core.code_graph import inject_roslyn_edges
|
|
241
|
+
inject_roslyn_edges(G, roslyn_data, all_files)
|
|
233
242
|
if G is not None:
|
|
234
243
|
metadata = _graph.compute_code_metadata(G)
|
|
235
244
|
_graph.persist_code_graph(G, metadata, repo_name, data_dir())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|