rdf-construct 0.2.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.
Files changed (88) hide show
  1. rdf_construct/__init__.py +12 -0
  2. rdf_construct/__main__.py +0 -0
  3. rdf_construct/cli.py +1762 -0
  4. rdf_construct/core/__init__.py +33 -0
  5. rdf_construct/core/config.py +116 -0
  6. rdf_construct/core/ordering.py +219 -0
  7. rdf_construct/core/predicate_order.py +212 -0
  8. rdf_construct/core/profile.py +157 -0
  9. rdf_construct/core/selector.py +64 -0
  10. rdf_construct/core/serialiser.py +232 -0
  11. rdf_construct/core/utils.py +89 -0
  12. rdf_construct/cq/__init__.py +77 -0
  13. rdf_construct/cq/expectations.py +365 -0
  14. rdf_construct/cq/formatters/__init__.py +45 -0
  15. rdf_construct/cq/formatters/json.py +104 -0
  16. rdf_construct/cq/formatters/junit.py +104 -0
  17. rdf_construct/cq/formatters/text.py +146 -0
  18. rdf_construct/cq/loader.py +300 -0
  19. rdf_construct/cq/runner.py +321 -0
  20. rdf_construct/diff/__init__.py +59 -0
  21. rdf_construct/diff/change_types.py +214 -0
  22. rdf_construct/diff/comparator.py +338 -0
  23. rdf_construct/diff/filters.py +133 -0
  24. rdf_construct/diff/formatters/__init__.py +71 -0
  25. rdf_construct/diff/formatters/json.py +192 -0
  26. rdf_construct/diff/formatters/markdown.py +210 -0
  27. rdf_construct/diff/formatters/text.py +195 -0
  28. rdf_construct/docs/__init__.py +60 -0
  29. rdf_construct/docs/config.py +238 -0
  30. rdf_construct/docs/extractors.py +603 -0
  31. rdf_construct/docs/generator.py +360 -0
  32. rdf_construct/docs/renderers/__init__.py +7 -0
  33. rdf_construct/docs/renderers/html.py +803 -0
  34. rdf_construct/docs/renderers/json.py +390 -0
  35. rdf_construct/docs/renderers/markdown.py +628 -0
  36. rdf_construct/docs/search.py +278 -0
  37. rdf_construct/docs/templates/html/base.html.jinja +44 -0
  38. rdf_construct/docs/templates/html/class.html.jinja +152 -0
  39. rdf_construct/docs/templates/html/hierarchy.html.jinja +28 -0
  40. rdf_construct/docs/templates/html/index.html.jinja +110 -0
  41. rdf_construct/docs/templates/html/instance.html.jinja +90 -0
  42. rdf_construct/docs/templates/html/namespaces.html.jinja +37 -0
  43. rdf_construct/docs/templates/html/property.html.jinja +124 -0
  44. rdf_construct/docs/templates/html/single_page.html.jinja +169 -0
  45. rdf_construct/lint/__init__.py +75 -0
  46. rdf_construct/lint/config.py +214 -0
  47. rdf_construct/lint/engine.py +396 -0
  48. rdf_construct/lint/formatters.py +327 -0
  49. rdf_construct/lint/rules.py +692 -0
  50. rdf_construct/main.py +6 -0
  51. rdf_construct/puml2rdf/__init__.py +103 -0
  52. rdf_construct/puml2rdf/config.py +230 -0
  53. rdf_construct/puml2rdf/converter.py +420 -0
  54. rdf_construct/puml2rdf/merger.py +200 -0
  55. rdf_construct/puml2rdf/model.py +202 -0
  56. rdf_construct/puml2rdf/parser.py +565 -0
  57. rdf_construct/puml2rdf/validators.py +451 -0
  58. rdf_construct/shacl/__init__.py +56 -0
  59. rdf_construct/shacl/config.py +166 -0
  60. rdf_construct/shacl/converters.py +520 -0
  61. rdf_construct/shacl/generator.py +364 -0
  62. rdf_construct/shacl/namespaces.py +93 -0
  63. rdf_construct/stats/__init__.py +29 -0
  64. rdf_construct/stats/collector.py +178 -0
  65. rdf_construct/stats/comparator.py +298 -0
  66. rdf_construct/stats/formatters/__init__.py +83 -0
  67. rdf_construct/stats/formatters/json.py +38 -0
  68. rdf_construct/stats/formatters/markdown.py +153 -0
  69. rdf_construct/stats/formatters/text.py +186 -0
  70. rdf_construct/stats/metrics/__init__.py +26 -0
  71. rdf_construct/stats/metrics/basic.py +147 -0
  72. rdf_construct/stats/metrics/complexity.py +137 -0
  73. rdf_construct/stats/metrics/connectivity.py +130 -0
  74. rdf_construct/stats/metrics/documentation.py +128 -0
  75. rdf_construct/stats/metrics/hierarchy.py +207 -0
  76. rdf_construct/stats/metrics/properties.py +88 -0
  77. rdf_construct/uml/__init__.py +22 -0
  78. rdf_construct/uml/context.py +194 -0
  79. rdf_construct/uml/mapper.py +371 -0
  80. rdf_construct/uml/odm_renderer.py +789 -0
  81. rdf_construct/uml/renderer.py +684 -0
  82. rdf_construct/uml/uml_layout.py +393 -0
  83. rdf_construct/uml/uml_style.py +613 -0
  84. rdf_construct-0.2.0.dist-info/METADATA +431 -0
  85. rdf_construct-0.2.0.dist-info/RECORD +88 -0
  86. rdf_construct-0.2.0.dist-info/WHEEL +4 -0
  87. rdf_construct-0.2.0.dist-info/entry_points.txt +3 -0
  88. rdf_construct-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,214 @@
