77from collections import ChainMap , OrderedDict
88from dataclasses import dataclass
99from functools import cached_property
10+ from typing import TYPE_CHECKING
11+
12+ if TYPE_CHECKING :
13+ from collections .abc import Mapping
1014
1115from lark import exceptions as lark_exceptions
1216
@@ -125,6 +129,7 @@ class _ValidationFrame:
125129 diagnostics : list [diagnostics .Diagnostic ]
126130 expected_definition_path : pathlib .PurePosixPath | None
127131 expected_universe_name : str | None
132+ universe_locations : Mapping [str , pathlib .PurePosixPath ]
128133
129134
130135class Validator :
@@ -138,6 +143,7 @@ def __init__(self):
138143 self ._seen_global_definitions : dict [str , ast .QualityDefinition ] = {}
139144 self ._validation_frames : list [_ValidationFrame ] = []
140145 self ._reference_not_found_paths : set [pathlib .PurePosixPath ] = set ()
146+ self ._unknown_universes : set [str ] = set ()
141147
142148 @cached_property
143149 def _parser (self ) -> parser .Parser :
@@ -148,9 +154,14 @@ def parse_and_validate_program(
148154 self ,
149155 path : pathlib .PurePath ,
150156 expected_universe_name : str | None = None ,
157+ universe_locations : Mapping [str , pathlib .PurePosixPath ] | None = None ,
151158 ) -> list [ValidationResult ]:
152159 """Parse, transform, and validate all reached files from one entrypoint."""
153- self ._parse_validate_and_collect (path , expected_universe_name )
160+ self ._parse_validate_and_collect (
161+ path ,
162+ expected_universe_name ,
163+ universe_locations if universe_locations is not None else {},
164+ )
154165 return [
155166 result
156167 for result in self .results_by_path .values ()
@@ -164,7 +175,8 @@ def parse_and_validate_program(
164175 def _parse_and_validate_file (
165176 self ,
166177 path : pathlib .PurePath ,
167- expected_universe_name : str | None = None ,
178+ expected_universe_name : str | None ,
179+ universe_locations : Mapping [str , pathlib .PurePosixPath ],
168180 ) -> ValidationResult :
169181 """Parse, transform, and validate one Define file."""
170182 # Ensure Windows-style paths are converted to POSIX paths for
@@ -201,13 +213,15 @@ def _parse_and_validate_file(
201213 program = program ,
202214 expected_definition_path = expected_definition_path ,
203215 expected_universe_name = expected_universe_name ,
216+ universe_locations = universe_locations ,
204217 )
205218 return run .complete (validation_diagnostics )
206219
207220 def _parse_validate_and_collect (
208221 self ,
209222 path : pathlib .PurePath ,
210223 expected_universe_name : str | None ,
224+ universe_locations : Mapping [str , pathlib .PurePosixPath ],
211225 ) -> None :
212226 """Parse/validate one file once and append its result in encounter order."""
213227 logical_path = pathlib .PurePosixPath (path .as_posix ())
@@ -217,6 +231,7 @@ def _parse_validate_and_collect(
217231 result = self ._parse_and_validate_file (
218232 path = logical_path ,
219233 expected_universe_name = expected_universe_name ,
234+ universe_locations = universe_locations ,
220235 )
221236 self .results_by_path [logical_path ] = result
222237
@@ -245,6 +260,7 @@ def validate(
245260 program : ast .Program ,
246261 expected_definition_path : pathlib .PurePosixPath | None = None ,
247262 expected_universe_name : str | None = None ,
263+ universe_locations : Mapping [str , pathlib .PurePosixPath ] | None = None ,
248264 ) -> list [diagnostics .Diagnostic ]:
249265 """Validate all semantic rules and return collected diagnostics.
250266
@@ -258,11 +274,17 @@ def validate(
258274 expected_universe_name: Optional FQUN string from the project config.
259275 When provided, validates that each definition's FQUN matches this
260276 value. When None, skips FQUN matching validation.
277+ universe_locations: Mapping from universe name to local path for
278+ configured external dependencies. Defaults to no configured
279+ dependencies.
261280 """
262281 frame = _ValidationFrame (
263282 diagnostics = [],
264283 expected_definition_path = expected_definition_path ,
265284 expected_universe_name = expected_universe_name ,
285+ universe_locations = universe_locations
286+ if universe_locations is not None
287+ else {},
266288 )
267289 self ._validation_frames .append (frame )
268290 try :
@@ -444,6 +466,23 @@ def _load_global_name_reference(
444466 reference = typed_global_name .global_name
445467 expected_type = typed_global_name .type_name
446468 if reference .fqun is not None :
469+ canonical = reference .fqun .canonical
470+ if canonical not in self ._frame .universe_locations :
471+ if canonical not in self ._unknown_universes :
472+ expected = self ._frame .expected_universe_name
473+ if expected is None :
474+ raise ValueError (
475+ "expected_universe_name must be set for cross-universe references"
476+ )
477+ self ._unknown_universes .add (canonical )
478+ self ._diagnostics .append (
479+ diagnostics .ExternalUniverseNotConfiguredDiagnostic (
480+ position = reference .fqun .position ,
481+ universe = canonical ,
482+ current_universe_name = expected ,
483+ )
484+ )
485+ return
447486 raise NotImplementedError (
448487 "Global-reference file walking for FQUN references is not implemented."
449488 )
@@ -453,6 +492,7 @@ def _load_global_name_reference(
453492 self ._parse_validate_and_collect (
454493 path = referenced_file ,
455494 expected_universe_name = self ._frame .expected_universe_name ,
495+ universe_locations = self ._frame .universe_locations ,
456496 )
457497
458498 referenced_result = self .results_by_path [referenced_file ]
0 commit comments