diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 10:21:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 10:21:50 +0000 |
commit | e4fd260f1696922f28424a967296183b0142e833 (patch) | |
tree | 2b2d1ee74820ba297dc876dbd3d52e4392d2f839 | |
parent | Adding debian version 0.1.41. (diff) | |
download | debputy-e4fd260f1696922f28424a967296183b0142e833.tar.xz debputy-e4fd260f1696922f28424a967296183b0142e833.zip |
Merging upstream version 0.1.42.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r-- | src/debputy/linting/lint_util.py | 10 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_control.py | 17 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_control_reference_data.py | 375 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_copyright.py | 6 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_tests_control.py | 6 | ||||
-rw-r--r-- | src/debputy/packager_provided_files.py | 14 | ||||
-rw-r--r-- | tests/lint_tests/lint_tutil.py | 19 | ||||
-rw-r--r-- | tests/lint_tests/test_lint_dctrl.py | 244 |
8 files changed, 631 insertions, 60 deletions
diff --git a/src/debputy/linting/lint_util.py b/src/debputy/linting/lint_util.py index ddce7c2..1ed881c 100644 --- a/src/debputy/linting/lint_util.py +++ b/src/debputy/linting/lint_util.py @@ -440,13 +440,9 @@ class TermLintReport(LintReport): ) return lines_to_print = _lines_to_print(diagnostic.range) - if lines_to_print == 1: - line = _highlight_range(fo, lines[start_line], start_line, diagnostic.range) - print(f" {start_line+1:{line_no_width}}: {line}") - else: - for line_no in range(start_line, end_line): - line = _highlight_range(fo, lines[line_no], line_no, diagnostic.range) - print(f" {line_no+1:{line_no_width}}: {line}") + for line_no in range(start_line, start_line + lines_to_print): + line = _highlight_range(fo, lines[line_no], line_no, diagnostic.range) + print(f" {line_no+1:{line_no_width}}: {line}") class LinterPositionCodec: diff --git a/src/debputy/lsp/lsp_debian_control.py b/src/debputy/lsp/lsp_debian_control.py index 05cd943..5a72222 100644 --- a/src/debputy/lsp/lsp_debian_control.py +++ b/src/debputy/lsp/lsp_debian_control.py @@ -11,11 +11,10 @@ from typing import ( List, Dict, Iterable, - Container, ) from debputy.analysis.analysis_util import flatten_ppfs -from debputy.analysis.debian_dir import scan_debian_dir, resolve_debhelper_config_files +from debputy.analysis.debian_dir import resolve_debhelper_config_files from debputy.dh.dh_assistant import extract_dh_compat_level from debputy.linting.lint_util import LintState from debputy.lsp.debputy_ls import DebputyLanguageServer @@ -99,7 +98,6 @@ from debputy.packager_provided_files import ( PackagerProvidedFile, detect_all_packager_provided_files, ) -from debputy.plugin.api import VirtualPath from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin from debputy.util import detect_possible_typo @@ -717,9 +715,9 @@ def _diagnostics_for_paragraph( normalized_field_name_lc = normalize_dctrl_field_name(field_name_lc) known_field = known_fields.get(normalized_field_name_lc) field_value = stanza[field_name] - kvpair_position = kvpair.position_in_parent().relative_to(stanza_position) + kvpair_range_te = kvpair.range_in_parent().relative_to(stanza_position) field_range_te = kvpair.field_token.range_in_parent().relative_to( - kvpair_position + kvpair_range_te.start_pos ) field_position_te = field_range_te.start_pos field_range_server_units = te_range_to_lsp(field_range_te) @@ -799,7 +797,7 @@ def _diagnostics_for_paragraph( kvpair, stanza, stanza_position, - kvpair_position, + kvpair_range_te, lint_state, field_name_typo_reported=field_name_typo_detected, ) @@ -1038,7 +1036,7 @@ def _package_range_of_stanza( binary_stanzas: List[Tuple[Deb822ParagraphElement, TEPosition]], ) -> Iterable[Tuple[str, Optional[str], Range]]: for stanza, stanza_position in binary_stanzas: - kvpair = stanza.get_kvpair_element("Package") + kvpair = stanza.get_kvpair_element("Package", use_get=True) if kvpair is None: continue representation_field_range = kvpair.range_in_parent().relative_to( @@ -1147,7 +1145,10 @@ def _detect_misspelled_packaging_files( stem = ppf.definition.stem if binary_package is None or stem is None: continue - declared_arch, diag_range = stanza_ranges.get(binary_package) + res = stanza_ranges.get(binary_package) + if res is None: + continue + declared_arch, diag_range = res if diag_range is None: continue path = ppf.path.path diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py index 1e32d3c..007c0dd 100644 --- a/src/debputy/lsp/lsp_debian_control_reference_data.py +++ b/src/debputy/lsp/lsp_debian_control_reference_data.py @@ -161,6 +161,7 @@ CustomFieldCheck = Callable[ Deb822FileElement, Deb822KeyValuePairElement, "TERange", + "TERange", Deb822ParagraphElement, "TEPosition", LintState, @@ -454,7 +455,8 @@ def _sv_field_validation( _known_field: "F", _deb822_file: Deb822FileElement, kvpair: Deb822KeyValuePairElement, - _field_range: "TERange", + _kvpair_range: "TERange", + _field_name_range_te: "TERange", _stanza: Deb822ParagraphElement, stanza_position: "TEPosition", lint_state: LintState, @@ -511,7 +513,8 @@ def _dctrl_ma_field_validation( _known_field: "F", _deb822_file: Deb822FileElement, _kvpair: Deb822KeyValuePairElement, - _field_range: "TERange", + _kvpair_range: "TERange", + _field_name_range: "TERange", stanza: Deb822ParagraphElement, stanza_position: "TEPosition", lint_state: LintState, @@ -537,14 +540,15 @@ def _udeb_only_field_validation( known_field: "F", _deb822_file: Deb822FileElement, _kvpair: Deb822KeyValuePairElement, - field_range_te: "TERange", + _kvpair_range: "TERange", + field_name_range: "TERange", stanza: Deb822ParagraphElement, _stanza_position: "TEPosition", lint_state: LintState, ) -> Iterable[Diagnostic]: package_type = stanza.get("Package-Type") if package_type != "udeb": - field_range_server_units = te_range_to_lsp(field_range_te) + field_range_server_units = te_range_to_lsp(field_name_range) field_range = lint_state.position_codec.range_to_client_units( lint_state.lines, field_range_server_units, @@ -584,14 +588,15 @@ def _arch_not_all_only_field_validation( known_field: "F", _deb822_file: Deb822FileElement, _kvpair: Deb822KeyValuePairElement, - field_range_te: "TERange", + _kvpair_range_te: "TERange", + field_name_range_te: "TERange", stanza: Deb822ParagraphElement, _stanza_position: "TEPosition", lint_state: LintState, ) -> Iterable[Diagnostic]: architecture = stanza.get("Architecture") if architecture == "all": - field_range_server_units = te_range_to_lsp(field_range_te) + field_range_server_units = te_range_to_lsp(field_name_range_te) field_range = lint_state.position_codec.range_to_client_units( lint_state.lines, field_range_server_units, @@ -604,7 +609,7 @@ def _arch_not_all_only_field_validation( ) -def _span_to_client_range( +def _single_line_span_to_client_range( span: Tuple[int, int], relative_to: "TEPosition", lint_state: LintState, @@ -655,7 +660,7 @@ def _check_synopsis( # TODO: Handle ${...} expansion if starts_with_article: yield Diagnostic( - _span_to_client_range( + _single_line_span_to_client_range( starts_with_article.span(1), synopsis_range_te.start_pos, lint_state, @@ -668,7 +673,7 @@ def _check_synopsis( # Policy says `certainly under 80 characters.`, so exactly 80 characters is considered bad too. span = synopsis_offset + 79, len(synopsis_text_with_leading_space) yield Diagnostic( - _span_to_client_range( + _single_line_span_to_client_range( span, synopsis_range_te.start_pos, lint_state, @@ -681,7 +686,7 @@ def _check_synopsis( synopsis_text_with_leading_space ): yield Diagnostic( - _span_to_client_range( + _single_line_span_to_client_range( template_match.span(1), synopsis_range_te.start_pos, lint_state, @@ -694,7 +699,7 @@ def _check_synopsis( synopsis_text_with_leading_space ): yield Diagnostic( - _span_to_client_range( + _single_line_span_to_client_range( too_short_match.span(1), synopsis_range_te.start_pos, lint_state, @@ -709,7 +714,8 @@ def dctrl_description_validator( _known_field: "F", _deb822_file: Deb822FileElement, kvpair: Deb822KeyValuePairElement, - field_range_te: "TERange", + kvpair_range_te: "TERange", + _field_name_range: "TERange", stanza: Deb822ParagraphElement, _stanza_position: "TEPosition", lint_state: LintState, @@ -720,11 +726,11 @@ def dctrl_description_validator( package = stanza.get("Package") synopsis_value_line = value_lines[0] value_range_te = kvpair.value_element.range_in_parent().relative_to( - field_range_te.start_pos + kvpair_range_te.start_pos ) if synopsis_value_line.continuation_line_token is None: field_name_range_te = kvpair.field_token.range_in_parent().relative_to( - field_range_te.start_pos + kvpair_range_te.start_pos ) synopsis_range_te = synopsis_value_line.range_in_parent().relative_to( value_range_te.start_pos @@ -748,14 +754,15 @@ def _each_value_match_regex_validation( _known_field: "F", _deb822_file: Deb822FileElement, kvpair: Deb822KeyValuePairElement, - field_range_te: "TERange", + kvpair_range_te: "TERange", + _field_name_range_te: "TERange", _stanza: Deb822ParagraphElement, _stanza_position: "TEPosition", lint_state: LintState, ) -> Iterable[Diagnostic]: value_element_pos = kvpair.value_element.position_in_parent().relative_to( - field_range_te.start_pos + kvpair_range_te.start_pos ) for value_ref in kvpair.interpret_as( LIST_SPACE_SEPARATED_INTERPRETATION @@ -787,6 +794,247 @@ def _each_value_match_regex_validation( return _validator +_DEP_OR_RELATION = re.compile(r"[|]") +_DEP_RELATION_CLAUSE = re.compile( + r""" + ^ + \s* + (?P<name_arch_qual>[-+.a-zA-Z0-9${}:]{2,}) + \s* + (?: [(] \s* (?P<operator>>>|>=|=|<=|<<) \s* (?P<version> [^)]+) \s* [)] \s* )? + (?: \[ (?P<arch_restriction> [\s!\w\-]+) ] \s*)? + (?: < (?P<build_profile_restriction> .+ ) > \s*)? + ((?P<garbage>\S.*)\s*)? + $ +""", + re.VERBOSE | re.MULTILINE, +) + + +def _span_to_te_range( + text: str, + start_pos: int, + end_pos: int, +) -> TERange: + prefix = text[0:start_pos] + prefix_plus_text = text[0:end_pos] + + start_line = prefix.count("\n") + if start_line: + start_newline_offset = prefix.rindex("\n") + # +1 to skip past the newline + start_cursor_pos = start_pos - (start_newline_offset + 1) + else: + start_cursor_pos = start_pos + + end_line = prefix_plus_text.count("\n") + if end_line == start_line: + end_cursor_pos = start_cursor_pos + (end_pos - start_pos) + else: + end_newline_offset = prefix_plus_text.rindex("\n") + end_cursor_pos = end_pos - (end_newline_offset + 1) + + return TERange( + TEPosition( + start_line, + start_cursor_pos, + ), + TEPosition( + end_line, + end_cursor_pos, + ), + ) + + +def _split_w_spans( + v: str, + sep: str, + *, + offset: int = 0, +) -> Sequence[Tuple[str, int, int]]: + separator_size = len(sep) + parts = v.split(sep) + for part in parts: + size = len(part) + end_offset = offset + size + yield part, offset, end_offset + offset = end_offset + separator_size + + +_COLLAPSE_WHITESPACE = re.compile(r"\s+") + + +def _cleanup_rel(rel: str) -> str: + return _COLLAPSE_WHITESPACE.sub(" ", rel.strip()) + + +def _text_to_te_position(text: str) -> "TEPosition": + newlines = text.count("\n") + if not newlines: + return TEPosition( + newlines, + len(text), + ) + last_newline_offset = text.rindex("\n") + line_offset = len(text) - (last_newline_offset + 1) + return TEPosition( + newlines, + line_offset, + ) + + +def _dctrl_validate_dep( + known_field: "F", + _deb822_file: Deb822FileElement, + kvpair: Deb822KeyValuePairElement, + kvpair_range_te: "TERange", + _field_name_range: "TERange", + _stanza: Deb822ParagraphElement, + _stanza_position: "TEPosition", + lint_state: LintState, +) -> Iterable[Diagnostic]: + value_element_pos = kvpair.value_element.position_in_parent().relative_to( + kvpair_range_te.start_pos + ) + raw_value_with_comments = kvpair.value_element.convert_to_text() + raw_value_masked_comments = "".join( + (line if not line.startswith("#") else (" " * (len(line) - 1)) + "\n") + for line in raw_value_with_comments.splitlines(keepends=True) + ) + if isinstance(known_field, DctrlRelationshipKnownField): + version_operators = known_field.allowed_version_operators + supports_or_relation = known_field.supports_or_relation + else: + version_operators = frozenset() + supports_or_relation = True + + for rel, rel_offset, rel_end_offset in _split_w_spans( + raw_value_masked_comments, "," + ): + seen_relation = False + for or_rel, offset, end_offset in _split_w_spans(rel, "|", offset=rel_offset): + if or_rel.isspace(): + continue + if seen_relation and not supports_or_relation: + separator_range_te = TERange( + _text_to_te_position(raw_value_masked_comments[: offset - 1]), + _text_to_te_position(raw_value_masked_comments[:offset]), + ).relative_to(value_element_pos) + separator_range = lint_state.position_codec.range_to_client_units( + lint_state.lines, + te_range_to_lsp(separator_range_te), + ) + yield Diagnostic( + lint_state.position_codec.range_to_client_units( + lint_state.lines, + separator_range, + ), + f'The field {known_field.name} does not support "|" (OR) in relations.', + DiagnosticSeverity.Error, + source="debputy", + ) + seen_relation = True + m = _DEP_RELATION_CLAUSE.fullmatch(or_rel) + + if m is not None: + garbage = m.group("garbage") + version_operator = m.group("operator") + if ( + version_operators + and version_operator is not None + and version_operator not in version_operators + ): + operator_span = m.span("operator") + v_start_offset = offset + operator_span[0] + v_end_offset = offset + operator_span[1] + version_problem_range_te = TERange( + _text_to_te_position( + raw_value_masked_comments[:v_start_offset] + ), + _text_to_te_position(raw_value_masked_comments[:v_end_offset]), + ).relative_to(value_element_pos) + + version_problem_range = ( + lint_state.position_codec.range_to_client_units( + lint_state.lines, + te_range_to_lsp(version_problem_range_te), + ) + ) + sorted_version_operators = sorted(version_operators) + yield Diagnostic( + lint_state.position_codec.range_to_client_units( + lint_state.lines, + version_problem_range, + ), + f'The version operator "{version_operator}" is not allowed in {known_field.name}', + DiagnosticSeverity.Error, + source="debputy", + data=DiagnosticData( + quickfixes=[ + propose_correct_text_quick_fix(n) + for n in sorted_version_operators + ] + ), + ) + else: + garbage = None + + if m is not None and not garbage: + continue + if m is not None: + garbage_span = m.span("garbage") + garbage_start, garbage_end = garbage_span + error_start_offset = offset + garbage_start + error_end_offset = offset + garbage_end + garbage_part = raw_value_masked_comments[ + error_start_offset:error_end_offset + ] + else: + garbage_part = None + error_start_offset = offset + error_end_offset = end_offset + + problem_range_te = TERange( + _text_to_te_position(raw_value_masked_comments[:error_start_offset]), + _text_to_te_position(raw_value_masked_comments[:error_end_offset]), + ).relative_to(value_element_pos) + + problem_range = lint_state.position_codec.range_to_client_units( + lint_state.lines, + te_range_to_lsp(problem_range_te), + ) + if garbage_part is not None: + if _DEP_RELATION_CLAUSE.fullmatch(garbage_part) is not None: + msg = ( + "Trailing data after a relationship that might be a second relationship." + " Is a separator missing before this part?" + ) + else: + msg = "Parse error of the relationship. Either a syntax error or a missing separator somewhere." + yield Diagnostic( + lint_state.position_codec.range_to_client_units( + lint_state.lines, + problem_range, + ), + msg, + DiagnosticSeverity.Error, + source="debputy", + ) + else: + dep = _cleanup_rel( + raw_value_masked_comments[error_start_offset:error_end_offset] + ) + yield Diagnostic( + lint_state.position_codec.range_to_client_units( + lint_state.lines, + problem_range, + ), + f'Could not parse "{dep}" as a dependency relation.', + DiagnosticSeverity.Error, + source="debputy", + ) + + class Dep5Matcher(BasenameGlobMatch): def __init__(self, basename_glob: str) -> None: super().__init__( @@ -882,7 +1130,8 @@ def _dep5_files_check( known_field: "F", _deb822_file: Deb822FileElement, kvpair: Deb822KeyValuePairElement, - field_range_te: "TERange", + kvpair_range_te: "TERange", + _field_name_range: "TERange", _stanza: Deb822ParagraphElement, _stanza_position: "TEPosition", lint_state: LintState, @@ -890,7 +1139,7 @@ def _dep5_files_check( interpreter = known_field.field_value_class.interpreter() assert interpreter is not None full_value_range = kvpair.value_element.range_in_parent().relative_to( - field_range_te.start_pos + kvpair_range_te.start_pos ) values_with_ranges = [] for value_ref in kvpair.interpret_as(interpreter).iter_value_references(): @@ -916,7 +1165,8 @@ def _combined_custom_field_check(*checks: CustomFieldCheck) -> CustomFieldCheck: known_field: "F", deb822_file: Deb822FileElement, kvpair: Deb822KeyValuePairElement, - field_range_te: "TERange", + kvpair_range_te: "TERange", + field_name_range_te: "TERange", stanza: Deb822ParagraphElement, stanza_position: "TEPosition", lint_state: LintState, @@ -926,7 +1176,8 @@ def _combined_custom_field_check(*checks: CustomFieldCheck) -> CustomFieldCheck: known_field, deb822_file, kvpair, - field_range_te, + kvpair_range_te, + field_name_range_te, stanza, stanza_position, lint_state, @@ -1437,18 +1688,18 @@ class Deb822KnownField: kvpair: Deb822KeyValuePairElement, stanza: Deb822ParagraphElement, stanza_position: "TEPosition", - kvpair_position: "TEPosition", + kvpair_range_te: "TERange", lint_state: LintState, *, field_name_typo_reported: bool = False, ) -> Iterable[Diagnostic]: field_name_token = kvpair.field_token - field_range_te = kvpair.field_token.range_in_parent().relative_to( - kvpair_position + field_name_range_te = kvpair.field_token.range_in_parent().relative_to( + kvpair_range_te.start_pos ) yield from self._diagnostics_for_field_name( field_name_token, - field_range_te, + field_name_range_te, field_name_typo_reported, lint_state, ) @@ -1457,16 +1708,19 @@ class Deb822KnownField: self, deb822_file, kvpair, - field_range_te, + kvpair_range_te, + field_name_range_te, stanza, stanza_position, lint_state, ) - yield from self._dep5_file_list_diagnostics(kvpair, kvpair_position, lint_state) + yield from self._dep5_file_list_diagnostics( + kvpair, kvpair_range_te.start_pos, lint_state + ) if not self.spellcheck_value: yield from self._known_value_diagnostics( kvpair, - kvpair_position, + kvpair_range_te.start_pos, lint_state, ) @@ -1930,6 +2184,16 @@ class DctrlKnownField(DctrlLikeKnownField): return self.is_relationship_field or self.name == "Uploaders" +@dataclasses.dataclass(slots=True, frozen=True) +class DctrlRelationshipKnownField(DctrlKnownField): + allowed_version_operators: FrozenSet[str] = frozenset() + supports_or_relation: bool = True + + @property + def is_relationship_field(self) -> bool: + return True + + SOURCE_FIELDS = _fields( DctrlKnownField( "Source", @@ -2186,6 +2450,7 @@ SOURCE_FIELDS = _fields( DctrlKnownField( "Build-Depends", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Dependencies requires for clean and full build actions", hover_text=textwrap.dedent( """\ @@ -2196,6 +2461,7 @@ SOURCE_FIELDS = _fields( DctrlKnownField( "Build-Depends-Arch", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Dependencies requires for arch:any action (build-arch/binary-arch)", hover_text=textwrap.dedent( """\ @@ -2212,6 +2478,7 @@ SOURCE_FIELDS = _fields( DctrlKnownField( "Build-Depends-Indep", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Dependencies requires for arch:all action (build-indep/binary-indep)", hover_text=textwrap.dedent( """\ @@ -2225,9 +2492,11 @@ SOURCE_FIELDS = _fields( """ ), ), - DctrlKnownField( + DctrlRelationshipKnownField( "Build-Conflicts", FieldValueClass.COMMA_SEPARATED_LIST, + supports_or_relation=False, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Package versions that will break the build or the clean target (use sparingly)", hover_text=textwrap.dedent( """\ @@ -2240,9 +2509,11 @@ SOURCE_FIELDS = _fields( """ ), ), - DctrlKnownField( + DctrlRelationshipKnownField( "Build-Conflicts-Arch", FieldValueClass.COMMA_SEPARATED_LIST, + supports_or_relation=False, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Package versions that will break an arch:any build (use sparingly)", hover_text=textwrap.dedent( """\ @@ -2255,9 +2526,11 @@ SOURCE_FIELDS = _fields( """ ), ), - DctrlKnownField( + DctrlRelationshipKnownField( "Build-Conflicts-Indep", FieldValueClass.COMMA_SEPARATED_LIST, + supports_or_relation=False, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Package versions that will break an arch:all build (use sparingly)", hover_text=textwrap.dedent( """\ @@ -2467,6 +2740,17 @@ SOURCE_FIELDS = _fields( ), ), DctrlKnownField( + "XS-Ruby-Versions", + FieldValueClass.FREE_TEXT_FIELD, + deprecated_with_no_replacement=True, + synopsis_doc="Obsolete", + hover_text=textwrap.dedent( + """\ + Obsolete according to https://bugs.debian.org/1075762 + """ + ), + ), + DctrlKnownField( "Description", FieldValueClass.FREE_TEXT_FIELD, spellcheck_value=True, @@ -2729,6 +3013,7 @@ BINARY_FIELDS = _fields( DctrlKnownField( "Depends", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Dependencies required to install and use this package", hover_text=textwrap.dedent( """\ @@ -2758,6 +3043,7 @@ BINARY_FIELDS = _fields( DctrlKnownField( "Recommends", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Optional dependencies **most** people should have", hover_text=textwrap.dedent( """\ @@ -2782,6 +3068,7 @@ BINARY_FIELDS = _fields( DctrlKnownField( "Suggests", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Optional dependencies that some people might want", hover_text=textwrap.dedent( """\ @@ -2799,6 +3086,7 @@ BINARY_FIELDS = _fields( DctrlKnownField( "Enhances", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="Packages enhanced by installing this package", hover_text=textwrap.dedent( """\ @@ -2814,9 +3102,12 @@ BINARY_FIELDS = _fields( """ ), ), - DctrlKnownField( + DctrlRelationshipKnownField( "Provides", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, + supports_or_relation=False, + allowed_version_operators=frozenset(["="]), synopsis_doc="Additional packages/versions this package dependency-wise satisfy", hover_text=textwrap.dedent( """\ @@ -2857,9 +3148,11 @@ BINARY_FIELDS = _fields( """ ), ), - DctrlKnownField( + DctrlRelationshipKnownField( "Conflicts", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, + supports_or_relation=False, synopsis_doc="Packages that this package is not co-installable with", hover_text=textwrap.dedent( """\ @@ -2886,9 +3179,11 @@ BINARY_FIELDS = _fields( """ ), ), - DctrlKnownField( + DctrlRelationshipKnownField( "Breaks", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, + supports_or_relation=False, synopsis_doc="Package/versions that does not work with this package", hover_text=textwrap.dedent( """\ @@ -2912,9 +3207,10 @@ BINARY_FIELDS = _fields( """ ), ), - DctrlKnownField( + DctrlRelationshipKnownField( "Replaces", FieldValueClass.COMMA_SEPARATED_LIST, + custom_field_check=_dctrl_validate_dep, synopsis_doc="This package replaces content from these packages/versions", hover_text=textwrap.dedent( """\ @@ -3416,6 +3712,17 @@ BINARY_FIELDS = _fields( """ ), ), + DctrlKnownField( + "XB-Ruby-Versions", + FieldValueClass.FREE_TEXT_FIELD, + deprecated_with_no_replacement=True, + synopsis_doc="Obsolete", + hover_text=textwrap.dedent( + """\ + Obsolete according to https://bugs.debian.org/1075762 + """ + ), + ), ) _DEP5_HEADER_FIELDS = _fields( Deb822KnownField( diff --git a/src/debputy/lsp/lsp_debian_copyright.py b/src/debputy/lsp/lsp_debian_copyright.py index 9cbac26..e5bcbff 100644 --- a/src/debputy/lsp/lsp_debian_copyright.py +++ b/src/debputy/lsp/lsp_debian_copyright.py @@ -184,9 +184,9 @@ def _diagnostics_for_paragraph( normalized_field_name_lc = normalize_dctrl_field_name(field_name_lc) known_field = known_fields.get(normalized_field_name_lc) field_value = stanza[field_name] - kvpair_position = kvpair.position_in_parent().relative_to(stanza_position) + kvpair_range_te = kvpair.range_in_parent().relative_to(stanza_position) field_range_te = kvpair.field_token.range_in_parent().relative_to( - kvpair_position + kvpair_range_te.start_pos ) field_position_te = field_range_te.start_pos field_range_server_units = te_range_to_lsp(field_range_te) @@ -268,7 +268,7 @@ def _diagnostics_for_paragraph( kvpair, stanza, stanza_position, - kvpair_position, + kvpair_range_te, lint_state, field_name_typo_reported=field_name_typo_detected, ) diff --git a/src/debputy/lsp/lsp_debian_tests_control.py b/src/debputy/lsp/lsp_debian_tests_control.py index f188762..ff0a209 100644 --- a/src/debputy/lsp/lsp_debian_tests_control.py +++ b/src/debputy/lsp/lsp_debian_tests_control.py @@ -199,9 +199,9 @@ def _diagnostics_for_paragraph( normalized_field_name_lc = normalize_dctrl_field_name(field_name_lc) known_field = known_fields.get(normalized_field_name_lc) field_value = stanza[field_name] - kvpair_position = kvpair.position_in_parent().relative_to(stanza_position) + kvpair_range_te = kvpair.range_in_parent().relative_to(stanza_position) field_range_te = kvpair.field_token.range_in_parent().relative_to( - kvpair_position + kvpair_range_te.start_pos ) field_position_te = field_range_te.start_pos field_range_server_units = te_range_to_lsp(field_range_te) @@ -268,7 +268,7 @@ def _diagnostics_for_paragraph( kvpair, stanza, stanza_position, - kvpair_position, + kvpair_range_te, lint_state, field_name_typo_reported=field_name_typo_detected, ) diff --git a/src/debputy/packager_provided_files.py b/src/debputy/packager_provided_files.py index d2512f4..a35beec 100644 --- a/src/debputy/packager_provided_files.py +++ b/src/debputy/packager_provided_files.py @@ -390,9 +390,17 @@ def detect_all_packager_provided_files( detect_typos: bool = False, ignore_paths: Container[str] = frozenset(), ) -> Dict[str, PerPackagePackagerProvidedResult]: - main_binary_package = [ - p.name for p in binary_packages.values() if p.is_main_package - ][0] + main_packages = [p.name for p in binary_packages.values() if p.is_main_package] + if not main_packages: + assert allow_fuzzy_matches + main_binary_package = next( + iter(p.name for p in binary_packages.values() if "Package" in p.fields), + None, + ) + if main_binary_package is None: + return {} + else: + main_binary_package = main_packages[0] provided_files: Dict[str, Dict[Tuple[str, str], PackagerProvidedFile]] = { n: {} for n in binary_packages } diff --git a/tests/lint_tests/lint_tutil.py b/tests/lint_tests/lint_tutil.py index 267f669..74e08db 100644 --- a/tests/lint_tests/lint_tutil.py +++ b/tests/lint_tests/lint_tutil.py @@ -1,5 +1,5 @@ import collections -from typing import List, Optional, Mapping, Any, Callable +from typing import List, Optional, Mapping, Any, Callable, Sequence import pytest @@ -13,7 +13,7 @@ from debputy.lsp.style_prefs import StylePreferenceTable, EffectivePreference from debputy.packages import DctrlParser from debputy.plugin.api.feature_set import PluginProvidedFeatureSet -from debputy.lsprotocol.types import Diagnostic, DiagnosticSeverity +from debputy.lsprotocol.types import Diagnostic, DiagnosticSeverity, Range try: @@ -108,3 +108,18 @@ def group_diagnostics_by_severity( by_severity[severity].append(diagnostic) return by_severity + + +def diag_range_to_text(lines: Sequence[str], range_: "Range") -> str: + parts = [] + for line_no in range(range_.start.line, range_.end.line + 1): + line = lines[line_no] + chunk = line + if line_no == range_.start.line and line_no == range_.end.line: + chunk = line[range_.start.character : range_.end.character] + elif line_no == range_.start.line: + chunk = line[range_.start.character :] + elif line_no == range_.end.line: + chunk = line[: range_.end.character] + parts.append(chunk) + return "".join(parts) diff --git a/tests/lint_tests/test_lint_dctrl.py b/tests/lint_tests/test_lint_dctrl.py index 840fabe..80d7525 100644 --- a/tests/lint_tests/test_lint_dctrl.py +++ b/tests/lint_tests/test_lint_dctrl.py @@ -13,6 +13,7 @@ from lint_tests.lint_tutil import ( group_diagnostics_by_severity, requires_levenshtein, LintWrapper, + diag_range_to_text, ) from debputy.lsprotocol.types import Diagnostic, DiagnosticSeverity @@ -965,3 +966,246 @@ def test_dctrl_lint_stem_typo_pkgfile_ignored_exts_or_files( "./debian/foo.intsall", "debian/foo.intsall", ) + + +def test_dctrl_lint_dep_field_missing_sep( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: bar, baz + # Missing separator between baz and libfubar1 + libfubar1, + Description: some short synopsis + A very interesting description + with a valid synopsis + . + Just so be clear, this is for a test. + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + assert diagnostics and len(diagnostics) == 1 + issue = diagnostics[0] + msg = ( + "Trailing data after a relationship that might be a second relationship." + " Is a separator missing before this part?" + ) + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == "libfubar1" + assert f"{issue.range}" == "11:1-11:10" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_missing_sep_or_syntax_error( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: bar, baz + # Missing separator between baz and libfubar1 + _libfubar1, + Description: some short synopsis + A very interesting description + with a valid synopsis + . + Just so be clear, this is for a test. + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + assert diagnostics and len(diagnostics) == 1 + issue = diagnostics[0] + msg = "Parse error of the relationship. Either a syntax error or a missing separator somewhere." + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == "_libfubar1" + assert f"{issue.range}" == "11:1-11:11" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_completely_busted( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: bar, baz, _asd + # This is just busted + _libfubar1, + Description: some short synopsis + A very interesting description + with a valid synopsis + . + Just so be clear, this is for a test. + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + assert diagnostics and len(diagnostics) == 1 + issue = diagnostics[0] + msg = 'Could not parse "_asd _libfubar1" as a dependency relation.' + problem_text = diag_range_to_text(lines, issue.range) + expected_problem_text = "\n".join((" _asd", "# This is just busted", " _libfubar1")) + assert issue.message == msg + assert problem_text == expected_problem_text + assert f"{issue.range}" == "9:18-11:11" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_completely_busted_first_line( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + # A wild field comment appeared! + Depends: _bar, + asd, + # This is fine (but the _bar part is not) + libfubar1, + Description: some short synopsis + A very interesting description + with a valid synopsis + . + Just so be clear, this is for a test. + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + assert diagnostics and len(diagnostics) == 1 + issue = diagnostics[0] + msg = 'Could not parse "_bar" as a dependency relation.' + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == " _bar" + assert f"{issue.range}" == "10:8-10:13" + assert issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_restricted_operator( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + # Some random field comment + Provides: bar (>= 2), + bar + # Inline comment to spice up things + (<= 1), + # This one is valid + fubar (= 2), + Description: some short synopsis + A very interesting description + with a valid synopsis + . + Just so be clear, this is for a test. + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + assert diagnostics and len(diagnostics) == 2 + first_issue, second_issue = diagnostics + + msg = 'The version operator ">=" is not allowed in Provides' + problem_text = diag_range_to_text(lines, first_issue.range) + assert first_issue.message == msg + assert problem_text == ">=" + assert f"{first_issue.range}" == "10:15-10:17" + assert first_issue.severity == DiagnosticSeverity.Error + + msg = 'The version operator "<=" is not allowed in Provides' + problem_text = diag_range_to_text(lines, second_issue.range) + assert second_issue.message == msg + assert problem_text == "<=" + assert f"{second_issue.range}" == "13:2-13:4" + assert second_issue.severity == DiagnosticSeverity.Error + + +def test_dctrl_lint_dep_field_restricted_or_relations( + line_linter: LintWrapper, +) -> None: + lines = textwrap.dedent( + f"""\ + Source: foo + Section: devel + Priority: optional + Standards-Version: {CURRENT_STANDARDS_VERSION} + Maintainer: Jane Developer <jane@example.com> + Build-Depends: debhelper-compat (= 13) + + Package: foo + Architecture: all + Depends: pkg-a + | pkg-b + # What goes in Depends do not always work in Provides + Provides: foo-a + | foo-b + Description: some short synopsis + A very interesting description + with a valid synopsis + . + Just so be clear, this is for a test. + """ + ).splitlines(keepends=True) + + diagnostics = line_linter(lines) + print(diagnostics) + assert diagnostics and len(diagnostics) == 1 + issue = diagnostics[0] + + msg = 'The field Provides does not support "|" (OR) in relations.' + problem_text = diag_range_to_text(lines, issue.range) + assert issue.message == msg + assert problem_text == "|" + assert f"{issue.range}" == "13:1-13:2" + assert issue.severity == DiagnosticSeverity.Error |