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