Coverage for src/debputy/lsp/diagnostics.py: 44%
66 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-24 16:38 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-24 16:38 +0000
1import dataclasses
2from bisect import bisect_left, bisect_right
3from collections.abc import Mapping
4from typing import (
5 TypedDict,
6 NotRequired,
7 List,
8 Any,
9 Literal,
10 Optional,
11 TYPE_CHECKING,
12 get_args,
13 FrozenSet,
14 cast,
15 Tuple,
16 Sequence,
17 TypeVar,
18)
20if TYPE_CHECKING:
21 import lsprotocol.types as types
22else:
23 import debputy.lsprotocol.types as types
25# These are in order of severity (most important to least important).
26#
27# Special cases:
28# - "spelling" is a specialized version of "pedantic" for textual spelling mistakes
29# (LSP uses the same severity for both; only `debputy lint` shows a difference
30# between them)
31#
32LintSeverity = Literal["error", "warning", "informational", "pedantic", "spelling"]
34LINT_SEVERITY2LSP_SEVERITY: Mapping[LintSeverity, types.DiagnosticSeverity] = {
35 "error": types.DiagnosticSeverity.Error,
36 "warning": types.DiagnosticSeverity.Warning,
37 "informational": types.DiagnosticSeverity.Information,
38 "pedantic": types.DiagnosticSeverity.Hint,
39 "spelling": types.DiagnosticSeverity.Hint,
40}
41NATIVELY_LSP_SUPPORTED_SEVERITIES: FrozenSet[LintSeverity] = cast(
42 "FrozenSet[LintSeverity]",
43 frozenset(
44 {
45 "error",
46 "warning",
47 "informational",
48 "pedantic",
49 }
50 ),
51)
54_delta = set(get_args(LintSeverity)).symmetric_difference(
55 LINT_SEVERITY2LSP_SEVERITY.keys()
56)
57assert (
58 not _delta
59), f"LintSeverity and LINT_SEVERITY2LSP_SEVERITY are not aligned. Delta: {_delta}"
60del _delta
63class DiagnosticData(TypedDict):
64 quickfixes: NotRequired[Optional[List[Any]]]
65 lint_severity: NotRequired[Optional[LintSeverity]]
66 report_for_related_file: NotRequired[str]
67 enable_non_interactive_auto_fix: bool
70@dataclasses.dataclass(slots=True)
71class DiagnosticReport:
72 doc_uri: str
73 doc_version: int
74 diagnostic_report_id: str
75 is_in_progress: bool
76 diagnostics: List[types.Diagnostic]
78 _diagnostic_range_helper: Optional["DiagnosticRangeHelper"] = None
80 def diagnostics_in_range(self, text_range: types.Range) -> List[types.Diagnostic]:
81 if not self.diagnostics:
82 return []
83 helper = self._diagnostic_range_helper
84 if helper is None:
85 helper = DiagnosticRangeHelper(self.diagnostics)
86 self._diagnostic_range_helper = helper
87 return helper.diagnostics_in_range(text_range)
90def _pos_as_tuple(pos: types.Position) -> Tuple[int, int]:
91 return pos.line, pos.character
94class DiagnosticRangeHelper:
96 __slots__ = ("diagnostics", "by_start_index", "by_end_index")
98 def __init__(self, diagnostics: List[types.Diagnostic]) -> None:
99 self.diagnostics = diagnostics
100 self.by_start_index = sorted(
101 (
102 (_pos_as_tuple(diagnostics[i].range.start), i)
103 for i in range(len(diagnostics))
104 ),
105 )
106 self.by_end_index = sorted(
107 (
108 (_pos_as_tuple(diagnostics[i].range.end), i)
109 for i in range(len(diagnostics))
110 ),
111 )
113 def diagnostics_in_range(self, text_range: types.Range) -> List[types.Diagnostic]:
114 start_pos = _pos_as_tuple(text_range.start)
115 end_pos = _pos_as_tuple(text_range.end)
117 try:
118 lower_index_limit = _find_gt(
119 self.by_end_index,
120 start_pos,
121 key=lambda t: t[0],
122 )[1]
123 except NoSuchElementError:
124 lower_index_limit = len(self.diagnostics)
126 try:
127 upper_index_limit = _find_lt(
128 self.by_start_index,
129 end_pos,
130 key=lambda t: t[0],
131 )[1]
133 upper_index_limit += 1
134 except NoSuchElementError:
135 upper_index_limit = 0
137 return self.diagnostics[lower_index_limit:upper_index_limit]
140T = TypeVar("T")
143class NoSuchElementError(ValueError):
144 pass
147def _find_lt(a: Sequence[Any], x: Any, *, key: Any = None):
148 "Find rightmost value less than x"
149 i = bisect_left(a, x, key=key)
150 if i:
151 return a[i - 1]
152 raise NoSuchElementError
155def _find_gt(a: Sequence[Any], x: Any, *, key: Any = None):
156 "Find leftmost value greater than x"
157 i = bisect_right(a, x, key=key)
158 if i != len(a):
159 return a[i]
160 raise NoSuchElementError