codeui-python 0.1.0__py3-none-any.whl → 0.1.1__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 (49) hide show
  1. codeui/__init__.py +1 -1
  2. codeui/agent/context.py +19 -3
  3. codeui/analysis/dead_code.py +13 -2
  4. codeui/analysis/undefined.py +70 -0
  5. codeui/analysis/unresolved_import.py +34 -4
  6. codeui/cli/main.py +101 -7
  7. codeui/core/cache.py +54 -0
  8. codeui/core/graph.py +233 -3
  9. codeui/core/override.py +1 -0
  10. codeui/core/resolver.py +51 -37
  11. codeui/errors.py +10 -0
  12. codeui/lang/base.py +5 -2
  13. codeui/lang/cpp.py +271 -0
  14. codeui/lang/csharp.py +262 -0
  15. codeui/lang/dart.py +278 -0
  16. codeui/lang/generic.py +6 -4
  17. codeui/lang/go.py +5 -2
  18. codeui/lang/java.py +320 -0
  19. codeui/lang/kotlin.py +303 -0
  20. codeui/lang/php.py +275 -0
  21. codeui/lang/python.py +49 -7
  22. codeui/lang/registry.py +22 -2
  23. codeui/lang/ruby.py +270 -0
  24. codeui/lang/rust.py +5 -2
  25. codeui/lang/scala.py +290 -0
  26. codeui/lang/swift.py +276 -0
  27. codeui/lang/ts.py +99 -8
  28. codeui/report/sarif_emitter.py +2 -1
  29. codeui/server/server.py +156 -85
  30. codeui/server/static/css/styles.css +1609 -0
  31. codeui/server/static/index.html +199 -0
  32. codeui/server/static/js/api.js +485 -0
  33. codeui/server/static/js/app.js +1044 -0
  34. codeui/server/static/js/graph_canvas.js +1485 -0
  35. codeui/server/static/js/minimap.js +383 -0
  36. codeui/server/static/js/sidebar.js +268 -0
  37. codeui/server/static/js/state.js +83 -0
  38. codeui/server/static/js/subgraph_window.js +1208 -0
  39. codeui/server/static/js/theme.js +332 -0
  40. codeui/tracer/__init__.py +4 -1
  41. codeui/tracer/tracer.py +109 -118
  42. codeui_python-0.1.1.dist-info/METADATA +171 -0
  43. codeui_python-0.1.1.dist-info/RECORD +71 -0
  44. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/licenses/LICENSE +1 -1
  45. codeui_python-0.1.0.dist-info/METADATA +0 -191
  46. codeui_python-0.1.0.dist-info/RECORD +0 -52
  47. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/WHEEL +0 -0
  48. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/entry_points.txt +0 -0
  49. {codeui_python-0.1.0.dist-info → codeui_python-0.1.1.dist-info}/top_level.txt +0 -0
codeui/lang/registry.py CHANGED
@@ -6,6 +6,15 @@ from codeui.lang.python import PythonLanguageAnalyzer
6
6
  from codeui.lang.ts import TSLanguageAnalyzer
7
7
  from codeui.lang.go import GoLanguageAnalyzer
8
8
  from codeui.lang.rust import RustLanguageAnalyzer
9
+ from codeui.lang.java import JavaLanguageAnalyzer
10
+ from codeui.lang.cpp import CppLanguageAnalyzer
11
+ from codeui.lang.csharp import CSharpLanguageAnalyzer
12
+ from codeui.lang.php import PHPLanguageAnalyzer
13
+ from codeui.lang.ruby import RubyLanguageAnalyzer
14
+ from codeui.lang.kotlin import KotlinLanguageAnalyzer
15
+ from codeui.lang.swift import SwiftLanguageAnalyzer
16
+ from codeui.lang.scala import ScalaLanguageAnalyzer
17
+ from codeui.lang.dart import DartLanguageAnalyzer
9
18
  from codeui.lang.generic import GenericLanguageAnalyzer
10
19
 
11
20
  class LanguageRegistry:
@@ -22,10 +31,19 @@ class LanguageRegistry:
22
31
  TSLanguageAnalyzer(),
23
32
  GoLanguageAnalyzer(),