1
+ """Configuration file handling for rdf-construct lint.
2
+
3
+ Supports loading .rdf-lint.yml files with rule settings and severity overrides.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ from rdf_construct.lint.engine import LintConfig
14
+ from rdf_construct.lint.rules import Severity, list_rules
15
+
16
+
17
+ def load_lint_config(config_path: Path) -> LintConfig:
18
+ """Load lint configuration from a YAML file.
19
+
20
+ The configuration file format:
21
+
22
+ ```yaml
23
+ # Global settings
24
+ level: standard # strict | standard | relaxed
25
+
26
+ # Rules to enable (empty means all)
27
+ enable:
28
+ - orphan-class
29
+ - missing-label
30
+
31
+ # Rules to disable
32
+ disable:
33
+ - inconsistent-naming
34
+
35
+ # Override severity for specific rules
36
+ severity:
37
+ missing-comment: info
38
+ orphan-class: warning
39
+ ```
40
+
41
+ Args:
42
+ config_path: Path to the YAML configuration file.
43
+
44
+ Returns:
45
+ LintConfig with settings from the file.
46
+
47
+ Raises:
48
+ FileNotFoundError: If config file doesn't exist.
49
+ ValueError: If config file is invalid.
50
+ """
51
+ if not config_path.exists():
52
+ raise FileNotFoundError(f"Config file not found: {config_path}")
53
+
54
+ with open(config_path, "r", encoding="utf-8") as f:
55
+ try:
56
+ data = yaml.safe_load(f)
57
+ except yaml.YAMLError as e:
58
+ raise ValueError(f"Invalid YAML in config file: {e}")
59
+
60
+ if data is None:
61
+ data = {}
62
+
63
+ return _parse_config(data, config_path)
64
+
65
+
66
+ def _parse_config(data: dict[str, Any], source: Path) -> LintConfig:
67
+ """Parse configuration dictionary into LintConfig.
68
+
69
+ Args:
70
+ data: Configuration dictionary from YAML.
71
+ source: Source file path (for error messages).
72
+
73
+ Returns:
74
+ Parsed LintConfig.
75
+
76
+ Raises:
77
+ ValueError: If configuration is invalid.
78
+ """
79
+ config = LintConfig()
80
+ known_rules = set(list_rules())
81
+
82
+ # Parse level
83
+ if "level" in data:
84
+ level = data["level"]
85
+ if level not in ("strict", "standard", "relaxed"):
86
+ raise ValueError(
87
+ f"Invalid level '{level}' in {source}. "
88
+ "Must be 'strict', 'standard', or 'relaxed'."
89
+ )
90
+ config.level = level
91
+
92
+ # Parse enabled rules
93
+ if "enable" in data:
94
+ enabled = data["enable"]
95
+ if not isinstance(enabled, list):
96
+ raise ValueError(f"'enable' must be a list in {source}")
97
+
98
+ for rule_id in enabled:
99
+ if rule_id not in known_rules:
100
+ raise ValueError(
101
+ f"Unknown rule '{rule_id}' in 'enable' section of {source}. "
102
+ f"Available rules: {', '.join(sorted(known_rules))}"
103
+ )
104
+ config.enabled_rules.add(rule_id)
105
+
106
+ # Parse disabled rules
107
+ if "disable" in data:
108
+ disabled = data["disable"]
109
+ if not isinstance(disabled, list):
110
+ raise ValueError(f"'disable' must be a list in {source}")
111
+
112
+ for rule_id in disabled:
113
+ if rule_id not in known_rules:
114
+ raise ValueError(
115
+ f"Unknown rule '{rule_id}' in 'disable' section of {source}. "
116
+ f"Available rules: {', '.join(sorted(known_rules))}"
117
+ )
118
+ config.disabled_rules.add(rule_id)
119
+
120
+ # Parse severity overrides
121
+ if "severity" in data:
122
+ severities = data["severity"]
123
+ if not isinstance(severities, dict):
124
+ raise ValueError(f"'severity' must be a mapping in {source}")
125
+
126
+ for rule_id, sev_str in severities.items():
127
+ if rule_id not in known_rules:
128
+ raise ValueError(
129
+ f"Unknown rule '{rule_id}' in 'severity' section of {source}. "
130
+ f"Available rules: {', '.join(sorted(known_rules))}"
131
+ )
132
+
133
+ try:
134
+ severity = Severity(sev_str)
135
+ except ValueError:
136
+ raise ValueError(
137
+ f"Invalid severity '{sev_str}' for rule '{rule_id}' in {source}. "
138
+ "Must be 'error', 'warning', or 'info'."
139
+ )
140
+
141
+ config.severity_overrides[rule_id] = severity
142
+
143
+ return config
144
+
145
+
146
+ def find_config_file(start_dir: Path | None = None) -> Path | None:
147
+ """Find a lint config file by searching up the directory tree.
148
+
149
+ Looks for files named '.rdf-lint.yml' or '.rdf-lint.yaml' starting
150
+ from start_dir and moving up to the filesystem root.
151
+
152
+ Args:
153
+ start_dir: Directory to start searching from. Defaults to cwd.
154
+
155
+ Returns:
156
+ Path to config file if found, None otherwise.
157
+ """
158
+ if start_dir is None:
159
+ start_dir = Path.cwd()
160
+
161
+ config_names = [".rdf-lint.yml", ".rdf-lint.yaml", "rdf-lint.yml", "rdf-lint.yaml"]
162
+
163
+ current = start_dir.resolve()
164
+
165
+ while True:
166
+ for name in config_names:
167
+ config_path = current / name
168
+ if config_path.exists():
169
+ return config_path
170
+
171
+ parent = current.parent
172
+ if parent == current:
173
+ # Reached filesystem root
174
+ break
175
+ current = parent
176
+
177
+ return None
178
+
179
+
180
+ def create_default_config() -> str:
181
+ """Generate a default configuration file as a string.
182
+
183
+ Returns:
184
+ YAML string with commented default configuration.
185
+ """
186
+ known_rules = sorted(list_rules())
187
+
188
+ return f"""\
189
+ # rdf-construct lint configuration
190
+ # Place this file as .rdf-lint.yml in your project root
191
+
192
+ # Strictness level: strict | standard | relaxed
193
+ # - strict: warnings become errors
194
+ # - standard: default severities
195
+ # - relaxed: warnings become info
196
+ level: standard
197
+
198
+ # Enable only specific rules (empty = all rules)
199
+ # enable:
200
+ # - orphan-class
201
+ # - missing-label
202
+
203
+ # Disable specific rules
204
+ # disable:
205
+ # - inconsistent-naming
206
+
207
+ # Override severity for specific rules
208
+ # severity:
209
+ # missing-comment: info
210
+ # orphan-class: warning
211
+
212
+ # Available rules:
213
+ # {chr(10).join(f'# - {r}' for r in known_rules)}
214
+ """
@@ -0,0 +1,396 @@
1
+ """Lint engine for running rules against RDF graphs.
2
+
3
+ The engine coordinates rule execution, applies configuration overrides,
4
+ and collects results for reporting.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Sequence
13
+
14
+ from rdflib import Graph, URIRef
15
+
16
+ from rdf_construct.lint.rules import (
17
+ get_all_rules,
18
+ get_rule,
19
+ LintIssue,
20
+ RuleSpec,
21
+ Severity,
22
+ )
23
+
24
+
25
+ def find_line_number(file_path: Path, entity: URIRef) -> int | None:
26
+ """Find approximate line number for an entity's definition in a Turtle file.
27
+
28
+ Prioritises finding the entity as a subject (its definition) rather than
29
+ as a predicate or object.
30
+ """
31
+ if not file_path.exists():
32
+ return None
33
+
34
+ uri_str = str(entity)
35
+
36
+ # Extract local name
37
+ if "#" in uri_str:
38
+ local_name = uri_str.split("#")[-1]
39
+ elif "/" in uri_str:
40
+ local_name = uri_str.rsplit("/", 1)[-1]
41
+ else:
42
+ local_name = uri_str
43
+
44
+ try:
45
+ with open(file_path, "r", encoding="utf-8") as f:
46
+ lines = f.readlines()
47
+ except (IOError, UnicodeDecodeError):
48
+ return None
49
+
50
+ # First pass: look for entity as a SUBJECT (definition)
51
+ # Pattern: entity at start of line (after optional whitespace), followed by 'a' or predicate
52
+ subject_patterns = [
53
+ # Full URI as subject
54
+ rf"^\s*<{re.escape(uri_str)}>\s+",
55
+ # Prefixed form as subject at start of line
56
+ rf"^\s*\w+:{re.escape(local_name)}\s+",
57
+ ]
58
+
59
+ for pattern in subject_patterns:
60
+ regex = re.compile(pattern)
61
+ for i, line in enumerate(lines, start=1):
62
+ if regex.match(line):
63
+ return i
64
+
65
+ # Second pass: find any occurrence (fallback)
66
+ fallback_patterns = [
67
+ re.escape(f"<{uri_str}>"),
68
+ rf"\b\w+:{re.escape(local_name)}\b",
69
+ ]
70
+
71
+ for pattern in fallback_patterns:
72
+ regex = re.compile(pattern)
73
+ for i, line in enumerate(lines, start=1):
74
+ if regex.search(line):
75
+ return i
76
+
77
+ return None
78
+
79
+
80
+ @dataclass
81
+ class LintConfig:
82
+ """Configuration for a lint run.
83
+
84
+ Attributes:
85
+ level: Strictness level (strict/standard/relaxed).
86
+ enabled_rules: Specific rules to enable (empty = all).
87
+ disabled_rules: Specific rules to disable.
88
+ severity_overrides: Override default severity for specific rules.
89
+ """
90
+
91
+ level: str = "standard"
92
+ enabled_rules: set[str] = field(default_factory=set)
93
+ disabled_rules: set[str] = field(default_factory=set)
94
+ severity_overrides: dict[str, Severity] = field(default_factory=dict)
95
+
96
+ def get_effective_rules(self) -> list[RuleSpec]:
97
+ """Get the list of rules that should run based on config.
98
+
99
+ Returns:
100
+ List of RuleSpec objects to execute.
101
+ """
102
+ all_rules = get_all_rules()
103
+
104
+ # If specific rules enabled, use only those
105
+ if self.enabled_rules:
106
+ rules = [all_rules[r] for r in self.enabled_rules if r in all_rules]
107
+ else:
108
+ rules = list(all_rules.values())
109
+
110
+ # Remove disabled rules
111
+ rules = [r for r in rules if r.rule_id not in self.disabled_rules]
112
+
113
+ # Apply level filtering
114
+ if self.level == "relaxed":
115
+ # Relaxed: skip INFO-level rules
116
+ rules = [r for r in rules if r.default_severity != Severity.INFO]
117
+ elif self.level == "strict":
118
+ # Strict: all rules, but bump warnings to errors
119
+ pass # No filtering, severity handled in get_effective_severity
120
+
121
+ return rules
122
+
123
+ def get_effective_severity(self, rule_id: str) -> Severity:
124
+ """Get the effective severity for a rule.
125
+
126
+ Args:
127
+ rule_id: The rule identifier.
128
+
129
+ Returns:
130
+ The severity to use for this rule.
131
+ """
132
+ # Check for explicit override
133
+ if rule_id in self.severity_overrides:
134
+ return self.severity_overrides[rule_id]
135
+
136
+ rule = get_rule(rule_id)
137
+ if rule is None:
138
+ return Severity.ERROR
139
+
140
+ # Apply level adjustments
141
+ if self.level == "strict":
142
+ # In strict mode, warnings become errors
143
+ if rule.default_severity == Severity.WARNING:
144
+ return Severity.ERROR
145
+ elif self.level == "relaxed":
146
+ # In relaxed mode, warnings become info
147
+ if rule.default_severity == Severity.WARNING:
148
+ return Severity.INFO
149
+
150
+ return rule.default_severity
151
+
152
+
153
+ @dataclass
154
+ class LintResult:
155
+ """Result of linting a single file.
156
+
157
+ Attributes:
158
+ file_path: Path to the linted file.
159
+ graph: The parsed RDF graph (for namespace resolution).
160
+ issues: List of issues found.
161
+ error_count: Number of error-level issues.
162
+ warning_count: Number of warning-level issues.
163
+ info_count: Number of info-level issues.
164
+ """
165
+
166
+ file_path: Path
167
+ graph: Graph | None = None
168
+ issues: list[LintIssue] = field(default_factory=list)
169
+ error_count: int = 0
170
+ warning_count: int = 0
171
+ info_count: int = 0
172
+
173
+ def add_issue(self, issue: LintIssue) -> None:
174
+ """Add an issue and update counts."""
175
+ self.issues.append(issue)
176
+ if issue.severity == Severity.ERROR:
177
+ self.error_count += 1
178
+ elif issue.severity == Severity.WARNING:
179
+ self.warning_count += 1
180
+ else:
181
+ self.info_count += 1
182
+
183
+ @property
184
+ def total_issues(self) -> int:
185
+ """Total number of issues found."""
186
+ return len(self.issues)
187
+
188
+ @property
189
+ def has_errors(self) -> bool:
190
+ """Whether any errors were found."""
191
+ return self.error_count > 0
192
+
193
+ @property
194
+ def has_warnings(self) -> bool:
195
+ """Whether any warnings were found."""
196
+ return self.warning_count > 0
197
+
198
+
199
+ @dataclass
200
+ class LintSummary:
201
+ """Summary of linting multiple files.
202
+
203
+ Attributes:
204
+ results: Individual file results.
205
+ total_errors: Total errors across all files.
206
+ total_warnings: Total warnings across all files.
207
+ total_info: Total info messages across all files.
208
+ """
209
+
210
+ results: list[LintResult] = field(default_factory=list)
211
+ total_errors: int = 0
212
+ total_warnings: int = 0
213
+ total_info: int = 0
214
+
215
+ def add_result(self, result: LintResult) -> None:
216
+ """Add a file result and update totals."""
217
+ self.results.append(result)
218
+ self.total_errors += result.error_count
219
+ self.total_warnings += result.warning_count
220
+ self.total_info += result.info_count
221
+
222
+ @property
223
+ def exit_code(self) -> int:
224
+ """Get appropriate exit code based on results.
225
+
226
+ Returns:
227
+ 0 if no issues, 1 if warnings only, 2 if errors.
228
+ """
229
+ if self.total_errors > 0:
230
+ return 2
231
+ if self.total_warnings > 0:
232
+ return 1
233
+ return 0
234
+
235
+ @property
236
+ def files_with_issues(self) -> int:
237
+ """Number of files that had at least one issue."""
238
+ return sum(1 for r in self.results if r.total_issues > 0)
239
+
240
+
241
+ class LintEngine:
242
+ """Engine for running lint rules against RDF graphs.
243
+
244
+ The engine loads graphs, runs configured rules, and collects results.
245
+ It supports linting single files or batches of files.
246
+ """
247
+
248
+ def __init__(self, config: LintConfig | None = None):
249
+ """Initialise the lint engine.
250
+
251
+ Args:
252
+ config: Configuration for the lint run. Defaults to standard config.
253
+ """
254
+ self.config = config or LintConfig()
255
+
256
+ def _populate_line_numbers(self, result: LintResult, file_path: Path) -> None:
257
+ """Add line numbers to issues by searching the source file."""
258
+ for issue in result.issues:
259
+ if issue.entity and issue.line is None:
260
+ issue.line = find_line_number(file_path, issue.entity)
261
+
262
+ def lint_file(self, file_path: Path) -> LintResult:
263
+ """Lint a single RDF file.
264
+
265
+ Args:
266
+ file_path: Path to the RDF file.
267
+
268
+ Returns:
269
+ LintResult containing all issues found.
270
+
271
+ Raises:
272
+ FileNotFoundError: If file doesn't exist.
273
+ ValueError: If file can't be parsed.
274
+ """
275
+ result = LintResult(file_path=file_path)
276
+
277
+ # Load the graph
278
+ graph = Graph()
279
+ try:
280
+ # Guess format from extension
281
+ suffix = file_path.suffix.lower()
282
+ if suffix in (".ttl", ".turtle"):
283
+ fmt = "turtle"
284
+ elif suffix in (".rdf", ".xml", ".owl"):
285
+ fmt = "xml"
286
+ elif suffix in (".nt", ".ntriples"):
287
+ fmt = "nt"
288
+ elif suffix in (".n3",):
289
+ fmt = "n3"
290
+ elif suffix in (".jsonld", ".json"):
291
+ fmt = "json-ld"
292
+ else:
293
+ fmt = "turtle" # Default
294
+
295
+ graph.parse(file_path.as_posix(), format=fmt)
296
+ result.graph = graph # Store for namespace resolution
297
+ except Exception as e:
298
+ # Return result with parse error
299
+ result.add_issue(
300
+ LintIssue(
301
+ rule_id="parse-error",
302
+ severity=Severity.ERROR,
303
+ entity=None,
304
+ message=f"Failed to parse file: {e}",
305
+ )
306
+ )
307
+ return result
308
+
309
+ # Run rules
310
+ rules = self.config.get_effective_rules()
311
+
312
+ for rule in rules:
313
+ try:
314
+ issues = rule.check_fn(graph)
315
+ for issue in issues:
316
+ # Apply severity override
317
+ effective_severity = self.config.get_effective_severity(issue.rule_id)
318
+ adjusted_issue = LintIssue(
319
+ rule_id=issue.rule_id,
320
+ severity=effective_severity,
321
+ entity=issue.entity,
322
+ message=issue.message,
323
+ line=issue.line,
324
+ )
325
+ result.add_issue(adjusted_issue)
326
+ except Exception as e:
327
+ # Rule execution error
328
+ result.add_issue(
329
+ LintIssue(
330
+ rule_id=f"rule-error:{rule.rule_id}",
331
+ severity=Severity.ERROR,
332
+ entity=None,
333
+ message=f"Rule '{rule.rule_id}' failed: {e}",
334
+ )
335
+ )
336
+
337
+ # Populate line numbers
338
+ self._populate_line_numbers(result, file_path)
339
+
340
+ return result
341
+
342
+ def lint_files(self, file_paths: Sequence[Path]) -> LintSummary:
343
+ """Lint multiple RDF files.
344
+
345
+ Args:
346
+ file_paths: Paths to RDF files.
347
+
348
+ Returns:
349
+ LintSummary containing all results.
350
+ """
351
+ summary = LintSummary()
352
+
353
+ for path in file_paths:
354
+ result = self.lint_file(path)
355
+ summary.add_result(result)
356
+
357
+ return summary
358
+
359
+ def lint_graph(self, graph: Graph, source_name: str = "<graph>") -> LintResult:
360
+ """Lint an in-memory RDF graph.
361
+
362
+ Args:
363
+ graph: The RDF graph to lint.
364
+ source_name: Name to use in result (for display).
365
+
366
+ Returns:
367
+ LintResult containing all issues found.
368
+ """
369
+ result = LintResult(file_path=Path(source_name), graph=graph)
370
+
371
+ rules = self.config.get_effective_rules()
372
+
373
+ for rule in rules:
374
+ try:
375
+ issues = rule.check_fn(graph)
376
+ for issue in issues:
377
+ effective_severity = self.config.get_effective_severity(issue.rule_id)
378
+ adjusted_issue = LintIssue(
379
+ rule_id=issue.rule_id,
380
+ severity=effective_severity,
381
+ entity=issue.entity,
382
+ message=issue.message,
383
+ line=issue.line,
384
+ )
385
+ result.add_issue(adjusted_issue)
386
+ except Exception as e:
387
+ result.add_issue(
388
+ LintIssue(
389
+ rule_id=f"rule-error:{rule.rule_id}",
390
+ severity=Severity.ERROR,
391
+ entity=None,
392
+ message=f"Rule '{rule.rule_id}' failed: {e}",
393
+ )
394
+ )
395
+
396
+ return result