agent-codinglanguage-mapper 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 (42) hide show
  1. agent_codinglanguage_mapper/__init__.py +20 -0
  2. agent_codinglanguage_mapper/__main__.py +4 -0
  3. agent_codinglanguage_mapper/_version.py +2 -0
  4. agent_codinglanguage_mapper/adapters/__init__.py +4 -0
  5. agent_codinglanguage_mapper/adapters/delphi.py +822 -0
  6. agent_codinglanguage_mapper/adapters/delphi_frontend/__init__.py +6 -0
  7. agent_codinglanguage_mapper/adapters/delphi_frontend/comment_builder.py +29 -0
  8. agent_codinglanguage_mapper/adapters/delphi_frontend/consts.py +345 -0
  9. agent_codinglanguage_mapper/adapters/delphi_frontend/grammar.py +460 -0
  10. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_builder.py +2674 -0
  11. agent_codinglanguage_mapper/adapters/delphi_frontend/lark_tokens.py +237 -0
  12. agent_codinglanguage_mapper/adapters/delphi_frontend/lsp_server.py +1832 -0
  13. agent_codinglanguage_mapper/adapters/delphi_frontend/nodes.py +371 -0
  14. agent_codinglanguage_mapper/adapters/delphi_frontend/parser.py +193 -0
  15. agent_codinglanguage_mapper/adapters/delphi_frontend/preprocessor.py +997 -0
  16. agent_codinglanguage_mapper/adapters/delphi_frontend/project_discovery.py +518 -0
  17. agent_codinglanguage_mapper/adapters/delphi_frontend/project_indexer.py +319 -0
  18. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic.py +375 -0
  19. agent_codinglanguage_mapper/adapters/delphi_frontend/semantic_builder.py +1384 -0
  20. agent_codinglanguage_mapper/adapters/delphi_frontend/source_reader.py +17 -0
  21. agent_codinglanguage_mapper/adapters/delphi_frontend/workspace.py +67 -0
  22. agent_codinglanguage_mapper/adapters/tree_sitter.py +1722 -0
  23. agent_codinglanguage_mapper/cli.py +138 -0
  24. agent_codinglanguage_mapper/discovery.py +1328 -0
  25. agent_codinglanguage_mapper/indexer.py +1102 -0
  26. agent_codinglanguage_mapper/integration.py +186 -0
  27. agent_codinglanguage_mapper/lsp_server.py +413 -0
  28. agent_codinglanguage_mapper/lsp_service.py +447 -0
  29. agent_codinglanguage_mapper/mapper.py +596 -0
  30. agent_codinglanguage_mapper/models.py +101 -0
  31. agent_codinglanguage_mapper/protocol.py +152 -0
  32. agent_codinglanguage_mapper/rendering.py +153 -0
  33. agent_codinglanguage_mapper/templates/opencode/plugins/codebase_map.ts +191 -0
  34. agent_codinglanguage_mapper/templates/skill/SKILL.md +52 -0
  35. agent_codinglanguage_mapper/worker.py +29 -0
  36. agent_codinglanguage_mapper/workspace.py +114 -0
  37. agent_codinglanguage_mapper-1.0.0.dist-info/METADATA +383 -0
  38. agent_codinglanguage_mapper-1.0.0.dist-info/RECORD +42 -0
  39. agent_codinglanguage_mapper-1.0.0.dist-info/WHEEL +5 -0
  40. agent_codinglanguage_mapper-1.0.0.dist-info/entry_points.txt +2 -0
  41. agent_codinglanguage_mapper-1.0.0.dist-info/licenses/LICENSE +373 -0
  42. agent_codinglanguage_mapper-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,319 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Callable, Iterable, Optional