24
33
  RustLanguageAnalyzer(),
34
+ JavaLanguageAnalyzer(),
35
+ CppLanguageAnalyzer(),
36
+ CSharpLanguageAnalyzer(),
37
+ PHPLanguageAnalyzer(),
38
+ RubyLanguageAnalyzer(),
39
+ KotlinLanguageAnalyzer(),
40
+ SwiftLanguageAnalyzer(),
41
+ ScalaLanguageAnalyzer(),
42
+ DartLanguageAnalyzer(),
25
43
  GenericLanguageAnalyzer(),
26
44
  ]
27
45
  self._ext_map: Dict[str, LanguageAnalyzer] = {}
28
- for analyzer in self._analyzers:
46
+ for analyzer in reversed(self._analyzers):
29
47
  for ext in analyzer.extensions:
30
48
  self._ext_map[ext.lower()] = analyzer
31
49
 
@@ -37,7 +55,9 @@ class LanguageRegistry:
37
55
  """
38
56
  self._analyzers.append(analyzer)
39
57
  for ext in analyzer.extensions:
40
- self._ext_map[ext.lower()] = analyzer
58
+ ext_lower = ext.lower()
59
+ if type(analyzer).__name__ != "GenericLanguageAnalyzer" or ext_lower not in self._ext_map:
60
+ self._ext_map[ext_lower] = analyzer
41
61
 
42
62
  def get_analyzer(self, path: Path) -> LanguageAnalyzer:
43
63
  """Find language analyzer matching file extension or filename.
