Coverage for src/debputy/lsp/lsp_debian_control_reference_data.py: 79%
1209 statements
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-24 16:38 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2025-03-24 16:38 +0000
1import collections
2import dataclasses
3import functools
4import importlib.resources
5import itertools
6import operator
7import os.path
8import re
9import textwrap
10from abc import ABC
11from typing import (
12 FrozenSet,
13 Optional,
14 cast,
15 Mapping,
16 Iterable,
17 List,
18 Generic,
19 TypeVar,
20 Union,
21 Callable,
22 Tuple,
23 Any,
24 Set,
25 TYPE_CHECKING,
26 Sequence,
27 Dict,
28 Iterator,
29 Container,
30)
32from debian.debian_support import DpkgArchTable, Version
34import debputy.lsp.data.deb822_data as deb822_ref_data_dir
35from debputy.filesystem_scan import VirtualPathBase
36from debputy.linting.lint_util import LintState
37from debputy.linting.lint_util import te_range_to_lsp
38from debputy.lsp.diagnostics import LintSeverity
39from debputy.lsp.lsp_reference_keyword import (
40 ALL_PUBLIC_NAMED_STYLES,
41 Keyword,
42 allowed_values,
43 format_comp_item_synopsis_doc,
44 LSP_DATA_DOMAIN,
45)
46from debputy.lsp.quickfixes import (
47 propose_correct_text_quick_fix,
48 propose_remove_range_quick_fix,
49)
50from debputy.lsp.ref_models.deb822_reference_parse_models import (
51 Deb822ReferenceData,
52 DEB822_REFERENCE_DATA_PARSER,
53 FieldValueClass,
54 StaticValue,
55 Deb822Field,
56 UsageHint,
57)
58from debputy.lsp.text_edit import apply_text_edits
59from debputy.lsp.text_util import (
60 normalize_dctrl_field_name,
61 LintCapablePositionCodec,
62 trim_end_of_line_whitespace,
63)
64from debputy.lsp.vendoring._deb822_repro.parsing import (
65 Deb822KeyValuePairElement,
66 LIST_SPACE_SEPARATED_INTERPRETATION,
67 Deb822ParagraphElement,
68 Deb822FileElement,
69 Interpretation,
70 parse_deb822_file,
71 Deb822ParsedTokenList,
72 Deb822ValueLineElement,
73)
74from debputy.lsp.vendoring._deb822_repro.tokens import (
75 Deb822FieldNameToken,
76)
77from debputy.lsp.vendoring._deb822_repro.types import FormatterCallback
78from debputy.lsp.vendoring._deb822_repro.types import TE
79from debputy.lsp.vendoring.wrap_and_sort import _sort_packages_key
80from debputy.lsprotocol.types import (
81 DiagnosticTag,
82 Range,
83 TextEdit,
84 Position,
85 CompletionItem,
86 MarkupContent,
87 CompletionItemTag,
88 MarkupKind,
89 CompletionItemKind,
90 CompletionItemLabelDetails,
91)
92from debputy.manifest_parser.util import AttributePath
93from debputy.path_matcher import BasenameGlobMatch
94from debputy.plugin.api import VirtualPath
95from debputy.util import PKGNAME_REGEX, _info, detect_possible_typo, _error
96from debputy.yaml import MANIFEST_YAML
98try:
99 from debputy.lsp.vendoring._deb822_repro.locatable import (
100 Position as TEPosition,
101 Range as TERange,
102 START_POSITION,
103 )
104except ImportError:
105 pass
108if TYPE_CHECKING:
109 from debputy.lsp.maint_prefs import EffectiveFormattingPreference
110 from debputy.lsp.debputy_ls import DebputyLanguageServer
113F = TypeVar("F", bound="Deb822KnownField", covariant=True)
114S = TypeVar("S", bound="StanzaMetadata")
117SUBSTVAR_RE = re.compile(r"[$][{][a-zA-Z0-9][a-zA-Z0-9-:]*[}]")
119_RE_SYNOPSIS_STARTS_WITH_ARTICLE = re.compile(r"^\s*(an?|the)(?:\s|$)", re.I)
120_RE_SV = re.compile(r"(\d+[.]\d+[.]\d+)([.]\d+)?")
121_RE_SYNOPSIS_IS_TEMPLATE = re.compile(
122 r"^\s*(missing|<insert up to \d+ chars description>)$"
123)
124_RE_SYNOPSIS_IS_TOO_SHORT = re.compile(r"^\s*(\S+)$")
125CURRENT_STANDARDS_VERSION = Version("4.7.0")
128CustomFieldCheck = Callable[
129 [
130 "F",
131 Deb822FileElement,
132 Deb822KeyValuePairElement,
133 "TERange",
134 "TERange",
135 Deb822ParagraphElement,
136 "TEPosition",
137 LintState,
138 ],
139 None,
140]
143@functools.lru_cache
144def all_package_relationship_fields() -> Mapping[str, str]:
145 # TODO: Pull from `dpkg-dev` when possible fallback only to the static list.
146 return {
147 f.lower(): f
148 for f in (
149 "Pre-Depends",
150 "Depends",
151 "Recommends",
152 "Suggests",
153 "Enhances",
154 "Conflicts",
155 "Breaks",
156 "Replaces",
157 "Provides",
158 "Built-Using",
159 "Static-Built-Using",
160 )
161 }
164@functools.lru_cache
165def all_source_relationship_fields() -> Mapping[str, str]:
166 # TODO: Pull from `dpkg-dev` when possible fallback only to the static list.
167 return {
168 f.lower(): f
169 for f in (
170 "Build-Depends",
171 "Build-Depends-Arch",
172 "Build-Depends-Indep",
173 "Build-Conflicts",
174 "Build-Conflicts-Arch",
175 "Build-Conflicts-Indep",
176 )
177 }
180ALL_SECTIONS_WITHOUT_COMPONENT = frozenset(
181 [
182 "admin",
183 "cli-mono",
184 "comm",
185 "database",
186 "debian-installer",
187 "debug",
188 "devel",
189 "doc",
190 "editors",
191 "education",
192 "electronics",
193 "embedded",
194 "fonts",
195 "games",
196 "gnome",
197 "gnu-r",
198 "gnustep",
199 "golang",
200 "graphics",
201 "hamradio",
202 "haskell",
203 "httpd",
204 "interpreters",
205 "introspection",
206 "java",
207 "javascript",
208 "kde",
209 "kernel",
210 "libdevel",
211 "libs",
212 "lisp",
213 "localization",
214 "mail",
215 "math",
216 "metapackages",
217 "misc",
218 "net",
219 "news",
220 "ocaml",
221 "oldlibs",
222 "otherosfs",
223 "perl",
224 "php",
225 "python",
226 "ruby",
227 "rust",
228 "science",
229 "shells",
230 "sound",
231 "tasks",
232 "tex",
233 "text",
234 "utils",
235 "vcs",
236 "video",
237 "virtual",
238 "web",
239 "x11",
240 "xfce",
241 "zope",
242 ]
243)
245ALL_COMPONENTS = frozenset(
246 [
247 "main",
248 "restricted", # Ubuntu
249 "non-free",
250 "non-free-firmware",
251 "contrib",
252 ]
253)
256def _fields(*fields: F) -> Mapping[str, F]:
257 return {normalize_dctrl_field_name(f.name.lower()): f for f in fields}
260def _complete_section_sort_hint(
261 keyword: Keyword,
262 _lint_state: LintState,
263 stanza_parts: Sequence[Deb822ParagraphElement],
264 _value_being_completed: str,
265) -> Optional[str]:
266 for stanza in stanza_parts: 266 ↛ 271line 266 didn't jump to line 271 because the loop on line 266 didn't complete
267 pkg = stanza.get("Package")
268 if pkg is not None: 268 ↛ 266line 268 didn't jump to line 266 because the condition on line 268 was always true
269 break
270 else:
271 return None
272 section = package_name_to_section(pkg)
273 value_parts = keyword.value.rsplit("/", 1)
274 keyword_section = value_parts[-1]
275 keyword_component = f" ({value_parts[0]})" if len(value_parts) > 1 else ""
276 if section is None:
277 if keyword_component == "":
278 return keyword_section
279 return f"zz-{keyword_section}{keyword_component}"
280 if keyword_section != section:
281 return f"zz-{keyword_section}{keyword_component}"
282 return f"aa-{keyword_section}{keyword_component}"
285ALL_SECTIONS = allowed_values(
286 *[
287 Keyword(
288 s if c is None else f"{c}/{s}",
289 sort_text=_complete_section_sort_hint,
290 replaced_by=s if c == "main" else None,
291 )
292 for c, s in itertools.product(
293 itertools.chain(cast("Iterable[Optional[str]]", [None]), ALL_COMPONENTS),
294 ALL_SECTIONS_WITHOUT_COMPONENT,
295 )
296 ]
297)
300def all_architectures_and_wildcards(
301 arch2table, *, allow_negations: bool = False
302) -> Iterable[Union[str, Keyword]]:
303 wildcards = set()
304 yield Keyword(
305 "any",
306 is_exclusive=True,
307 synopsis="Built once per machine architecture (native code, such as C/C++, interpreter to C bindings)",
308 long_description=textwrap.dedent(
309 """\
310 This is an architecture-dependent package, and needs to be
311 compiled for each and every architecture.
313 The name `any` refers to the fact that this is an architecture
314 *wildcard* matching *any machine architecture* supported by
315 dpkg.
316 """
317 ),
318 )
319 yield Keyword(
320 "all",
321 is_exclusive=True,
322 synopsis="Independent of machine architecture (scripts, data, documentation, or Java without JNI)",
323 long_description=textwrap.dedent(
324 """\
325 The package is an architecture independent package. This is
326 typically appropriate for packages containing only scripts,
327 data or documentation.
329 The name `all` refers to the fact that the same build of a package
330 can be used for *all* architectures. Though note that it is still
331 subject to the rules of the `Multi-Arch` field.
332 """
333 ),
334 )
335 for arch_name, quad_tuple in arch2table.items():
336 yield arch_name
337 if allow_negations:
338 yield f"!{arch_name}"
339 cpu_wc = "any-" + quad_tuple.cpu_name
340 os_wc = quad_tuple.os_name + "-any"
341 if cpu_wc not in wildcards:
342 yield cpu_wc
343 if allow_negations:
344 yield f"!{cpu_wc}"
345 wildcards.add(cpu_wc)
346 if os_wc not in wildcards:
347 yield os_wc
348 if allow_negations:
349 yield f"!{os_wc}"
350 wildcards.add(os_wc)
351 # Add the remaining wildcards
354@functools.lru_cache
355def dpkg_arch_and_wildcards(*, allow_negations=False) -> FrozenSet[Union[str, Keyword]]:
356 dpkg_arch_table = DpkgArchTable.load_arch_table()
357 return frozenset(
358 all_architectures_and_wildcards(
359 dpkg_arch_table._arch2table,
360 allow_negations=allow_negations,
361 )
362 )
365def extract_first_value_and_position(
366 kvpair: Deb822KeyValuePairElement,
367 stanza_pos: "TEPosition",
368 *,
369 interpretation: Interpretation[
370 Deb822ParsedTokenList[Any, Any]
371 ] = LIST_SPACE_SEPARATED_INTERPRETATION,
372) -> Tuple[Optional[str], Optional[TERange]]:
373 kvpair_pos = kvpair.position_in_parent().relative_to(stanza_pos)
374 value_element_pos = kvpair.value_element.position_in_parent().relative_to(
375 kvpair_pos
376 )
377 for value_ref in kvpair.interpret_as(interpretation).iter_value_references(): 377 ↛ 384line 377 didn't jump to line 384 because the loop on line 377 didn't complete
378 v = value_ref.value
379 section_value_loc = value_ref.locatable
380 value_range_te = section_value_loc.range_in_parent().relative_to(
381 value_element_pos
382 )
383 return v, value_range_te
384 return None, None
387def _sv_field_validation(
388 known_field: "F",
389 _deb822_file: Deb822FileElement,
390 kvpair: Deb822KeyValuePairElement,
391 _kvpair_range: "TERange",
392 _field_name_range_te: "TERange",
393 _stanza: Deb822ParagraphElement,
394 stanza_position: "TEPosition",
395 lint_state: LintState,
396) -> None:
397 sv_value, sv_value_range = extract_first_value_and_position(
398 kvpair,
399 stanza_position,
400 )
401 m = _RE_SV.fullmatch(sv_value)
402 if m is None:
403 lint_state.emit_diagnostic(
404 sv_value_range,
405 f'Not a valid standards version. Current version is "{CURRENT_STANDARDS_VERSION}"',
406 "warning",
407 known_field.unknown_value_authority,
408 )
409 return
411 sv_version = Version(sv_value)
412 if sv_version < CURRENT_STANDARDS_VERSION:
413 lint_state.emit_diagnostic(
414 sv_value_range,
415 f"Latest Standards-Version is {CURRENT_STANDARDS_VERSION}",
416 "informational",
417 known_field.unknown_value_authority,
418 )
419 return
420 extra = m.group(2)
421 if extra:
422 extra_len = lint_state.position_codec.client_num_units(extra)
423 lint_state.emit_diagnostic(
424 TERange.between(
425 TEPosition(
426 sv_value_range.end_pos.line_position,
427 sv_value_range.end_pos.cursor_position - extra_len,
428 ),
429 sv_value_range.end_pos,
430 ),
431 "Unnecessary version segment. This part of the version is only used for editorial changes",
432 "informational",
433 known_field.unknown_value_authority,
434 quickfixes=[
435 propose_remove_range_quick_fix(
436 proposed_title="Remove unnecessary version part"
437 )
438 ],
439 )
442def _dctrl_ma_field_validation(
443 _known_field: "F",
444 _deb822_file: Deb822FileElement,
445 _kvpair: Deb822KeyValuePairElement,
446 _kvpair_range: "TERange",
447 _field_name_range: "TERange",
448 stanza: Deb822ParagraphElement,
449 stanza_position: "TEPosition",
450 lint_state: LintState,
451) -> None:
452 ma_kvpair = stanza.get_kvpair_element(("Multi-Arch", 0), use_get=True)
453 arch = stanza.get("Architecture", "any")
454 if arch == "all" and ma_kvpair is not None: 454 ↛ exitline 454 didn't return from function '_dctrl_ma_field_validation' because the condition on line 454 was always true
455 ma_value, ma_value_range = extract_first_value_and_position(
456 ma_kvpair,
457 stanza_position,
458 )
459 if ma_value == "same":
460 lint_state.emit_diagnostic(
461 ma_value_range,
462 "Multi-Arch: same is not valid for Architecture: all packages. Maybe you want foreign?",
463 "error",
464 "debputy",
465 )
468def _udeb_only_field_validation(
469 known_field: "F",
470 _deb822_file: Deb822FileElement,
471 _kvpair: Deb822KeyValuePairElement,
472 _kvpair_range: "TERange",
473 field_name_range: "TERange",
474 stanza: Deb822ParagraphElement,
475 _stanza_position: "TEPosition",
476 lint_state: LintState,
477) -> None:
478 package_type = stanza.get("Package-Type")
479 if package_type != "udeb":
480 lint_state.emit_diagnostic(
481 field_name_range,
482 f"The {known_field.name} field is only applicable to udeb packages (`Package-Type: udeb`)",
483 "warning",
484 "debputy",
485 )
488def _complete_only_in_arch_dep_pkgs(
489 stanza_parts: Iterable[Deb822ParagraphElement],
490) -> bool:
491 for stanza in stanza_parts:
492 arch = stanza.get("Architecture")
493 if arch is None:
494 continue
495 archs = arch.split()
496 return "all" not in archs
497 return False
500def _complete_only_for_udeb_pkgs(
501 stanza_parts: Iterable[Deb822ParagraphElement],
502) -> bool:
503 for stanza in stanza_parts:
504 for option in ("Package-Type", "XC-Package-Type"):
505 pkg_type = stanza.get(option)
506 if pkg_type is not None:
507 return pkg_type == "udeb"
508 return False
511def _arch_not_all_only_field_validation(
512 known_field: "F",
513 _deb822_file: Deb822FileElement,
514 _kvpair: Deb822KeyValuePairElement,
515 _kvpair_range_te: "TERange",
516 field_name_range_te: "TERange",
517 stanza: Deb822ParagraphElement,
518 _stanza_position: "TEPosition",
519 lint_state: LintState,
520) -> None:
521 architecture = stanza.get("Architecture")
522 if architecture == "all": 522 ↛ exitline 522 didn't return from function '_arch_not_all_only_field_validation' because the condition on line 522 was always true
523 lint_state.emit_diagnostic(
524 field_name_range_te,
525 f"The {known_field.name} field is not applicable to arch:all packages (`Architecture: all`)",
526 "warning",
527 "debputy",
528 )
531def _single_line_span_range_relative_to_pos(
532 span: Tuple[int, int],
533 relative_to: "TEPosition",
534) -> Range:
535 return TERange(
536 TEPosition(
537 relative_to.line_position,
538 relative_to.cursor_position + span[0],
539 ),
540 TEPosition(
541 relative_to.line_position,
542 relative_to.cursor_position + span[1],
543 ),
544 )
547def _check_extended_description_line(
548 description_value_line: Deb822ValueLineElement,
549 description_line_range_te: "TERange",
550 package: Optional[str],
551 lint_state: LintState,
552) -> None:
553 if description_value_line.comment_element is not None: 553 ↛ 556line 553 didn't jump to line 556 because the condition on line 553 was never true
554 # TODO: Fix this limitation (we get the content and the range wrong with comments.
555 # They are rare inside a Description, so this is a 80:20 trade off
556 return
557 description_line_with_leading_space = (
558 description_value_line.convert_to_text().rstrip()
559 )
560 try:
561 idx = description_line_with_leading_space.index(
562 "<insert long description, indented with spaces>"
563 )
564 except ValueError:
565 pass
566 else:
567 template_span = idx, idx + len(
568 "<insert long description, indented with spaces>"
569 )
570 lint_state.emit_diagnostic(
571 _single_line_span_range_relative_to_pos(
572 template_span,
573 description_line_range_te.start_pos,
574 ),
575 "Unfilled or left-over template from dh_make",
576 "error",
577 "debputy",
578 )
579 if len(description_line_with_leading_space) > 80:
580 # Policy says nothing here, but lintian has 80 characters as hard limit and that
581 # probably matches the limitation of package manager UIs (TUIs/GUIs) somewhere.
582 #
583 # See also debputy#122
584 span = 80, len(description_line_with_leading_space)
585 lint_state.emit_diagnostic(
586 _single_line_span_range_relative_to_pos(
587 span,
588 description_line_range_te.start_pos,
589 ),
590 "Package description line is too long; please line wrap it.",
591 "warning",
592 "debputy",
593 )
596def _check_synopsis(
597 synopsis_value_line: Deb822ValueLineElement,
598 synopsis_range_te: "TERange",
599 field_name_range_te: "TERange",
600 package: Optional[str],
601 lint_state: LintState,
602) -> None:
603 # This function would compute range would be wrong if there is a comment
604 assert synopsis_value_line.comment_element is None
605 synopsis_text_with_leading_space = synopsis_value_line.convert_to_text().rstrip()
606 if not synopsis_text_with_leading_space:
607 lint_state.emit_diagnostic(
608 field_name_range_te,
609 "Package synopsis is missing",
610 "warning",
611 "debputy",
612 )
613 return
614 synopsis_text_trimmed = synopsis_text_with_leading_space.lstrip()
615 synopsis_offset = len(synopsis_text_with_leading_space) - len(synopsis_text_trimmed)
616 starts_with_article = _RE_SYNOPSIS_STARTS_WITH_ARTICLE.search(
617 synopsis_text_with_leading_space
618 )
619 # TODO: Handle ${...} expansion
620 if starts_with_article:
621 lint_state.emit_diagnostic(
622 _single_line_span_range_relative_to_pos(
623 starts_with_article.span(1),
624 synopsis_range_te.start_pos,
625 ),
626 "Package synopsis starts with an article (a/an/the).",
627 "warning",
628 "DevRef 6.2.2",
629 )
630 if len(synopsis_text_trimmed) >= 80:
631 # Policy says `certainly under 80 characters.`, so exactly 80 characters is considered bad too.
632 span = synopsis_offset + 79, len(synopsis_text_with_leading_space)
633 lint_state.emit_diagnostic(
634 _single_line_span_range_relative_to_pos(
635 span,
636 synopsis_range_te.start_pos,
637 ),
638 "Package synopsis is too long.",
639 "warning",
640 "Policy 3.4.1",
641 )
642 if template_match := _RE_SYNOPSIS_IS_TEMPLATE.match(
643 synopsis_text_with_leading_space
644 ):
645 lint_state.emit_diagnostic(
646 _single_line_span_range_relative_to_pos(
647 template_match.span(1),
648 synopsis_range_te.start_pos,
649 ),
650 "Package synopsis is a placeholder",
651 "warning",
652 "debputy",
653 )
654 elif too_short_match := _RE_SYNOPSIS_IS_TOO_SHORT.match(
655 synopsis_text_with_leading_space
656 ):
657 if not SUBSTVAR_RE.match(synopsis_text_with_leading_space.strip()):
658 lint_state.emit_diagnostic(
659 _single_line_span_range_relative_to_pos(
660 too_short_match.span(1),
661 synopsis_range_te.start_pos,
662 ),
663 "Package synopsis is too short",
664 "warning",
665 "debputy",
666 )
669def dctrl_description_validator(
670 _known_field: "F",
671 _deb822_file: Deb822FileElement,
672 kvpair: Deb822KeyValuePairElement,
673 kvpair_range_te: "TERange",
674 _field_name_range: "TERange",
675 stanza: Deb822ParagraphElement,
676 _stanza_position: "TEPosition",
677 lint_state: LintState,
678) -> None:
679 value_lines = kvpair.value_element.value_lines
680 if not value_lines: 680 ↛ 681line 680 didn't jump to line 681 because the condition on line 680 was never true
681 return
682 package = stanza.get("Package")
683 synopsis_value_line = value_lines[0]
684 value_range_te = kvpair.value_element.range_in_parent().relative_to(
685 kvpair_range_te.start_pos
686 )
687 synopsis_line_range_te = synopsis_value_line.range_in_parent().relative_to(
688 value_range_te.start_pos
689 )
690 if synopsis_value_line.continuation_line_token is None: 690 ↛ 703line 690 didn't jump to line 703 because the condition on line 690 was always true
691 field_name_range_te = kvpair.field_token.range_in_parent().relative_to(
692 kvpair_range_te.start_pos
693 )
694 _check_synopsis(
695 synopsis_value_line,
696 synopsis_line_range_te,
697 field_name_range_te,
698 package,
699 lint_state,
700 )
701 description_lines = value_lines[1:]
702 else:
703 description_lines = value_lines
704 for description_line in description_lines:
705 description_line_range_te = description_line.range_in_parent().relative_to(
706 value_range_te.start_pos
707 )
708 _check_extended_description_line(
709 description_line,
710 description_line_range_te,
711 package,
712 lint_state,
713 )
716def _has_packaging_expected_file(
717 name: str,
718 msg: str,
719 severity: LintSeverity = "error",
720) -> CustomFieldCheck:
722 def _impl(
723 _known_field: "F",
724 _deb822_file: Deb822FileElement,
725 _kvpair: Deb822KeyValuePairElement,
726 kvpair_range_te: "TERange",
727 _field_name_range_te: "TERange",
728 _stanza: Deb822ParagraphElement,
729 _stanza_position: "TEPosition",
730 lint_state: LintState,
731 ) -> None:
732 debian_dir = lint_state.debian_dir
733 if debian_dir is None:
734 return
735 cpy = debian_dir.lookup(name)
736 if not cpy: 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true
737 lint_state.emit_diagnostic(
738 kvpair_range_te,
739 msg,
740 severity,
741 "debputy",
742 diagnostic_applies_to_another_file=f"debian/{name}",
743 )
745 return _impl
748_check_missing_debian_rules = _has_packaging_expected_file(
749 "rules",
750 'Missing debian/rules when the Build-Driver is unset or set to "debian-rules"',
751)
754def _has_build_instructions(
755 known_field: "F",
756 deb822_file: Deb822FileElement,
757 kvpair: Deb822KeyValuePairElement,
758 kvpair_range_te: "TERange",
759 field_name_range_te: "TERange",
760 stanza: Deb822ParagraphElement,
761 stanza_position: "TEPosition",
762 lint_state: LintState,
763) -> None:
764 if stanza.get("Build-Driver", "debian-rules").lower() != "debian-rules":
765 return
767 _check_missing_debian_rules(
768 known_field,
769 deb822_file,
770 kvpair,
771 kvpair_range_te,
772 field_name_range_te,
773 stanza,
774 stanza_position,
775 lint_state,
776 )
779def _maintainer_field_validator(
780 known_field: "F",
781 _deb822_file: Deb822FileElement,
782 kvpair: Deb822KeyValuePairElement,
783 kvpair_range_te: "TERange",
784 _field_name_range_te: "TERange",
785 _stanza: Deb822ParagraphElement,
786 _stanza_position: "TEPosition",
787 lint_state: LintState,
788) -> None:
790 value_element_pos = kvpair.value_element.position_in_parent().relative_to(
791 kvpair_range_te.start_pos
792 )
793 interpreted_value = kvpair.interpret_as(known_field.field_value_class.interpreter())
794 for part in interpreted_value.iter_parts():
795 if not part.is_separator:
796 continue
797 value_range_te = part.range_in_parent().relative_to(value_element_pos)
798 severity = known_field.unknown_value_diagnostic_severity
799 assert severity is not None
800 # TODO: Check for a follow up maintainer and based on that the quick fix is either
801 # to remove the dead separator OR move the trailing data into `Uploaders`
802 lint_state.emit_diagnostic(
803 value_range_te,
804 'The "Maintainer" field has a trailing separator, but it is a single value field.',
805 severity,
806 known_field.unknown_value_authority,
807 )
810def _each_value_match_regex_validation(
811 regex: re.Pattern,
812 *,
813 diagnostic_severity: LintSeverity = "error",
814 authority_reference: Optional[str] = None,
815) -> CustomFieldCheck:
817 def _validator(
818 known_field: "F",
819 _deb822_file: Deb822FileElement,
820 kvpair: Deb822KeyValuePairElement,
821 kvpair_range_te: "TERange",
822 _field_name_range_te: "TERange",
823 _stanza: Deb822ParagraphElement,
824 _stanza_position: "TEPosition",
825 lint_state: LintState,
826 ) -> None:
827 nonlocal authority_reference
828 interpreter = known_field.field_value_class.interpreter()
829 if interpreter is None:
830 raise AssertionError(
831 f"{known_field.name} has field type {known_field.field_value_class}, which cannot be"
832 f" regex validated since it does not have a tokenization"
833 )
834 auth_ref = (
835 authority_reference
836 if authority_reference is not None
837 else known_field.unknown_value_authority
838 )
840 value_element_pos = kvpair.value_element.position_in_parent().relative_to(
841 kvpair_range_te.start_pos
842 )
843 for value_ref in kvpair.interpret_as(interpreter).iter_value_references():
844 v = value_ref.value
845 m = regex.fullmatch(v)
846 if m is not None:
847 continue
849 if "${" in v:
850 # Ignore substvars
851 continue
853 section_value_loc = value_ref.locatable
854 value_range_te = section_value_loc.range_in_parent().relative_to(
855 value_element_pos
856 )
857 lint_state.emit_diagnostic(
858 value_range_te,
859 f'The value "{v}" does not match the regex {regex.pattern}.',
860 diagnostic_severity,
861 auth_ref,
862 )
864 return _validator
867_DEP_OR_RELATION = re.compile(r"[|]")
868_DEP_RELATION_CLAUSE = re.compile(
869 r"""
870 ^
871 \s*
872 (?P<name_arch_qual>[-+.a-zA-Z0-9${}:]{2,})
873 \s*
874 (?: [(] \s* (?P<operator>>>|>=|>|=|<|<=|<<) \s* (?P<version> [0-9$][^)]*) \s* [)] \s* )?
875 (?: \[ (?P<arch_restriction> [\s!\w\-]+) ] \s*)?
876 (?: < (?P<build_profile_restriction> .+ ) > \s*)?
877 ((?P<garbage>\S.*)\s*)?
878 $
879""",
880 re.VERBOSE | re.MULTILINE,
881)
884def _span_to_te_range(
885 text: str,
886 start_pos: int,
887 end_pos: int,
888) -> TERange:
889 prefix = text[0:start_pos]
890 prefix_plus_text = text[0:end_pos]
892 start_line = prefix.count("\n")
893 if start_line:
894 start_newline_offset = prefix.rindex("\n")
895 # +1 to skip past the newline
896 start_cursor_pos = start_pos - (start_newline_offset + 1)
897 else:
898 start_cursor_pos = start_pos
900 end_line = prefix_plus_text.count("\n")
901 if end_line == start_line:
902 end_cursor_pos = start_cursor_pos + (end_pos - start_pos)
903 else:
904 end_newline_offset = prefix_plus_text.rindex("\n")
905 end_cursor_pos = end_pos - (end_newline_offset + 1)
907 return TERange(
908 TEPosition(
909 start_line,
910 start_cursor_pos,
911 ),
912 TEPosition(
913 end_line,
914 end_cursor_pos,
915 ),
916 )
919def _split_w_spans(
920 v: str,
921 sep: str,
922 *,
923 offset: int = 0,
924) -> Sequence[Tuple[str, int, int]]:
925 separator_size = len(sep)
926 parts = v.split(sep)
927 for part in parts:
928 size = len(part)
929 end_offset = offset + size
930 yield part, offset, end_offset
931 offset = end_offset + separator_size
934_COLLAPSE_WHITESPACE = re.compile(r"\s+")
937def _cleanup_rel(rel: str) -> str:
938 return _COLLAPSE_WHITESPACE.sub(" ", rel.strip())
941def _text_to_te_position(text: str) -> "TEPosition":
942 newlines = text.count("\n")
943 if not newlines:
944 return TEPosition(
945 newlines,
946 len(text),
947 )
948 last_newline_offset = text.rindex("\n")
949 line_offset = len(text) - (last_newline_offset + 1)
950 return TEPosition(
951 newlines,
952 line_offset,
953 )
956@dataclasses.dataclass(slots=True, frozen=True)
957class Relation:
958 name: str
959 arch_qual: Optional[str] = None
960 version_operator: Optional[str] = None
961 version: Optional[str] = None
962 arch_restriction: Optional[str] = None
963 build_profile_restriction: Optional[str] = None
964 # These offsets are intended to show the relation itself. They are not
965 # the relation boundary offsets (they will omit leading whitespace as
966 # an example).
967 content_display_offset: int = -1
968 content_display_end_offset: int = -1
971def relation_key_variations(
972 relation: Relation,
973) -> Tuple[str, Optional[str], Optional[str]]:
974 operator_variants = (
975 [relation.version_operator, None]
976 if relation.version_operator is not None
977 else [None]
978 )
979 arch_qual_variants = (
980 [relation.arch_qual, None]
981 if relation.arch_qual is not None and relation.arch_qual != "any"
982 else [None]
983 )
984 for arch_qual, version_operator in itertools.product(
985 arch_qual_variants,
986 operator_variants,
987 ):
988 yield relation.name, arch_qual, version_operator
991def dup_check_relations(
992 known_field: "F",
993 relations: Sequence[Relation],
994 raw_value_masked_comments: str,
995 value_element_pos: "TEPosition",
996 lint_state: LintState,
997) -> None:
998 overlap_table = {}
999 for relation in relations:
1000 version_operator = relation.version_operator
1001 arch_qual = relation.arch_qual
1002 if relation.arch_restriction or relation.build_profile_restriction: 1002 ↛ 1003line 1002 didn't jump to line 1003 because the condition on line 1002 was never true
1003 continue
1005 for relation_key in relation_key_variations(relation):
1006 prev_relation = overlap_table.get(relation_key)
1007 if prev_relation is None:
1008 overlap_table[relation_key] = relation
1009 else:
1010 prev_version_operator = prev_relation.version_operator
1012 if (
1013 prev_version_operator
1014 and version_operator
1015 and prev_version_operator[0] != version_operator[0]
1016 and version_operator[0] in ("<", ">")
1017 and prev_version_operator[0] in ("<", ">")
1018 ):
1019 # foo (>= 1), foo (<< 2) and similar should not trigger a warning.
1020 continue
1022 prev_arch_qual = prev_relation.arch_qual
1023 if (
1024 arch_qual != prev_arch_qual
1025 and prev_arch_qual != "any"
1026 and arch_qual != "any"
1027 ):
1028 # foo:amd64 != foo:native and that might matter - especially for "libfoo-dev, libfoo-dev:native"
1029 #
1030 # This check is probably a too forgiving in some corner cases.
1031 continue
1033 orig_relation_range = TERange(
1034 _text_to_te_position(
1035 raw_value_masked_comments[
1036 : prev_relation.content_display_offset
1037 ]
1038 ),
1039 _text_to_te_position(
1040 raw_value_masked_comments[
1041 : prev_relation.content_display_end_offset
1042 ]
1043 ),
1044 ).relative_to(value_element_pos)
1046 duplicate_relation_range = TERange(
1047 _text_to_te_position(
1048 raw_value_masked_comments[: relation.content_display_offset]
1049 ),
1050 _text_to_te_position(
1051 raw_value_masked_comments[: relation.content_display_end_offset]
1052 ),
1053 ).relative_to(value_element_pos)
1055 lint_state.emit_diagnostic(
1056 duplicate_relation_range,
1057 "Duplicate relationship. Merge with the previous relationship",
1058 "warning",
1059 known_field.unknown_value_authority,
1060 related_information=[
1061 lint_state.related_diagnostic_information(
1062 orig_relation_range,
1063 "The previous definition",
1064 ),
1065 ],
1066 )
1067 # We only emit for the first duplicate "key" for each relation. Odds are remaining
1068 # keys point to the same match. Even if they do not, it does not really matter as
1069 # we already pointed out an issue for the user to follow up on.
1070 break
1073def _dctrl_check_dep_version_operator(
1074 known_field: "F",
1075 version_operator: str,
1076 version_operator_span: Tuple[int, int],
1077 version_operators: FrozenSet[str],
1078 raw_value_masked_comments: str,
1079 offset: int,
1080 value_element_pos: "TEPosition",
1081 lint_state: LintState,
1082) -> bool:
1083 if (
1084 version_operators
1085 and version_operator is not None
1086 and version_operator not in version_operators
1087 ):
1088 v_start_offset = offset + version_operator_span[0]
1089 v_end_offset = offset + version_operator_span[1]
1090 version_problem_range_te = TERange(
1091 _text_to_te_position(raw_value_masked_comments[:v_start_offset]),
1092 _text_to_te_position(raw_value_masked_comments[:v_end_offset]),
1093 ).relative_to(value_element_pos)
1095 sorted_version_operators = sorted(version_operators)
1097 excluding_equal = f"{version_operator}{version_operator}"
1098 including_equal = f"{version_operator}="
1100 if version_operator in (">", "<") and (
1101 excluding_equal in version_operators or including_equal in version_operators
1102 ):
1103 lint_state.emit_diagnostic(
1104 version_problem_range_te,
1105 f'Obsolete version operator "{version_operator}" that is no longer supported.',
1106 "error",
1107 "Policy 7.1",
1108 quickfixes=[
1109 propose_correct_text_quick_fix(n)
1110 for n in (excluding_equal, including_equal)
1111 if not version_operators or n in version_operators
1112 ],
1113 )
1114 else:
1115 lint_state.emit_diagnostic(
1116 version_problem_range_te,
1117 f'The version operator "{version_operator}" is not allowed in {known_field.name}',
1118 "error",
1119 known_field.unknown_value_authority,
1120 quickfixes=[
1121 propose_correct_text_quick_fix(n) for n in sorted_version_operators
1122 ],
1123 )
1124 return True
1125 return False
1128def _dctrl_validate_dep(
1129 known_field: "DF",
1130 _deb822_file: Deb822FileElement,
1131 kvpair: Deb822KeyValuePairElement,
1132 kvpair_range_te: "TERange",
1133 _field_name_range: "TERange",
1134 _stanza: Deb822ParagraphElement,
1135 _stanza_position: "TEPosition",
1136 lint_state: LintState,
1137) -> None:
1138 value_element_pos = kvpair.value_element.position_in_parent().relative_to(
1139 kvpair_range_te.start_pos
1140 )
1141 raw_value_with_comments = kvpair.value_element.convert_to_text()
1142 raw_value_masked_comments = "".join(
1143 (line if not line.startswith("#") else (" " * (len(line) - 1)) + "\n")
1144 for line in raw_value_with_comments.splitlines(keepends=True)
1145 )
1146 if isinstance(known_field, DctrlRelationshipKnownField):
1147 version_operators = known_field.allowed_version_operators
1148 supports_or_relation = known_field.supports_or_relation
1149 else:
1150 version_operators = frozenset({">>", ">=", "=", "<=", "<<"})
1151 supports_or_relation = True
1153 relation_dup_table = collections.defaultdict(list)
1155 for rel, rel_offset, rel_end_offset in _split_w_spans(
1156 raw_value_masked_comments, ","
1157 ):
1158 sub_relations = []
1159 for or_rel, offset, end_offset in _split_w_spans(rel, "|", offset=rel_offset):
1160 if or_rel.isspace():
1161 continue
1162 if sub_relations and not supports_or_relation:
1163 separator_range_te = TERange(
1164 _text_to_te_position(raw_value_masked_comments[: offset - 1]),
1165 _text_to_te_position(raw_value_masked_comments[:offset]),
1166 ).relative_to(value_element_pos)
1167 lint_state.emit_diagnostic(
1168 separator_range_te,
1169 f'The field {known_field.name} does not support "|" (OR) in relations.',
1170 "error",
1171 known_field.unknown_value_authority,
1172 )
1173 m = _DEP_RELATION_CLAUSE.fullmatch(or_rel)
1175 if m is not None:
1176 garbage = m.group("garbage")
1177 version_operator = m.group("operator")
1178 version_operator_span = m.span("operator")
1179 if _dctrl_check_dep_version_operator(
1180 known_field,
1181 version_operator,
1182 version_operator_span,
1183 version_operators,
1184 raw_value_masked_comments,
1185 offset,
1186 value_element_pos,
1187 lint_state,
1188 ):
1189 sub_relations.append(Relation("<BROKEN>"))
1190 else:
1191 name_arch_qual = m.group("name_arch_qual")
1192 if ":" in name_arch_qual:
1193 name, arch_qual = name_arch_qual.split(":", 1)
1194 else:
1195 name = name_arch_qual
1196 arch_qual = None
1197 sub_relations.append(
1198 Relation(
1199 name,
1200 arch_qual=arch_qual,
1201 version_operator=version_operator,
1202 version=m.group("version"),
1203 arch_restriction=m.group("build_profile_restriction"),
1204 build_profile_restriction=m.group(
1205 "build_profile_restriction"
1206 ),
1207 content_display_offset=offset + m.start("name_arch_qual"),
1208 # TODO: This should be trimmed in the end.
1209 content_display_end_offset=rel_end_offset,
1210 )
1211 )
1212 else:
1213 garbage = None
1214 sub_relations.append(Relation("<BROKEN>"))
1216 if m is not None and not garbage:
1217 continue
1218 if m is not None:
1219 garbage_span = m.span("garbage")
1220 garbage_start, garbage_end = garbage_span
1221 error_start_offset = offset + garbage_start
1222 error_end_offset = offset + garbage_end
1223 garbage_part = raw_value_masked_comments[
1224 error_start_offset:error_end_offset
1225 ]
1226 else:
1227 garbage_part = None
1228 error_start_offset = offset
1229 error_end_offset = end_offset
1231 problem_range_te = TERange(
1232 _text_to_te_position(raw_value_masked_comments[:error_start_offset]),
1233 _text_to_te_position(raw_value_masked_comments[:error_end_offset]),
1234 ).relative_to(value_element_pos)
1236 if garbage_part is not None:
1237 if _DEP_RELATION_CLAUSE.fullmatch(garbage_part) is not None:
1238 msg = (
1239 "Trailing data after a relationship that might be a second relationship."
1240 " Is a separator missing before this part?"
1241 )
1242 else:
1243 msg = "Parse error of the relationship. Either a syntax error or a missing separator somewhere."
1244 lint_state.emit_diagnostic(
1245 problem_range_te,
1246 msg,
1247 "error",
1248 known_field.unknown_value_authority,
1249 )
1250 else:
1251 dep = _cleanup_rel(
1252 raw_value_masked_comments[error_start_offset:error_end_offset]
1253 )
1254 lint_state.emit_diagnostic(
1255 problem_range_te,
1256 f'Could not parse "{dep}" as a dependency relation.',
1257 "error",
1258 known_field.unknown_value_authority,
1259 )
1260 if (
1261 len(sub_relations) == 1
1262 and (relation := sub_relations[0]).name != "<BROKEN>"
1263 ):
1264 # We ignore OR-relations in the dup-check for now. We also skip relations with problems.
1265 relation_dup_table[relation.name].append(relation)
1267 for relations in relation_dup_table.values():
1268 if len(relations) > 1:
1269 dup_check_relations(
1270 known_field,
1271 relations,
1272 raw_value_masked_comments,
1273 value_element_pos,
1274 lint_state,
1275 )
1278def _rrr_build_driver_mismatch(
1279 _known_field: "F",
1280 _deb822_file: Deb822FileElement,
1281 _kvpair: Deb822KeyValuePairElement,
1282 kvpair_range_te: "TERange",
1283 _field_name_range: "TERange",
1284 stanza: Deb822ParagraphElement,
1285 _stanza_position: "TEPosition",
1286 lint_state: LintState,
1287) -> None:
1288 dr = stanza.get("Build-Driver", "debian-rules")
1289 if dr != "debian-rules":
1290 lint_state.emit_diagnostic(
1291 kvpair_range_te,
1292 f'The Rules-Requires-Root field is irrelevant for the Build-Driver "{dr}".',
1293 "informational",
1294 "debputy",
1295 quickfixes=[
1296 propose_remove_range_quick_fix(
1297 proposed_title="Remove Rules-Requires-Root"
1298 )
1299 ],
1300 )
1303class Dep5Matcher(BasenameGlobMatch):
1304 def __init__(self, basename_glob: str) -> None:
1305 super().__init__(
1306 basename_glob,
1307 only_when_in_directory=None,
1308 path_type=None,
1309 recursive_match=False,
1310 )
1313def _match_dep5_segment(
1314 current_dir: VirtualPathBase, basename_glob: str
1315) -> Iterable[VirtualPathBase]:
1316 if "*" in basename_glob or "?" in basename_glob:
1317 return Dep5Matcher(basename_glob).finditer(current_dir)
1318 else:
1319 res = current_dir.get(basename_glob)
1320 if res is None:
1321 return tuple()
1322 return (res,)
1325_RE_SLASHES = re.compile(r"//+")
1328def _dep5_unnecessary_symbols(
1329 value: str,
1330 value_range: TERange,
1331 lint_state: LintState,
1332) -> None:
1333 slash_check_index = 0
1334 if value.startswith(("./", "/")):
1335 prefix_len = 1 if value[0] == "/" else 2
1336 if value[prefix_len - 1 : prefix_len + 2].startswith("//"): 1336 ↛ 1340line 1336 didn't jump to line 1340 because the condition on line 1336 was always true
1337 _, slashes_end = _RE_SLASHES.search(value).span()
1338 prefix_len = slashes_end
1340 slash_check_index = prefix_len
1341 prefix_range = TERange(
1342 value_range.start_pos,
1343 TEPosition(
1344 value_range.start_pos.line_position,
1345 value_range.start_pos.cursor_position + prefix_len,
1346 ),
1347 )
1348 lint_state.emit_diagnostic(
1349 prefix_range,
1350 f'Unnecessary prefix "{value[0:prefix_len]}"',
1351 "warning",
1352 "debputy",
1353 quickfixes=[
1354 propose_remove_range_quick_fix(
1355 proposed_title=f'Delete "{value[0:prefix_len]}"'
1356 )
1357 ],
1358 )
1360 for m in _RE_SLASHES.finditer(value, slash_check_index):
1361 m_start, m_end = m.span(0)
1363 prefix_range = TERange(
1364 TEPosition(
1365 value_range.start_pos.line_position,
1366 value_range.start_pos.cursor_position + m_start,
1367 ),
1368 TEPosition(
1369 value_range.start_pos.line_position,
1370 value_range.start_pos.cursor_position + m_end,
1371 ),
1372 )
1373 lint_state.emit_diagnostic(
1374 prefix_range,
1375 'Simplify to a single "/"',
1376 "warning",
1377 "debputy",
1378 quickfixes=[propose_correct_text_quick_fix("/")],
1379 )
1382def _dep5_files_check(
1383 known_field: "F",
1384 _deb822_file: Deb822FileElement,
1385 kvpair: Deb822KeyValuePairElement,
1386 kvpair_range_te: "TERange",
1387 _field_name_range: "TERange",
1388 _stanza: Deb822ParagraphElement,
1389 _stanza_position: "TEPosition",
1390 lint_state: LintState,
1391) -> None:
1392 interpreter = known_field.field_value_class.interpreter()
1393 assert interpreter is not None
1394 full_value_range = kvpair.value_element.range_in_parent().relative_to(
1395 kvpair_range_te.start_pos
1396 )
1397 values_with_ranges = []
1398 for value_ref in kvpair.interpret_as(interpreter).iter_value_references():
1399 value_range = value_ref.locatable.range_in_parent().relative_to(
1400 full_value_range.start_pos
1401 )
1402 value = value_ref.value
1403 values_with_ranges.append((value_ref.value, value_range))
1404 _dep5_unnecessary_symbols(value, value_range, lint_state)
1406 source_root = lint_state.source_root
1407 if source_root is None:
1408 return
1409 i = 0
1410 limit = len(values_with_ranges)
1411 while i < limit:
1412 value, value_range = values_with_ranges[i]
1413 i += 1
1416_HOMEPAGE_CLUTTER_RE = re.compile(r"<(?:UR[LI]:)?(.*)>")
1417_URI_RE = re.compile(r"(?P<protocol>[a-z0-9]+)://(?P<host>[^\s/+]+)(?P<path>/[^\s?]*)?")
1418_KNOWN_HTTPS_HOSTS = frozenset(
1419 [
1420 "debian.org",
1421 "bioconductor.org",
1422 "cran.r-project.org",
1423 "github.com",
1424 "gitlab.com",
1425 "metacpan.org",
1426 "gnu.org",
1427 ]
1428)
1429_REPLACED_HOSTS = frozenset({"alioth.debian.org"})
1430_NO_DOT_GIT_HOMEPAGE_HOSTS = frozenset(
1431 {
1432 "salsa.debian.org",
1433 "github.com",
1434 "gitlab.com",
1435 }
1436)
1439def _is_known_host(host: str, known_hosts: Container[str]) -> bool:
1440 if host in known_hosts:
1441 return True
1442 while host: 1442 ↛ 1450line 1442 didn't jump to line 1450 because the condition on line 1442 was always true
1443 try:
1444 idx = host.index(".")
1445 host = host[idx + 1 :]
1446 except ValueError:
1447 break
1448 if host in known_hosts:
1449 return True
1450 return False
1453def _validate_homepage_field(
1454 _known_field: "F",
1455 _deb822_file: Deb822FileElement,
1456 kvpair: Deb822KeyValuePairElement,
1457 kvpair_range_te: "TERange",
1458 _field_name_range_te: "TERange",
1459 _stanza: Deb822ParagraphElement,
1460 _stanza_position: "TEPosition",
1461 lint_state: LintState,
1462) -> None:
1463 value = kvpair.value_element.convert_to_text()
1464 offset = 0
1465 homepage = value
1466 if "<" in value and (m := _HOMEPAGE_CLUTTER_RE.search(value)):
1467 expected_value = m.group(1)
1468 quickfixes = []
1469 if expected_value: 1469 ↛ 1473line 1469 didn't jump to line 1473 because the condition on line 1469 was always true
1470 homepage = expected_value.strip()
1471 offset = m.start(1)
1472 quickfixes.append(propose_correct_text_quick_fix(expected_value))
1473 lint_state.emit_diagnostic(
1474 _single_line_span_range_relative_to_pos(
1475 m.span(),
1476 kvpair.value_element.position_in_parent().relative_to(
1477 kvpair_range_te.start_pos
1478 ),
1479 ),
1480 "Superfluous URL/URI wrapping",
1481 "informational",
1482 "Policy 5.6.23",
1483 quickfixes=quickfixes,
1484 )
1485 # Note falling through here can cause "two rounds" for debputy lint --auto-fix
1486 m = _URI_RE.search(homepage)
1487 if not m: 1487 ↛ 1488line 1487 didn't jump to line 1488 because the condition on line 1487 was never true
1488 return
1489 # TODO relative to lintian: `bad-homepage` and most of the `fields/bad-homepages` hints.
1490 protocol = m.group("protocol")
1491 host = m.group("host")
1492 path = m.group("path") or ""
1493 if _is_known_host(host, _REPLACED_HOSTS):
1494 span = m.span("host")
1495 lint_state.emit_diagnostic(
1496 _single_line_span_range_relative_to_pos(
1497 (span[0] + offset, span[1] + offset),
1498 kvpair.value_element.position_in_parent().relative_to(
1499 kvpair_range_te.start_pos
1500 ),
1501 ),
1502 f'The server "{host}" is no longer in use.',
1503 "warning",
1504 "debputy",
1505 )
1506 return
1507 if (
1508 protocol == "ftp"
1509 or protocol == "http"
1510 and _is_known_host(host, _KNOWN_HTTPS_HOSTS)
1511 ):
1512 span = m.span("protocol")
1513 if protocol == "ftp" and not _is_known_host(host, _KNOWN_HTTPS_HOSTS): 1513 ↛ 1514line 1513 didn't jump to line 1514 because the condition on line 1513 was never true
1514 msg = "Insecure protocol for website (check if a https:// variant is available)"
1515 quickfixes = []
1516 else:
1517 msg = "Replace with https://. The host is known to support https"
1518 quickfixes = [propose_correct_text_quick_fix("https")]
1519 lint_state.emit_diagnostic(
1520 _single_line_span_range_relative_to_pos(
1521 (span[0] + offset, span[1] + offset),
1522 kvpair.value_element.position_in_parent().relative_to(
1523 kvpair_range_te.start_pos
1524 ),
1525 ),
1526 msg,
1527 "pedantic",
1528 "debputy",
1529 quickfixes=quickfixes,
1530 )
1531 if path.endswith(".git") and _is_known_host(host, _NO_DOT_GIT_HOMEPAGE_HOSTS):
1532 span = m.span("path")
1533 msg = "Unnecessary suffix"
1534 quickfixes = [propose_correct_text_quick_fix(path[:-4])]
1535 lint_state.emit_diagnostic(
1536 _single_line_span_range_relative_to_pos(
1537 (span[1] - 4 + offset, span[1] + offset),
1538 kvpair.value_element.position_in_parent().relative_to(
1539 kvpair_range_te.start_pos
1540 ),
1541 ),
1542 msg,
1543 "pedantic",
1544 "debputy",
1545 quickfixes=quickfixes,
1546 )
1549def _combined_custom_field_check(*checks: CustomFieldCheck) -> CustomFieldCheck:
1550 def _validator(
1551 known_field: "F",
1552 deb822_file: Deb822FileElement,
1553 kvpair: Deb822KeyValuePairElement,
1554 kvpair_range_te: "TERange",
1555 field_name_range_te: "TERange",
1556 stanza: Deb822ParagraphElement,
1557 stanza_position: "TEPosition",
1558 lint_state: LintState,
1559 ) -> None:
1560 for check in checks:
1561 check(
1562 known_field,
1563 deb822_file,
1564 kvpair,
1565 kvpair_range_te,
1566 field_name_range_te,
1567 stanza,
1568 stanza_position,
1569 lint_state,
1570 )
1572 return _validator
1575@dataclasses.dataclass(slots=True, frozen=True)
1576class PackageNameSectionRule:
1577 section: str
1578 check: Callable[[str], bool]
1581def _package_name_section_rule(
1582 section: str,
1583 check: Union[Callable[[str], bool], re.Pattern],
1584 *,
1585 confirm_re: Optional[re.Pattern] = None,
1586) -> PackageNameSectionRule:
1587 if confirm_re is not None:
1588 assert callable(check)
1590 def _impl(v: str) -> bool:
1591 return check(v) and confirm_re.search(v)
1593 elif isinstance(check, re.Pattern): 1593 ↛ 1595line 1593 didn't jump to line 1595 because the condition on line 1593 was never true
1595 def _impl(v: str) -> bool:
1596 return check.search(v) is not None
1598 else:
1599 _impl = check
1601 return PackageNameSectionRule(section, _impl)
1604# rules: order is important (first match wins in case of a conflict)
1605_PKGNAME_VS_SECTION_RULES = [
1606 _package_name_section_rule("debian-installer", lambda n: n.endswith("-udeb")),
1607 _package_name_section_rule("doc", lambda n: n.endswith(("-doc", "-docs"))),
1608 _package_name_section_rule("debug", lambda n: n.endswith(("-dbg", "-dbgsym"))),
1609 _package_name_section_rule(
1610 "httpd",
1611 lambda n: n.startswith(("lighttpd-mod", "libapache2-mod-", "libnginx-mod-")),
1612 ),
1613 _package_name_section_rule("gnustep", lambda n: n.startswith("gnustep-")),
1614 _package_name_section_rule(
1615 "gnustep",
1616 lambda n: n.endswith(
1617 (
1618 ".framework",
1619 ".framework-common",
1620 ".tool",
1621 ".tool-common",
1622 ".app",
1623 ".app-common",
1624 )
1625 ),
1626 ),
1627 _package_name_section_rule("embedded", lambda n: n.startswith("moblin-")),
1628 _package_name_section_rule("javascript", lambda n: n.startswith("node-")),
1629 _package_name_section_rule(
1630 "zope",
1631 lambda n: n.startswith(("python-zope", "python3-zope", "zope")),
1632 ),
1633 _package_name_section_rule(
1634 "python",
1635 lambda n: n.startswith(("python-", "python3-")),
1636 ),
1637 _package_name_section_rule(
1638 "gnu-r",
1639 lambda n: n.startswith(("r-cran-", "r-bioc-", "r-other-")),
1640 ),
1641 _package_name_section_rule("editors", lambda n: n.startswith("elpa-")),
1642 _package_name_section_rule("lisp", lambda n: n.startswith("cl-")),
1643 _package_name_section_rule(
1644 "lisp",
1645 lambda n: "-elisp-" in n or n.endswith("-elisp"),
1646 ),
1647 _package_name_section_rule(
1648 "lisp",
1649 lambda n: n.startswith("lib") and n.endswith("-guile"),
1650 ),
1651 _package_name_section_rule("lisp", lambda n: n.startswith("guile-")),
1652 _package_name_section_rule("golang", lambda n: n.startswith("golang-")),
1653 _package_name_section_rule(
1654 "perl",
1655 lambda n: n.startswith("lib") and n.endswith("-perl"),
1656 ),
1657 _package_name_section_rule(
1658 "cli-mono",
1659 lambda n: n.startswith("lib") and n.endswith(("-cil", "-cil-dev")),
1660 ),
1661 _package_name_section_rule(
1662 "java",
1663 lambda n: n.startswith("lib") and n.endswith(("-java", "-gcj", "-jni")),
1664 ),
1665 _package_name_section_rule(
1666 "php",
1667 lambda n: n.startswith(("libphp", "php")),
1668 confirm_re=re.compile(r"^(?:lib)?php(?:\d(?:\.\d)?)?-"),
1669 ),
1670 _package_name_section_rule(
1671 "php", lambda n: n.startswith("lib-") and n.endswith("-php")
1672 ),
1673 _package_name_section_rule(
1674 "haskell",
1675 lambda n: n.startswith(("haskell-", "libhugs-", "libghc-", "libghc6-")),
1676 ),
1677 _package_name_section_rule(
1678 "ruby",
1679 lambda n: "-ruby" in n,
1680 confirm_re=re.compile(r"^lib.*-ruby(?:1\.\d)?$"),
1681 ),
1682 _package_name_section_rule("ruby", lambda n: n.startswith("ruby-")),
1683 _package_name_section_rule(
1684 "rust",
1685 lambda n: n.startswith("librust-") and n.endswith("-dev"),
1686 ),
1687 _package_name_section_rule("rust", lambda n: n.startswith("rust-")),
1688 _package_name_section_rule(
1689 "ocaml",
1690 lambda n: n.startswith("lib-") and n.endswith(("-ocaml-dev", "-camlp4-dev")),
1691 ),
1692 _package_name_section_rule("javascript", lambda n: n.startswith("libjs-")),
1693 _package_name_section_rule(
1694 "interpreters",
1695 lambda n: n.startswith("lib-") and n.endswith(("-tcl", "-lua", "-gst")),
1696 ),
1697 _package_name_section_rule(
1698 "introspection",
1699 lambda n: n.startswith("gir-"),
1700 confirm_re=re.compile(r"^gir\d+\.\d+-.*-\d+\.\d+$"),
1701 ),
1702 _package_name_section_rule(
1703 "fonts",
1704 lambda n: n.startswith(("xfonts-", "fonts-", "ttf-")),
1705 ),
1706 _package_name_section_rule("admin", lambda n: n.startswith(("libnss-", "libpam-"))),
1707 _package_name_section_rule(
1708 "localization",
1709 lambda n: n.startswith(
1710 (
1711 "aspell-",
1712 "hunspell-",
1713 "myspell-",
1714 "mythes-",
1715 "dict-freedict-",
1716 "gcompris-sound-",
1717 )
1718 ),
1719 ),
1720 _package_name_section_rule(
1721 "localization",
1722 lambda n: n.startswith("hyphen-"),
1723 confirm_re=re.compile(r"^hyphen-[a-z]{2}(?:-[a-z]{2})?$"),
1724 ),
1725 _package_name_section_rule(
1726 "localization",
1727 lambda n: "-l10n-" in n or n.endswith("-l10n"),
1728 ),
1729 _package_name_section_rule("kernel", lambda n: n.endswith(("-dkms", "-firmware"))),
1730 _package_name_section_rule(
1731 "libdevel",
1732 lambda n: n.startswith("lib") and n.endswith(("-dev", "-headers")),
1733 ),
1734 _package_name_section_rule(
1735 "libs",
1736 lambda n: n.startswith("lib"),
1737 confirm_re=re.compile(r"^lib.*\d[ad]?$"),
1738 ),
1739]
1742# Fiddling with the package name can cause a lot of changes (diagnostic scans), so we have an upper bound
1743# on the cache. The number is currently just taken out of a hat.
1744@functools.lru_cache(64)
1745def package_name_to_section(name: str) -> Optional[str]:
1746 for rule in _PKGNAME_VS_SECTION_RULES:
1747 if rule.check(name):
1748 return rule.section
1749 return None
1752def _unknown_value_check(
1753 field_name: str,
1754 value: str,
1755 known_values: Mapping[str, Keyword],
1756 unknown_value_severity: Optional[LintSeverity],
1757) -> Tuple[Optional[Keyword], Optional[str], Optional[LintSeverity], Optional[Any]]:
1758 known_value = known_values.get(value)
1759 message = None
1760 severity = unknown_value_severity
1761 fix_data = None
1762 if known_value is None:
1763 candidates = detect_possible_typo(
1764 value,
1765 known_values,
1766 )
1767 if len(known_values) < 5: 1767 ↛ 1768line 1767 didn't jump to line 1768 because the condition on line 1767 was never true
1768 values = ", ".join(sorted(known_values))
1769 hint_text = f" Known values for this field: {values}"
1770 else:
1771 hint_text = ""
1772 fix_data = None
1773 severity = unknown_value_severity
1774 fix_text = hint_text
1775 if candidates:
1776 match = candidates[0]
1777 if len(candidates) == 1: 1777 ↛ 1779line 1777 didn't jump to line 1779
1778 known_value = known_values[match]
1779 fix_text = (
1780 f' It is possible that the value is a typo of "{match}".{fix_text}'
1781 )
1782 fix_data = [propose_correct_text_quick_fix(m) for m in candidates]
1783 elif severity is None: 1783 ↛ 1784line 1783 didn't jump to line 1784 because the condition on line 1783 was never true
1784 return None, None, None, None
1785 if severity is None:
1786 severity = cast("LintSeverity", "warning")
1787 # It always has leading whitespace
1788 message = fix_text.strip()
1789 else:
1790 message = f'The value "{value}" is not supported in {field_name}.{fix_text}'
1791 return known_value, message, severity, fix_data
1794def _dep5_escape_path(path: str) -> str:
1795 return path.replace(" ", "?")
1798def _noop_escape_path(path: str) -> str:
1799 return path
1802def _should_ignore_dir(
1803 path: VirtualPath,
1804 *,
1805 supports_dir_match: bool = False,
1806 match_non_persistent_paths: bool = False,
1807) -> bool:
1808 if not supports_dir_match and not any(path.iterdir):
1809 return True
1810 cachedir_tag = path.get("CACHEDIR.TAG")
1811 if (
1812 not match_non_persistent_paths
1813 and cachedir_tag is not None
1814 and cachedir_tag.is_file
1815 ):
1816 # https://bford.info/cachedir/
1817 with cachedir_tag.open(byte_io=True, buffering=64) as fd:
1818 start = fd.read(43)
1819 if start == b"Signature: 8a477f597d28d172789f06886806bc55":
1820 return True
1821 return False
1824@dataclasses.dataclass(slots=True)
1825class Deb822KnownField:
1826 name: str
1827 field_value_class: FieldValueClass
1828 warn_if_default: bool = True
1829 unknown_value_authority: str = "debputy"
1830 missing_field_authority: str = "debputy"
1831 replaced_by: Optional[str] = None
1832 deprecated_with_no_replacement: bool = False
1833 missing_field_severity: Optional[LintSeverity] = None
1834 default_value: Optional[str] = None
1835 known_values: Optional[Mapping[str, Keyword]] = None
1836 unknown_value_diagnostic_severity: Optional[LintSeverity] = "error"
1837 translation_context: str = ""
1838 # One-line description for space-constrained docs (such as completion docs)
1839 synopsis: Optional[str] = None
1840 usage_hint: Optional[UsageHint] = None
1841 long_description: Optional[str] = None
1842 spellcheck_value: bool = False
1843 inheritable_from_other_stanza: bool = False
1844 show_as_inherited: bool = True
1845 custom_field_check: Optional[CustomFieldCheck] = None
1846 can_complete_field_in_stanza: Optional[
1847 Callable[[Iterable[Deb822ParagraphElement]], bool]
1848 ] = None
1849 is_substvars_disabled_even_if_allowed_by_stanza: bool = False
1851 def synopsis_translated(
1852 self, translation_provider: Union["DebputyLanguageServer", "LintState"]
1853 ) -> str:
1854 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
1855 self.translation_context,
1856 self.synopsis,
1857 )
1859 def long_description_translated(
1860 self, translation_provider: Union["DebputyLanguageServer", "LintState"]
1861 ) -> str:
1862 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext(
1863 self.translation_context,
1864 self.long_description,
1865 )
1867 def _can_complete_field_in_stanza(
1868 self,
1869 stanza_parts: Sequence[Deb822ParagraphElement],
1870 ) -> bool:
1871 return (
1872 self.can_complete_field_in_stanza is None
1873 or self.can_complete_field_in_stanza(stanza_parts)
1874 )
1876 def complete_field(
1877 self,
1878 lint_state: LintState,
1879 stanza_parts: Sequence[Deb822ParagraphElement],
1880 markdown_kind: MarkupKind,
1881 ) -> Optional[CompletionItem]:
1882 if not self._can_complete_field_in_stanza(stanza_parts):
1883 return None
1884 name = self.name
1885 complete_as = name + ": "
1886 options = self.value_options_for_completer(
1887 lint_state,
1888 stanza_parts,
1889 "",
1890 markdown_kind,
1891 is_completion_for_field=True,
1892 )
1893 if options is not None and len(options) == 1:
1894 value = options[0].insert_text
1895 if value is not None: 1895 ↛ 1897line 1895 didn't jump to line 1897 because the condition on line 1895 was always true
1896 complete_as += value
1897 tags = []
1898 is_deprecated = False
1899 if self.replaced_by or self.deprecated_with_no_replacement:
1900 is_deprecated = True
1901 tags.append(CompletionItemTag.Deprecated)
1903 doc = self.long_description
1904 if doc:
1905 doc = MarkupContent(
1906 value=doc,
1907 kind=markdown_kind,
1908 )
1909 else:
1910 doc = None
1912 return CompletionItem(
1913 name,
1914 insert_text=complete_as,
1915 deprecated=is_deprecated,
1916 tags=tags,
1917 detail=format_comp_item_synopsis_doc(
1918 self.usage_hint,
1919 self.synopsis_translated(lint_state),
1920 is_deprecated,
1921 ),
1922 documentation=doc,
1923 )
1925 def _complete_files(
1926 self,
1927 base_dir: Optional[VirtualPathBase],
1928 value_being_completed: str,
1929 *,
1930 is_dep5_file_list: bool = False,
1931 supports_dir_match: bool = False,
1932 supports_spaces_in_filename: bool = False,
1933 match_non_persistent_paths: bool = False,
1934 ) -> Optional[Sequence[CompletionItem]]:
1935 _info(f"_complete_files: {base_dir.fs_path} - {value_being_completed!r}")
1936 if base_dir is None or not base_dir.is_dir:
1937 return None
1939 if is_dep5_file_list:
1940 supports_spaces_in_filename = True
1941 supports_dir_match = False
1942 match_non_persistent_paths = False
1944 if value_being_completed == "":
1945 current_dir = base_dir
1946 unmatched_parts: Sequence[str] = ()
1947 else:
1948 current_dir, unmatched_parts = base_dir.attempt_lookup(
1949 value_being_completed
1950 )
1952 if len(unmatched_parts) > 1:
1953 # Unknown directory part / glob, and we currently do not deal with that.
1954 return None
1955 if len(unmatched_parts) == 1 and unmatched_parts[0] == "*":
1956 # Avoid convincing the client to remove the star (seen with emacs)
1957 return None
1958 items = []
1960 path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path
1962 for child in current_dir.iterdir:
1963 if child.is_symlink and is_dep5_file_list:
1964 continue
1965 if not supports_spaces_in_filename and (
1966 " " in child.name or "\t" in child.name
1967 ):
1968 continue
1969 sort_text = (
1970 f"z-{child.name}" if child.name.startswith(".") else f"a-{child.name}"
1971 )
1972 if child.is_dir:
1973 if _should_ignore_dir(
1974 child,
1975 supports_dir_match=supports_dir_match,
1976 match_non_persistent_paths=match_non_persistent_paths,
1977 ):
1978 continue
1979 items.append(
1980 CompletionItem(
1981 f"{child.path}/",
1982 label_details=CompletionItemLabelDetails(
1983 description=child.path,
1984 ),
1985 insert_text=path_escaper(f"{child.path}/"),
1986 filter_text=f"{child.path}/",
1987 sort_text=sort_text,
1988 kind=CompletionItemKind.Folder,
1989 )
1990 )
1991 else:
1992 items.append(
1993 CompletionItem(
1994 child.path,
1995 label_details=CompletionItemLabelDetails(
1996 description=child.path,
1997 ),
1998 insert_text=path_escaper(child.path),
1999 filter_text=child.path,
2000 sort_text=sort_text,
2001 kind=CompletionItemKind.File,
2002 )
2003 )
2004 return items
2006 def value_options_for_completer(
2007 self,
2008 lint_state: LintState,
2009 stanza_parts: Sequence[Deb822ParagraphElement],
2010 value_being_completed: str,
2011 markdown_kind: MarkupKind,
2012 *,
2013 is_completion_for_field: bool = False,
2014 ) -> Optional[Sequence[CompletionItem]]:
2015 known_values = self.known_values
2016 if self.field_value_class == FieldValueClass.DEP5_FILE_LIST: 2016 ↛ 2017line 2016 didn't jump to line 2017 because the condition on line 2016 was never true
2017 if is_completion_for_field:
2018 return None
2019 return self._complete_files(
2020 lint_state.source_root,
2021 value_being_completed,
2022 is_dep5_file_list=True,
2023 )
2025 if known_values is None:
2026 return None
2027 if is_completion_for_field and (
2028 len(known_values) == 1
2029 or (
2030 len(known_values) == 2
2031 and self.warn_if_default
2032 and self.default_value is not None
2033 )
2034 ):
2035 value = next( 2035 ↛ exitline 2035 didn't jump to the function exit
2036 iter(v for v in self.known_values if v != self.default_value),
2037 None,
2038 )
2039 if value is None: 2039 ↛ 2040line 2039 didn't jump to line 2040 because the condition on line 2039 was never true
2040 return None
2041 return [CompletionItem(value, insert_text=value)]
2042 return [
2043 CompletionItem(
2044 keyword.value,
2045 insert_text=keyword.value,
2046 sort_text=keyword.resolve_sort_text(
2047 lint_state,
2048 stanza_parts,
2049 value_being_completed,
2050 ),
2051 detail=format_comp_item_synopsis_doc(
2052 keyword.usage_hint,
2053 keyword.synopsis_translated(lint_state),
2054 keyword.is_deprecated,
2055 ),
2056 deprecated=keyword.is_deprecated,
2057 tags=[CompletionItemTag.Deprecated] if keyword.is_deprecated else None,
2058 documentation=(
2059 MarkupContent(value=keyword.long_description, kind=markdown_kind)
2060 if keyword.long_description
2061 else None
2062 ),
2063 )
2064 for keyword in known_values.values()
2065 if keyword.is_keyword_valid_completion_in_stanza(stanza_parts)
2066 ]
2068 def field_omitted_diagnostics(
2069 self,
2070 deb822_file: Deb822FileElement,
2071 representation_field_range: "TERange",
2072 stanza: Deb822ParagraphElement,
2073 stanza_position: "TEPosition",
2074 header_stanza: Optional[Deb822FileElement],
2075 lint_state: LintState,
2076 ) -> None:
2077 missing_field_severity = self.missing_field_severity
2078 if missing_field_severity is None: 2078 ↛ 2081line 2078 didn't jump to line 2081 because the condition on line 2078 was always true
2079 return
2081 if (
2082 self.inheritable_from_other_stanza
2083 and header_stanza is not None
2084 and self.name in header_stanza
2085 ):
2086 return
2088 lint_state.emit_diagnostic(
2089 representation_field_range,
2090 f"Stanza is missing field {self.name}",
2091 missing_field_severity,
2092 self.missing_field_authority,
2093 )
2095 async def field_diagnostics(
2096 self,
2097 deb822_file: Deb822FileElement,
2098 kvpair: Deb822KeyValuePairElement,
2099 stanza: Deb822ParagraphElement,
2100 stanza_position: "TEPosition",
2101 kvpair_range_te: "TERange",
2102 lint_state: LintState,
2103 *,
2104 field_name_typo_reported: bool = False,
2105 ) -> None:
2106 field_name_token = kvpair.field_token
2107 field_name_range_te = kvpair.field_token.range_in_parent().relative_to(
2108 kvpair_range_te.start_pos
2109 )
2110 field_name = field_name_token.text
2111 # The `self.name` attribute is the canonical name whereas `field_name` is the name used.
2112 # This distinction is important for `d/control` where `X[CBS]-` prefixes might be used
2113 # in one but not the other.
2114 field_value = stanza[field_name]
2115 self._diagnostics_for_field_name(
2116 kvpair_range_te,
2117 field_name_token,
2118 field_name_range_te,
2119 field_name_typo_reported,
2120 lint_state,
2121 )
2122 if self.custom_field_check is not None:
2123 self.custom_field_check(
2124 self,
2125 deb822_file,
2126 kvpair,
2127 kvpair_range_te,
2128 field_name_range_te,
2129 stanza,
2130 stanza_position,
2131 lint_state,
2132 )
2133 self._dep5_file_list_diagnostics(kvpair, kvpair_range_te.start_pos, lint_state)
2134 if self.spellcheck_value:
2135 words = kvpair.interpret_as(LIST_SPACE_SEPARATED_INTERPRETATION)
2136 spell_checker = lint_state.spellchecker()
2137 value_position = kvpair.value_element.position_in_parent().relative_to(
2138 kvpair_range_te.start_pos
2139 )
2140 async for word_ref in lint_state.slow_iter(
2141 words.iter_value_references(), yield_every=25
2142 ):
2143 token = word_ref.value
2144 for word, pos, endpos in spell_checker.iter_words(token):
2145 corrections = spell_checker.provide_corrections_for(word)
2146 if not corrections:
2147 continue
2148 word_loc = word_ref.locatable
2149 word_pos_te = word_loc.position_in_parent().relative_to(
2150 value_position
2151 )
2152 if pos: 2152 ↛ 2153line 2152 didn't jump to line 2153 because the condition on line 2152 was never true
2153 word_pos_te = TEPosition(0, pos).relative_to(word_pos_te)
2154 word_size = TERange(
2155 START_POSITION,
2156 TEPosition(0, endpos - pos),
2157 )
2158 lint_state.emit_diagnostic(
2159 TERange.from_position_and_size(word_pos_te, word_size),
2160 f'Spelling "{word}"',
2161 "spelling",
2162 "debputy",
2163 quickfixes=[
2164 propose_correct_text_quick_fix(c) for c in corrections
2165 ],
2166 enable_non_interactive_auto_fix=False,
2167 )
2168 else:
2169 self._known_value_diagnostics(
2170 kvpair,
2171 kvpair_range_te.start_pos,
2172 lint_state,
2173 )
2175 if self.warn_if_default and field_value == self.default_value: 2175 ↛ 2176line 2175 didn't jump to line 2176 because the condition on line 2175 was never true
2176 lint_state.emit_diagnostic(
2177 kvpair_range_te,
2178 f'The field "{field_name}" is redundant as it is set to the default value and the field'
2179 " should only be used in exceptional cases.",
2180 "warning",
2181 "debputy",
2182 )
2184 def _diagnostics_for_field_name(
2185 self,
2186 kvpair_range: "TERange",
2187 token: Deb822FieldNameToken,
2188 token_range: "TERange",
2189 typo_detected: bool,
2190 lint_state: LintState,
2191 ) -> None:
2192 field_name = token.text
2193 # Defeat the case-insensitivity from python-debian
2194 field_name_cased = str(field_name)
2195 if self.deprecated_with_no_replacement:
2196 lint_state.emit_diagnostic(
2197 kvpair_range,
2198 f'"{field_name_cased}" is deprecated and no longer used',
2199 "warning",
2200 "debputy",
2201 quickfixes=[propose_remove_range_quick_fix()],
2202 tags=[DiagnosticTag.Deprecated],
2203 )
2204 elif self.replaced_by is not None:
2205 lint_state.emit_diagnostic(
2206 token_range,
2207 f'"{field_name_cased}" has been replaced by "{self.replaced_by}"',
2208 "warning",
2209 "debputy",
2210 tags=[DiagnosticTag.Deprecated],
2211 quickfixes=[propose_correct_text_quick_fix(self.replaced_by)],
2212 )
2214 if not typo_detected and field_name_cased != self.name:
2215 lint_state.emit_diagnostic(
2216 token_range,
2217 f'Non-canonical spelling of "{self.name}"',
2218 "pedantic",
2219 self.unknown_value_authority,
2220 quickfixes=[propose_correct_text_quick_fix(self.name)],
2221 )
2223 def _dep5_file_list_diagnostics(
2224 self,
2225 kvpair: Deb822KeyValuePairElement,
2226 kvpair_position: "TEPosition",
2227 lint_state: LintState,
2228 ) -> None:
2229 source_root = lint_state.source_root
2230 if (
2231 self.field_value_class != FieldValueClass.DEP5_FILE_LIST
2232 or source_root is None
2233 ):
2234 return
2235 interpreter = self.field_value_class.interpreter()
2236 values = kvpair.interpret_as(interpreter)
2237 value_off = kvpair.value_element.position_in_parent().relative_to(
2238 kvpair_position
2239 )
2241 assert interpreter is not None
2243 for token in values.iter_parts():
2244 if token.is_whitespace:
2245 continue
2246 text = token.convert_to_text()
2247 if "?" in text or "*" in text: 2247 ↛ 2249line 2247 didn't jump to line 2249 because the condition on line 2247 was never true
2248 # TODO: We should validate these as well
2249 continue
2250 matched_path, missing_part = source_root.attempt_lookup(text)
2251 # It is common practice to delete "dirty" files during clean. This causes files listed
2252 # in `debian/copyright` to go missing and as a consequence, we do not validate whether
2253 # they are present (that would require us to check the `.orig.tar`, which we could but
2254 # do not have the infrastructure for).
2255 if not missing_part and matched_path.is_dir: 2255 ↛ 2243line 2255 didn't jump to line 2243 because the condition on line 2255 was always true
2256 path_range_te = token.range_in_parent().relative_to(value_off)
2257 lint_state.emit_diagnostic(
2258 path_range_te,
2259 "Directories cannot be a match. Use `dir/*` to match everything in it",
2260 "warning",
2261 self.unknown_value_authority,
2262 quickfixes=[
2263 propose_correct_text_quick_fix(f"{matched_path.path}/*")
2264 ],
2265 )
2267 def _known_value_diagnostics(
2268 self,
2269 kvpair: Deb822KeyValuePairElement,
2270 kvpair_position: "TEPosition",
2271 lint_state: LintState,
2272 ) -> None:
2273 unknown_value_severity = self.unknown_value_diagnostic_severity
2274 interpreter = self.field_value_class.interpreter()
2275 if interpreter is None:
2276 return
2277 try:
2278 values = kvpair.interpret_as(interpreter)
2279 except ValueError:
2280 value_range = kvpair.value_element.range_in_parent().relative_to(
2281 kvpair_position
2282 )
2283 lint_state.emit_diagnostic(
2284 value_range,
2285 "Error while parsing field (diagnostics related to this field may be incomplete)",
2286 "pedantic",
2287 "debputy",
2288 )
2289 return
2290 value_off = kvpair.value_element.position_in_parent().relative_to(
2291 kvpair_position
2292 )
2294 last_token_non_ws_sep_token: Optional[TE] = None
2295 for token in values.iter_parts():
2296 if token.is_whitespace:
2297 continue
2298 if not token.is_separator:
2299 last_token_non_ws_sep_token = None
2300 continue
2301 if last_token_non_ws_sep_token is not None:
2302 sep_range_te = token.range_in_parent().relative_to(value_off)
2303 lint_state.emit_diagnostic(
2304 sep_range_te,
2305 "Duplicate separator",
2306 "error",
2307 self.unknown_value_authority,
2308 )
2309 last_token_non_ws_sep_token = token
2311 allowed_values = self.known_values
2312 if not allowed_values:
2313 return
2315 first_value = None
2316 first_exclusive_value_ref = None
2317 first_exclusive_value = None
2318 has_emitted_for_exclusive = False
2320 for value_ref in values.iter_value_references():
2321 value = value_ref.value
2322 if ( 2322 ↛ 2326line 2322 didn't jump to line 2326
2323 first_value is not None
2324 and self.field_value_class == FieldValueClass.SINGLE_VALUE
2325 ):
2326 value_loc = value_ref.locatable
2327 range_position_te = value_loc.range_in_parent().relative_to(value_off)
2328 lint_state.emit_diagnostic(
2329 range_position_te,
2330 f"The field {self.name} can only have exactly one value.",
2331 "error",
2332 self.unknown_value_authority,
2333 )
2334 # TODO: Add quickfix if the value is also invalid
2335 continue
2337 if first_exclusive_value_ref is not None and not has_emitted_for_exclusive:
2338 assert first_exclusive_value is not None
2339 value_loc = first_exclusive_value_ref.locatable
2340 value_range_te = value_loc.range_in_parent().relative_to(value_off)
2341 lint_state.emit_diagnostic(
2342 value_range_te,
2343 f'The value "{first_exclusive_value}" cannot be used with other values.',
2344 "error",
2345 self.unknown_value_authority,
2346 )
2348 known_value, unknown_value_message, unknown_severity, typo_fix_data = (
2349 _unknown_value_check(
2350 self.name,
2351 value,
2352 self.known_values,
2353 unknown_value_severity,
2354 )
2355 )
2356 value_loc = value_ref.locatable
2357 value_range = value_loc.range_in_parent().relative_to(value_off)
2359 if known_value and known_value.is_exclusive:
2360 first_exclusive_value = known_value.value # In case of typos.
2361 first_exclusive_value_ref = value_ref
2362 if first_value is not None:
2363 has_emitted_for_exclusive = True
2364 lint_state.emit_diagnostic(
2365 value_range,
2366 f'The value "{known_value.value}" cannot be used with other values.',
2367 "error",
2368 self.unknown_value_authority,
2369 )
2371 if first_value is None:
2372 first_value = value
2374 if unknown_value_message is not None:
2375 assert unknown_severity is not None
2376 lint_state.emit_diagnostic(
2377 value_range,
2378 unknown_value_message,
2379 unknown_severity,
2380 self.unknown_value_authority,
2381 quickfixes=typo_fix_data,
2382 )
2384 if known_value is not None and known_value.is_deprecated:
2385 replacement = known_value.replaced_by
2386 if replacement is not None: 2386 ↛ 2392line 2386 didn't jump to line 2392
2387 obsolete_value_message = (
2388 f'The value "{value}" has been replaced by "{replacement}"'
2389 )
2390 obsolete_fix_data = [propose_correct_text_quick_fix(replacement)]
2391 else:
2392 obsolete_value_message = (
2393 f'The value "{value}" is obsolete without a single replacement'
2394 )
2395 obsolete_fix_data = None
2396 lint_state.emit_diagnostic(
2397 value_range,
2398 obsolete_value_message,
2399 "warning",
2400 "debputy",
2401 quickfixes=obsolete_fix_data,
2402 )
2404 def reformat_field(
2405 self,
2406 effective_preference: "EffectiveFormattingPreference",
2407 stanza_range: TERange,
2408 kvpair: Deb822KeyValuePairElement,
2409 formatter: FormatterCallback,
2410 position_codec: LintCapablePositionCodec,
2411 lines: List[str],
2412 ) -> Iterable[TextEdit]:
2413 kvpair_range = kvpair.range_in_parent().relative_to(stanza_range.start_pos)
2414 return trim_end_of_line_whitespace(
2415 position_codec,
2416 lines,
2417 line_range=range(
2418 kvpair_range.start_pos.line_position,
2419 kvpair_range.end_pos.line_position,
2420 ),
2421 )
2424@dataclasses.dataclass(slots=True)
2425class DctrlLikeKnownField(Deb822KnownField):
2427 def reformat_field(
2428 self,
2429 effective_preference: "EffectiveFormattingPreference",
2430 stanza_range: TERange,
2431 kvpair: Deb822KeyValuePairElement,
2432 formatter: FormatterCallback,
2433 position_codec: LintCapablePositionCodec,
2434 lines: List[str],
2435 ) -> Iterable[TextEdit]:
2436 interpretation = self.field_value_class.interpreter()
2437 if (
2438 not effective_preference.deb822_normalize_field_content
2439 or interpretation is None
2440 ):
2441 yield from super(DctrlLikeKnownField, self).reformat_field(
2442 effective_preference,
2443 stanza_range,
2444 kvpair,
2445 formatter,
2446 position_codec,
2447 lines,
2448 )
2449 return
2450 if not self.reformattable_field:
2451 yield from super(DctrlLikeKnownField, self).reformat_field(
2452 effective_preference,
2453 stanza_range,
2454 kvpair,
2455 formatter,
2456 position_codec,
2457 lines,
2458 )
2459 return
2460 seen: Set[str] = set()
2461 old_kvpair_range = kvpair.range_in_parent()
2462 sort = self.is_sortable_field
2464 # Avoid the context manager as we do not want to perform the change (it would contaminate future ranges)
2465 field_content = kvpair.interpret_as(interpretation)
2466 old_value = field_content.convert_to_text(with_field_name=True)
2467 for package_ref in field_content.iter_value_references():
2468 value = package_ref.value
2469 value_range = package_ref.locatable.range_in_parent().relative_to(
2470 stanza_range.start_pos
2471 )
2472 sublines = lines[
2473 value_range.start_pos.line_position : value_range.end_pos.line_position
2474 ]
2476 # debputy#112: Avoid truncating "inline comments"
2477 if any(line.startswith("#") for line in sublines):
2478 return
2479 if self.is_relationship_field:
2480 new_value = " | ".join(x.strip() for x in value.split("|"))
2481 else:
2482 new_value = value
2483 if not sort or new_value not in seen:
2484 if new_value != value:
2485 package_ref.value = new_value
2486 seen.add(new_value)
2487 else:
2488 package_ref.remove()
2489 if sort:
2490 field_content.sort(key=_sort_packages_key)
2491 field_content.value_formatter(formatter)
2492 field_content.reformat_when_finished()
2494 new_value = field_content.convert_to_text(with_field_name=True)
2495 if new_value != old_value:
2496 range_server_units = te_range_to_lsp(
2497 old_kvpair_range.relative_to(stanza_range.start_pos)
2498 )
2499 yield TextEdit(
2500 position_codec.range_to_client_units(lines, range_server_units),
2501 new_value,
2502 )
2504 @property
2505 def reformattable_field(self) -> bool:
2506 return self.is_relationship_field or self.is_sortable_field
2508 @property
2509 def is_relationship_field(self) -> bool:
2510 return False
2512 @property
2513 def is_sortable_field(self) -> bool:
2514 return self.is_relationship_field
2517@dataclasses.dataclass(slots=True)
2518class DTestsCtrlKnownField(DctrlLikeKnownField):
2519 @property
2520 def is_relationship_field(self) -> bool:
2521 return self.name == "Depends"
2523 @property
2524 def is_sortable_field(self) -> bool:
2525 return self.is_relationship_field or self.name in (
2526 "Features",
2527 "Restrictions",
2528 "Tests",
2529 )
2532@dataclasses.dataclass(slots=True)
2533class DctrlKnownField(DctrlLikeKnownField):
2535 def field_omitted_diagnostics(
2536 self,
2537 deb822_file: Deb822FileElement,
2538 representation_field_range: "TERange",
2539 stanza: Deb822ParagraphElement,
2540 stanza_position: "TEPosition",
2541 header_stanza: Optional[Deb822FileElement],
2542 lint_state: LintState,
2543 ) -> None:
2544 missing_field_severity = self.missing_field_severity
2545 if missing_field_severity is None:
2546 return
2548 if (
2549 self.inheritable_from_other_stanza
2550 and header_stanza is not None
2551 and self.name in header_stanza
2552 ):
2553 return
2555 if self.name == "Standards-Version":
2556 stanzas = list(deb822_file)[1:]
2557 if all(s.get("Package-Type") == "udeb" for s in stanzas):
2558 return
2560 lint_state.emit_diagnostic(
2561 representation_field_range,
2562 f"Stanza is missing field {self.name}",
2563 missing_field_severity,
2564 self.missing_field_authority,
2565 )
2567 def reformat_field(
2568 self,
2569 effective_preference: "EffectiveFormattingPreference",
2570 stanza_range: TERange,
2571 kvpair: Deb822KeyValuePairElement,
2572 formatter: FormatterCallback,
2573 position_codec: LintCapablePositionCodec,
2574 lines: List[str],
2575 ) -> Iterable[TextEdit]:
2576 if (
2577 self.name == "Architecture"
2578 and effective_preference.deb822_normalize_field_content
2579 ):
2580 interpretation = self.field_value_class.interpreter()
2581 assert interpretation is not None
2582 archs = list(kvpair.interpret_as(interpretation))
2583 # Sort, with wildcard entries (such as linux-any) first:
2584 archs = sorted(archs, key=lambda x: ("any" not in x, x))
2585 new_value = f"{kvpair.field_name}: {' '.join(archs)}\n"
2586 if new_value != kvpair.convert_to_text():
2587 kvpair_range = te_range_to_lsp(
2588 kvpair.range_in_parent().relative_to(stanza_range.start_pos)
2589 )
2590 return [
2591 TextEdit(
2592 position_codec.range_to_client_units(lines, kvpair_range),
2593 new_value,
2594 )
2595 ]
2596 return tuple()
2598 return super(DctrlKnownField, self).reformat_field(
2599 effective_preference,
2600 stanza_range,
2601 kvpair,
2602 formatter,
2603 position_codec,
2604 lines,
2605 )
2607 @property
2608 def is_relationship_field(self) -> bool:
2609 name_lc = self.name.lower()
2610 return (
2611 name_lc in all_package_relationship_fields()
2612 or name_lc in all_source_relationship_fields()
2613 )
2615 @property
2616 def reformattable_field(self) -> bool:
2617 return self.is_relationship_field or self.name == "Uploaders"
2620@dataclasses.dataclass(slots=True)
2621class DctrlRelationshipKnownField(DctrlKnownField):
2622 allowed_version_operators: FrozenSet[str] = frozenset()
2623 supports_or_relation: bool = True
2625 @property
2626 def is_relationship_field(self) -> bool:
2627 return True
2630SOURCE_FIELDS = _fields(
2631 DctrlKnownField(
2632 "Source",
2633 FieldValueClass.SINGLE_VALUE,
2634 custom_field_check=_combined_custom_field_check(
2635 _each_value_match_regex_validation(PKGNAME_REGEX),
2636 _has_packaging_expected_file(
2637 "copyright",
2638 "No copyright file (package license)",
2639 severity="warning",
2640 ),
2641 _has_packaging_expected_file(
2642 "changelog",
2643 "No Debian changelog file",
2644 severity="error",
2645 ),
2646 _has_build_instructions,
2647 ),
2648 missing_field_severity="error",
2649 ),
2650 DctrlKnownField(
2651 "Standards-Version",
2652 FieldValueClass.SINGLE_VALUE,
2653 # Conditionally mandatory (special-cased by field_omitted_diagnostics)
2654 missing_field_severity="error",
2655 custom_field_check=_sv_field_validation,
2656 ),
2657 DctrlKnownField(
2658 "Section",
2659 FieldValueClass.SINGLE_VALUE,
2660 known_values=ALL_SECTIONS,
2661 unknown_value_diagnostic_severity="warning",
2662 ),
2663 DctrlKnownField(
2664 "Priority",
2665 FieldValueClass.SINGLE_VALUE,
2666 ),
2667 DctrlKnownField(
2668 "Maintainer",
2669 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST,
2670 missing_field_severity="error",
2671 custom_field_check=_maintainer_field_validator,
2672 ),
2673 DctrlRelationshipKnownField(
2674 "Build-Depends",
2675 FieldValueClass.COMMA_SEPARATED_LIST,
2676 custom_field_check=_dctrl_validate_dep,
2677 ),
2678 DctrlRelationshipKnownField(
2679 "Build-Depends-Arch",
2680 FieldValueClass.COMMA_SEPARATED_LIST,
2681 custom_field_check=_dctrl_validate_dep,
2682 ),
2683 DctrlRelationshipKnownField(
2684 "Build-Depends-Indep",
2685 FieldValueClass.COMMA_SEPARATED_LIST,
2686 custom_field_check=_dctrl_validate_dep,
2687 ),
2688 DctrlRelationshipKnownField(
2689 "Build-Conflicts",
2690 FieldValueClass.COMMA_SEPARATED_LIST,
2691 supports_or_relation=False,
2692 custom_field_check=_dctrl_validate_dep,
2693 ),
2694 DctrlRelationshipKnownField(
2695 "Build-Conflicts-Arch",
2696 FieldValueClass.COMMA_SEPARATED_LIST,
2697 supports_or_relation=False,
2698 custom_field_check=_dctrl_validate_dep,
2699 ),
2700 DctrlRelationshipKnownField(
2701 "Build-Conflicts-Indep",
2702 FieldValueClass.COMMA_SEPARATED_LIST,
2703 supports_or_relation=False,
2704 custom_field_check=_dctrl_validate_dep,
2705 ),
2706 DctrlKnownField(
2707 "Rules-Requires-Root",
2708 FieldValueClass.SPACE_SEPARATED_LIST,
2709 unknown_value_diagnostic_severity=None,
2710 custom_field_check=_rrr_build_driver_mismatch,
2711 ),
2712 DctrlKnownField(
2713 "X-Style",
2714 FieldValueClass.SINGLE_VALUE,
2715 known_values=ALL_PUBLIC_NAMED_STYLES,
2716 unknown_value_diagnostic_severity="warning",
2717 ),
2718 DctrlKnownField(
2719 "Homepage",
2720 FieldValueClass.SINGLE_VALUE,
2721 missing_field_severity="pedantic",
2722 custom_field_check=_validate_homepage_field,
2723 ),
2724)
2727BINARY_FIELDS = _fields(
2728 DctrlKnownField(
2729 "Package",
2730 FieldValueClass.SINGLE_VALUE,
2731 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
2732 missing_field_severity="error",
2733 ),
2734 DctrlKnownField(
2735 "Architecture",
2736 FieldValueClass.SPACE_SEPARATED_LIST,
2737 missing_field_severity="error",
2738 unknown_value_diagnostic_severity=None,
2739 # FIXME: Specialize validation for architecture ("!foo" is not a "typo" and should have a better warning)
2740 known_values=allowed_values(*dpkg_arch_and_wildcards()),
2741 ),
2742 DctrlKnownField(
2743 "Pre-Depends",
2744 FieldValueClass.COMMA_SEPARATED_LIST,
2745 custom_field_check=_dctrl_validate_dep,
2746 ),
2747 DctrlKnownField(
2748 "Depends",
2749 FieldValueClass.COMMA_SEPARATED_LIST,
2750 custom_field_check=_dctrl_validate_dep,
2751 ),
2752 DctrlKnownField(
2753 "Recommends",
2754 FieldValueClass.COMMA_SEPARATED_LIST,
2755 custom_field_check=_dctrl_validate_dep,
2756 ),
2757 DctrlKnownField(
2758 "Suggests",
2759 FieldValueClass.COMMA_SEPARATED_LIST,
2760 custom_field_check=_dctrl_validate_dep,
2761 ),
2762 DctrlKnownField(
2763 "Enhances",
2764 FieldValueClass.COMMA_SEPARATED_LIST,
2765 custom_field_check=_dctrl_validate_dep,
2766 ),
2767 DctrlRelationshipKnownField(
2768 "Provides",
2769 FieldValueClass.COMMA_SEPARATED_LIST,
2770 custom_field_check=_dctrl_validate_dep,
2771 supports_or_relation=False,
2772 allowed_version_operators=frozenset(["="]),
2773 ),
2774 DctrlRelationshipKnownField(
2775 "Conflicts",
2776 FieldValueClass.COMMA_SEPARATED_LIST,
2777 custom_field_check=_dctrl_validate_dep,
2778 supports_or_relation=False,
2779 ),
2780 DctrlRelationshipKnownField(
2781 "Breaks",
2782 FieldValueClass.COMMA_SEPARATED_LIST,
2783 custom_field_check=_dctrl_validate_dep,
2784 supports_or_relation=False,
2785 ),
2786 DctrlRelationshipKnownField(
2787 "Replaces",
2788 FieldValueClass.COMMA_SEPARATED_LIST,
2789 custom_field_check=_dctrl_validate_dep,
2790 ),
2791 DctrlKnownField(
2792 "Build-Profiles",
2793 FieldValueClass.BUILD_PROFILES_LIST,
2794 ),
2795 DctrlKnownField(
2796 "Section",
2797 FieldValueClass.SINGLE_VALUE,
2798 missing_field_severity="error",
2799 inheritable_from_other_stanza=True,
2800 known_values=ALL_SECTIONS,
2801 unknown_value_diagnostic_severity="warning",
2802 ),
2803 DctrlKnownField(
2804 "Priority",
2805 FieldValueClass.SINGLE_VALUE,
2806 warn_if_default=False,
2807 missing_field_severity="error",
2808 inheritable_from_other_stanza=True,
2809 ),
2810 DctrlRelationshipKnownField(
2811 "Built-Using",
2812 FieldValueClass.COMMA_SEPARATED_LIST,
2813 custom_field_check=_arch_not_all_only_field_validation,
2814 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs,
2815 supports_or_relation=False,
2816 allowed_version_operators=frozenset(["="]),
2817 ),
2818 DctrlRelationshipKnownField(
2819 "Static-Built-Using",
2820 FieldValueClass.COMMA_SEPARATED_LIST,
2821 custom_field_check=_arch_not_all_only_field_validation,
2822 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs,
2823 supports_or_relation=False,
2824 allowed_version_operators=frozenset(["="]),
2825 ),
2826 DctrlKnownField(
2827 "Multi-Arch",
2828 FieldValueClass.SINGLE_VALUE,
2829 # Explicit "no" tends to be used as "someone reviewed this and concluded no", so we do
2830 # not warn about it being explicitly "no".
2831 warn_if_default=False,
2832 custom_field_check=_dctrl_ma_field_validation,
2833 known_values=allowed_values(
2834 Keyword(
2835 "same",
2836 can_complete_keyword_in_stanza=_complete_only_in_arch_dep_pkgs,
2837 ),
2838 ),
2839 ),
2840 DctrlKnownField(
2841 "XB-Installer-Menu-Item",
2842 FieldValueClass.SINGLE_VALUE,
2843 can_complete_field_in_stanza=_complete_only_for_udeb_pkgs,
2844 custom_field_check=_combined_custom_field_check(
2845 _udeb_only_field_validation,
2846 _each_value_match_regex_validation(re.compile(r"^[1-9]\d{3,4}$")),
2847 ),
2848 ),
2849 DctrlKnownField(
2850 "X-DH-Build-For-Type",
2851 FieldValueClass.SINGLE_VALUE,
2852 custom_field_check=_arch_not_all_only_field_validation,
2853 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs,
2854 ),
2855 DctrlKnownField(
2856 "X-Time64-Compat",
2857 FieldValueClass.SINGLE_VALUE,
2858 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs,
2859 custom_field_check=_combined_custom_field_check(
2860 _each_value_match_regex_validation(PKGNAME_REGEX),
2861 _arch_not_all_only_field_validation,
2862 ),
2863 ),
2864 DctrlKnownField(
2865 "Description",
2866 FieldValueClass.FREE_TEXT_FIELD,
2867 # It will build just fine. But no one will know what it is for, so it probably won't be installed
2868 missing_field_severity="warning",
2869 custom_field_check=dctrl_description_validator,
2870 ),
2871 DctrlKnownField(
2872 "XB-Cnf-Visible-Pkgname",
2873 FieldValueClass.SINGLE_VALUE,
2874 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
2875 ),
2876 DctrlKnownField(
2877 "Homepage",
2878 FieldValueClass.SINGLE_VALUE,
2879 inheritable_from_other_stanza=True,
2880 show_as_inherited=False,
2881 custom_field_check=_validate_homepage_field,
2882 ),
2883)
2884_DEP5_HEADER_FIELDS = _fields(
2885 Deb822KnownField(
2886 "Format",
2887 FieldValueClass.SINGLE_VALUE,
2888 missing_field_severity="error",
2889 ),
2890)
2891_DEP5_FILES_FIELDS = _fields(
2892 Deb822KnownField(
2893 "Files",
2894 FieldValueClass.DEP5_FILE_LIST,
2895 custom_field_check=_dep5_files_check,
2896 missing_field_severity="error",
2897 ),
2898 Deb822KnownField(
2899 "License",
2900 FieldValueClass.FREE_TEXT_FIELD,
2901 missing_field_severity="error",
2902 ),
2903 Deb822KnownField(
2904 "Comment",
2905 FieldValueClass.FREE_TEXT_FIELD,
2906 ),
2907)
2908_DEP5_LICENSE_FIELDS = _fields(
2909 Deb822KnownField(
2910 "License",
2911 FieldValueClass.FREE_TEXT_FIELD,
2912 missing_field_severity="error",
2913 ),
2914)
2916_DTESTSCTRL_FIELDS = _fields(
2917 DTestsCtrlKnownField(
2918 "Architecture",
2919 FieldValueClass.SPACE_SEPARATED_LIST,
2920 unknown_value_diagnostic_severity=None,
2921 # FIXME: Specialize validation for architecture ("!fou" to "foo" would be bad)
2922 known_values=allowed_values(*dpkg_arch_and_wildcards(allow_negations=True)),
2923 ),
2924 DTestsCtrlKnownField(
2925 "Restrictions",
2926 FieldValueClass.COMMA_OR_SPACE_SEPARATED_LIST,
2927 unknown_value_diagnostic_severity="warning",
2928 ),
2929)
2932@dataclasses.dataclass(slots=True)
2933class StanzaMetadata(Mapping[str, F], Generic[F], ABC):
2934 stanza_type_name: str
2935 stanza_fields: Mapping[str, F]
2936 is_substvars_allowed_in_stanza: bool
2938 async def stanza_diagnostics(
2939 self,
2940 deb822_file: Deb822FileElement,
2941 stanza: Deb822ParagraphElement,
2942 stanza_position_in_file: "TEPosition",
2943 lint_state: LintState,
2944 *,
2945 inherit_from_stanza: Optional[Deb822ParagraphElement] = None,
2946 confusable_with_stanza_name: Optional[str] = None,
2947 confusable_with_stanza_metadata: Optional["StanzaMetadata[F]"] = None,
2948 ) -> None:
2949 if (confusable_with_stanza_name is None) ^ ( 2949 ↛ 2952line 2949 didn't jump to line 2952 because the condition on line 2949 was never true
2950 confusable_with_stanza_metadata is None
2951 ):
2952 raise ValueError(
2953 "confusable_with_stanza_name and confusable_with_stanza_metadata must be used together"
2954 )
2955 _, representation_field_range = self.stanza_representation(
2956 stanza,
2957 stanza_position_in_file,
2958 )
2959 known_fields = self.stanza_fields
2960 self.omitted_field_diagnostics(
2961 lint_state,
2962 deb822_file,
2963 stanza,
2964 stanza_position_in_file,
2965 inherit_from_stanza=inherit_from_stanza,
2966 representation_field_range=representation_field_range,
2967 )
2968 seen_fields: Dict[str, Tuple[str, str, "TERange", List[Range]]] = {}
2970 async for kvpair in lint_state.slow_iter(
2971 stanza.iter_parts_of_type(Deb822KeyValuePairElement),
2972 yield_every=1,
2973 ):
2974 field_name_token = kvpair.field_token
2975 field_name = field_name_token.text
2976 field_name_lc = field_name.lower()
2977 normalized_field_name_lc = self.normalize_field_name(field_name_lc)
2978 known_field = known_fields.get(normalized_field_name_lc)
2979 field_value = stanza[field_name]
2980 kvpair_range_te = kvpair.range_in_parent().relative_to(
2981 stanza_position_in_file
2982 )
2983 field_range = kvpair.field_token.range_in_parent().relative_to(
2984 kvpair_range_te.start_pos
2985 )
2986 field_position_te = field_range.start_pos
2987 field_name_typo_detected = False
2988 existing_field_range = seen_fields.get(normalized_field_name_lc)
2989 if existing_field_range is not None:
2990 existing_field_range[3].append(field_range)
2991 else:
2992 normalized_field_name = self.normalize_field_name(field_name)
2993 seen_fields[field_name_lc] = (
2994 field_name,
2995 normalized_field_name,
2996 field_range,
2997 [],
2998 )
3000 if known_field is None:
3001 candidates = detect_possible_typo(
3002 normalized_field_name_lc, known_fields
3003 )
3004 if candidates:
3005 known_field = known_fields[candidates[0]]
3006 field_range = TERange.from_position_and_size(
3007 field_position_te, kvpair.field_token.size()
3008 )
3009 field_name_typo_detected = True
3010 lint_state.emit_diagnostic(
3011 field_range,
3012 f'The "{field_name}" looks like a typo of "{known_field.name}".',
3013 "warning",
3014 "debputy",
3015 quickfixes=[
3016 propose_correct_text_quick_fix(known_fields[m].name)
3017 for m in candidates
3018 ],
3019 )
3020 if field_value.strip() == "": 3020 ↛ 3021line 3020 didn't jump to line 3021 because the condition on line 3020 was never true
3021 lint_state.emit_diagnostic(
3022 field_range,
3023 f"The {field_name} has no value. Either provide a value or remove it.",
3024 "error",
3025 "Policy 5.1",
3026 )
3027 continue
3028 if known_field is None:
3029 known_else_where = confusable_with_stanza_metadata.stanza_fields.get(
3030 normalized_field_name_lc
3031 )
3032 if known_else_where is not None:
3033 lint_state.emit_diagnostic(
3034 field_range,
3035 f"The {kvpair.field_name} is defined for use in the"
3036 f' "{confusable_with_stanza_name}" stanza. Please move it to the right place or remove it',
3037 "error",
3038 known_else_where.missing_field_authority,
3039 )
3040 continue
3041 await known_field.field_diagnostics(
3042 deb822_file,
3043 kvpair,
3044 stanza,
3045 stanza_position_in_file,
3046 kvpair_range_te,
3047 lint_state,
3048 field_name_typo_reported=field_name_typo_detected,
3049 )
3051 inherit_value = (
3052 inherit_from_stanza.get(field_name) if inherit_from_stanza else None
3053 )
3055 if (
3056 known_field.inheritable_from_other_stanza
3057 and inherit_value is not None
3058 and field_value == inherit_value
3059 ):
3060 quick_fix = propose_remove_range_quick_fix(
3061 proposed_title="Remove redundant definition"
3062 )
3063 lint_state.emit_diagnostic(
3064 kvpair_range_te,
3065 f"The field {field_name} duplicates the value from the Source stanza.",
3066 "informational",
3067 "debputy",
3068 quickfixes=[quick_fix],
3069 )
3070 for (
3071 field_name,
3072 normalized_field_name,
3073 field_range,
3074 duplicates,
3075 ) in seen_fields.values():
3076 if not duplicates:
3077 continue
3078 for dup_range in duplicates:
3079 lint_state.emit_diagnostic(
3080 dup_range,
3081 f"The {normalized_field_name} field name was used multiple times in this stanza."
3082 " Please ensure the field is only used once per stanza.",
3083 "error",
3084 "Policy 5.1",
3085 related_information=[
3086 lint_state.related_diagnostic_information(
3087 field_range,
3088 message=f"First definition of {field_name}",
3089 ),
3090 ],
3091 )
3093 def __getitem__(self, key: str) -> F:
3094 key_lc = key.lower()
3095 key_norm = normalize_dctrl_field_name(key_lc)
3096 return self.stanza_fields[key_norm]
3098 def __len__(self) -> int:
3099 return len(self.stanza_fields)
3101 def __iter__(self) -> Iterator[str]:
3102 return iter(self.stanza_fields.keys())
3104 def omitted_field_diagnostics(
3105 self,
3106 lint_state: LintState,
3107 deb822_file: Deb822FileElement,
3108 stanza: Deb822ParagraphElement,
3109 stanza_position: "TEPosition",
3110 *,
3111 inherit_from_stanza: Optional[Deb822ParagraphElement] = None,
3112 representation_field_range: Optional[Range] = None,
3113 ) -> None:
3114 if representation_field_range is None: 3114 ↛ 3115line 3114 didn't jump to line 3115 because the condition on line 3114 was never true
3115 _, representation_field_range = self.stanza_representation(
3116 stanza,
3117 stanza_position,
3118 )
3119 for known_field in self.stanza_fields.values():
3120 if known_field.name in stanza:
3121 continue
3123 known_field.field_omitted_diagnostics(
3124 deb822_file,
3125 representation_field_range,
3126 stanza,
3127 stanza_position,
3128 inherit_from_stanza,
3129 lint_state,
3130 )
3132 def _paragraph_representation_field(
3133 self,
3134 paragraph: Deb822ParagraphElement,
3135 ) -> Deb822KeyValuePairElement:
3136 return next(iter(paragraph.iter_parts_of_type(Deb822KeyValuePairElement)))
3138 def normalize_field_name(self, field_name: str) -> str:
3139 return field_name
3141 def stanza_representation(
3142 self,
3143 stanza: Deb822ParagraphElement,
3144 stanza_position: TEPosition,
3145 ) -> Tuple[Deb822KeyValuePairElement, TERange]:
3146 representation_field = self._paragraph_representation_field(stanza)
3147 representation_field_range = representation_field.range_in_parent().relative_to(
3148 stanza_position
3149 )
3150 return representation_field, representation_field_range
3152 def reformat_stanza(
3153 self,
3154 effective_preference: "EffectiveFormattingPreference",
3155 stanza: Deb822ParagraphElement,
3156 stanza_range: TERange,
3157 formatter: FormatterCallback,
3158 position_codec: LintCapablePositionCodec,
3159 lines: List[str],
3160 ) -> Iterable[TextEdit]:
3161 for known_field in self.stanza_fields.values():
3162 kvpair = stanza.get_kvpair_element((known_field.name, 0), use_get=True)
3163 if kvpair is None:
3164 continue
3165 yield from known_field.reformat_field(
3166 effective_preference,
3167 stanza_range,
3168 kvpair,
3169 formatter,
3170 position_codec,
3171 lines,
3172 )
3175@dataclasses.dataclass(slots=True)
3176class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]):
3177 pass
3180@dataclasses.dataclass(slots=True)
3181class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]):
3183 def normalize_field_name(self, field_name: str) -> str:
3184 return normalize_dctrl_field_name(field_name)
3187@dataclasses.dataclass(slots=True)
3188class DTestsCtrlStanzaMetadata(StanzaMetadata[DTestsCtrlKnownField]):
3190 def omitted_field_diagnostics(
3191 self,
3192 lint_state: LintState,
3193 deb822_file: Deb822FileElement,
3194 stanza: Deb822ParagraphElement,
3195 stanza_position: "TEPosition",
3196 *,
3197 inherit_from_stanza: Optional[Deb822ParagraphElement] = None,
3198 representation_field_range: Optional[Range] = None,
3199 ) -> None:
3200 if representation_field_range is None: 3200 ↛ 3201line 3200 didn't jump to line 3201 because the condition on line 3200 was never true
3201 _, representation_field_range = self.stanza_representation(
3202 stanza,
3203 stanza_position,
3204 )
3205 super(DTestsCtrlStanzaMetadata, self).omitted_field_diagnostics(
3206 lint_state,
3207 deb822_file,
3208 stanza,
3209 stanza_position,
3210 representation_field_range=representation_field_range,
3211 inherit_from_stanza=inherit_from_stanza,
3212 )
3213 auth_ref = self.stanza_fields["tests"].missing_field_authority
3214 if "Tests" not in stanza and "Test-Command" not in stanza:
3215 lint_state.emit_diagnostic(
3216 representation_field_range,
3217 'Stanza must have either a "Tests" or a "Test-Command" field',
3218 "error",
3219 # TODO: Better authority_reference
3220 auth_ref,
3221 )
3222 if "Tests" in stanza and "Test-Command" in stanza:
3223 lint_state.emit_diagnostic(
3224 representation_field_range,
3225 'Stanza cannot have both a "Tests" and a "Test-Command" field',
3226 "error",
3227 # TODO: Better authority_reference
3228 auth_ref,
3229 )
3232def lsp_reference_data_dir() -> str:
3233 return os.path.join(
3234 os.path.dirname(__file__),
3235 "data",
3236 )
3239class Deb822FileMetadata(Generic[S, F]):
3241 def __init__(self) -> None:
3242 self._is_initialized = False
3243 self._data: Optional[Deb822ReferenceData] = None
3245 @property
3246 def reference_data_basename(self) -> str:
3247 raise NotImplementedError
3249 def _new_field(
3250 self,
3251 name: str,
3252 field_value_type: FieldValueClass,
3253 ) -> F:
3254 raise NotImplementedError
3256 def _reference_data(self) -> Deb822ReferenceData:
3257 ref = self._data
3258 if ref is not None: 3258 ↛ 3259line 3258 didn't jump to line 3259 because the condition on line 3258 was never true
3259 return ref
3261 p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath(
3262 self.reference_data_basename
3263 )
3265 with p.open("r", encoding="utf-8") as fd:
3266 raw = MANIFEST_YAML.load(fd)
3268 attr_path = AttributePath.root_path(p)
3269 ref = DEB822_REFERENCE_DATA_PARSER.parse_input(raw, attr_path)
3270 self._data = ref
3271 return ref
3273 @property
3274 def is_initialized(self) -> bool:
3275 return self._is_initialized
3277 def ensure_initialized(self) -> None:
3278 if self.is_initialized:
3279 return
3280 # Enables us to use __getitem__
3281 self._is_initialized = True
3282 ref_data = self._reference_data()
3283 ref_defs = ref_data.get("definitions")
3284 variables = {}
3285 ref_variables = ref_defs.get("variables", []) if ref_defs else []
3286 for ref_variable in ref_variables:
3287 name = ref_variable["name"]
3288 fallback = ref_variable["fallback"]
3289 variables[name] = fallback
3291 def _resolve_doc(template: Optional[str]) -> Optional[str]:
3292 if template is None: 3292 ↛ 3293line 3292 didn't jump to line 3293 because the condition on line 3292 was never true
3293 return None
3294 try:
3295 return template.format(**variables)
3296 except ValueError as e:
3297 template_escaped = template.replace("\n", "\\r")
3298 _error(f"Bad template: {template_escaped}: {e}")
3300 for ref_stanza_type in ref_data["stanza_types"]:
3301 stanza_name = ref_stanza_type["stanza_name"]
3302 stanza = self[stanza_name]
3303 stanza_fields = dict(stanza.stanza_fields)
3304 stanza.stanza_fields = stanza_fields
3305 for ref_field in ref_stanza_type["fields"]:
3306 _resolve_field(
3307 ref_field,
3308 stanza_fields,
3309 self._new_field,
3310 _resolve_doc,
3311 f"Stanza:{stanza.stanza_type_name}|Field:{ref_field['canonical_name']}",
3312 )
3314 def classify_stanza(self, stanza: Deb822ParagraphElement, stanza_idx: int) -> S:
3315 return self.guess_stanza_classification_by_idx(stanza_idx)
3317 def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S:
3318 raise NotImplementedError
3320 def stanza_types(self) -> Iterable[S]:
3321 raise NotImplementedError
3323 def __getitem__(self, item: str) -> S:
3324 raise NotImplementedError
3326 def get(self, item: str) -> Optional[S]:
3327 try:
3328 return self[item]
3329 except KeyError:
3330 return None
3332 def reformat(
3333 self,
3334 effective_preference: "EffectiveFormattingPreference",
3335 deb822_file: Deb822FileElement,
3336 formatter: FormatterCallback,
3337 _content: str,
3338 position_codec: LintCapablePositionCodec,
3339 lines: List[str],
3340 ) -> Iterable[TextEdit]:
3341 stanza_idx = -1
3342 for token_or_element in deb822_file.iter_parts():
3343 if isinstance(token_or_element, Deb822ParagraphElement):
3344 stanza_range = token_or_element.range_in_parent()
3345 stanza_idx += 1
3346 stanza_metadata = self.classify_stanza(token_or_element, stanza_idx)
3347 yield from stanza_metadata.reformat_stanza(
3348 effective_preference,
3349 token_or_element,
3350 stanza_range,
3351 formatter,
3352 position_codec,
3353 lines,
3354 )
3355 else:
3356 token_range = token_or_element.range_in_parent()
3357 yield from trim_end_of_line_whitespace(
3358 position_codec,
3359 lines,
3360 line_range=range(
3361 token_range.start_pos.line_position,
3362 token_range.end_pos.line_position,
3363 ),
3364 )
3367_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata(
3368 "Source",
3369 SOURCE_FIELDS,
3370 is_substvars_allowed_in_stanza=False,
3371)
3372_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata(
3373 "Package",
3374 BINARY_FIELDS,
3375 is_substvars_allowed_in_stanza=True,
3376)
3378_DEP5_HEADER_STANZA = Dep5StanzaMetadata(
3379 "Header",
3380 _DEP5_HEADER_FIELDS,
3381 is_substvars_allowed_in_stanza=False,
3382)
3383_DEP5_FILES_STANZA = Dep5StanzaMetadata(
3384 "Files",
3385 _DEP5_FILES_FIELDS,
3386 is_substvars_allowed_in_stanza=False,
3387)
3388_DEP5_LICENSE_STANZA = Dep5StanzaMetadata(
3389 "License",
3390 _DEP5_LICENSE_FIELDS,
3391 is_substvars_allowed_in_stanza=False,
3392)
3394_DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata(
3395 "Tests",
3396 _DTESTSCTRL_FIELDS,
3397 is_substvars_allowed_in_stanza=False,
3398)
3401class Dep5FileMetadata(Deb822FileMetadata[Dep5StanzaMetadata, Deb822KnownField]):
3403 @property
3404 def reference_data_basename(self) -> str:
3405 return "debian_copyright_reference_data.yaml"
3407 def _new_field(
3408 self,
3409 name: str,
3410 field_value_type: FieldValueClass,
3411 ) -> F:
3412 return Deb822KnownField(name, field_value_type)
3414 def classify_stanza(
3415 self,
3416 stanza: Deb822ParagraphElement,
3417 stanza_idx: int,
3418 ) -> Dep5StanzaMetadata:
3419 self.ensure_initialized()
3420 if stanza_idx == 0: 3420 ↛ 3421line 3420 didn't jump to line 3421 because the condition on line 3420 was never true
3421 return _DEP5_HEADER_STANZA
3422 if stanza_idx > 0: 3422 ↛ 3426line 3422 didn't jump to line 3426 because the condition on line 3422 was always true
3423 if "Files" in stanza:
3424 return _DEP5_FILES_STANZA
3425 return _DEP5_LICENSE_STANZA
3426 raise ValueError("The stanza_idx must be 0 or greater")
3428 def guess_stanza_classification_by_idx(self, stanza_idx: int) -> Dep5StanzaMetadata:
3429 self.ensure_initialized()
3430 if stanza_idx == 0:
3431 return _DEP5_HEADER_STANZA
3432 if stanza_idx > 0:
3433 return _DEP5_FILES_STANZA
3434 raise ValueError("The stanza_idx must be 0 or greater")
3436 def stanza_types(self) -> Iterable[Dep5StanzaMetadata]:
3437 self.ensure_initialized()
3438 # Order assumption made in the LSP code.
3439 yield _DEP5_HEADER_STANZA
3440 yield _DEP5_FILES_STANZA
3441 yield _DEP5_LICENSE_STANZA
3443 def __getitem__(self, item: str) -> Dep5StanzaMetadata:
3444 self.ensure_initialized()
3445 if item == "Header":
3446 return _DEP5_HEADER_STANZA
3447 if item == "Files":
3448 return _DEP5_FILES_STANZA
3449 if item == "License": 3449 ↛ 3451line 3449 didn't jump to line 3451 because the condition on line 3449 was always true
3450 return _DEP5_LICENSE_STANZA
3451 raise KeyError(item)
3454def _resolve_keyword(
3455 ref_value: StaticValue,
3456 known_values: Dict[str, Keyword],
3457 resolve_template: Callable[[Optional[str]], Optional[str]],
3458 translation_context: str,
3459) -> None:
3460 value_key = ref_value["value"]
3461 changes = {
3462 "translation_context": translation_context,
3463 }
3464 try:
3465 known_value = known_values[value_key]
3466 except KeyError:
3467 known_value = Keyword(value_key)
3468 known_values[value_key] = known_value
3469 value_doc = ref_value.get("documentation")
3470 if value_doc is not None:
3471 changes["synopsis"] = value_doc.get("synopsis")
3472 changes["long_description"] = resolve_template(
3473 value_doc.get("long_description")
3474 )
3475 if "sort_text" in ref_value: 3475 ↛ 3476line 3475 didn't jump to line 3476 because the condition on line 3475 was never true
3476 changes["sort_text"] = ref_value.get("sort_key")
3477 if "usage_hint" in ref_value:
3478 changes["usage_hint"] = ref_value.get("usage_hint")
3479 if changes: 3479 ↛ exitline 3479 didn't return from function '_resolve_keyword' because the condition on line 3479 was always true
3480 known_value = known_value.replace(**changes)
3481 known_values[value_key] = known_value
3484def _resolve_field(
3485 ref_field: Deb822Field,
3486 stanza_fields: Dict[str, F],
3487 field_constructor: Callable[[str, FieldValueClass], F],
3488 resolve_template: Callable[[Optional[str]], Optional[str]],
3489 translation_context: str,
3490) -> None:
3491 field_name = ref_field["canonical_name"]
3492 field_value_type = FieldValueClass.from_key(ref_field["field_value_type"])
3493 doc = ref_field.get("documentation")
3494 ref_values = ref_field.get("values", [])
3495 norm_field_name = normalize_dctrl_field_name(field_name.lower())
3497 try:
3498 field = stanza_fields[norm_field_name]
3499 except KeyError:
3500 field = field_constructor(
3501 field_name,
3502 field_value_type,
3503 )
3504 stanza_fields[norm_field_name] = field
3505 else:
3506 if field.name != field_name: 3506 ↛ 3507line 3506 didn't jump to line 3507 because the condition on line 3506 was never true
3507 _error(
3508 f'Error in reference data: Code uses "{field.name}" as canonical name and the data file'
3509 f" uses {field_name}. Please ensure the data is correctly aligned."
3510 )
3511 if field.field_value_class != field_value_type: 3511 ↛ 3512line 3511 didn't jump to line 3512 because the condition on line 3511 was never true
3512 _error(
3513 f'Error in reference data for field "{field.name}": Code has'
3514 f" {field.field_value_class.key} and the data file uses {field_value_type.key}"
3515 f" for field-value-type. Please ensure the data is correctly aligned."
3516 )
3517 if doc is not None: 3517 ↛ 3521line 3517 didn't jump to line 3521 because the condition on line 3517 was always true
3518 field.synopsis = doc.get("synopsis")
3519 field.long_description = resolve_template(doc.get("long_description"))
3521 field.default_value = ref_field.get("default_value")
3522 field.spellcheck_value = ref_field.get("spellcheck_value", False)
3523 field.deprecated_with_no_replacement = ref_field.get(
3524 "is_obsolete_without_replacement", False
3525 )
3526 field.replaced_by = ref_field.get("replaced_by")
3527 field.translation_context = translation_context
3528 field.usage_hint = ref_field.get("usage_hint")
3529 field.unknown_value_authority = ref_field.get("unknown_value_authority", "debputy")
3530 field.missing_field_authority = ref_field.get("missing_field_authority", "debputy")
3531 field.is_substvars_disabled_even_if_allowed_by_stanza = not ref_field.get(
3532 "supports_substvars", True
3533 )
3535 known_values = field.known_values
3536 if known_values is None:
3537 known_values = {}
3538 else:
3539 known_values = dict(known_values)
3541 for ref_value in ref_values:
3542 _resolve_keyword(ref_value, known_values, resolve_template, translation_context)
3544 if known_values:
3545 field.known_values = known_values
3548class DctrlFileMetadata(Deb822FileMetadata[DctrlStanzaMetadata, DctrlKnownField]):
3550 @property
3551 def reference_data_basename(self) -> str:
3552 return "debian_control_reference_data.yaml"
3554 def _new_field(
3555 self,
3556 name: str,
3557 field_value_type: FieldValueClass,
3558 ) -> F:
3559 return DctrlKnownField(name, field_value_type)
3561 def guess_stanza_classification_by_idx(
3562 self,
3563 stanza_idx: int,
3564 ) -> DctrlStanzaMetadata:
3565 self.ensure_initialized()
3566 if stanza_idx == 0:
3567 return _DCTRL_SOURCE_STANZA
3568 if stanza_idx > 0: 3568 ↛ 3570line 3568 didn't jump to line 3570 because the condition on line 3568 was always true
3569 return _DCTRL_PACKAGE_STANZA
3570 raise ValueError("The stanza_idx must be 0 or greater")
3572 def stanza_types(self) -> Iterable[DctrlStanzaMetadata]:
3573 self.ensure_initialized()
3574 # Order assumption made in the LSP code.
3575 yield _DCTRL_SOURCE_STANZA
3576 yield _DCTRL_PACKAGE_STANZA
3578 def __getitem__(self, item: str) -> DctrlStanzaMetadata:
3579 self.ensure_initialized()
3580 if item == "Source":
3581 return _DCTRL_SOURCE_STANZA
3582 if item == "Package": 3582 ↛ 3584line 3582 didn't jump to line 3584 because the condition on line 3582 was always true
3583 return _DCTRL_PACKAGE_STANZA
3584 raise KeyError(item)
3586 def reformat(
3587 self,
3588 effective_preference: "EffectiveFormattingPreference",
3589 deb822_file: Deb822FileElement,
3590 formatter: FormatterCallback,
3591 content: str,
3592 position_codec: LintCapablePositionCodec,
3593 lines: List[str],
3594 ) -> Iterable[TextEdit]:
3595 edits = list(
3596 super().reformat(
3597 effective_preference,
3598 deb822_file,
3599 formatter,
3600 content,
3601 position_codec,
3602 lines,
3603 )
3604 )
3606 if (
3607 not effective_preference.deb822_normalize_stanza_order
3608 or deb822_file.find_first_error_element() is not None
3609 ):
3610 return edits
3611 names = []
3612 for idx, stanza in enumerate(deb822_file):
3613 if idx < 2:
3614 continue
3615 name = stanza.get("Package")
3616 if name is None:
3617 return edits
3618 names.append(name)
3620 reordered = sorted(names)
3621 if names == reordered:
3622 return edits
3624 if edits:
3625 content = apply_text_edits(content, lines, edits)
3626 lines = content.splitlines(keepends=True)
3627 deb822_file = parse_deb822_file(
3628 lines,
3629 accept_files_with_duplicated_fields=True,
3630 accept_files_with_error_tokens=True,
3631 )
3633 stanzas = list(deb822_file)
3634 reordered_stanza = stanzas[:2] + sorted(
3635 stanzas[2:], key=operator.itemgetter("Package")
3636 )
3637 bits = []
3638 stanza_idx = 0
3639 for token_or_element in deb822_file.iter_parts():
3640 if isinstance(token_or_element, Deb822ParagraphElement):
3641 bits.append(reordered_stanza[stanza_idx].dump())
3642 stanza_idx += 1
3643 else:
3644 bits.append(token_or_element.convert_to_text())
3646 new_content = "".join(bits)
3648 return [
3649 TextEdit(
3650 Range(
3651 Position(0, 0),
3652 Position(len(lines) + 1, 0),
3653 ),
3654 new_content,
3655 )
3656 ]
3659class DTestsCtrlFileMetadata(
3660 Deb822FileMetadata[DTestsCtrlStanzaMetadata, DTestsCtrlKnownField]
3661):
3663 @property
3664 def reference_data_basename(self) -> str:
3665 return "debian_tests_control_reference_data.yaml"
3667 def _new_field(
3668 self,
3669 name: str,
3670 field_value_type: FieldValueClass,
3671 ) -> F:
3672 return DTestsCtrlKnownField(name, field_value_type)
3674 def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S:
3675 if stanza_idx >= 0: 3675 ↛ 3678line 3675 didn't jump to line 3678 because the condition on line 3675 was always true
3676 self.ensure_initialized()
3677 return _DTESTSCTRL_STANZA
3678 raise ValueError("The stanza_idx must be 0 or greater")
3680 def stanza_types(self) -> Iterable[S]:
3681 self.ensure_initialized()
3682 yield _DTESTSCTRL_STANZA
3684 def __getitem__(self, item: str) -> S:
3685 self.ensure_initialized()
3686 if item == "Tests": 3686 ↛ 3688line 3686 didn't jump to line 3688 because the condition on line 3686 was always true
3687 return _DTESTSCTRL_STANZA
3688 raise KeyError(item)
3691TRANSLATABLE_DEB822_FILE_METADATA: Sequence[
3692 Callable[[], Deb822FileMetadata[Any, Any]]
3693] = [
3694 DctrlFileMetadata,
3695 Dep5FileMetadata,
3696 DTestsCtrlFileMetadata,
3697]