Coverage for src/debputy/lsp/lsp_debian_changelog.py: 21%
108 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 sys
2from email.utils import parsedate_to_datetime
3from typing import (
4 Union,
5 List,
6 Dict,
7 Iterator,
8 Optional,
9 Iterable,
10)
12from lsprotocol.types import (
13 Diagnostic,
14 DidOpenTextDocumentParams,
15 DidChangeTextDocumentParams,
16 TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
17 TEXT_DOCUMENT_CODE_ACTION,
18 DidCloseTextDocumentParams,
19 Range,
20 Position,
21 DiagnosticSeverity,
22)
24from debputy.linting.lint_util import LintState
25from debputy.lsp.lsp_features import lsp_diagnostics, lsp_standard_handler
26from debputy.lsp.quickfixes import (
27 propose_correct_text_quick_fix,
28)
29from debputy.lsp.spellchecking import spellcheck_line
30from debputy.lsp.text_util import (
31 LintCapablePositionCodec,
32)
34try:
35 from debian._deb822_repro.locatable import Position as TEPosition, Ranage as TERange
37 from pygls.server import LanguageServer
38 from pygls.workspace import TextDocument
39except ImportError:
40 pass
43# Same as Lintian
44_MAXIMUM_WIDTH: int = 82
45_LANGUAGE_IDS = [
46 "debian/changelog",
47 # emacs's name
48 "debian-changelog",
49 # vim's name
50 "debchangelog",
51]
53_WEEKDAYS_BY_IDX = [
54 "Mon",
55 "Tue",
56 "Wed",
57 "Thu",
58 "Fri",
59 "Sat",
60 "Sun",
61]
62_KNOWN_WEEK_DAYS = frozenset(_WEEKDAYS_BY_IDX)
64DOCUMENT_VERSION_TABLE: Dict[str, int] = {}
67def _handle_close(
68 ls: "LanguageServer",
69 params: DidCloseTextDocumentParams,
70) -> None:
71 try:
72 del DOCUMENT_VERSION_TABLE[params.text_document.uri]
73 except KeyError:
74 pass
77def is_doc_at_version(uri: str, version: int) -> bool:
78 dv = DOCUMENT_VERSION_TABLE.get(uri)
79 return dv == version
82lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION)
83lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)
86@lsp_diagnostics(_LANGUAGE_IDS)
87def _diagnostics_debian_changelog(
88 ls: "LanguageServer",
89 params: Union[DidOpenTextDocumentParams, DidChangeTextDocumentParams],
90) -> Iterable[List[Diagnostic]]:
91 doc_uri = params.text_document.uri
92 doc = ls.workspace.get_text_document(doc_uri)
93 lines = doc.lines
94 max_words = 1_000
95 delta_update_size = 10
96 max_lines_between_update = 10
97 scanner = _scan_debian_changelog_for_diagnostics(
98 lines,
99 doc.position_codec,
100 delta_update_size,
101 max_words,
102 max_lines_between_update,
103 )
105 yield from scanner
108def _check_footer_line(
109 line: str,
110 line_no: int,
111 lines: List[str],
112 position_codec: LintCapablePositionCodec,
113) -> Iterator[Diagnostic]:
114 try:
115 end_email_idx = line.rindex("> ")
116 except ValueError:
117 # Syntax error; flag later
118 return
119 line_len = len(line)
120 start_date_idx = end_email_idx + 3
121 # 3 characters for the day name (Mon), then a comma plus a space followed by the
122 # actual date. The 6 characters limit is a gross under estimation of the real
123 # size.
124 if line_len < start_date_idx + 6:
125 range_server_units = Range(
126 Position(
127 line_no,
128 start_date_idx,
129 ),
130 Position(
131 line_no,
132 line_len,
133 ),
134 )
135 yield Diagnostic(
136 position_codec.range_to_client_units(lines, range_server_units),
137 "Expected a date in RFC822 format (Tue, 12 Mar 2024 12:34:56 +0000)",
138 severity=DiagnosticSeverity.Error,
139 source="debputy",
140 )
141 return
142 day_name_range_server_units = Range(
143 Position(
144 line_no,
145 start_date_idx,
146 ),
147 Position(
148 line_no,
149 start_date_idx + 3,
150 ),
151 )
152 day_name = line[start_date_idx : start_date_idx + 3]
153 if day_name not in _KNOWN_WEEK_DAYS:
154 yield Diagnostic(
155 position_codec.range_to_client_units(lines, day_name_range_server_units),
156 "Expected a three letter date here (Mon, Tue, ..., Sun).",
157 severity=DiagnosticSeverity.Error,
158 source="debputy",
159 )
160 return
162 date_str = line[start_date_idx + 5 :]
164 if line[start_date_idx + 3 : start_date_idx + 5] != ", ":
165 sep = line[start_date_idx + 3 : start_date_idx + 5]
166 range_server_units = Range(
167 Position(
168 line_no,
169 start_date_idx + 3,
170 ),
171 Position(
172 line_no,
173 start_date_idx + 4,
174 ),
175 )
176 yield Diagnostic(
177 position_codec.range_to_client_units(lines, range_server_units),
178 f'Improper formatting of date. Expected ", " here, not "{sep}"',
179 severity=DiagnosticSeverity.Error,
180 source="debputy",
181 )
182 return
184 try:
185 # FIXME: this parser is too forgiving (it ignores trailing garbage)
186 date = parsedate_to_datetime(date_str)
187 except ValueError as e:
188 range_server_units = Range(
189 Position(
190 line_no,
191 start_date_idx + 5,
192 ),
193 Position(
194 line_no,
195 line_len,
196 ),
197 )
198 yield Diagnostic(
199 position_codec.range_to_client_units(lines, range_server_units),
200 f"Unable to the date as a valid RFC822 date: {e.args[0]}",
201 severity=DiagnosticSeverity.Error,
202 source="debputy",
203 )
204 return
205 expected_week_day = _WEEKDAYS_BY_IDX[date.weekday()]
206 if expected_week_day != day_name:
207 yield Diagnostic(
208 position_codec.range_to_client_units(lines, day_name_range_server_units),
209 f"The date was a {expected_week_day}day.",
210 severity=DiagnosticSeverity.Warning,
211 source="debputy",
212 data=[propose_correct_text_quick_fix(expected_week_day)],
213 )
216def _scan_debian_changelog_for_diagnostics(
217 lines: List[str],
218 position_codec: LintCapablePositionCodec,
219 delta_update_size: int,
220 max_words: int,
221 max_lines_between_update: int,
222 *,
223 max_line_length: int = _MAXIMUM_WIDTH,
224) -> Iterator[List[Diagnostic]]:
225 diagnostics = []
226 diagnostics_at_last_update = 0
227 lines_since_last_update = 0
228 for line_no, line in enumerate(lines):
229 orig_line = line
230 line = line.rstrip()
231 if not line:
232 continue
233 if line.startswith(" --"):
234 diagnostics.extend(_check_footer_line(line, line_no, lines, position_codec))
235 continue
236 if not line.startswith(" "):
237 continue
238 # minus 1 for newline
239 orig_line_len = len(orig_line) - 1
240 if orig_line_len > max_line_length:
241 range_server_units = Range(
242 Position(
243 line_no,
244 max_line_length,
245 ),
246 Position(
247 line_no,
248 orig_line_len,
249 ),
250 )
251 diagnostics.append(
252 Diagnostic(
253 position_codec.range_to_client_units(lines, range_server_units),
254 f"Line exceeds {max_line_length} characters",
255 severity=DiagnosticSeverity.Hint,
256 source="debputy",
257 )
258 )
259 if len(line) > 3 and line[2] == "[" and line[-1] == "]":
260 # Do not spell check [ X ] as X is usually a name
261 continue
262 lines_since_last_update += 1
263 if max_words > 0:
264 typos = list(spellcheck_line(lines, position_codec, line_no, line))
265 new_diagnostics = len(typos)
266 max_words -= new_diagnostics
267 diagnostics.extend(typos)
269 current_diagnostics_len = len(diagnostics)
270 if (
271 lines_since_last_update >= max_lines_between_update
272 or current_diagnostics_len - diagnostics_at_last_update > delta_update_size
273 ):
274 diagnostics_at_last_update = current_diagnostics_len
275 lines_since_last_update = 0
277 yield diagnostics
278 if not diagnostics or diagnostics_at_last_update != len(diagnostics):
279 yield diagnostics
282def _lint_debian_changelog(
283 lint_state: LintState,
284) -> Optional[List[Diagnostic]]:
285 limits = sys.maxsize
286 scanner = _scan_debian_changelog_for_diagnostics(
287 lint_state.lines,
288 lint_state.position_codec,
289 limits,
290 limits,
291 limits,
292 )
293 return next(iter(scanner), None)