codeui/lang/ruby.py ADDED
@@ -0,0 +1,270 @@
1
+ """Ruby language analyzer for codeui."""
2
+ from __future__ import annotations
3
+ import hashlib
4
+ import re
5
+ from pathlib import Path
6
+ from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
7
+ from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
8
+ from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
9
+
10
+ if TYPE_CHECKING:
11
+ from codeui.core.resolver import ResolveContext
12
+
13
+ class RubyLanguageAnalyzer(LanguageAnalyzer):
14
+ """Ruby language analyzer.
15
+ Example:
16
+ >>> analyzer = RubyLanguageAnalyzer()
17
+ >>> res = analyzer.parse(Path("app.rb"), 'class App < Base\\n def run\\n end\\nend')
18
+ >>> syms = list(analyzer.extract_symbols(res))
19
+ >>> any(s.name == "App" for s in syms)
20
+ True
21
+ """
22
+ language: ClassVar[str] = "ruby"
23
+ extensions: ClassVar[tuple[str, ...]] = (".rb",)
24
+ has_ast_support: ClassVar[bool] = True
25
+ is_supported: ClassVar[bool] = True
26
+
27
+ def parse(self, path: Path, source: str) -> ParseResult:
28
+ """Parse Ruby source code into ParseResult.
29
+ Example:
30
+ >>> analyzer = RubyLanguageAnalyzer()
31
+ >>> res = analyzer.parse(Path("main.rb"), "require 'json'")
32
+ >>> res.errors
33
+ []
34
+ """
35
+ content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
36
+ return ParseResult(
37
+ file_path=str(path),
38
+ source=source,
39
+ ast=source,
40
+ content_hash=content_hash,
41
+ errors=[],
42
+ )
43
+
44
+ def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
45
+ """Extract modules, classes, and methods from Ruby source.
46
+ Example:
47
+ >>> analyzer = RubyLanguageAnalyzer()
48
+ >>> res = analyzer.parse(Path("user.rb"), "class User\\n def name\\n end\\nend")
49
+ >>> syms = list(analyzer.extract_symbols(res))
50
+ >>> len(syms) >= 2
51
+ True
52
+ """
53
+ source = parse.source
54
+ file_path = parse.file_path
55
+ lines = source.splitlines()
56
+ max_line = len(lines) if lines else 1
57
+ symbols: List[Symbol] = []
58
+
59
+ file_sym = Symbol(
60
+ id=file_path,
61
+ name=Path(file_path).name,
62
+ qualified_name=file_path,
63
+ kind=SymbolKind.FILE,
64
+ language=self.language,
65
+ location=Location(file_path, 1, 0, max_line, 0),
66
+ parent_id=None,
67
+ signature=None,
68
+ visibility=Visibility.PUBLIC,
69
+ modifiers=(),
70
+ content_hash=parse.content_hash,
71
+ )
72
+ symbols.append(file_sym)
73
+
74
+ require_pattern = re.compile(r'require(?:_relative)?\s+["\']([^"\']+)["\']')
75
+ class_pattern = re.compile(r'(?:class|module)\s+([A-Za-z0-9_:]+)(?:\s*<\s*([A-Za-z0-9_:]+))?')
76
+ def_pattern = re.compile(r'def\s+(?:self\.)?([A-Za-z0-9_?!]+)(?:\s*\(([^)]*)\))?')
77
+
78
+ ruby_keywords = {"if", "for", "while", "unless", "until", "case", "when", "then", "begin", "rescue", "ensure", "end", "return", "class", "module", "def"}
79
+ class_ranges: List[tuple[str, str, int, int, str]] = []
80
+
81
+ for idx, line in enumerate(lines, start=1):
82
+ line_str = line.strip()
83
+ if not line_str or line_str.startswith("#"):
84
+ continue
85
+
86
+ for match in require_pattern.finditer(line_str):
87
+ req_path = match.group(1)
88
+ sym_id = f"import:{req_path}"
89
+ symbols.append(Symbol(
90
+ id=sym_id,
91
+ name=req_path.split("/")[-1],
92
+ qualified_name=f"import:{req_path}",
93
+ kind=SymbolKind.IMPORT,
94
+ language=self.language,
95
+ location=Location(file_path, idx, line.find(req_path), idx, line.find(req_path) + len(req_path)),
96
+ parent_id=file_path,
97
+ signature=f"require '{req_path}'",
98
+ visibility=Visibility.PUBLIC,
99
+ modifiers=(),
100
+ content_hash=self._extract_body_hash(lines, idx - 1),
101
+ ))
102
+
103
+ for match in class_pattern.finditer(line_str):
104
+ cls_name = match.group(1)
105
+ if cls_name in ruby_keywords:
106
+ continue
107
+ base_name = match.group(2) or ""
108
+ sym_id = f"{file_path}::{cls_name}"
109
+ end_line = self._find_ruby_block_end(lines, idx - 1)
110
+ class_ranges.append((cls_name, sym_id, idx, end_line, base_name))
111
+ loc = Location(file_path, idx, line.find(cls_name), end_line, line.find(cls_name) + len(cls_name))
112
+ symbols.append(Symbol(
113
+ id=sym_id,
114
+ name=cls_name,
115
+ qualified_name=cls_name,
116
+ kind=SymbolKind.CLASS,
117
+ language=self.language,
118
+ location=loc,
119
+ parent_id=file_path,
120
+ signature=f"class {cls_name}",
121
+ visibility=Visibility.PUBLIC,
122
+ modifiers=(),
123
+ content_hash=self._extract_body_hash(lines, idx - 1),
124
+ ))
125
+
126
+ for match in def_pattern.finditer(line_str):
127
+ m_name = match.group(1)
128
+ params = match.group(2) or ""
129
+ if m_name in ruby_keywords:
130
+ continue
131
+ enclosing = next((c for c in reversed(class_ranges) if c[2] <= idx <= c[3]), None)
132
+ if enclosing:
133
+ cls_name, cls_sym_id, _, _, _ = enclosing
134
+ sym_id = f"{file_path}::{cls_name}::{m_name}"
135
+ qual_name = f"{cls_name}#{m_name}"
136
+ parent_sym_id = cls_sym_id
137
+ sym_kind = SymbolKind.METHOD
138
+ else:
139
+ sym_id = f"{file_path}::{m_name}"
140
+ qual_name = m_name
141
+ parent_sym_id = file_path
142
+ sym_kind = SymbolKind.FUNCTION
143
+ end_line = self._find_ruby_block_end(lines, idx - 1)
144
+ loc = Location(file_path, idx, line.find(m_name), end_line, line.find(m_name) + len(m_name))
145
+ symbols.append(Symbol(
146
+ id=sym_id,
147
+ name=m_name,
148
+ qualified_name=qual_name,
149
+ kind=sym_kind,
150
+ language=self.language,
151
+ location=loc,
152
+ parent_id=parent_sym_id,
153
+ signature=f"def {m_name}({params})",
154
+ visibility=Visibility.PUBLIC,
155
+ modifiers=(),
156
+ content_hash=self._extract_body_hash(lines, idx - 1),
157
+ ))
158
+
159
+ return symbols
160
+
161
+ def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
162
+ """Extract relationship edges for Ruby AST.
163
+ Example:
164
+ >>> analyzer = RubyLanguageAnalyzer()
165
+ >>> res = analyzer.parse(Path("app.rb"), "require 'helper'")
166
+ >>> syms = list(analyzer.extract_symbols(res))
167
+ >>> edges = list(analyzer.extract_edges(res, syms))
168
+ >>> len(edges) >= 1
169
+ True
170
+ """
171
+ edges: List[Edge] = []
172
+ file_path = parse.file_path
173
+ sym_map = {s.id: s for s in symbols}
174
+ class_pattern = re.compile(r'class\s+([A-Za-z0-9_:]+)(?:\s*<\s*([A-Za-z0-9_:]+))?')
175
+ call_pattern = re.compile(r'(?<![A-Za-z0-9_$-])\b([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(|\s+[A-Za-z0-9_:"\'])')
176
+ ruby_keywords = {"if", "for", "while", "unless", "until", "case", "when", "then", "begin", "rescue", "ensure", "end", "return", "class", "module", "def", "puts", "print"}
177
+
178
+ for s in symbols:
179
+ if s.parent_id and s.parent_id in sym_map:
180
+ edges.append(Edge(
181
+ source_id=s.parent_id,
182
+ target_id=s.id,
183
+ kind=EdgeKind.CONTAINS,
184
+ weight=1.0,
185
+ confidence=1.0,
186
+ location=s.location,
187
+ ))
188
+ if s.kind == SymbolKind.IMPORT:
189
+ target_mod = s.qualified_name.replace("import:", "")
190
+ edges.append(Edge(
191
+ source_id=file_path,
192
+ target_id=f"module::{target_mod}",
193
+ kind=EdgeKind.IMPORTS,
194
+ weight=1.0,
195
+ confidence=1.0,
196
+ location=s.location,
197
+ ))
198
+
199
+ lines = parse.source.splitlines()
200
+ for idx, line in enumerate(lines, start=1):
201
+ line_str = line.strip()
202
+ if not line_str or line_str.startswith("#"):
203
+ continue
204
+ for match in class_pattern.finditer(line_str):
205
+ cls_name = match.group(1)
206
+ base_name = match.group(2)
207
+ if base_name and base_name not in ruby_keywords:
208
+ cls_sym_id = f"{file_path}::{cls_name}"
209
+ loc = Location(file_path, idx, line.find(base_name), idx, line.find(base_name) + len(base_name))
210
+ edges.append(Edge(
211
+ source_id=cls_sym_id,
212
+ target_id=f"module::{base_name}",
213
+ kind=EdgeKind.INHERITS,
214
+ weight=1.0,
215
+ confidence=0.9,
216
+ location=loc,
217
+ ))
218
+
219
+ func_syms = [s for s in symbols if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)]
220
+ name_to_syms: dict[str, list[Symbol]] = {}
221
+ for s in func_syms:
222
+ name_to_syms.setdefault(s.name, []).append(s)
223
+
224
+ for f in func_syms:
225
+ if not f.location:
226
+ continue
227
+ f_lines = lines[f.location.start_line - 1 : f.location.end_line]
228
+ for f_line in f_lines:
229
+ for match in call_pattern.finditer(f_line):
230
+ callee_name = match.group(1)
231
+ if callee_name in ruby_keywords or callee_name == f.name:
232
+ continue
233
+ if callee_name in name_to_syms:
234
+ target = name_to_syms[callee_name][0]
235
+ edges.append(Edge(
236
+ source_id=f.id,
237
+ target_id=target.id,
238
+ kind=EdgeKind.CALLS,
239
+ weight=1.0,
240
+ confidence=0.85,
241
+ location=f.location,
242
+ ))
243
+
244
+ return edges
245
+
246
+ def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
247
+ """Resolve Ruby require references to project files.
248
+ Example:
249
+ >>> analyzer = RubyLanguageAnalyzer()
250
+ >>> ctx = ResolveContext(Path("."))
251
+ >>> list(analyzer.resolve_import(ImportRef("app.rb", "helper", None, False), ctx))
252
+ []
253
+ """
254
+ clean_path = ref.module_name.replace(".", "/")
255
+ return ctx.resolve_path(ref.file_path, clean_path, self.language)
256
+
257
+ def _find_ruby_block_end(self, lines: List[str], start_idx: int) -> int:
258
+ nesting = 0
259
+ block_starters = ("def ", "class ", "module ", "if ", "unless ", "case ", "while ", "until ", "for ", "begin ", "do ")
260
+ for i in range(start_idx, len(lines)):
261
+ l = lines[i].strip()
262
+ if not l or l.startswith("#"):
263
+ continue
264
+ if any(l.startswith(bs) for bs in block_starters) or l == "do":
265
+ nesting += 1
266
+ if l == "end" or l.startswith("end ") or l.endswith(" end"):
267
+ nesting -= 1
268
+ if nesting <= 0:
269
+ return i + 1
270
+ return min(start_idx + 30, len(lines))
codeui/lang/rust.py CHANGED
@@ -1,12 +1,15 @@
1
1
  """Rust language adapter for codeui."""
