Repository: linkml/linkml

Issue:
linter is inconsistent in how it mentions meta model elements
For example, in the `message` column of the TSV output, one may see wither of these styles:

- SlotDefinition 'source_file_size' has no title
- slot_definition 'associated_part' does not have recommended slot 'description'


Discussion:

Patch:
diff --git a/linkml/linter/rules.py b/linkml/linter/rules.py
index d3893001..9deef5d4 100644
--- a/linkml/linter/rules.py
+++ b/linkml/linter/rules.py
@@ -1,10 +1,10 @@
-from functools import lru_cache
 import re
 from abc import ABC, abstractmethod
+from functools import lru_cache
 from typing import Callable, Iterable, List
 
 from linkml_runtime.linkml_model import (ClassDefinition, ClassDefinitionName,
-                                         SlotDefinition)
+                                         Element, SlotDefinition)
 from linkml_runtime.utils.schemaview import SchemaView
 from prefixmaps.io.parser import load_multi_context
 
@@ -45,6 +45,11 @@ class LinterRule(ABC):
     def uncamel(n: str) -> str:
         return LinterRule.PATTERNS._uncamel.sub(" ", n)
 
+    @staticmethod
+    def format_element(element: Element):
+        class_name = element.__class__.__name__.replace("Definition", "")
+        return f"{class_name} '{element.name}'"
+
 
 class NoEmptyTitleRule(LinterRule):
 
@@ -60,7 +65,7 @@ class NoEmptyTitleRule(LinterRule):
                 e.title = title
             if e.title is None:
                 problem = LinterProblem(
-                    message=f"{type(e).__name__} '{e.name}' has no title"
+                    message=f"{self.format_element(e)} has no title"
                 )
                 yield problem
 
@@ -70,12 +75,14 @@ class NoXsdIntTypeRule(LinterRule):
     id = "no_xsd_int_type"
 
     def check(self, schema_view: SchemaView, fix: bool = False):
-        for type_name, type_definition in schema_view.all_types(imports=False).items():
+        for type_definition in schema_view.all_types(imports=False).values():
             if type_definition.uri == "xsd:int":
                 if fix:
                     type_definition.uri = "xsd:integer"
                 else:
-                    yield LinterProblem(f"Type '{type_name}' has uri xsd:int")
+                    yield LinterProblem(
+                        f"{self.format_element(type_definition)} has uri xsd:int"
+                    )
 
 
 class PermissibleValuesFormatRule(LinterRule):
@@ -86,11 +93,11 @@ class PermissibleValuesFormatRule(LinterRule):
         self, schema_view: SchemaView, fix: bool = False
     ) -> Iterable[LinterProblem]:
         pattern = self.PATTERNS.get(self.config.format, re.compile(self.config.format))
-        for enum_name, enum_def in schema_view.all_enums(imports=False).items():
+        for enum_def in schema_view.all_enums(imports=False).values():
             for value in enum_def.permissible_values.keys():
                 if pattern.fullmatch(value) is None:
                     yield LinterProblem(
-                        f"Enum '{enum_name}' has permissible value '{value}'"
+                        f"{self.format_element(enum_def)} has permissible value '{value}'"
                     )
 
 
@@ -123,11 +130,10 @@ class RecommendedRule(LinterRule):
             if element_name in self.config.exclude:
                 continue
             for meta_slot_name, meta_slot_value in vars(element_definition).items():
-                meta_class_name = type(element_definition).class_name
-                key = f"{meta_class_name}__{meta_slot_name}"
+                key = f"{element_definition.class_name}__{meta_slot_name}"
                 if key in recommended_meta_slots and not meta_slot_value:
                     yield LinterProblem(
-                        f"{meta_class_name} '{element_name}' does not have recommended slot '{meta_slot_name}'"
+                        f"{self.format_element(element_definition)} does not have recommended slot '{meta_slot_name}'"
                     )
 
 
