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

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) 

11 

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) 

23 

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) 

33 

34try: 

35 from debian._deb822_repro.locatable import Position as TEPosition, Ranage as TERange 

36 

37 from pygls.server import LanguageServer 

38 from pygls.workspace import TextDocument 

39except ImportError: 

40 pass 

41 

42 

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] 

52 

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) 

63 

64DOCUMENT_VERSION_TABLE: Dict[str, int] = {} 

65 

66 

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 

75 

76 

77def is_doc_at_version(uri: str, version: int) -> bool: 

78 dv = DOCUMENT_VERSION_TABLE.get(uri) 

79 return dv == version 

80 

81 

82lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION) 

83lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) 

84 

85 

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 ) 

104 

105 yield from scanner 

106 

107 

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 

161 

162 date_str = line[start_date_idx + 5 :] 

163 

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 

183 

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 ) 

214 

215 

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) 

268 

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 

276 

277 yield diagnostics 

278 if not diagnostics or diagnostics_at_last_update != len(diagnostics): 

279 yield diagnostics 

280 

281 

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)