7
+
8
+ from .consts import AttributeName, SyntaxNodeType
9
+ from .nodes import SyntaxNode
10
+ from .parser import DelphiParser
11
+ from .preprocessor import IncludeLoader
12
+ from .source_reader import read_source_text
13
+
14
+
15
+ class ProjectProblemType(str, Enum):
16
+ CANT_FIND_FILE = 'cant_find_file'
17
+ CANT_OPEN_FILE = 'cant_open_file'
18
+ CANT_PARSE_FILE = 'cant_parse_file'
19
+
20
+
21
+ @dataclass
22
+ class ProjectProblem:
23
+ problem_type: ProjectProblemType
24
+ file_name: str
25
+ description: str
26
+
27
+
28
+ @dataclass
29
+ class UnitErrorInfo:
30
+ line: int = 0
31
+ col: int = 0
32
+ error: str = ''
33
+
34
+
35
+ @dataclass
36
+ class UnitInfo:
37
+ name: str
38
+ path: str
39
+ syntax_tree: Optional[SyntaxNode]
40
+ has_error: bool = False
41
+ error_info: UnitErrorInfo = field(default_factory=UnitErrorInfo)
42
+
43
+
44
+ @dataclass
45
+ class IncludeFileInfo:
46
+ name: str
47
+ path: str
48
+
49
+
50
+ @dataclass
51
+ class ProjectIndexResult:
52
+ parsed_units: list[UnitInfo]
53
+ include_files: list[IncludeFileInfo]
54
+ problems: list[ProjectProblem]
55
+ not_found_units: list[str]
56
+
57
+
58
+ GetUnitSyntaxHook = Callable[[str], tuple[Optional[SyntaxNode], bool, bool]]
59
+ UnitParsedHook = Callable[[str, str, SyntaxNode, bool], bool]
60
+ SourceTransform = Callable[[str], str]
61
+
62
+
63
+ class ProjectIndexer:
64
+ def __init__(
65
+ self,
66
+ *,
67
+ search_paths: Iterable[str] = (),
68
+ include_paths: Iterable[str] = (),
69
+ defines: Iterable[str] = (),
70
+ include_loader: IncludeLoader | None = None,
71
+ on_get_unit_syntax: GetUnitSyntaxHook | None = None,
72
+ on_unit_parsed: UnitParsedHook | None = None,
73
+ source_transform: SourceTransform | None = None,
74
+ ) -> None:
75
+ self.search_paths = [Path(path) for path in search_paths]
76
+ self.include_paths = [Path(path) for path in include_paths]
77
+ self.defines = list(defines)
78
+ self.include_loader = include_loader
79
+ self.on_get_unit_syntax = on_get_unit_syntax
80
+ self.on_unit_parsed = on_unit_parsed
81
+ self.source_transform = source_transform
82
+
83
+ self._parsed_units: dict[str, UnitInfo] = {}
84
+ self._problems: list[ProjectProblem] = []
85
+ self._not_found_units: set[str] = set()
86
+ self._include_files: dict[str, IncludeFileInfo] = {}
87
+ self._project_folder: Path = Path('.')
88
+ self._aborting: bool = False
89
+
90
+ def index(self, file_name: str) -> ProjectIndexResult:
91
+ self._parsed_units = {}
92
+ self._problems = []
93
+ self._not_found_units = set()
94
+ self._include_files = {}
95
+ self._aborting = False
96
+
97
+ entry = Path(file_name).expanduser().resolve()
98
+ self._project_folder = entry.parent
99
+
100
+ is_project = entry.suffix.casefold() in {'.dpr', '.dpk'}
101
+ self._parse_unit(entry.stem, entry, is_project=is_project)
102
+
103
+ parsed_units = sorted(self._parsed_units.values(), key=lambda item: item.name.casefold())
104
+ include_files = sorted(self._include_files.values(), key=lambda item: (item.name.casefold(), item.path.casefold()))
105
+ problems = list(self._problems)
106
+ not_found = sorted(self._not_found_units, key=str.casefold)
107
+
108
+ return ProjectIndexResult(
109
+ parsed_units=parsed_units,
110
+ include_files=include_files,
111
+ problems=problems,
112
+ not_found_units=not_found,
113
+ )
114
+
115
+ def _parse_unit(self, unit_name: str, file_path: Path, *, is_project: bool) -> None:
116
+ if self._aborting:
117
+ return
118
+
119
+ normalized_name = unit_name.casefold()
120
+ if normalized_name in self._parsed_units:
121
+ return
122
+
123
+ hook_tree: Optional[SyntaxNode] = None
124
+ do_parse_unit = True
125
+ if self.on_get_unit_syntax is not None:
126
+ hook_tree, do_parse_unit, do_abort = self.on_get_unit_syntax(str(file_path))
127
+ if do_abort:
128
+ self._aborting = True
129
+ return
130
+
131
+ syntax_tree: Optional[SyntaxNode] = hook_tree
132
+ from_parser = False
133
+
134
+ if syntax_tree is None and do_parse_unit:
135
+ source = self._read_file(file_path)
136
+ if source is None:
137
+ return
138
+ parser = DelphiParser(
139
+ include_paths=[str(path) for path in self.include_paths],
140
+ defines=self.defines,
141
+ include_loader=self._build_include_loader(),
142
+ )
143
+ try:
144
+ if self.source_transform is not None:
145
+ source = self.source_transform(source)
146
+ result = parser.parse(source, str(file_path), build_semantic=False)
147
+ syntax_tree = result.root
148
+ from_parser = True
149
+ except Exception as exc: # pragma: no cover - parser error branch
150
+ self._problems.append(
151
+ ProjectProblem(
152
+ problem_type=ProjectProblemType.CANT_PARSE_FILE,
153
+ file_name=str(file_path),
154
+ description=str(exc),
155
+ )
156
+ )
157
+ self._parsed_units[normalized_name] = UnitInfo(
158
+ name=unit_name,
159
+ path=str(file_path),
160
+ syntax_tree=None,
161
+ has_error=True,
162
+ error_info=UnitErrorInfo(error=str(exc)),
163
+ )
164
+ return
165
+
166
+ if syntax_tree is None:
167
+ return
168
+
169
+ actual_name = syntax_tree.get_attribute(AttributeName.anName) or unit_name
170
+ normalized_name = actual_name.casefold()
171
+ unit_info = UnitInfo(name=actual_name, path=str(file_path), syntax_tree=syntax_tree)
172
+ self._parsed_units[normalized_name] = unit_info
173
+
174
+ if self.on_unit_parsed is not None:
175
+ do_abort = self.on_unit_parsed(actual_name, str(file_path), syntax_tree, from_parser)
176
+ if do_abort:
177
+ self._aborting = True
178
+ return
179
+
180
+ for dep_name, dep_path in self._collect_dependencies(syntax_tree, file_path, is_project=is_project):
181
+ if self._aborting:
182
+ return
183
+ resolved = self._resolve_unit_path(dep_name, dep_path, relative_to=file_path.parent)
184
+ if resolved is None:
185
+ self._not_found_units.add(dep_name)
186
+ self._problems.append(
187
+ ProjectProblem(
188
+ problem_type=ProjectProblemType.CANT_FIND_FILE,
189
+ file_name=dep_name,
190
+ description=f'Unit not found: {dep_name}',
191
+ )
192
+ )
193
+ continue
194
+ self._parse_unit(dep_name, resolved, is_project=False)
195
+
196
+ def _read_file(self, file_path: Path) -> Optional[str]:
197
+ try:
198
+ return read_source_text(file_path)
199
+ except OSError as exc:
200
+ self._problems.append(
201
+ ProjectProblem(
202
+ problem_type=ProjectProblemType.CANT_OPEN_FILE,
203
+ file_name=str(file_path),
204
+ description=str(exc),
205
+ )
206
+ )
207
+ return None
208
+
209
+ def _collect_dependencies(
210
+ self,
211
+ syntax_tree: SyntaxNode,
212
+ file_path: Path,
213
+ *,
214
+ is_project: bool,
215
+ ) -> list[tuple[str, Optional[str]]]:
216
+ deps: list[tuple[str, Optional[str]]] = []
217
+ if is_project:
218
+ self._append_units_from_node(syntax_tree, SyntaxNodeType.ntUses, deps)
219
+ self._append_units_from_node(syntax_tree, SyntaxNodeType.ntContains, deps)
220
+ return deps
221
+
222
+ intf = syntax_tree.find_node(SyntaxNodeType.ntInterface)
223
+ impl = syntax_tree.find_node(SyntaxNodeType.ntImplementation)
224
+ if intf is not None:
225
+ self._append_units_from_node(intf, SyntaxNodeType.ntUses, deps)
226
+ if impl is not None:
227
+ self._append_units_from_node(impl, SyntaxNodeType.ntUses, deps)
228
+ return deps
229
+
230
+ def _append_units_from_node(
231
+ self,
232
+ root: SyntaxNode,
233
+ node_type: SyntaxNodeType,
234
+ deps: list[tuple[str, Optional[str]]],
235
+ ) -> None:
236
+ target = root.find_node(node_type)
237
+ if target is None:
238
+ return
239
+ for child in target.child_nodes:
240
+ if child.typ != SyntaxNodeType.ntUnit:
241
+ continue
242
+ name = child.get_attribute(AttributeName.anName)
243
+ if not name:
244
+ continue
245
+ dep_path = child.get_attribute(AttributeName.anPath) or None
246
+ deps.append((name, dep_path))
247
+
248
+ def _resolve_unit_path(self, unit_name: str, unit_path: Optional[str], *, relative_to: Path) -> Optional[Path]:
249
+ candidates: list[Path] = []
250
+
251
+ if unit_path:
252
+ hint = Path(unit_path)
253
+ if hint.is_absolute():
254
+ candidates.append(hint)
255
+ else:
256
+ candidates.append((relative_to / hint).resolve())
257
+
258
+ unit_file_names: list[str]
259
+ if Path(unit_name).suffix:
260
+ unit_file_names = [unit_name]
261
+ else:
262
+ unit_file_names = [f'{unit_name}.pas', f'{unit_name}.dpr', f'{unit_name}.dpk']
263
+
264
+ search_roots = [relative_to, self._project_folder, *self.search_paths, *self.include_paths]
265
+ for root in search_roots:
266
+ for file_name in unit_file_names:
267
+ candidates.append((root / file_name).resolve())
268
+
269
+ seen: set[str] = set()
270
+ for candidate in candidates:
271
+ key = str(candidate).casefold()
272
+ if key in seen:
273
+ continue
274
+ seen.add(key)
275
+ if candidate.exists() and candidate.is_file():
276
+ return candidate
277
+ return None
278
+
279
+ def _build_include_loader(self) -> IncludeLoader:
280
+ base_loader = self.include_loader or self._default_include_loader
281
+
282
+ def wrapped(parent_file: str, include_name: str) -> Optional[tuple[str, str]]:
283
+ resolved = base_loader(parent_file, include_name)
284
+ if resolved is None:
285
+ return None
286
+ content, resolved_path = resolved
287
+ cache_key = f'{include_name.casefold()}::{resolved_path.casefold()}'
288
+ self._include_files[cache_key] = IncludeFileInfo(name=include_name, path=resolved_path)
289
+ return content, resolved_path
290
+
291
+ return wrapped
292
+
293
+ def _default_include_loader(self, parent_file: str, include_name: str) -> Optional[tuple[str, str]]:
294
+ parent = Path(parent_file).resolve().parent
295
+ include_path = Path(include_name.replace('\\', '/'))
296
+ search_roots = [parent, self._project_folder, *self.include_paths, *self.search_paths]
297
+ for root in search_roots:
298
+ candidate = (root / include_path).resolve()
299
+ if candidate.exists() and candidate.is_file():
300
+ try:
301
+ content = read_source_text(candidate)
302
+ except OSError:
303
+ continue
304
+ return content, str(candidate)
305
+ return None
306
+
307
+
308
+ __all__ = [
309
+ 'ProjectIndexer',
310
+ 'ProjectIndexResult',
311
+ 'ProjectProblemType',
312
+ 'ProjectProblem',
313
+ 'UnitInfo',
314
+ 'UnitErrorInfo',
315
+ 'IncludeFileInfo',
316
+ 'GetUnitSyntaxHook',
317
+ 'UnitParsedHook',
318
+ 'SourceTransform',
319
+ ]
@@ -0,0 +1,375 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+
8
+ def normalize_name(name: str) -> str:
9
+ return name.casefold()
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class SourceRange:
14
+ file_name: str
15
+ start_line: int
16
+ start_col: int
17
+ end_line: int
18
+ end_col: int
19
+
20
+ def contains(self, line: int, col: int) -> bool:
21
+ if line < self.start_line or line > self.end_line:
22
+ return False
23
+ if line == self.start_line and col < self.start_col:
24
+ return False
25
+ if line == self.end_line and col > self.end_col:
26
+ return False
27
+ return True
28
+
29
+
30
+ class Visibility(Enum):
31
+ UNKNOWN = 'unknown'
32
+ PRIVATE = 'private'
33
+ PROTECTED = 'protected'
34
+ PUBLIC = 'public'
35
+ PUBLISHED = 'published'
36
+ STRICT_PRIVATE = 'strict_private'
37
+ STRICT_PROTECTED = 'strict_protected'
38
+
39
+
40
+ class SymbolKind(Enum):
41
+ UNIT = 'unit'
42
+ MODULE = 'module'
43
+ NAMESPACE = 'namespace'
44
+ PACKAGE = 'package'
45
+ TYPE = 'type'
46
+ TYPE_PARAMETER = 'type_parameter'
47
+ CLASS = 'class'
48
+ RECORD = 'record'
49
+ INTERFACE = 'interface'
50
+ ENUM = 'enum'
51
+ ENUM_VALUE = 'enum_value'
52
+ FIELD = 'field'
53
+ PROPERTY = 'property'
54
+ METHOD = 'method'
55
+ FUNCTION = 'function'
56
+ PROCEDURE = 'procedure'
57
+ CONSTRUCTOR = 'constructor'
58
+ DESTRUCTOR = 'destructor'
59
+ VARIABLE = 'variable'
60
+ PARAMETER = 'parameter'
61
+ CONSTANT = 'constant'
62
+ LABEL = 'label'
63
+
64
+
65
+ class ScopeKind(Enum):
66
+ UNIT = 'unit'
67
+ TYPE = 'type'
68
+ ROUTINE = 'routine'
69
+ BLOCK = 'block'
70
+ WITH = 'with'
71
+
72
+
73
+ class Modifier(Enum):
74
+ ABSTRACT = 'abstract'
75
+ SEALED = 'sealed'
76
+ FINAL = 'final'
77
+ VIRTUAL = 'virtual'
78
+ OVERRIDE = 'override'
79
+ OVERLOAD = 'overload'
80
+ REINTRODUCE = 'reintroduce'
81
+ INLINE = 'inline'
82
+ STATIC = 'static'
83
+ CLASS = 'class'
84
+ EXTERNAL = 'external'
85
+ FORWARD = 'forward'
86
+ DEPRECATED = 'deprecated'
87
+ NORETURN = 'noreturn'
88
+
89
+
90
+ class ReferenceKind(Enum):
91
+ UNKNOWN = 'unknown'
92
+ VALUE = 'value'
93
+ TYPE = 'type'
94
+ CALL = 'call'
95
+ PROPERTY = 'property'
96
+ UNIT = 'unit'
97
+ LABEL = 'label'
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class TypeRef:
102
+ def display_name(self) -> str:
103
+ return '<unknown>'
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class UnknownTypeRef(TypeRef):
108
+ reason: str = ''
109
+
110
+ def display_name(self) -> str:
111
+ return '<unknown>' if not self.reason else f'<unknown:{self.reason}>'
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class NamedTypeRef(TypeRef):
116
+ name: str
117
+ unit_name: str | None = None
118
+
119
+ def display_name(self) -> str:
120
+ if self.unit_name:
121
+ return f'{self.unit_name}.{self.name}'
122
+ return self.name
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class PointerTypeRef(TypeRef):
127
+ target: TypeRef
128
+
129
+ def display_name(self) -> str:
130
+ return f'^{self.target.display_name()}'
131
+
132
+
133
+ @dataclass(frozen=True)
134
+ class ClassOfTypeRef(TypeRef):
135
+ target: TypeRef
136
+
137
+ def display_name(self) -> str:
138
+ return f'class of {self.target.display_name()}'
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class ReferenceTypeRef(TypeRef):
143
+ target: TypeRef
144
+
145
+ def display_name(self) -> str:
146
+ return f'reference to {self.target.display_name()}'
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class FileTypeRef(TypeRef):
151
+ element_type: TypeRef | None = None
152
+
153
+ def display_name(self) -> str:
154
+ if self.element_type is None:
155
+ return 'file'
156
+ return f'file of {self.element_type.display_name()}'
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class GenericInstanceTypeRef(TypeRef):
161
+ base: NamedTypeRef
162
+ args: tuple[TypeRef, ...]
163
+
164
+ def display_name(self) -> str:
165
+ arg_list = ', '.join(arg.display_name() for arg in self.args)
166
+ return f'{self.base.display_name()}<{arg_list}>'
167
+
168
+
169
+ @dataclass(frozen=True)
170
+ class ArrayTypeRef(TypeRef):
171
+ element_type: TypeRef
172
+ dimensions: tuple[TypeRef, ...] = ()
173
+ is_dynamic: bool = False
174
+
175
+ def display_name(self) -> str:
176
+ if self.is_dynamic or not self.dimensions:
177
+ return f'array of {self.element_type.display_name()}'
178
+ dims = ', '.join(dim.display_name() for dim in self.dimensions)
179
+ return f'array[{dims}] of {self.element_type.display_name()}'
180
+
181
+
182
+ @dataclass(frozen=True)
183
+ class SetTypeRef(TypeRef):
184
+ element_type: TypeRef
185
+
186
+ def display_name(self) -> str:
187
+ return f'set of {self.element_type.display_name()}'
188
+
189
+
190
+ @dataclass(frozen=True)
191
+ class SubrangeTypeRef(TypeRef):
192
+ lower: str
193
+ upper: str
194
+
195
+ def display_name(self) -> str:
196
+ return f'{self.lower}..{self.upper}'
197
+
198
+
199
+ @dataclass(frozen=True)
200
+ class EnumTypeRef(TypeRef):
201
+ name: str
202
+ members: tuple[str, ...]
203
+
204
+ def display_name(self) -> str:
205
+ return self.name if self.name else 'enum'
206
+
207
+
208
+ @dataclass(frozen=True)
209
+ class RecordTypeRef(TypeRef):
210
+ name: str
211
+ scope: Optional['Scope'] = None
212
+ packed: bool = False
213
+
214
+ def display_name(self) -> str:
215
+ return self.name if self.name else 'record'
216
+
217
+
218
+ @dataclass(frozen=True)
219
+ class ClassTypeRef(TypeRef):
220
+ name: str
221
+ scope: Optional['Scope'] = None
222
+ bases: tuple[TypeRef, ...] = ()
223
+ is_abstract: bool = False
224
+
225
+ def display_name(self) -> str:
226
+ return self.name if self.name else 'class'
227
+
228
+
229
+ @dataclass(frozen=True)
230
+ class InterfaceTypeRef(TypeRef):
231
+ name: str
232
+ scope: Optional['Scope'] = None
233
+ bases: tuple[TypeRef, ...] = ()
234
+
235
+ def display_name(self) -> str:
236
+ return self.name if self.name else 'interface'
237
+
238
+
239
+ @dataclass(frozen=True)
240
+ class TypeParameterRef(TypeRef):
241
+ name: str
242
+
243
+ def display_name(self) -> str:
244
+ return self.name
245
+
246
+
247
+ @dataclass(frozen=True)
248
+ class ProcTypeRef(TypeRef):
249
+ name: str
250
+ params: tuple[TypeRef, ...] = ()
251
+ return_type: TypeRef | None = None
252
+
253
+ def display_name(self) -> str:
254
+ params = ', '.join(p.display_name() for p in self.params)
255
+ if self.return_type is None:
256
+ return f'procedure({params})'
257
+ return f'function({params}): {self.return_type.display_name()}'
258
+
259
+
260
+ @dataclass
261
+ class Symbol:
262
+ name: str
263
+ kind: SymbolKind
264
+ decl_range: SourceRange
265
+ name_range: SourceRange
266
+ scope: 'Scope'
267
+ visibility: Visibility = Visibility.UNKNOWN
268
+ type_ref: TypeRef = field(default_factory=UnknownTypeRef)
269
+ modifiers: set[Modifier] = field(default_factory=set)
270
+ attributes: dict[str, str] = field(default_factory=dict)
271
+ doc: str = ''
272
+ overloads: list['Symbol'] = field(default_factory=list)
273
+ member_scope: Optional['Scope'] = None
274
+ base_types: tuple[TypeRef, ...] = ()
275
+
276
+ def add_overload(self, symbol: 'Symbol') -> None:
277
+ self.overloads.append(symbol)
278
+
279
+
280
+ @dataclass
281
+ class SymbolReference:
282
+ name: str
283
+ kind: ReferenceKind
284
+ ref_range: SourceRange
285
+ scope: 'Scope'
286
+ resolved: Optional[Symbol] = None
287
+
288
+
289
+ @dataclass
290
+ class Scope:
291
+ kind: ScopeKind
292
+ name: str
293
+ parent: Optional['Scope'] = None
294
+ owner: Optional[Symbol] = None
295
+ symbols: dict[str, list[Symbol]] = field(default_factory=dict)
296
+ imports: list['Scope'] = field(default_factory=list)
297
+ with_scopes: list['Scope'] = field(default_factory=list)
298
+
299
+ def define(self, symbol: Symbol) -> None:
300
+ key = normalize_name(symbol.name)
301
+ if key not in self.symbols:
302
+ self.symbols[key] = []
303
+ self.symbols[key].append(symbol)
304
+
305
+ def lookup_local(self, name: str) -> list[Symbol]:
306
+ return list(self.symbols.get(normalize_name(name), []))
307
+
308
+ def resolve(self, name: str) -> list[Symbol]:
309
+ results: list[Symbol] = []
310
+ results.extend(self.lookup_local(name))
311
+ if results:
312
+ return results
313
+ for scope in self.with_scopes:
314
+ scoped = scope.lookup_local(name)
315
+ if scoped:
316
+ return scoped
317
+ if self.parent is not None:
318
+ results.extend(self.parent.resolve(name))
319
+ if results:
320
+ return results
321
+ for scope in self.imports:
322
+ scoped = scope.lookup_local(name)
323
+ if scoped:
324
+ results.extend(scoped)
325
+ return results
326
+
327
+ def add_import(self, scope: 'Scope') -> None:
328
+ if scope not in self.imports:
329
+ self.imports.append(scope)
330
+
331
+ def add_with_scope(self, scope: 'Scope') -> None:
332
+ if scope not in self.with_scopes:
333
+ self.with_scopes.append(scope)
334
+
335
+
336
+ @dataclass
337
+ class SymbolIndex:
338
+ units: dict[str, Scope] = field(default_factory=dict)
339
+ name_index: dict[str, list[Symbol]] = field(default_factory=dict)
340
+
341
+ def register_unit(self, unit_name: str, unit_scope: Scope, *, index_symbols: bool = True) -> None:
342
+ self.units[normalize_name(unit_name)] = unit_scope
343
+ if index_symbols:
344
+ self._index_scope(unit_scope)
345
+
346
+ def lookup(self, name: str) -> list[Symbol]:
347
+ return list(self.name_index.get(normalize_name(name), []))
348
+
349
+ def lookup_unit(self, unit_name: str) -> Optional[Scope]:
350
+ return self.units.get(normalize_name(unit_name))
351
+
352
+ def resolve_qualified(self, qualified_name: str) -> list[Symbol]:
353
+ parts = qualified_name.split('.')
354
+ if len(parts) < 2:
355
+ return self.lookup(qualified_name)
356
+ unit_name = '.'.join(parts[:-1])
357
+ symbol_name = parts[-1]
358
+ unit_scope = self.lookup_unit(unit_name)
359
+ if unit_scope is None:
360
+ return []
361
+ return unit_scope.lookup_local(symbol_name)
362
+
363
+ def _index_scope(self, scope: Scope, seen: set[int] | None = None) -> None:
364
+ if seen is None:
365
+ seen = set()
366
+ scope_id = id(scope)
367
+ if scope_id in seen:
368
+ return
369
+ seen.add(scope_id)
370
+ for symbols in scope.symbols.values():
371
+ for symbol in symbols:
372
+ key = normalize_name(symbol.name)
373
+ self.name_index.setdefault(key, []).append(symbol)
374
+ for child_scope in scope.imports:
375
+ self._index_scope(child_scope, seen)