Skip to content

Commit cd5cb50

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 cd5cb50

3 files changed

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

0 commit comments

Comments
 (0)