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
« 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
5from lsprotocol.types import Position, Range, Diagnostic, DiagnosticSeverity
7from debputy.commands.debputy_cmd.output import OutputStylingBase
8from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
9from debputy.util import _DEFAULT_LOGGER, _warn
11if TYPE_CHECKING:
12 from debputy.lsp.text_util import LintCapablePositionCodec
15LinterImpl = Callable[["LintState"], Optional[List[Diagnostic]]]
18class LintState:
20 @property
21 def plugin_feature_set(self) -> PluginProvidedFeatureSet:
22 raise NotImplementedError
24 @property
25 def doc_uri(self) -> str:
26 raise NotImplementedError
28 @property
29 def path(self) -> str:
30 raise NotImplementedError
32 @property
33 def lines(self) -> List[str]:
34 raise NotImplementedError
36 @property
37 def position_codec(self) -> "LintCapablePositionCodec":
38 raise NotImplementedError
41@dataclasses.dataclass(slots=True)
42class LintStateImpl(LintState):
43 plugin_feature_set: PluginProvidedFeatureSet
44 path: str
45 lines: List[str]
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}"
53 @property
54 def position_codec(self) -> "LintCapablePositionCodec":
55 return LINTER_POSITION_CODEC
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
69class LinterPositionCodec:
71 def client_num_units(self, chars: str):
72 return len(chars)
74 def position_from_client_units(
75 self, lines: List[str], position: Position
76 ) -> Position:
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
84 def position_to_client_units(
85 self, _lines: List[str], position: Position
86 ) -> Position:
87 return position
89 def range_from_client_units(self, _lines: List[str], range: Range) -> Range:
90 return range
92 def range_to_client_units(self, _lines: List[str], range: Range) -> Range:
93 return range
96LINTER_POSITION_CODEC = LinterPositionCodec()
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}
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
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)
150 marked_part = fo.colored(line_wo_nl[start_pos:end_pos], fg="red", style="bold")
152 return prefix + marked_part + suffix
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}")