python-delphi-lsp 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.
@@ -0,0 +1,313 @@
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
+
61
+
62
+ class ProjectIndexer:
63
+ def __init__(
64
+ self,
65
+ *,
66
+ search_paths: Iterable[str] = (),
67
+ include_paths: Iterable[str] = (),
68
+ defines: Iterable[str] = (),
69
+ include_loader: IncludeLoader | None = None,
70
+ on_get_unit_syntax: GetUnitSyntaxHook | None = None,
71
+ on_unit_parsed: UnitParsedHook | None = None,
72
+ ) -> None:
73
+ self.search_paths = [Path(path) for path in search_paths]
74
+ self.include_paths = [Path(path) for path in include_paths]
75
+ self.defines = list(defines)
76
+ self.include_loader = include_loader
77
+ self.on_get_unit_syntax = on_get_unit_syntax
78
+ self.on_unit_parsed = on_unit_parsed
79
+
80
+ self._parsed_units: dict[str, UnitInfo] = {}
81
+ self._problems: list[ProjectProblem] = []
82
+ self._not_found_units: set[str] = set()
83
+ self._include_files: dict[str, IncludeFileInfo] = {}
84
+ self._project_folder: Path = Path('.')
85
+ self._aborting: bool = False
86
+
87
+ def index(self, file_name: str) -> ProjectIndexResult:
88
+ self._parsed_units = {}
89
+ self._problems = []
90
+ self._not_found_units = set()
91
+ self._include_files = {}
92
+ self._aborting = False
93
+
94
+ entry = Path(file_name).expanduser().resolve()
95
+ self._project_folder = entry.parent
96
+
97
+ is_project = entry.suffix.casefold() in {'.dpr', '.dpk'}
98
+ self._parse_unit(entry.stem, entry, is_project=is_project)
99
+
100
+ parsed_units = sorted(self._parsed_units.values(), key=lambda item: item.name.casefold())
101
+ include_files = sorted(self._include_files.values(), key=lambda item: (item.name.casefold(), item.path.casefold()))
102
+ problems = list(self._problems)
103
+ not_found = sorted(self._not_found_units, key=str.casefold)
104
+
105
+ return ProjectIndexResult(
106
+ parsed_units=parsed_units,
107
+ include_files=include_files,
108
+ problems=problems,
109
+ not_found_units=not_found,
110
+ )
111
+
112
+ def _parse_unit(self, unit_name: str, file_path: Path, *, is_project: bool) -> None:
113
+ if self._aborting:
114
+ return
115
+
116
+ normalized_name = unit_name.casefold()
117
+ if normalized_name in self._parsed_units:
118
+ return
119
+
120
+ hook_tree: Optional[SyntaxNode] = None
121
+ do_parse_unit = True
122
+ if self.on_get_unit_syntax is not None:
123
+ hook_tree, do_parse_unit, do_abort = self.on_get_unit_syntax(str(file_path))
124
+ if do_abort:
125
+ self._aborting = True
126
+ return
127
+
128
+ syntax_tree: Optional[SyntaxNode] = hook_tree
129
+ from_parser = False
130
+
131
+ if syntax_tree is None and do_parse_unit:
132
+ source = self._read_file(file_path)
133
+ if source is None:
134
+ return
135
+ parser = DelphiParser(
136
+ include_paths=[str(path) for path in self.include_paths],
137
+ defines=self.defines,
138
+ include_loader=self._build_include_loader(),
139
+ )
140
+ try:
141
+ result = parser.parse(source, str(file_path), build_semantic=False)
142
+ syntax_tree = result.root
143
+ from_parser = True
144
+ except Exception as exc: # pragma: no cover - parser error branch
145
+ self._problems.append(
146
+ ProjectProblem(
147
+ problem_type=ProjectProblemType.CANT_PARSE_FILE,
148
+ file_name=str(file_path),
149
+ description=str(exc),
150
+ )
151
+ )
152
+ self._parsed_units[normalized_name] = UnitInfo(
153
+ name=unit_name,
154
+ path=str(file_path),
155
+ syntax_tree=None,
156
+ has_error=True,
157
+ error_info=UnitErrorInfo(error=str(exc)),
158
+ )
159
+ return
160
+
161
+ if syntax_tree is None:
162
+ return
163
+
164
+ actual_name = syntax_tree.get_attribute(AttributeName.anName) or unit_name
165
+ normalized_name = actual_name.casefold()
166
+ unit_info = UnitInfo(name=actual_name, path=str(file_path), syntax_tree=syntax_tree)
167
+ self._parsed_units[normalized_name] = unit_info
168
+
169
+ if self.on_unit_parsed is not None:
170
+ do_abort = self.on_unit_parsed(actual_name, str(file_path), syntax_tree, from_parser)
171
+ if do_abort:
172
+ self._aborting = True
173
+ return
174
+
175
+ for dep_name, dep_path in self._collect_dependencies(syntax_tree, file_path, is_project=is_project):
176
+ if self._aborting:
177
+ return
178
+ resolved = self._resolve_unit_path(dep_name, dep_path, relative_to=file_path.parent)
179
+ if resolved is None:
180
+ self._not_found_units.add(dep_name)
181
+ self._problems.append(
182
+ ProjectProblem(
183
+ problem_type=ProjectProblemType.CANT_FIND_FILE,
184
+ file_name=dep_name,
185
+ description=f'Unit not found: {dep_name}',
186
+ )
187
+ )
188
+ continue
189
+ self._parse_unit(dep_name, resolved, is_project=False)
190
+
191
+ def _read_file(self, file_path: Path) -> Optional[str]:
192
+ try:
193
+ return read_source_text(file_path)
194
+ except OSError as exc:
195
+ self._problems.append(
196
+ ProjectProblem(
197
+ problem_type=ProjectProblemType.CANT_OPEN_FILE,
198
+ file_name=str(file_path),
199
+ description=str(exc),
200
+ )
201
+ )
202
+ return None
203
+
204
+ def _collect_dependencies(
205
+ self,
206
+ syntax_tree: SyntaxNode,
207
+ file_path: Path,
208
+ *,
209
+ is_project: bool,
210
+ ) -> list[tuple[str, Optional[str]]]:
211
+ deps: list[tuple[str, Optional[str]]] = []
212
+ if is_project:
213
+ self._append_units_from_node(syntax_tree, SyntaxNodeType.ntUses, deps)
214
+ self._append_units_from_node(syntax_tree, SyntaxNodeType.ntContains, deps)
215
+ return deps
216
+
217
+ intf = syntax_tree.find_node(SyntaxNodeType.ntInterface)
218
+ impl = syntax_tree.find_node(SyntaxNodeType.ntImplementation)
219
+ if intf is not None:
220
+ self._append_units_from_node(intf, SyntaxNodeType.ntUses, deps)
221
+ if impl is not None:
222
+ self._append_units_from_node(impl, SyntaxNodeType.ntUses, deps)
223
+ return deps
224
+
225
+ def _append_units_from_node(
226
+ self,
227
+ root: SyntaxNode,
228
+ node_type: SyntaxNodeType,
229
+ deps: list[tuple[str, Optional[str]]],
230
+ ) -> None:
231
+ target = root.find_node(node_type)
232
+ if target is None:
233
+ return
234
+ for child in target.child_nodes:
235
+ if child.typ != SyntaxNodeType.ntUnit:
236
+ continue
237
+ name = child.get_attribute(AttributeName.anName)
238
+ if not name:
239
+ continue
240
+ dep_path = child.get_attribute(AttributeName.anPath) or None
241
+ deps.append((name, dep_path))
242
+
243
+ def _resolve_unit_path(self, unit_name: str, unit_path: Optional[str], *, relative_to: Path) -> Optional[Path]:
244
+ candidates: list[Path] = []
245
+
246
+ if unit_path:
247
+ hint = Path(unit_path)
248
+ if hint.is_absolute():
249
+ candidates.append(hint)
250
+ else:
251
+ candidates.append((relative_to / hint).resolve())
252
+
253
+ unit_file_names: list[str]
254
+ if Path(unit_name).suffix:
255
+ unit_file_names = [unit_name]
256
+ else:
257
+ unit_file_names = [f'{unit_name}.pas', f'{unit_name}.dpr', f'{unit_name}.dpk']
258
+
259
+ search_roots = [relative_to, self._project_folder, *self.search_paths, *self.include_paths]
260
+ for root in search_roots:
261
+ for file_name in unit_file_names:
262
+ candidates.append((root / file_name).resolve())
263
+
264
+ seen: set[str] = set()
265
+ for candidate in candidates:
266
+ key = str(candidate).casefold()
267
+ if key in seen:
268
+ continue
269
+ seen.add(key)
270
+ if candidate.exists() and candidate.is_file():
271
+ return candidate
272
+ return None
273
+
274
+ def _build_include_loader(self) -> IncludeLoader:
275
+ base_loader = self.include_loader or self._default_include_loader
276
+
277
+ def wrapped(parent_file: str, include_name: str) -> Optional[tuple[str, str]]:
278
+ resolved = base_loader(parent_file, include_name)
279
+ if resolved is None:
280
+ return None
281
+ content, resolved_path = resolved
282
+ cache_key = f'{include_name.casefold()}::{resolved_path.casefold()}'
283
+ self._include_files[cache_key] = IncludeFileInfo(name=include_name, path=resolved_path)
284
+ return content, resolved_path
285
+
286
+ return wrapped
287
+
288
+ def _default_include_loader(self, parent_file: str, include_name: str) -> Optional[tuple[str, str]]:
289
+ parent = Path(parent_file).resolve().parent
290
+ include_path = Path(include_name.replace('\\', '/'))
291
+ search_roots = [parent, self._project_folder, *self.include_paths, *self.search_paths]
292
+ for root in search_roots:
293
+ candidate = (root / include_path).resolve()
294
+ if candidate.exists() and candidate.is_file():
295
+ try:
296
+ content = read_source_text(candidate)
297
+ except OSError:
298
+ continue
299
+ return content, str(candidate)
300
+ return None
301
+
302
+
303
+ __all__ = [
304
+ 'ProjectIndexer',
305
+ 'ProjectIndexResult',
306
+ 'ProjectProblemType',
307
+ 'ProjectProblem',
308
+ 'UnitInfo',
309
+ 'UnitErrorInfo',
310
+ 'IncludeFileInfo',
311
+ 'GetUnitSyntaxHook',
312
+ 'UnitParsedHook',
313
+ ]
delphiast/semantic.py ADDED
@@ -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 Iterable, 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)