2
+ from __future__ import annotations
2
3
  import hashlib
3
4
  import re
4
5
  from pathlib import Path
5
- from typing import ClassVar, Iterable, List, Sequence
6
+ from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
6
7
  from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
7
- from codeui.core.resolver import ResolveContext
8
8
  from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
9
9
 
10
+ if TYPE_CHECKING:
11
+ from codeui.core.resolver import ResolveContext
12
+
10
13
  class RustLanguageAnalyzer(LanguageAnalyzer):
11
14
  """Rust language analyzer.
12
15
  Example:
codeui/lang/scala.py ADDED
@@ -0,0 +1,290 @@
1
+ """Scala language adapter for codeui."""
2
+ from __future__ import annotations
3
+ import hashlib
4
+ import re
5
+ from pathlib import Path
6
+ from typing import ClassVar, Iterable, List, Sequence, TYPE_CHECKING
7
+ from codeui.core.ir import Edge, EdgeKind, Location, Symbol, SymbolKind, Visibility
8
+ from codeui.lang.base import ImportRef, LanguageAnalyzer, ParseResult
9
+
10
+ if TYPE_CHECKING:
11
+ from codeui.core.resolver import ResolveContext
12
+
13
+ class ScalaLanguageAnalyzer(LanguageAnalyzer):
14
+ """Scala language adapter extracting traits, objects, case classes, defs, and vals.
15
+ Example:
16
+ >>> analyzer = ScalaLanguageAnalyzer()
17
+ >>> res = analyzer.parse(Path("App.scala"), "case class User(id: Long, name: String)")
18
+ >>> syms = list(analyzer.extract_symbols(res))
19
+ >>> any(s.kind == SymbolKind.CLASS for s in syms)
20
+ True
21
+ """
22
+ language: ClassVar[str] = "scala"
23
+ extensions: ClassVar[tuple[str, ...]] = (".scala", ".sc")
24
+
25
+ def parse(self, path: Path, source: str) -> ParseResult:
26
+ """Parse Scala source into AST-emulating representation.
27
+ Example:
28
+ >>> analyzer = ScalaLanguageAnalyzer()
29
+ >>> res = analyzer.parse(Path("Main.scala"), "object Main extends App {}")
30
+ >>> res.errors
31
+ []
32
+ """
33
+ content_hash = hashlib.sha256(source.encode("utf-8")).hexdigest()
34
+ return ParseResult(
35
+ file_path=str(path),
36
+ source=source,
37
+ ast=source,
38
+ content_hash=content_hash,
39
+ errors=[],
40
+ )
41
+
42
+ def extract_symbols(self, parse: ParseResult) -> Iterable[Symbol]:
43
+ """Extract Scala packages, classes, traits, objects, defs, and vals.
44
+ Example:
45
+ >>> analyzer = ScalaLanguageAnalyzer()
46
+ >>> res = analyzer.parse(Path("Service.scala"), "trait Service { def run(): Unit }")
47
+ >>> syms = list(analyzer.extract_symbols(res))
48
+ >>> len(syms) >= 2
49
+ True
50
+ """
51
+ source = parse.source
52
+ file_path = parse.file_path
53
+ lines = source.splitlines()
54
+ max_line = len(lines) if lines else 1
55
+ symbols: List[Symbol] = []
56
+
57
+ file_sym = Symbol(
58
+ id=file_path,
59
+ name=Path(file_path).name,
60
+ qualified_name=file_path,
61
+ kind=SymbolKind.FILE,
62
+ language=self.language,
63
+ location=Location(file_path, 1, 0, max_line, 0),
64
+ parent_id=None,
65
+ signature=None,
66
+ visibility=Visibility.PUBLIC,
67
+ modifiers=(),
68
+ content_hash=parse.content_hash,
69
+ )
70
+ symbols.append(file_sym)
71
+
72
+ package_pattern = re.compile(r'^\s*package\s+([a-zA-Z0-9_.]+)')
73
+ import_pattern = re.compile(r'^\s*import\s+([a-zA-Z0-9_.*{}]+)')
74
+ type_pattern = re.compile(r'^\s*(?:(private|protected)(?:\[[a-zA-Z0-9_]+\])?\s+)?(?:(sealed|abstract|final|case|implicit|lazy)\s+)*(class|trait|object|enum)\s+([a-zA-Z0-9_]+)(?:\[[^\]]+\])?(?:\s*\([^)]*\))?(?:\s+extends\s+([a-zA-Z0-9_.,\s\[\]()]+))?(?:\s+with\s+([a-zA-Z0-9_.,\s\[\]()]+))?')
75
+ def_pattern = re.compile(r'^\s*(?:(private|protected)(?:\[[a-zA-Z0-9_]+\])?\s+)?(?:(override|final|implicit|def)\s+)*def\s+([a-zA-Z0-9_=+<>*/&|~^%-]+)(?:\[[^\]]+\])?\s*(?:\(([^)]*)\))?(?:\s*:\s*([a-zA-Z0-9_?<>,\s\[\]]+))?')
76
+ val_pattern = re.compile(r'^\s*(?:(private|protected)(?:\[[a-zA-Z0-9_]+\])?\s+)?(?:(override|final|implicit|lazy)\s+)*(val|var)\s+([a-zA-Z0-9_]+)(?:\s*:\s*([a-zA-Z0-9_?<>,\s\[\]]+))?')
77
+
78
+ current_package = ""
79
+ current_type: str | None = None
80
+ brace_depth = 0
81
+ type_depth = 0
82
+
83
+ for line_idx, line in enumerate(lines, start=1):
84
+ stripped = line.strip()
85
+ if not stripped or stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("*"):
86
+ continue
87
+
88
+ pkg_match = package_pattern.match(line)
89
+ if pkg_match:
90
+ current_package = pkg_match.group(1)
91
+ symbols.append(Symbol(
92
+ id=f"{file_path}::package::{current_package}",
93
+ name=current_package,
94
+ qualified_name=current_package,
95
+ kind=SymbolKind.NAMESPACE,
96
+ language=self.language,
97
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
98
+ parent_id=file_path,
99
+ signature=f"package {current_package}",
100
+ visibility=Visibility.PUBLIC,
101
+ modifiers=(),
102
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
103
+ ))
104
+ continue
105
+
106
+ imp_match = import_pattern.match(line)
107
+ if imp_match:
108
+ imp_name = imp_match.group(1)
109
+ symbols.append(Symbol(
110
+ id=f"{file_path}::import::{imp_name}",
111
+ name=imp_name.split(".")[-1],
112
+ qualified_name=imp_name,
113
+ kind=SymbolKind.IMPORT,
114
+ language=self.language,
115
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
116
+ parent_id=file_path,
117
+ signature=f"import {imp_name}",
118
+ visibility=Visibility.PUBLIC,
119
+ modifiers=(),
120
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
121
+ ))
122
+ continue
123
+
124
+ type_match = type_pattern.match(line)
125
+ if type_match:
126
+ vis_str = type_match.group(1) or "public"
127
+ mod_str = type_match.group(2)
128
+ type_kind = type_match.group(3)
129
+ type_name = type_match.group(4)
130
+ vis = Visibility.PRIVATE if vis_str == "private" else (Visibility.PROTECTED if vis_str == "protected" else Visibility.PUBLIC)
131
+ kind = SymbolKind.INTERFACE if type_kind == "trait" else SymbolKind.CLASS
132
+ modifiers = tuple(filter(None, [mod_str, type_kind]))
133
+ qual_prefix = f"{current_package}." if current_package else ""
134
+ sym_id = f"{file_path}::{type_name}"
135
+ current_type = sym_id
136
+ type_depth = brace_depth
137
+
138
+ symbols.append(Symbol(
139
+ id=sym_id,
140
+ name=type_name,
141
+ qualified_name=f"{qual_prefix}{type_name}",
142
+ kind=kind,
143
+ language=self.language,
144
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
145
+ parent_id=file_path,
146
+ signature=f"{vis_str} {type_kind} {type_name}",
147
+ visibility=vis,
148
+ modifiers=modifiers,
149
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
150
+ ))
151
+
152
+ def_match = def_pattern.match(line)
153
+ if def_match:
154
+ vis_str = def_match.group(1) or "public"
155
+ def_mod = def_match.group(2)
156
+ def_name = def_match.group(3)
157
+ def_args = def_match.group(4) or ""
158
+ def_ret = def_match.group(5) or "Unit"
159
+ vis = Visibility.PRIVATE if vis_str == "private" else (Visibility.PROTECTED if vis_str == "protected" else Visibility.PUBLIC)
160
+ parent_id = current_type if current_type and brace_depth > type_depth else file_path
161
+ sym_id = f"{parent_id}::{def_name}"
162
+ symbols.append(Symbol(
163
+ id=sym_id,
164
+ name=def_name,
165
+ qualified_name=f"{current_package}.{def_name}" if current_package else def_name,
166
+ kind=SymbolKind.FUNCTION,
167
+ language=self.language,
168
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
169
+ parent_id=parent_id,
170
+ signature=f"def {def_name}({def_args}): {def_ret}",
171
+ visibility=vis,
172
+ modifiers=tuple(filter(None, [def_mod])),
173
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
174
+ ))
175
+
176
+ val_match = val_pattern.match(line)
177
+ if val_match:
178
+ vis_str = val_match.group(1) or "public"
179
+ val_mod = val_match.group(2)
180
+ val_or_var = val_match.group(3)
181
+ val_name = val_match.group(4)
182
+ val_type = val_match.group(5) or "Any"
183
+ vis = Visibility.PRIVATE if vis_str == "private" else (Visibility.PROTECTED if vis_str == "protected" else Visibility.PUBLIC)
184
+ parent_id = current_type if current_type and brace_depth > type_depth else file_path
185
+ sym_id = f"{parent_id}::{val_name}"
186
+ symbols.append(Symbol(
187
+ id=sym_id,
188
+ name=val_name,
189
+ qualified_name=f"{current_package}.{val_name}" if current_package else val_name,
190
+ kind=SymbolKind.VARIABLE,
191
+ language=self.language,
192
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
193
+ parent_id=parent_id,
194
+ signature=f"{val_or_var} {val_name}: {val_type}",
195
+ visibility=vis,
196
+ modifiers=tuple(filter(None, [val_mod, val_or_var])),
197
+ content_hash=hashlib.sha256(line.encode("utf-8")).hexdigest()[:16],
198
+ ))
199
+
200
+ brace_depth += line.count("{") - line.count("}")
201
+ if current_type and brace_depth <= type_depth:
202
+ current_type = None
203
+
204
+ return sorted(symbols, key=lambda s: (s.location.start_line if s.location else 0, s.name))
205
+
206
+ def extract_edges(self, parse: ParseResult, symbols: Sequence[Symbol]) -> Iterable[Edge]:
207
+ """Extract containment, inheritance, and import edges for Scala.
208
+ Example:
209
+ >>> analyzer = ScalaLanguageAnalyzer()
210
+ >>> res = analyzer.parse(Path("App.scala"), "class Child extends Parent with TraitA")
211
+ >>> syms = list(analyzer.extract_symbols(res))
212
+ >>> edges = list(analyzer.extract_edges(res, syms))
213
+ >>> len(edges) >= 1
214
+ True
215
+ """
216
+ edges: List[Edge] = []
217
+ file_path = parse.file_path
218
+ symbol_map = {s.name: s for s in symbols}
219
+ type_inherit_pattern = re.compile(r'^\s*(?:[a-zA-Z0-9_\s<>]+)?(?:class|trait|object)\s+([a-zA-Z0-9_]+)(?:\[[^\]]+\])?(?:\s*\([^)]*\))?(?:\s+extends\s+([a-zA-Z0-9_.,\s\[\]()]+))?(?:\s+with\s+([a-zA-Z0-9_.,\s\[\]()]+))?')
220
+
221
+ for s in symbols:
222
+ if s.parent_id and s.parent_id != s.id:
223
+ edges.append(Edge(
224
+ source_id=s.parent_id,
225
+ target_id=s.id,
226
+ kind=EdgeKind.CONTAINS,
227
+ weight=1.0,
228
+ confidence=1.0,
229
+ location=s.location,
230
+ ))
231
+
232
+ for line_idx, line in enumerate(parse.source.splitlines(), start=1):
233
+ type_match = type_inherit_pattern.match(line)
234
+ if type_match:
235
+ type_name = type_match.group(1)
236
+ extends_raw = type_match.group(2)
237
+ with_raw = type_match.group(3)
238
+ type_sym = symbol_map.get(type_name)
239
+ if type_sym:
240
+ super_candidates = []
241
+ if extends_raw:
242
+ super_candidates.append(extends_raw.strip().split("(")[0].split("[")[0].strip())
243
+ if with_raw:
244
+ for w in with_raw.split("with"):
245
+ clean_w = w.strip().split("(")[0].split("[")[0].strip()
246
+ if clean_w:
247
+ super_candidates.append(clean_w)
248
+ for sup in super_candidates:
249
+ if sup and sup not in ("App", "Any", "Serializable", "Product"):
250
+ target_id = symbol_map[sup].id if sup in symbol_map else sup
251
+ edges.append(Edge(
252
+ source_id=type_sym.id,
253
+ target_id=target_id,
254
+ kind=EdgeKind.INHERITS,
255
+ weight=1.0,
256
+ confidence=0.9,
257
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
258
+ ))
259
+
260
+ if line.strip().startswith("import "):
261
+ imp_parts = line.strip().replace("import ", "").rstrip(";").split()
262
+ if imp_parts:
263
+ target_mod = imp_parts[0].rstrip("._")
264
+ edges.append(Edge(
265
+ source_id=file_path,
266
+ target_id=target_mod,
267
+ kind=EdgeKind.IMPORTS,
268
+ weight=1.0,
269
+ confidence=1.0,
270
+ location=Location(file_path, line_idx, 0, line_idx, len(line)),
271
+ ))
272
+
273
+ return sorted(edges, key=lambda e: (e.source_id, e.target_id, e.kind.value))
274
+
275
+ def resolve_import(self, ref: ImportRef, ctx: ResolveContext) -> Iterable[str]:
276
+ """Resolve Scala package or object imports against project files.
277
+ Example:
278
+ >>> analyzer = ScalaLanguageAnalyzer()
279
+ >>> from codeui.core.resolver import ResolveContext
280
+ >>> ctx = ResolveContext(Path("."))
281
+ >>> ref = ImportRef("src/Main.scala", "com.example.Service", None, False)
282
+ >>> list(analyzer.resolve_import(ref, ctx))
283
+ []
284
+ """
285
+ mod_name = ref.module_name.rstrip("._")
286
+ candidates = ctx.resolve_path(ref.from_file, mod_name, "scala")
287
+ if not candidates and "." in mod_name:
288
+ leaf = mod_name.split(".")[-1]
289
+ candidates = ctx.resolve_path(ref.from_file, leaf, "scala")
290
+ return candidates