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

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) 

19 

20if TYPE_CHECKING: 

21 import lsprotocol.types as types 

22else: 

23 import debputy.lsprotocol.types as types 

24 

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"] 

33 

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) 

52 

53 

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 

61 

62 

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 

68 

69 

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] 

77 

78 _diagnostic_range_helper: Optional["DiagnosticRangeHelper"] = None 

79 

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) 

88 

89 

90def _pos_as_tuple(pos: types.Position) -> Tuple[int, int]: 

91 return pos.line, pos.character 

92 

93 

94class DiagnosticRangeHelper: 

95 

96 __slots__ = ("diagnostics", "by_start_index", "by_end_index") 

97 

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 ) 

112 

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) 

116 

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) 

125 

126 try: 

127 upper_index_limit = _find_lt( 

128 self.by_start_index, 

129 end_pos, 

130 key=lambda t: t[0], 

131 )[1] 

132 

133 upper_index_limit += 1 

134 except NoSuchElementError: 

135 upper_index_limit = 0 

136 

137 return self.diagnostics[lower_index_limit:upper_index_limit] 

138 

139 

140T = TypeVar("T") 

141 

142 

143class NoSuchElementError(ValueError): 

144 pass 

145 

146 

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 

153 

154 

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