Skip to content

Commit ef00047

Browse files
committed
feat: add code href that links to the rule
- adds an href to the rule in the lsp which makes it possible to show a link to in the linting error that can take you to the code
1 parent c42bce4 commit ef00047

3 files changed

Lines changed: 97 additions & 0 deletions

File tree

sqlmesh/core/linter/rule.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,34 @@ def violation(self, violation_msg: t.Optional[str] = None) -> RuleViolation:
4040
"""Create a RuleViolation instance for this rule"""
4141
return RuleViolation(rule=self, violation_msg=violation_msg or self.summary)
4242

43+
def get_definition_location(self) -> t.Dict[str, t.Any]:
44+
"""Return the file path and position information for this rule.
45+
46+
This method returns information about where this rule is defined,
47+
which can be used in diagnostics to link to the rule's documentation.
48+
49+
Returns:
50+
A dictionary containing file path and position information.
51+
"""
52+
import inspect
53+
54+
# Get the file where the rule class is defined
55+
file_path = inspect.getfile(self.__class__)
56+
57+
try:
58+
# Get the source code and line number
59+
source_lines, start_line = inspect.getsourcelines(self.__class__)
60+
end_line = start_line + len(source_lines) - 1
61+
62+
return {
63+
"file_path": file_path,
64+
"start_line": start_line,
65+
"end_line": end_line,
66+
}
67+
except (IOError, TypeError):
68+
# Fall back to just returning the file path if we can't get source lines
69+
return {"file_path": file_path}
70+
4371
def __repr__(self) -> str:
4472
return self.name
4573

sqlmesh/lsp/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ def _diagnostic_to_lsp_diagnostic(
293293
return None
294294
with open(diagnostic.model._path, "r", encoding="utf-8") as file:
295295
lines = file.readlines()
296+
297+
# Get rule definition location for diagnostics link
298+
rule_location = diagnostic.rule.get_definition_location()
299+
300+
# Create URI for the rule definition file
301+
rule_uri = f"file://{rule_location['file_path']}#L{rule_location['start_line']}"
302+
303+
# Use URI format to create a link for "related information"
296304
return types.Diagnostic(
297305
range=types.Range(
298306
start=types.Position(line=0, character=0),
@@ -302,6 +310,9 @@ def _diagnostic_to_lsp_diagnostic(
302310
severity=types.DiagnosticSeverity.Error
303311
if diagnostic.violation_type == "error"
304312
else types.DiagnosticSeverity.Warning,
313+
source="sqlmesh",
314+
code=diagnostic.rule.name,
315+
code_description=types.CodeDescription(href=rule_uri),
305316
)
306317

307318
@staticmethod

tests/core/test_rule.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
import typing as t
5+
from unittest.mock import MagicMock
6+
7+
import pytest
8+
from sqlmesh.core.model import Model
9+
from sqlmesh.core.linter.rule import Rule, RuleViolation
10+
11+
12+
class TestRule(Rule):
13+
"""A test rule for testing the get_definition_location method."""
14+
15+
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
16+
"""The evaluation function that'll check for a violation of this rule."""
17+
return None
18+
19+
20+
def test_get_definition_location():
21+
"""Test the get_definition_location method returns correct file and line information."""
22+
# Create a mock context
23+
mock_context = MagicMock()
24+
rule = TestRule(mock_context)
25+
26+
# Get the expected location using the inspect module
27+
expected_file = inspect.getfile(TestRule)
28+
expected_source, expected_start_line = inspect.getsourcelines(TestRule)
29+
expected_end_line = expected_start_line + len(expected_source) - 1
30+
31+
# Get the location using the Rule method
32+
location = rule.get_definition_location()
33+
34+
# Assert the file path matches
35+
assert location["file_path"] == expected_file
36+
37+
# Assert the line numbers match
38+
assert location["start_line"] == expected_start_line
39+
assert location["end_line"] == expected_end_line
40+
41+
# Test the fallback case for a class without source
42+
with pytest.MonkeyPatch.context() as mp:
43+
# Mock inspect.getsourcelines to raise an exception
44+
def mock_getsourcelines(*args, **kwargs):
45+
raise IOError("Mock error")
46+
47+
mp.setattr(inspect, "getsourcelines", mock_getsourcelines)
48+
49+
# Get the location with the mocked function
50+
fallback_location = rule.get_definition_location()
51+
52+
# It should still have the file path
53+
assert "file_path" in fallback_location
54+
assert fallback_location["file_path"] == expected_file
55+
56+
# But not the line numbers
57+
assert "start_line" not in fallback_location
58+
assert "end_line" not in fallback_location

0 commit comments

Comments
 (0)