@@ -272,7 +278,7 @@ class StandardNamingRule(LinterRule):
             for permissible_value_name in enum_definition.permissible_values.keys():
                 if permissible_value_pattern.fullmatch(permissible_value_name) is None:
                     yield LinterProblem(
-                        f"Permissible value of enum '{enum_name}' has name '{permissible_value_name}'"
+                        f"Permissible value of {self.format_element(enum_definition)} has name '{permissible_value_name}'"
                     )
 
 


Test patch:
diff --git a/tests/test_linter/test_cli.py b/tests/test_linter/test_cli.py
index ee4cb89d..5a95a434 100644
--- a/tests/test_linter/test_cli.py
+++ b/tests/test_linter/test_cli.py
@@ -64,7 +64,7 @@ class TestLinterCli(unittest.TestCase):
             result = self.runner.invoke(main, [SCHEMA_FILE])
             self.assertEqual(result.exit_code, 1)
             self.assertIn(
-                "warning  class_definition 'Adult' does not have recommended slot 'description'  (recommended)",
+                "warning  Class 'Adult' does not have recommended slot 'description'  (recommended)",
                 result.stdout,
             )
             self.assertIn(
diff --git a/tests/test_linter/test_rule_no_empty_title.py b/tests/test_linter/test_rule_no_empty_title.py
new file mode 100644
index 00000000..201a06ee
--- /dev/null
+++ b/tests/test_linter/test_rule_no_empty_title.py
@@ -0,0 +1,31 @@
+import unittest
+
+from linkml_runtime import SchemaView
+
+from linkml.linter.config.datamodel.config import RuleConfig, RuleLevel
+from linkml.linter.rules import NoEmptyTitleRule
+from linkml.utils.schema_builder import SchemaBuilder
+
+
+class TestRuleNoEmptyTitle(unittest.TestCase):
+    def test_elements_with_empty_title(self):
+        builder = SchemaBuilder()
+        builder.add_class("AClass")
+        builder.add_slot("a_slot")
+        builder.add_enum("AnEnum")
+        builder.add_type("a_type")
+        builder.add_class("WithTitle", title="With title")
+
+        schema_view = SchemaView(builder.schema)
+        config = RuleConfig(level=RuleLevel.error.text)
+
+        rule = NoEmptyTitleRule(config)
+        problems = list(rule.check(schema_view))
+
+        self.assertEqual(len(problems), 4)
+
+        messages = [p.message for p in problems]
+        self.assertIn("Class 'AClass' has no title", messages)
+        self.assertIn("Slot 'a_slot' has no title", messages)
+        self.assertIn("Enum 'AnEnum' has no title", messages)
+        self.assertIn("Type 'a_type' has no title", messages)
diff --git a/tests/test_linter/test_rule_recommended.py b/tests/test_linter/test_rule_recommended.py
index df0d29bc..6958fa9f 100644
--- a/tests/test_linter/test_rule_recommended.py
+++ b/tests/test_linter/test_rule_recommended.py
@@ -28,15 +28,15 @@ class TestRecommendedRule(unittest.TestCase):
 
         messages = [p.message for p in problems]
         self.assertIn(
-            "class_definition 'MyClass' does not have recommended slot 'description'",
+            "Class 'MyClass' does not have recommended slot 'description'",
             messages,
         )
         self.assertIn(
-            "slot_definition 'my_slot' does not have recommended slot 'description'",
+            "Slot 'my_slot' does not have recommended slot 'description'",
             messages,
         )
         self.assertIn(
-            "enum_definition 'MyEnum' does not have recommended slot 'description'",
+            "Enum 'MyEnum' does not have recommended slot 'description'",
             messages,
         )
 
@@ -74,7 +74,7 @@ class TestRecommendedRule(unittest.TestCase):
 
         messages = [p.message for p in problems]
         self.assertIn(
-            "slot_definition 'my_slot' does not have recommended slot 'description'",
+            "Slot 'my_slot' does not have recommended slot 'description'",
             messages,
         )
 
@@ -95,10 +95,10 @@ class TestRecommendedRule(unittest.TestCase):
 
         messages = [p.message for p in problems]
         self.assertIn(
-            "class_definition 'MyClass' does not have recommended slot 'description'",
+            "Class 'MyClass' does not have recommended slot 'description'",
             messages,
         )
         self.assertIn(
-            "enum_definition 'MyEnum' does not have recommended slot 'description'",
+            "Enum 'MyEnum' does not have recommended slot 'description'",
             messages,
         )
diff --git a/tests/test_linter/test_rule_standard_naming.py b/tests/test_linter/test_rule_standard_naming.py
index a2831c3c..bfc1665d 100644
--- a/tests/test_linter/test_rule_standard_naming.py
+++ b/tests/test_linter/test_rule_standard_naming.py
@@ -47,18 +47,18 @@ class TestStandardNamingRule(unittest.TestCase):
         self.assertIn("Slot has name 'BadSlot'", messages)
         self.assertIn("Slot has name 'worse slot'", messages)
         self.assertIn(
-            "Permissible value of enum 'GoodEnumWithBadPV' has name 'Bad_PV'", messages
+            "Permissible value of Enum 'GoodEnumWithBadPV' has name 'Bad_PV'", messages
         )
         self.assertIn(
-            "Permissible value of enum 'GoodEnumUpperPV' has name 'GOOD_UPPER_PV'",
+            "Permissible value of Enum 'GoodEnumUpperPV' has name 'GOOD_UPPER_PV'",
             messages,
         )
         self.assertIn(
-            "Permissible value of enum 'GoodEnumUpperPV' has name 'GREAT_UPPER_PV'",
+            "Permissible value of Enum 'GoodEnumUpperPV' has name 'GREAT_UPPER_PV'",
             messages,
         )
         self.assertIn(
-            "Permissible value of enum 'GoodEnumBadUpperPV' has name 'GOOD_UPPER_PV'",
+            "Permissible value of Enum 'GoodEnumBadUpperPV' has name 'GOOD_UPPER_PV'",
             messages,
         )
         self.assertIn("Enum has name 'bad_enum'", messages)
@@ -79,25 +79,25 @@ class TestStandardNamingRule(unittest.TestCase):
         self.assertIn("Slot has name 'BadSlot'", messages)
         self.assertIn("Slot has name 'worse slot'", messages)
         self.assertIn(
-            "Permissible value of enum 'GoodEnum' has name 'good_lower_pv'", messages
+            "Permissible value of Enum 'GoodEnum' has name 'good_lower_pv'", messages
         )
         self.assertIn(
-            "Permissible value of enum 'GoodEnum' has name 'great_lower_pv'", messages
+            "Permissible value of Enum 'GoodEnum' has name 'great_lower_pv'", messages
         )
         self.assertIn(
-            "Permissible value of enum 'GoodEnumWithBadPV' has name 'good_lower_pv'",
+            "Permissible value of Enum 'GoodEnumWithBadPV' has name 'good_lower_pv'",
             messages,
         )
         self.assertIn(
-            "Permissible value of enum 'GoodEnumWithBadPV' has name 'Bad_PV'", messages
+            "Permissible value of Enum 'GoodEnumWithBadPV' has name 'Bad_PV'", messages
         )
         self.assertIn(
-            "Permissible value of enum 'GoodEnumBadUpperPV' has name 'bad_pv'", messages
+            "Permissible value of Enum 'GoodEnumBadUpperPV' has name 'bad_pv'", messages
         )
         self.assertIn("Enum has name 'bad_enum'", messages)
         self.assertIn(
-            "Permissible value of enum 'bad_enum' has name 'good_lower_pv'", messages
+            "Permissible value of Enum 'bad_enum' has name 'good_lower_pv'", messages
         )
         self.assertIn(
-            "Permissible value of enum 'bad_enum' has name 'great_lower_pv'", messages
+            "Permissible value of Enum 'bad_enum' has name 'great_lower_pv'", messages
         )