Coverage for src/debputy/lsp/languages/lsp_debian_control.py: 63%
423 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-09-07 09:27 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-09-07 09:27 +0000
1import dataclasses
2import importlib.resources
3import os.path
4import textwrap
5from functools import lru_cache
6from itertools import chain
7from typing import (
8 Union,
9 Sequence,
10 Tuple,
11 Optional,
12 Mapping,
13 List,
14 Iterable,
15 Self,
16 TYPE_CHECKING,
17)
19import debputy.lsp.data.deb822_data as deb822_ref_data_dir
20from debputy.analysis.analysis_util import flatten_ppfs
21from debputy.analysis.debian_dir import resolve_debhelper_config_files
22from debputy.dh.dh_assistant import extract_dh_compat_level
23from debputy.linting.lint_util import (
24 LintState,
25 te_range_to_lsp,
26 te_position_to_lsp,
27 with_range_in_continuous_parts,
28)
29from debputy.lsp.apt_cache import PackageLookup
30from debputy.lsp.debputy_ls import DebputyLanguageServer
31from debputy.lsp.lsp_debian_control_reference_data import (
32 DctrlKnownField,
33 DctrlFileMetadata,
34 package_name_to_section,
35 all_package_relationship_fields,
36 extract_first_value_and_position,
37 all_source_relationship_fields,
38 StanzaMetadata,
39 SUBSTVAR_RE,
40)
41from debputy.lsp.lsp_features import (
42 lint_diagnostics,
43 lsp_completer,
44 lsp_hover,
45 lsp_standard_handler,
46 lsp_folding_ranges,
47 lsp_semantic_tokens_full,
48 lsp_will_save_wait_until,
49 lsp_format_document,
50 lsp_text_doc_inlay_hints,
51 LanguageDispatchRule,
52 SecondaryLanguage,
53 lsp_cli_reformat_document,
54)
55from debputy.lsp.lsp_generic_deb822 import (
56 deb822_completer,
57 deb822_hover,
58 deb822_folding_ranges,
59 deb822_semantic_tokens_full,
60 deb822_format_file,
61 scan_for_syntax_errors_and_token_level_diagnostics,
62)
63from debputy.lsp.lsp_reference_keyword import LSP_DATA_DOMAIN
64from debputy.lsp.quickfixes import (
65 propose_correct_text_quick_fix,
66 propose_insert_text_on_line_after_diagnostic_quick_fix,
67 propose_remove_range_quick_fix,
68)
69from debputy.lsp.ref_models.deb822_reference_parse_models import (
70 DCTRL_SUBSTVARS_REFERENCE_DATA_PARSER,
71 DCtrlSubstvar,
72)
73from debputy.lsp.text_util import markdown_urlify
74from debputy.lsp.vendoring._deb822_repro import (
75 Deb822ParagraphElement,
76)
77from debputy.lsp.vendoring._deb822_repro.parsing import (
78 Deb822KeyValuePairElement,
79)
80from debputy.lsprotocol.types import (
81 Position,
82 FoldingRange,
83 FoldingRangeParams,
84 CompletionItem,
85 CompletionList,
86 CompletionParams,
87 HoverParams,
88 Hover,
89 TEXT_DOCUMENT_CODE_ACTION,
90 SemanticTokens,
91 SemanticTokensParams,
92 WillSaveTextDocumentParams,
93 TextEdit,
94 DocumentFormattingParams,
95 InlayHint,
96)
97from debputy.manifest_parser.util import AttributePath
98from debputy.packager_provided_files import (
99 PackagerProvidedFile,
100 detect_all_packager_provided_files,
101)
102from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin
103from debputy.util import PKGNAME_REGEX, _info, _trace_log, _is_trace_log_enabled
104from debputy.yaml import MANIFEST_YAML
106if TYPE_CHECKING:
107 import lsprotocol.types as types
108else:
109 import debputy.lsprotocol.types as types
111try:
112 from debputy.lsp.vendoring._deb822_repro.locatable import (
113 Position as TEPosition,
114 Range as TERange,
115 START_POSITION,
116 )
118 from pygls.workspace import TextDocument
119except ImportError:
120 pass
123_DISPATCH_RULE = LanguageDispatchRule.new_rule(
124 "debian/control",
125 None,
126 "debian/control",
127 [
128 # emacs's name
129 SecondaryLanguage("debian-control"),
130 # vim's name
131 SecondaryLanguage("debcontrol"),
132 ],
133)
136@dataclasses.dataclass(slots=True, frozen=True)
137class SubstvarMetadata:
138 name: str
139 defined_by: str
140 dh_sequence: Optional[str]
141 doc_uris: Sequence[str]
142 synopsis: str
143 description: str
145 def render_metadata_fields(self) -> str:
146 def_by = f"Defined by: {self.defined_by}"
147 dh_seq = (
148 f"DH Sequence: {self.dh_sequence}" if self.dh_sequence is not None else None
149 )
150 doc_uris = self.doc_uris
151 parts = [def_by, dh_seq]
152 if doc_uris: 152 ↛ 158line 152 didn't jump to line 158 because the condition on line 152 was always true
153 if len(doc_uris) == 1: 153 ↛ 156line 153 didn't jump to line 156 because the condition on line 153 was always true
154 parts.append(f"Documentation: {markdown_urlify(doc_uris[0])}")
155 else:
156 parts.append("Documentation:")
157 parts.extend(f" - {markdown_urlify(uri)}" for uri in doc_uris)
158 return "\n".join(parts)
160 @classmethod
161 def from_ref_data(cls, x: DCtrlSubstvar) -> "Self":
162 doc = x.get("documentation", {})
163 return cls(
164 x["name"],
165 x["defined_by"],
166 x.get("dh_sequence"),
167 doc.get("uris", []),
168 doc.get("synopsis", ""),
169 doc.get("long_description", ""),
170 )
173def relationship_substvar_for_field(substvar: str) -> Optional[str]:
174 relationship_fields = all_package_relationship_fields()
175 try:
176 col_idx = substvar.rindex(":")
177 except ValueError:
178 return None
179 return relationship_fields.get(substvar[col_idx + 1 : -1].lower())
182def _as_substvars_metadata(
183 args: List[SubstvarMetadata],
184) -> Mapping[str, SubstvarMetadata]:
185 r = {s.name: s for s in args}
186 assert len(r) == len(args)
187 return r
190def dctrl_variables_metadata_basename() -> str:
191 return "debian_control_variables_data.yaml"
194@lru_cache
195def dctrl_substvars_metadata() -> Mapping[str, SubstvarMetadata]:
196 p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath(
197 dctrl_variables_metadata_basename()
198 )
200 with p.open("r", encoding="utf-8") as fd:
201 raw = MANIFEST_YAML.load(fd)
203 attr_path = AttributePath.root_path(p)
204 ref = DCTRL_SUBSTVARS_REFERENCE_DATA_PARSER.parse_input(raw, attr_path)
205 return _as_substvars_metadata(
206 [SubstvarMetadata.from_ref_data(x) for x in ref["variables"]]
207 )
210_DCTRL_FILE_METADATA = DctrlFileMetadata()
213lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION)
216@lsp_hover(_DISPATCH_RULE)
217def _debian_control_hover(
218 ls: "DebputyLanguageServer",
219 params: HoverParams,
220) -> Optional[Hover]:
221 return deb822_hover(ls, params, _DCTRL_FILE_METADATA, custom_handler=_custom_hover)
224def _custom_hover_description(
225 _ls: "DebputyLanguageServer",
226 _known_field: DctrlKnownField,
227 line: str,
228 _word_at_position: str,
229) -> Optional[Union[Hover, str]]:
230 if line[0].isspace(): 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true
231 return None
232 try:
233 col_idx = line.index(":")
234 except ValueError:
235 return None
237 content = line[col_idx + 1 :].strip()
239 # Synopsis
240 return textwrap.dedent(
241 f"""\
242 # Package synopsis
244 The synopsis functions as a phrase describing the package, not a
245 complete sentence, so sentential punctuation is inappropriate: it
246 does not need extra capital letters or a final period (full stop).
247 It should also omit any initial indefinite or definite article
248 - "a", "an", or "the". Thus for instance:
250 ```
251 Package: libeg0
252 Description: exemplification support library
253 ```
255 Technically this is a noun phrase minus articles, as opposed to a
256 verb phrase. A good heuristic is that it should be possible to
257 substitute the package name and synopsis into this formula:
259 ```
260 # Generic
261 The package provides { a,an,the,some} synopsis.
263 # The current package for comparison
264 The package provides { a,an,the,some} {content}.
265 ```
267 Other advice for writing synopsis:
268 * Avoid using the package name. Any software would display the
269 package name already and it generally does not help the user
270 understand what they are looking at.
271 * In many situations, the user will only see the package name
272 and its synopsis. The synopsis must be able to stand alone.
274 **Example renderings in various terminal UIs**:
275 ```
276 # apt search TERM
277 package/stable,now 1.0-1 all:
278 {content}
280 # apt-get search TERM
281 package - {content}
282 ```
284 ## Reference example
286 An reference example for comparison: The Sphinx package
287 (python3-sphinx/7.2.6-6) had the following synopsis:
289 ```
290 Description: documentation generator for Python projects
291 ```
293 In the test sentence, it would read as:
295 ```
296 The python3-sphinx package provides a documentation generator for Python projects.
297 ```
299 **Side-by-side comparison in the terminal UIs**:
300 ```
301 # apt search TERM
302 python3-sphinx/stable,now 7.2.6-6 all:
303 documentation generator for Python projects
305 package/stable,now 1.0-1 all:
306 {content}
309 # apt-get search TERM
310 package - {content}
311 python3-sphinx - documentation generator for Python projects
312 ```
313 """
314 )
317def _render_package_lookup(
318 package_lookup: PackageLookup,
319 known_field: DctrlKnownField,
320) -> str:
321 name = package_lookup.name
322 provider = package_lookup.package
323 if package_lookup.package is None and len(package_lookup.provided_by) == 1:
324 provider = package_lookup.provided_by[0]
326 if provider:
327 segments = [
328 f"# {name} ({provider.version}, {provider.architecture}) ",
329 "",
330 ]
332 if (
333 _is_bd_field(known_field)
334 and name.startswith("dh-sequence-")
335 and len(name) > 12
336 ):
337 sequence = name[12:]
338 segments.append(
339 f"This build-dependency will activate the `dh` sequence called `{sequence}`."
340 )
341 segments.append("")
343 elif (
344 known_field.name == "Build-Depends"
345 and name.startswith("debputy-plugin-")
346 and len(name) > 15
347 ):
348 plugin_name = name[15:]
349 segments.append(
350 f"This build-dependency will activate the `debputy` plugin called `{plugin_name}`."
351 )
352 segments.append("")
354 segments.extend(
355 [
356 f"Synopsis: {provider.synopsis}",
357 "",
358 f"Multi-Arch: {provider.multi_arch}",
359 "",
360 f"Section: {provider.section}",
361 ]
362 )
363 if provider.upstream_homepage is not None:
364 segments.append("")
365 segments.append(f"Upstream homepage: {provider.upstream_homepage}")
366 segments.append("")
367 segments.append(
368 "Data is from the system's APT cache, which may not match the target distribution."
369 )
370 return "\n".join(segments)
372 segments = [
373 f"# {name} [virtual]",
374 "",
375 "The package {name} is a virtual package provided by one of:",
376 ]
377 segments.extend(f" * {p.name}" for p in package_lookup.provided_by)
378 segments.append("")
379 segments.append(
380 "Data is from the system's APT cache, which may not match the target distribution."
381 )
382 return "\n".join(segments)
385def _disclaimer(is_empty: bool) -> str:
386 if is_empty:
387 return textwrap.dedent(
388 """\
389 The system's APT cache is empty, so it was not possible to verify that the
390 package exist.
391"""
392 )
393 return textwrap.dedent(
394 """\
395 The package is not known by the APT cache on this system, so there may be typo
396 or the package may not be available in the version of your distribution.
397"""
398 )
401def _render_package_by_name(
402 name: str, known_field: DctrlKnownField, is_empty: bool
403) -> Optional[str]:
404 if _is_bd_field(known_field) and name.startswith("dh-sequence-") and len(name) > 12:
405 sequence = name[12:]
406 return (
407 textwrap.dedent(
408 f"""\
409 # {name}
411 This build-dependency will activate the `dh` sequence called `{sequence}`.
413 """
414 )
415 + _disclaimer(is_empty)
416 )
417 if (
418 known_field.name == "Build-Depends"
419 and name.startswith("debputy-plugin-")
420 and len(name) > 15
421 ):
422 plugin_name = name[15:]
423 return (
424 textwrap.dedent(
425 f"""\
426 # {name}
428 This build-dependency will activate the `debputy` plugin called `{plugin_name}`.
430 """
431 )
432 + _disclaimer(is_empty)
433 )
434 return (
435 textwrap.dedent(
436 f"""\
437 # {name}
439 """
440 )
441 + _disclaimer(is_empty)
442 )
445def _is_bd_field(known_field: DctrlKnownField) -> bool:
446 return known_field.name in (
447 "Build-Depends",
448 "Build-Depends-Arch",
449 "Build-Depends-Indep",
450 )
453def _custom_hover_relationship_field(
454 ls: "DebputyLanguageServer",
455 known_field: DctrlKnownField,
456 _line: str,
457 word_at_position: str,
458) -> Optional[Union[Hover, str]]:
459 apt_cache = ls.apt_cache
460 state = apt_cache.state
461 is_empty = False
462 _info(f"Rel field: {known_field.name} - {word_at_position} - {state}")
463 if "|" in word_at_position: 463 ↛ 464line 463 didn't jump to line 464 because the condition on line 463 was never true
464 return textwrap.dedent(
465 f"""\
466 Sorry, no hover docs for OR relations at the moment.
468 The relation being matched: `{word_at_position}`
470 The code is missing logic to determine which side of the OR the lookup is happening.
471 """
472 )
473 match = next(iter(PKGNAME_REGEX.finditer(word_at_position)), None)
474 if match is None: 474 ↛ 475line 474 didn't jump to line 475 because the condition on line 474 was never true
475 return
476 package = match.group()
477 if state == "empty-cache": 477 ↛ 478line 477 didn't jump to line 478 because the condition on line 477 was never true
478 state = "loaded"
479 is_empty = True
480 if state == "loaded": 480 ↛ 481line 480 didn't jump to line 481 because the condition on line 480 was never true
481 result = apt_cache.lookup(package)
482 if result is None:
483 return _render_package_by_name(
484 package,
485 known_field,
486 is_empty=is_empty,
487 )
488 return _render_package_lookup(result, known_field)
490 if state in ( 490 ↛ 504line 490 didn't jump to line 504 because the condition on line 490 was always true
491 "not-loaded",
492 "failed",
493 "tooling-not-available",
494 ):
495 details = apt_cache.load_error if apt_cache.load_error else "N/A"
496 return textwrap.dedent(
497 f"""\
498 Sorry, the APT cache data is not available due to an error or missing tool.
500 Details: {details}
501 """
502 )
504 if state == "empty-cache":
505 return f"Cannot lookup {package}: APT cache data was empty"
507 if state == "loading":
508 return f"Cannot lookup {package}: APT cache data is still being indexed. Please try again in a moment."
509 return None
512_CUSTOM_FIELD_HOVER = {
513 field: _custom_hover_relationship_field
514 for field in chain(
515 all_package_relationship_fields().values(),
516 all_source_relationship_fields().values(),
517 )
518 if field != "Provides"
519}
521_CUSTOM_FIELD_HOVER["Description"] = _custom_hover_description
524def _custom_hover(
525 ls: "DebputyLanguageServer",
526 server_position: Position,
527 _current_field: Optional[str],
528 word_at_position: str,
529 known_field: Optional[DctrlKnownField],
530 in_value: bool,
531 _doc: "TextDocument",
532 lines: List[str],
533) -> Optional[Union[Hover, str]]:
534 if not in_value:
535 return None
537 line_no = server_position.line
538 line = lines[line_no]
539 substvar_search_ref = server_position.character
540 substvar = ""
541 try:
542 if line and line[substvar_search_ref] in ("$", "{"):
543 substvar_search_ref += 2
544 substvar_start = line.rindex("${", 0, substvar_search_ref)
545 substvar_end = line.index("}", substvar_start)
546 if server_position.character <= substvar_end:
547 substvar = line[substvar_start : substvar_end + 1]
548 except (ValueError, IndexError):
549 pass
551 if substvar == "${}" or SUBSTVAR_RE.fullmatch(substvar):
552 substvar_md = dctrl_substvars_metadata().get(substvar)
554 computed_doc = ""
555 for_field = relationship_substvar_for_field(substvar)
556 if for_field: 556 ↛ 558line 556 didn't jump to line 558 because the condition on line 556 was never true
557 # Leading empty line is intentional!
558 computed_doc = textwrap.dedent(
559 f"""
560 This substvar is a relationship substvar for the field {for_field}.
561 Relationship substvars are automatically added in the field they
562 are named after in `debhelper-compat (= 14)` or later, or with
563 `debputy` (any integration mode after 0.1.21).
564 """
565 )
567 if substvar_md is None: 567 ↛ 568line 567 didn't jump to line 568 because the condition on line 567 was never true
568 doc = f"No documentation for {substvar}.\n"
569 md_fields = ""
570 else:
571 doc = ls.translation(LSP_DATA_DOMAIN).pgettext(
572 f"Variable:{substvar_md.name}",
573 substvar_md.description,
574 )
575 md_fields = "\n" + substvar_md.render_metadata_fields()
576 return f"# Substvar `{substvar}`\n\n{doc}{computed_doc}{md_fields}"
578 if known_field is None: 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true
579 return None
580 dispatch = _CUSTOM_FIELD_HOVER.get(known_field.name)
581 if dispatch is None: 581 ↛ 582line 581 didn't jump to line 582 because the condition on line 581 was never true
582 return None
583 return dispatch(ls, known_field, line, word_at_position)
586@lsp_completer(_DISPATCH_RULE)
587def _debian_control_completions(
588 ls: "DebputyLanguageServer",
589 params: CompletionParams,
590) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
591 return deb822_completer(ls, params, _DCTRL_FILE_METADATA)
594@lsp_folding_ranges(_DISPATCH_RULE)
595def _debian_control_folding_ranges(
596 ls: "DebputyLanguageServer",
597 params: FoldingRangeParams,
598) -> Optional[Sequence[FoldingRange]]:
599 return deb822_folding_ranges(ls, params, _DCTRL_FILE_METADATA)
602@lsp_text_doc_inlay_hints(_DISPATCH_RULE)
603async def _doc_inlay_hint(
604 ls: "DebputyLanguageServer",
605 params: types.InlayHintParams,
606) -> Optional[List[InlayHint]]:
607 doc = ls.workspace.get_text_document(params.text_document.uri)
608 lint_state = ls.lint_state(doc)
609 deb822_file = lint_state.parsed_deb822_file_content
610 if not deb822_file:
611 return None
612 inlay_hints = []
613 stanzas = list(deb822_file)
614 if len(stanzas) < 2:
615 return None
616 source_stanza = stanzas[0]
617 source_stanza_pos = source_stanza.position_in_file()
618 inherited_inlay_label_part = {}
619 stanza_no = 0
621 async for stanza_range, stanza in lint_state.slow_iter(
622 with_range_in_continuous_parts(deb822_file.iter_parts())
623 ):
624 if not isinstance(stanza, Deb822ParagraphElement):
625 continue
626 stanza_def = _DCTRL_FILE_METADATA.classify_stanza(stanza, stanza_no)
627 stanza_no += 1
628 pkg_kvpair = stanza.get_kvpair_element(("Package", 0), use_get=True)
629 if pkg_kvpair is None:
630 continue
632 parts = []
633 async for known_field in ls.slow_iter(
634 stanza_def.stanza_fields.values(), yield_every=25
635 ):
636 if (
637 not known_field.inheritable_from_other_stanza
638 or not known_field.show_as_inherited
639 or known_field.name in stanza
640 ):
641 continue
643 inherited_value = source_stanza.get(known_field.name)
644 if inherited_value is not None:
645 inlay_hint_label_part = inherited_inlay_label_part.get(known_field.name)
646 if inlay_hint_label_part is None:
647 kvpair = source_stanza.get_kvpair_element(known_field.name)
648 value_range_te = kvpair.range_in_parent().relative_to(
649 source_stanza_pos
650 )
651 value_range = doc.position_codec.range_to_client_units(
652 lint_state.lines,
653 te_range_to_lsp(value_range_te),
654 )
655 inlay_hint_label_part = types.InlayHintLabelPart(
656 f" ({known_field.name}: {inherited_value})",
657 tooltip="Inherited from Source stanza",
658 location=types.Location(
659 params.text_document.uri,
660 value_range,
661 ),
662 )
663 inherited_inlay_label_part[known_field.name] = inlay_hint_label_part
664 parts.append(inlay_hint_label_part)
666 if parts:
667 known_field = stanza_def["Package"]
668 values = known_field.field_value_class.interpreter().interpret(pkg_kvpair)
669 assert values is not None
670 anchor_value = list(values.iter_value_references())[-1]
671 anchor_position = (
672 anchor_value.locatable.range_in_parent().end_pos.relative_to(
673 pkg_kvpair.value_element.position_in_parent().relative_to(
674 stanza_range.start_pos
675 )
676 )
677 )
678 anchor_position_client_units = doc.position_codec.position_to_client_units(
679 lint_state.lines,
680 te_position_to_lsp(anchor_position),
681 )
682 inlay_hints.append(
683 types.InlayHint(
684 anchor_position_client_units,
685 parts,
686 padding_left=True,
687 padding_right=False,
688 )
689 )
690 return inlay_hints
693def _source_package_checks(
694 stanza: Deb822ParagraphElement,
695 stanza_position: "TEPosition",
696 stanza_metadata: StanzaMetadata[DctrlKnownField],
697 lint_state: LintState,
698) -> None:
699 vcs_fields = {}
700 source_fields = _DCTRL_FILE_METADATA["Source"].stanza_fields
701 for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement):
702 name = stanza_metadata.normalize_field_name(kvpair.field_name.lower())
703 if (
704 not name.startswith("vcs-")
705 or name == "vcs-browser"
706 or name not in source_fields
707 ):
708 continue
709 vcs_fields[name] = kvpair
711 if len(vcs_fields) < 2:
712 return
713 for kvpair in vcs_fields.values():
714 lint_state.emit_diagnostic(
715 kvpair.range_in_parent().relative_to(stanza_position),
716 f'Multiple Version Control fields defined ("{kvpair.field_name}")',
717 "warning",
718 "Policy 5.6.26",
719 quickfixes=[
720 propose_remove_range_quick_fix(
721 proposed_title=f'Remove "{kvpair.field_name}"'
722 )
723 ],
724 )
727def _binary_package_checks(
728 stanza: Deb822ParagraphElement,
729 stanza_position: "TEPosition",
730 source_stanza: Deb822ParagraphElement,
731 representation_field_range: "TERange",
732 lint_state: LintState,
733) -> None:
734 package_name = stanza.get("Package", "")
735 source_section = source_stanza.get("Section")
736 section_kvpair = stanza.get_kvpair_element(("Section", 0), use_get=True)
737 section: Optional[str] = None
738 section_range: Optional["TERange"] = None
739 if section_kvpair is not None:
740 section, section_range = extract_first_value_and_position(
741 section_kvpair,
742 stanza_position,
743 )
745 if section_range is None:
746 section_range = representation_field_range
747 effective_section = section or source_section or "unknown"
748 package_type = stanza.get("Package-Type", "")
749 component_prefix = ""
750 if "/" in effective_section:
751 component_prefix, effective_section = effective_section.split("/", maxsplit=1)
752 component_prefix += "/"
754 if package_name.endswith("-udeb") or package_type == "udeb":
755 if package_type != "udeb": 755 ↛ 756line 755 didn't jump to line 756 because the condition on line 755 was never true
756 package_type_kvpair = stanza.get_kvpair_element(
757 "Package-Type", use_get=True
758 )
759 package_type_range: Optional["TERange"] = None
760 if package_type_kvpair is not None:
761 _, package_type_range = extract_first_value_and_position(
762 package_type_kvpair,
763 stanza_position,
764 )
765 if package_type_range is None:
766 package_type_range = representation_field_range
767 lint_state.emit_diagnostic(
768 package_type_range,
769 'The Package-Type should be "udeb" given the package name',
770 "warning",
771 "debputy",
772 )
773 guessed_section = "debian-installer"
774 section_diagnostic_rationale = " since it is an udeb"
775 else:
776 guessed_section = package_name_to_section(package_name)
777 section_diagnostic_rationale = " based on the package name"
778 if guessed_section is not None and guessed_section != effective_section: 778 ↛ 779line 778 didn't jump to line 779 because the condition on line 778 was never true
779 if section is not None:
780 quickfix_data = [
781 propose_correct_text_quick_fix(f"{component_prefix}{guessed_section}")
782 ]
783 else:
784 quickfix_data = [
785 propose_insert_text_on_line_after_diagnostic_quick_fix(
786 f"Section: {component_prefix}{guessed_section}\n"
787 )
788 ]
789 assert section_range is not None # mypy hint
790 lint_state.emit_diagnostic(
791 section_range,
792 f'The Section should be "{component_prefix}{guessed_section}"{section_diagnostic_rationale}',
793 "warning",
794 "debputy",
795 quickfixes=quickfix_data,
796 )
799@lint_diagnostics(_DISPATCH_RULE)
800async def _lint_debian_control(lint_state: LintState) -> None:
801 deb822_file = lint_state.parsed_deb822_file_content
803 if not _DCTRL_FILE_METADATA.file_metadata_applies_to_file(deb822_file): 803 ↛ 804line 803 didn't jump to line 804 because the condition on line 803 was never true
804 return
806 first_error = await scan_for_syntax_errors_and_token_level_diagnostics(
807 deb822_file,
808 lint_state,
809 )
811 stanzas = list(deb822_file)
812 source_stanza = stanzas[0] if stanzas else None
813 binary_stanzas_w_pos = []
815 source_stanza_metadata, binary_stanza_metadata = _DCTRL_FILE_METADATA.stanza_types()
816 stanza_no = 0
818 async for stanza_range, stanza in lint_state.slow_iter(
819 with_range_in_continuous_parts(deb822_file.iter_parts())
820 ):
821 if not isinstance(stanza, Deb822ParagraphElement):
822 continue
823 stanza_position = stanza_range.start_pos
824 if stanza_position.line_position >= first_error: 824 ↛ 825line 824 didn't jump to line 825 because the condition on line 824 was never true
825 break
826 stanza_no += 1
827 is_binary_stanza = stanza_no != 1
828 if is_binary_stanza:
829 stanza_metadata = binary_stanza_metadata
830 other_stanza_metadata = source_stanza_metadata
831 other_stanza_name = "Source"
832 binary_stanzas_w_pos.append((stanza, stanza_position))
833 _, representation_field_range = stanza_metadata.stanza_representation(
834 stanza, stanza_position
835 )
836 _binary_package_checks(
837 stanza,
838 stanza_position,
839 source_stanza,
840 representation_field_range,
841 lint_state,
842 )
843 else:
844 stanza_metadata = source_stanza_metadata
845 other_stanza_metadata = binary_stanza_metadata
846 other_stanza_name = "Binary"
847 _source_package_checks(
848 stanza,
849 stanza_position,
850 stanza_metadata,
851 lint_state,
852 )
854 await stanza_metadata.stanza_diagnostics(
855 deb822_file,
856 stanza,
857 stanza_position,
858 lint_state,
859 confusable_with_stanza_metadata=other_stanza_metadata,
860 confusable_with_stanza_name=other_stanza_name,
861 inherit_from_stanza=source_stanza if is_binary_stanza else None,
862 )
864 _detect_misspelled_packaging_files(
865 lint_state,
866 binary_stanzas_w_pos,
867 )
870def _package_range_of_stanza(
871 binary_stanzas: List[Tuple[Deb822ParagraphElement, TEPosition]],
872) -> Iterable[Tuple[str, Optional[str], "TERange"]]:
873 for stanza, stanza_position in binary_stanzas:
874 kvpair = stanza.get_kvpair_element(("Package", 0), use_get=True)
875 if kvpair is None: 875 ↛ 876line 875 didn't jump to line 876 because the condition on line 875 was never true
876 continue
877 representation_field_range = kvpair.range_in_parent().relative_to(
878 stanza_position
879 )
880 yield stanza["Package"], stanza.get("Architecture"), representation_field_range
883def _packaging_files(
884 lint_state: LintState,
885) -> Iterable[PackagerProvidedFile]:
886 source_root = lint_state.source_root
887 debian_dir = lint_state.debian_dir
888 binary_packages = lint_state.binary_packages
889 if (
890 source_root is None
891 or not source_root.has_fs_path
892 or debian_dir is None
893 or binary_packages is None
894 ):
895 return
897 debputy_integration_mode = lint_state.debputy_metadata.debputy_integration_mode
898 dh_sequencer_data = lint_state.dh_sequencer_data
899 dh_sequences = dh_sequencer_data.sequences
900 is_debputy_package = debputy_integration_mode is not None
901 feature_set = lint_state.plugin_feature_set
902 known_packaging_files = feature_set.known_packaging_files
903 static_packaging_files = {
904 kpf.detection_value: kpf
905 for kpf in known_packaging_files.values()
906 if kpf.detection_method == "path"
907 }
908 ignored_path = set(static_packaging_files)
910 if is_debputy_package:
911 all_debputy_ppfs = list(
912 flatten_ppfs(
913 detect_all_packager_provided_files(
914 feature_set,
915 debian_dir,
916 binary_packages,
917 allow_fuzzy_matches=True,
918 detect_typos=True,
919 ignore_paths=ignored_path,
920 )
921 )
922 )
923 for ppf in all_debputy_ppfs:
924 if ppf.path.path in ignored_path: 924 ↛ 925line 924 didn't jump to line 925 because the condition on line 924 was never true
925 continue
926 ignored_path.add(ppf.path.path)
927 yield ppf
929 # FIXME: This should read the editor data, but dh_assistant does not support that.
930 dh_compat_level, _ = extract_dh_compat_level(cwd=source_root.fs_path)
931 if dh_compat_level is not None: 931 ↛ exitline 931 didn't return from function '_packaging_files' because the condition on line 931 was always true
932 debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin()
933 (
934 all_dh_ppfs,
935 _,
936 _,
937 _,
938 ) = resolve_debhelper_config_files(
939 debian_dir,
940 binary_packages,
941 debputy_plugin_metadata,
942 feature_set,
943 dh_sequences,
944 dh_compat_level,
945 saw_dh=dh_sequencer_data.uses_dh_sequencer,
946 ignore_paths=ignored_path,
947 debputy_integration_mode=debputy_integration_mode,
948 cwd=source_root.fs_path,
949 )
950 for ppf in all_dh_ppfs:
951 if ppf.path.path in ignored_path: 951 ↛ 952line 951 didn't jump to line 952 because the condition on line 951 was never true
952 continue
953 ignored_path.add(ppf.path.path)
954 yield ppf
957def _detect_misspelled_packaging_files(
958 lint_state: LintState,
959 binary_stanzas_w_pos: List[Tuple[Deb822ParagraphElement, TEPosition]],
960) -> None:
961 stanza_ranges = {
962 p: (a, r) for p, a, r in _package_range_of_stanza(binary_stanzas_w_pos)
963 }
964 for ppf in _packaging_files(lint_state):
965 binary_package = ppf.package_name
966 explicit_package = ppf.uses_explicit_package_name
967 name_segment = ppf.name_segment is not None
968 stem = ppf.definition.stem
969 if _is_trace_log_enabled(): 969 ↛ 970line 969 didn't jump to line 970 because the condition on line 969 was never true
970 _trace_log(
971 f"PPF check: {binary_package} {stem=} {explicit_package=} {name_segment=} {ppf.expected_path=} {ppf.definition.has_active_command=}"
972 )
973 if binary_package is None or stem is None: 973 ↛ 974line 973 didn't jump to line 974 because the condition on line 973 was never true
974 continue
975 res = stanza_ranges.get(binary_package)
976 if res is None: 976 ↛ 977line 976 didn't jump to line 977 because the condition on line 976 was never true
977 continue
978 declared_arch, diag_range = res
979 if diag_range is None: 979 ↛ 980line 979 didn't jump to line 980 because the condition on line 979 was never true
980 continue
981 path = ppf.path.path
982 likely_typo_of = ppf.expected_path
983 arch_restriction = ppf.architecture_restriction
984 if likely_typo_of is not None:
985 # Handles arch_restriction == 'all' at the same time due to how
986 # the `likely-typo-of` is created
987 lint_state.emit_diagnostic(
988 diag_range,
989 f'The file "{path}" is likely a typo of "{likely_typo_of}"',
990 "warning",
991 "debputy",
992 diagnostic_applies_to_another_file=path,
993 )
994 continue
995 if declared_arch == "all" and arch_restriction is not None: 995 ↛ 996line 995 didn't jump to line 996 because the condition on line 995 was never true
996 lint_state.emit_diagnostic(
997 diag_range,
998 f'The file "{path}" has an architecture restriction but is for an `arch:all` package, so'
999 f" the restriction does not make sense.",
1000 "warning",
1001 "debputy",
1002 diagnostic_applies_to_another_file=path,
1003 )
1004 elif arch_restriction == "all": 1004 ↛ 1005line 1004 didn't jump to line 1005 because the condition on line 1004 was never true
1005 lint_state.emit_diagnostic(
1006 diag_range,
1007 f'The file "{path}" has an architecture restriction of `all` rather than a real architecture',
1008 "warning",
1009 "debputy",
1010 diagnostic_applies_to_another_file=path,
1011 )
1013 if not ppf.definition.has_active_command: 1013 ↛ 1014line 1013 didn't jump to line 1014 because the condition on line 1013 was never true
1014 lint_state.emit_diagnostic(
1015 diag_range,
1016 f"The file {path} is related to a command that is not active in the dh sequence"
1017 " with the current addons",
1018 "warning",
1019 "debputy",
1020 diagnostic_applies_to_another_file=path,
1021 )
1022 continue
1024 if not explicit_package and name_segment is not None:
1025 basename = os.path.basename(path)
1026 if basename == ppf.definition.stem:
1027 continue
1028 alt_name = f"{binary_package}.{stem}"
1029 if arch_restriction is not None: 1029 ↛ 1030line 1029 didn't jump to line 1030 because the condition on line 1029 was never true
1030 alt_name = f"{alt_name}.{arch_restriction}"
1031 if ppf.definition.allow_name_segment:
1032 or_alt_name = f' (or maybe "debian/{binary_package}.{basename}")'
1033 else:
1034 or_alt_name = ""
1036 lint_state.emit_diagnostic(
1037 diag_range,
1038 f'Possible typo in "{path}". Consider renaming the file to "debian/{alt_name}"'
1039 f"{or_alt_name} if it is intended for {binary_package}",
1040 "warning",
1041 "debputy",
1042 diagnostic_applies_to_another_file=path,
1043 )
1046@lsp_will_save_wait_until(_DISPATCH_RULE)
1047def _debian_control_on_save_formatting(
1048 ls: "DebputyLanguageServer",
1049 params: WillSaveTextDocumentParams,
1050) -> Optional[Sequence[TextEdit]]:
1051 doc = ls.workspace.get_text_document(params.text_document.uri)
1052 lint_state = ls.lint_state(doc)
1053 return _reformat_debian_control(lint_state)
1056@lsp_cli_reformat_document(_DISPATCH_RULE)
1057def _reformat_debian_control(
1058 lint_state: LintState,
1059) -> Optional[Sequence[TextEdit]]:
1060 return deb822_format_file(lint_state, _DCTRL_FILE_METADATA)
1063@lsp_format_document(_DISPATCH_RULE)
1064def _debian_control_format_file(
1065 ls: "DebputyLanguageServer",
1066 params: DocumentFormattingParams,
1067) -> Optional[Sequence[TextEdit]]:
1068 doc = ls.workspace.get_text_document(params.text_document.uri)
1069 lint_state = ls.lint_state(doc)
1070 return _reformat_debian_control(lint_state)
1073@lsp_semantic_tokens_full(_DISPATCH_RULE)
1074async def _debian_control_semantic_tokens_full(
1075 ls: "DebputyLanguageServer",
1076 request: SemanticTokensParams,
1077) -> Optional[SemanticTokens]:
1078 return await deb822_semantic_tokens_full(
1079 ls,
1080 request,
1081 _DCTRL_FILE_METADATA,
1082 )