Coverage for src/debputy/linting/lint_util.py: 42%

117 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1import dataclasses 

2import os 

3from typing import List, Optional, Callable, Counter, TYPE_CHECKING 

4 

5from lsprotocol.types import Position, Range, Diagnostic, DiagnosticSeverity 

6 

7from debputy.commands.debputy_cmd.output import OutputStylingBase 

8from debputy.plugin.api.feature_set import PluginProvidedFeatureSet 

9from debputy.util import _DEFAULT_LOGGER, _warn 

10 

11if TYPE_CHECKING: 

12 from debputy.lsp.text_util import LintCapablePositionCodec 

13 

14 

15LinterImpl = Callable[["LintState"], Optional[List[Diagnostic]]] 

16 

17 

18class LintState: 

19 

20 @property 

21 def plugin_feature_set(self) -> PluginProvidedFeatureSet: 

22 raise NotImplementedError 

23 

24 @property 

25 def doc_uri(self) -> str: 

26 raise NotImplementedError 

27 

28 @property 

29 def path(self) -> str: 

30 raise NotImplementedError 

31 

32 @property 

33 def lines(self) -> List[str]: 

34 raise NotImplementedError 

35 

36 @property 

37 def position_codec(self) -> "LintCapablePositionCodec": 

38 raise NotImplementedError 

39 

40 

41@dataclasses.dataclass(slots=True) 

42class LintStateImpl(LintState): 

43 plugin_feature_set: PluginProvidedFeatureSet 

44 path: str 

45 lines: List[str] 

46 

47 @property 

48 def doc_uri(self) -> str: 

49 path = self.path 

50 abs_path = os.path.join(os.path.curdir, path) 

51 return f"file://{abs_path}" 

52 

53 @property 

54 def position_codec(self) -> "LintCapablePositionCodec": 

55 return LINTER_POSITION_CODEC 

56 

57 

58@dataclasses.dataclass(slots=True) 

59class LintReport: 

60 diagnostics_count: Counter[DiagnosticSeverity] = dataclasses.field( 

61 default_factory=Counter 

62 ) 

63 diagnostics_without_severity: int = 0 

64 diagnostic_errors: int = 0 

65 fixed: int = 0 

66 fixable: int = 0 

67 

68 

69class LinterPositionCodec: 

70 

71 def client_num_units(self, chars: str): 

72 return len(chars) 

73 

74 def position_from_client_units( 

75 self, lines: List[str], position: Position 

76 ) -> Position: 

77 

78 if len(lines) == 0: 

79 return Position(0, 0) 

80 if position.line >= len(lines): 

81 return Position(len(lines) - 1, self.client_num_units(lines[-1])) 

82 return position 

83 

84 def position_to_client_units( 

85 self, _lines: List[str], position: Position 

86 ) -> Position: 

87 return position 

88 

89 def range_from_client_units(self, _lines: List[str], range: Range) -> Range: 

90 return range 

91 

92 def range_to_client_units(self, _lines: List[str], range: Range) -> Range: 

93 return range 

94 

95 

96LINTER_POSITION_CODEC = LinterPositionCodec() 

97 

98 

99_SEVERITY2TAG = { 99 ↛ exitline 99 didn't jump to the function exit

100 DiagnosticSeverity.Error: lambda fo: fo.colored( 

101 "error", 

102 fg="red", 

103 bg="black", 

104 style="bold", 

105 ), 

106 DiagnosticSeverity.Warning: lambda fo: fo.colored( 

107 "warning", 

108 fg="yellow", 

109 bg="black", 

110 style="bold", 

111 ), 

112 DiagnosticSeverity.Information: lambda fo: fo.colored( 

113 "informational", 

114 fg="blue", 

115 bg="black", 

116 style="bold", 

117 ), 

118 DiagnosticSeverity.Hint: lambda fo: fo.colored( 

119 "pedantic", 

120 fg="green", 

121 bg="black", 

122 style="bold", 

123 ), 

124} 

125 

126 

127def _lines_to_print(range_: Range) -> int: 

128 count = range_.end.line - range_.start.line 

129 if range_.end.character > 0: 

130 count += 1 

131 return count 

132 

133 

134def _highlight_range( 

135 fo: OutputStylingBase, line: str, line_no: int, range_: Range 

136) -> str: 

137 line_wo_nl = line.rstrip("\r\n") 

138 start_pos = 0 

139 prefix = "" 

140 suffix = "" 

141 if line_no == range_.start.line: 

142 start_pos = range_.start.character 

143 prefix = line_wo_nl[0:start_pos] 

144 if line_no == range_.end.line: 

145 end_pos = range_.end.character 

146 suffix = line_wo_nl[end_pos:] 

147 else: 

148 end_pos = len(line_wo_nl) 

149 

150 marked_part = fo.colored(line_wo_nl[start_pos:end_pos], fg="red", style="bold") 

151 

152 return prefix + marked_part + suffix 

153 

154 

155def report_diagnostic( 

156 fo: OutputStylingBase, 

157 filename: str, 

158 diagnostic: Diagnostic, 

159 lines: List[str], 

160 auto_fixable: bool, 

161 auto_fixed: bool, 

162 lint_report: LintReport, 

163) -> None: 

164 logger = _DEFAULT_LOGGER 

165 assert logger is not None 

166 severity = diagnostic.severity 

167 missing_severity = False 

168 if severity is None: 

169 severity = DiagnosticSeverity.Warning 

170 missing_severity = True 

171 if not auto_fixed: 

172 tag_unresolved = _SEVERITY2TAG.get(severity) 

173 if tag_unresolved is None: 

174 tag_unresolved = _SEVERITY2TAG[DiagnosticSeverity.Warning] 

175 lint_report.diagnostics_without_severity += 1 

176 else: 

177 lint_report.diagnostics_count[severity] += 1 

178 tag = tag_unresolved(fo) 

179 else: 

180 tag = fo.colored( 

181 "auto-fixing", 

182 fg="magenta", 

183 bg="black", 

184 style="bold", 

185 ) 

186 start_line = diagnostic.range.start.line 

187 start_position = diagnostic.range.start.character 

188 end_line = diagnostic.range.end.line 

189 end_position = diagnostic.range.end.character 

190 has_fixit = "" 

191 line_no_width = len(str(len(lines))) 

192 if not auto_fixed and auto_fixable: 

193 has_fixit = " [Correctable via --auto-fix]" 

194 lint_report.fixable += 1 

195 print( 

196 f"{tag}: File: {filename}:{start_line+1}:{start_position}:{end_line+1}:{end_position}: {diagnostic.message}{has_fixit}", 

197 ) 

198 if missing_severity: 

199 _warn( 

200 " This warning did not have an explicit severity; Used Warning as a fallback!" 

201 ) 

202 if auto_fixed: 

203 # If it is fixed, there is no reason to show additional context. 

204 lint_report.fixed += 1 

205 return 

206 lines_to_print = _lines_to_print(diagnostic.range) 

207 if diagnostic.range.end.line > len(lines) or diagnostic.range.start.line < 0: 

208 lint_report.diagnostic_errors += 1 

209 _warn( 

210 "Bug in the underlying linter: The line numbers of the warning does not fit in the file..." 

211 ) 

212 return 

213 if lines_to_print == 1: 

214 line = _highlight_range(fo, lines[start_line], start_line, diagnostic.range) 

215 print(f" {start_line+1:{line_no_width}}: {line}") 

216 else: 

217 for line_no in range(start_line, end_line): 

218 line = _highlight_range(fo, lines[line_no], line_no, diagnostic.range) 

219 print(f" {line_no+1:{line_no_width}}: {line}")