Skip to content

Commit 82b98be

Browse files
authored
fix(linter): don't flag None range on any_of/exactly_one_of slots
1 parent cd16360 commit 82b98be

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

packages/linkml/src/linkml/linter/rules.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,14 @@ def check(self, schema_view: SchemaView, fix: bool = False) -> Iterable[LinterPr
274274
for slot in schema_view.class_induced_slots(class_name):
275275
slot_range: set[ElementName] = set(schema_view.slot_range_as_union(slot))
276276

277+
# slot_range_as_union includes a None entry for a slot whose
278+
# top-level `range` is unset. That is only undeclared when the
279+
# slot offers no other range; when a boolean range expression
280+
# (any_of / exactly_one_of) supplies concrete ranges, the None
281+
# is expected and must not be reported (#3477).
282+
if None in slot_range and len(slot_range) > 1:
283+
slot_range.discard(None)
284+
277285
# check slot range is valid
278286
for range_name in slot_range:
279287
if range_name not in all_possible_ranges:

tests/linkml/test_linter/test_rule_no_undeclared_ranges.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,49 @@ def test_undeclared_ranges_Any(range_type: str) -> None:
361361
assert problems[0].message == "Class 'SomeClass' slot 'some_slot' range 'spreadsheet' is not defined."
362362

363363

364+
@pytest.mark.parametrize("range_type", ["any_of", "exactly_one_of"])
365+
def test_expression_range_without_top_level_range_no_false_positive(range_type: str) -> None:
366+
"""A slot whose range is expressed only through a boolean expression (no top-level
367+
``range``) must not be reported as having an undeclared ``None`` range (#3477)."""
368+
test_schema = f"""id: http://example.org/test_undeclared_ranges
369+
370+
classes:
371+
Foo:
372+
attributes:
373+
bar:
374+
{range_type}:
375+
- range: integer
376+
- range: string
377+
378+
types:
379+
integer:
380+
string:
381+
"""
382+
assert not check_schema(test_schema)
383+
384+
385+
@pytest.mark.parametrize("range_type", ["any_of", "exactly_one_of"])
386+
def test_expression_range_without_top_level_range_still_flags_undefined(range_type: str) -> None:
387+
"""An undefined range inside a boolean expression is still flagged by name even
388+
when the slot has no top-level ``range`` — and never reported as ``None`` (#3477)."""
389+
test_schema = f"""id: http://example.org/test_undeclared_ranges
390+
391+
classes:
392+
Foo:
393+
attributes:
394+
bar:
395+
{range_type}:
396+
- range: string
397+
- range: spreadsheet
398+
399+
types:
400+
string:
401+
"""
402+
problems = check_schema(test_schema)
403+
assert len(problems) == 1
404+
assert problems[0].message == "Class 'Foo' slot 'bar' range 'spreadsheet' is not defined."
405+
406+
364407
@pytest.mark.parametrize("range_type", ["any_of", "exactly_one_of"])
365408
def test_undeclared_ranges_Any_in_slots(range_type: str) -> None:
366409
"""Test that declaring an 'Any' range with undefined types throws an error.

0 commit comments

Comments
 (0)