|
27 | 27 | EnumDefinition, |
28 | 28 | EnumDefinitionName, |
29 | 29 | SlotDefinition, |
| 30 | + SlotDefinitionName, |
30 | 31 | TypeDefinition, |
31 | 32 | TypeDefinitionName, |
32 | 33 | ) |
@@ -455,3 +456,94 @@ def check(self, schema_view: SchemaView, fix: bool = False) -> Iterable[LinterPr |
455 | 456 | f"'{prefix.prefix_reference}' instead of using prefix " |
456 | 457 | f"'{namespace_to_prefix[prefix.prefix_reference]}'" |
457 | 458 | ) |
| 459 | + |
| 460 | + |
| 461 | +class NoInvalidSlotGroupRule(LinterRule): |
| 462 | + """Disallow `slot_group` references that do not resolve to a grouping slot. |
| 463 | +
|
| 464 | + A slot may declare `slot_group: B` only if B is a defined slot and B is marked |
| 465 | + `is_grouping_slot: true`. The LinkML metamodel gives `slot_group` the range |
| 466 | + `slot_definition` but does not enforce that the referenced name resolves to a |
| 467 | + declared slot, nor that the target is a grouping slot, so dangling or |
| 468 | + non-grouping `slot_group` references pass silently. This rule is the |
| 469 | + `slot_group` analogue of `no_undeclared_ranges`. Not auto-fixable. |
| 470 | + """ |
| 471 | + |
| 472 | + id = "no_invalid_slot_group" |
| 473 | + |
| 474 | + def check(self, schema_view: SchemaView, fix: bool = False) -> Iterable[LinterProblem]: |
| 475 | + # Lookup table for resolving slot_group targets. all_slots() is acceptable here |
| 476 | + # because we only need to know whether a grouping slot of a given name exists. |
| 477 | + all_slots: dict[SlotDefinitionName, SlotDefinition] = schema_view.all_slots() |
| 478 | + |
| 479 | + def check_slot(slot: SlotDefinition, descriptor: str) -> Iterable[LinterProblem]: |
| 480 | + group = slot.slot_group |
| 481 | + if not group: |
| 482 | + return |
| 483 | + if group not in all_slots: |
| 484 | + yield LinterProblem(f"{descriptor} has slot_group '{group}' which is not a defined slot.") |
| 485 | + elif not all_slots[group].is_grouping_slot: |
| 486 | + yield LinterProblem( |
| 487 | + f"{descriptor} has slot_group '{group}' which is not marked 'is_grouping_slot: true'." |
| 488 | + ) |
| 489 | + |
| 490 | + # Iterate global slots, then each class's attributes and slot_usage separately. |
| 491 | + # all_slots() de-duplicates attributes by name, so iterating it would miss |
| 492 | + # slot_group on attributes that share a name across classes or override a global |
| 493 | + # slot. attributes=False yields only the global slots; attributes are checked |
| 494 | + # per class below, each in its own context. |
| 495 | + for slot_name, slot_def in schema_view.all_slots(attributes=False).items(): |
| 496 | + yield from check_slot(slot_def, f"Slot '{slot_name}'") |
| 497 | + |
| 498 | + for class_name, class_def in schema_view.all_classes().items(): |
| 499 | + for attr_name, attr in (class_def.attributes or {}).items(): |
| 500 | + yield from check_slot(attr, f"Class '{class_name}' attribute '{attr_name}'") |
| 501 | + for usage_name, usage in (class_def.slot_usage or {}).items(): |
| 502 | + yield from check_slot(usage, f"Class '{class_name}' slot_usage '{usage_name}'") |
| 503 | + |
| 504 | + |
| 505 | +class NoUndeclaredSubsetsRule(LinterRule): |
| 506 | + """Disallow `in_subset` references to subsets not declared in the `subsets` section. |
| 507 | +
|
| 508 | + An element may declare `in_subset: S` only if S is a declared `SubsetDefinition`. |
| 509 | + The LinkML metamodel gives `in_subset` the range `subset_definition` but does not |
| 510 | + enforce that the referenced name resolves to a declared subset, so dangling |
| 511 | + `in_subset` references pass silently. This rule is the `in_subset` analogue of |
| 512 | + `no_undeclared_ranges`. Not auto-fixable. |
| 513 | + """ |
| 514 | + |
| 515 | + id = "no_undeclared_subsets" |
| 516 | + |
| 517 | + def check(self, schema_view: SchemaView, fix: bool = False) -> Iterable[LinterProblem]: |
| 518 | + declared_subsets: set[str] = set(schema_view.all_subsets()) |
| 519 | + |
| 520 | + def check_element(element: Element, descriptor: str) -> Iterable[LinterProblem]: |
| 521 | + for subset_name in getattr(element, "in_subset", None) or []: |
| 522 | + if subset_name not in declared_subsets: |
| 523 | + yield LinterProblem(f"{descriptor} asserts membership in undeclared subset '{subset_name}'.") |
| 524 | + |
| 525 | + # Iterate global slots, then each class's attributes and slot_usage separately. |
| 526 | + # all_slots() de-duplicates attributes by name, so iterating it would miss |
| 527 | + # in_subset on attributes that share a name across classes or override a global |
| 528 | + # slot. attributes=False yields only the global slots; attributes are checked |
| 529 | + # per class below, each in its own context. |
| 530 | + for slot_name, slot_def in schema_view.all_slots(attributes=False).items(): |
| 531 | + yield from check_element(slot_def, f"Slot '{slot_name}'") |
| 532 | + |
| 533 | + # classes, plus their attributes and slot_usage overrides |
| 534 | + for class_name, class_def in schema_view.all_classes().items(): |
| 535 | + yield from check_element(class_def, f"Class '{class_name}'") |
| 536 | + for attr_name, attr in (class_def.attributes or {}).items(): |
| 537 | + yield from check_element(attr, f"Class '{class_name}' attribute '{attr_name}'") |
| 538 | + for usage_name, usage in (class_def.slot_usage or {}).items(): |
| 539 | + yield from check_element(usage, f"Class '{class_name}' slot_usage '{usage_name}'") |
| 540 | + |
| 541 | + # enums and their permissible values |
| 542 | + for enum_name, enum_def in schema_view.all_enums().items(): |
| 543 | + yield from check_element(enum_def, f"Enum '{enum_name}'") |
| 544 | + for pv_name, pv in (enum_def.permissible_values or {}).items(): |
| 545 | + yield from check_element(pv, f"Enum '{enum_name}' permissible value '{pv_name}'") |
| 546 | + |
| 547 | + # types |
| 548 | + for type_name, type_def in schema_view.all_types().items(): |
| 549 | + yield from check_element(type_def, f"Type '{type_name}'") |
0 commit comments