Coverage for src/debputy/lsp/lsp_debian_copyright.py: 75%
100 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-01-27 13:59 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-01-27 13:59 +0000
1import re
2from typing import (
3 Union,
4 Sequence,
5 Tuple,
6 Optional,
7 List,
8 Dict,
9)
11from debputy.linting.lint_util import LintState
12from debputy.lsp.debputy_ls import DebputyLanguageServer
13from debputy.lsp.lsp_debian_control_reference_data import (
14 Deb822KnownField,
15 Dep5FileMetadata,
16 StanzaMetadata,
17)
18from debputy.lsp.lsp_features import (
19 lint_diagnostics,
20 lsp_completer,
21 lsp_hover,
22 lsp_standard_handler,
23 lsp_folding_ranges,
24 lsp_semantic_tokens_full,
25 lsp_will_save_wait_until,
26 lsp_format_document,
27 SecondaryLanguage,
28 LanguageDispatchRule,
29)
30from debputy.lsp.lsp_generic_deb822 import (
31 deb822_completer,
32 deb822_hover,
33 deb822_folding_ranges,
34 deb822_semantic_tokens_full,
35 deb822_token_iter,
36 deb822_format_file,
37)
38from debputy.lsp.quickfixes import (
39 propose_correct_text_quick_fix,
40)
41from debputy.lsp.vendoring._deb822_repro import (
42 Deb822FileElement,
43 Deb822ParagraphElement,
44)
45from debputy.lsp.vendoring._deb822_repro.parsing import (
46 Deb822KeyValuePairElement,
47)
48from debputy.lsprotocol.types import (
49 Range,
50 CompletionItem,
51 CompletionList,
52 CompletionParams,
53 DiagnosticRelatedInformation,
54 Location,
55 HoverParams,
56 Hover,
57 TEXT_DOCUMENT_CODE_ACTION,
58 SemanticTokens,
59 SemanticTokensParams,
60 FoldingRangeParams,
61 FoldingRange,
62 WillSaveTextDocumentParams,
63 TextEdit,
64 DocumentFormattingParams,
65)
66from debputy.util import detect_possible_typo
68try:
69 from debputy.lsp.vendoring._deb822_repro.locatable import (
70 Position as TEPosition,
71 Range as TERange,
72 )
74 from pygls.server import LanguageServer
75 from pygls.workspace import TextDocument
76except ImportError:
77 pass
80_CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]")
82_DISPATCH_RULE = LanguageDispatchRule.new_rule(
83 "debian/copyright",
84 "debian/copyright",
85 [
86 # emacs's name
87 SecondaryLanguage("debian-copyright"),
88 # vim's name
89 SecondaryLanguage("debcopyright"),
90 ],
91)
93_DEP5_FILE_METADATA = Dep5FileMetadata()
95lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION)
98@lsp_hover(_DISPATCH_RULE)
99def _debian_copyright_hover(
100 ls: "DebputyLanguageServer",
101 params: HoverParams,
102) -> Optional[Hover]:
103 return deb822_hover(ls, params, _DEP5_FILE_METADATA)
106@lsp_completer(_DISPATCH_RULE)
107def _debian_copyright_completions(
108 ls: "DebputyLanguageServer",
109 params: CompletionParams,
110) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
111 return deb822_completer(ls, params, _DEP5_FILE_METADATA)
114@lsp_folding_ranges(_DISPATCH_RULE)
115def _debian_copyright_folding_ranges(
116 ls: "DebputyLanguageServer",
117 params: FoldingRangeParams,
118) -> Optional[Sequence[FoldingRange]]:
119 return deb822_folding_ranges(ls, params, _DEP5_FILE_METADATA)
122def _scan_for_syntax_errors_and_token_level_diagnostics(
123 deb822_file: Deb822FileElement,
124 lint_state: LintState,
125) -> int:
126 first_error = len(lint_state.lines) + 1
127 spell_checker = lint_state.spellchecker()
128 for (
129 token,
130 start_line,
131 start_offset,
132 end_line,
133 end_offset,
134 ) in deb822_token_iter(deb822_file.iter_tokens()):
135 if token.is_error: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true
136 first_error = min(first_error, start_line)
137 start_pos = TEPosition(
138 start_line,
139 start_offset,
140 )
141 end_pos = TEPosition(
142 end_line,
143 end_offset,
144 )
145 token_range = TERange.between(start_pos, end_pos)
146 lint_state.emit_diagnostic(
147 token_range,
148 "Syntax error",
149 "error",
150 "debputy",
151 )
152 elif token.is_comment: 152 ↛ 153line 152 didn't jump to line 153 because the condition on line 152 was never true
153 for word, col_pos, end_col_pos in spell_checker.iter_words(token.text):
154 corrections = spell_checker.provide_corrections_for(word)
155 if not corrections:
156 continue
157 start_pos = TEPosition(
158 start_line,
159 col_pos,
160 )
161 end_pos = TEPosition(
162 start_line,
163 end_col_pos,
164 )
165 word_range = TERange.between(start_pos, end_pos)
166 lint_state.emit_diagnostic(
167 word_range,
168 f'Spelling "{word}"',
169 "spelling",
170 "debputy",
171 quickfixes=[propose_correct_text_quick_fix(c) for c in corrections],
172 enable_non_interactive_auto_fix=False,
173 )
174 return first_error
177def _looks_like_a_dep5_file(
178 deb822_file: Deb822FileElement,
179 stanzas: List[Deb822ParagraphElement],
180) -> bool:
181 if not stanzas or "Format" not in stanzas[0]:
182 # No parseable stanzas or the first one did not have a Format, which is necessary.
183 return False
185 for part in deb822_file.iter_parts(): 185 ↛ 191line 185 didn't jump to line 191 because the loop on line 185 didn't complete
186 if part.is_error:
187 # Error first, then it might just be a "Format:" in the middle of a free-text file.
188 return False
189 if isinstance(part, Deb822ParagraphElement): 189 ↛ 185line 189 didn't jump to line 185 because the condition on line 189 was always true
190 break
191 return True
194@lint_diagnostics(_DISPATCH_RULE)
195def _lint_debian_copyright(lint_state: LintState) -> None:
196 doc_reference = lint_state.doc_uri
197 deb822_file = lint_state.parsed_deb822_file_content
198 stanzas = list(deb822_file)
200 if not _looks_like_a_dep5_file(deb822_file, stanzas):
201 return
203 first_error = _scan_for_syntax_errors_and_token_level_diagnostics(
204 deb822_file,
205 lint_state,
206 )
207 header_stanza, files_stanza, _ = _DEP5_FILE_METADATA.stanza_types()
209 for paragraph_no, paragraph in enumerate(stanzas, start=1):
210 paragraph_pos = paragraph.position_in_file()
211 if paragraph_pos.line_position >= first_error: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 break
213 is_files_or_license_stanza = paragraph_no != 1
214 if is_files_or_license_stanza:
215 stanza_metadata = _DEP5_FILE_METADATA.classify_stanza(
216 paragraph,
217 paragraph_no,
218 )
219 other_stanza_metadata = header_stanza
220 other_stanza_name = "Header"
221 elif "Format" in paragraph: 221 ↛ 227line 221 didn't jump to line 227 because the condition on line 221 was always true
222 is_dep5 = True
223 stanza_metadata = header_stanza
224 other_stanza_metadata = files_stanza
225 other_stanza_name = "Files/License"
226 else:
227 break
229 stanza_metadata.stanza_diagnostics(
230 deb822_file,
231 paragraph,
232 paragraph_pos,
233 doc_reference,
234 lint_state,
235 confusable_with_stanza_name=other_stanza_name,
236 confusable_with_stanza_metadata=other_stanza_metadata,
237 )
240@lsp_will_save_wait_until(_DISPATCH_RULE)
241def _debian_copyright_on_save_formatting(
242 ls: "DebputyLanguageServer",
243 params: WillSaveTextDocumentParams,
244) -> Optional[Sequence[TextEdit]]:
245 doc = ls.workspace.get_text_document(params.text_document.uri)
246 lint_state = ls.lint_state(doc)
247 return deb822_format_file(lint_state, _DEP5_FILE_METADATA)
250def _reformat_debian_copyright(
251 lint_state: LintState,
252) -> Optional[Sequence[TextEdit]]:
253 return deb822_format_file(lint_state, _DEP5_FILE_METADATA)
256@lsp_format_document(_DISPATCH_RULE)
257def _debian_copyright_on_save_formatting(
258 ls: "DebputyLanguageServer",
259 params: DocumentFormattingParams,
260) -> Optional[Sequence[TextEdit]]:
261 doc = ls.workspace.get_text_document(params.text_document.uri)
262 lint_state = ls.lint_state(doc)
263 return deb822_format_file(lint_state, _DEP5_FILE_METADATA)
266@lsp_semantic_tokens_full(_DISPATCH_RULE)
267def _debian_copyright_semantic_tokens_full(
268 ls: "DebputyLanguageServer",
269 request: SemanticTokensParams,
270) -> Optional[SemanticTokens]:
271 return deb822_semantic_tokens_full(
272 ls,
273 request,
274 _DEP5_FILE_METADATA,
275 )