diff options
-rw-r--r-- | src/debputy/commands/debputy_cmd/context.py | 9 | ||||
-rw-r--r-- | src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py | 3 | ||||
-rw-r--r-- | src/debputy/commands/debputy_cmd/plugin_cmds.py | 2 | ||||
-rw-r--r-- | src/debputy/linting/lint_util.py | 2 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_control.py | 74 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_control_reference_data.py | 219 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_copyright.py | 81 |
7 files changed, 226 insertions, 164 deletions
diff --git a/src/debputy/commands/debputy_cmd/context.py b/src/debputy/commands/debputy_cmd/context.py index 3363e96..47f65f3 100644 --- a/src/debputy/commands/debputy_cmd/context.py +++ b/src/debputy/commands/debputy_cmd/context.py @@ -219,6 +219,12 @@ class CommandContext: ) return self._substitution + def must_be_called_in_source_root(self) -> None: + if self.debian_dir.get("control") is None: + _error( + "This subcommand must be run from a source package root; expecting debian/control to exist." + ) + def _parse_dctrl( self, ) -> Tuple[ @@ -257,6 +263,9 @@ class CommandContext: ) assert packages <= binary_packages.keys() except FileNotFoundError: + # We are not using `must_be_called_in_source_root`, because we (in this case) require + # the file to be readable (that is, parse_source_debian_control can also raise a + # FileNotFoundError when trying to open the file). _error( "This subcommand must be run from a source package root; expecting debian/control to exist." ) diff --git a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py index b30b98d..e72a6ce 100644 --- a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py +++ b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py @@ -199,8 +199,7 @@ def lint_cmd(context: CommandContext) -> None: from debputy.linting.lint_impl import perform_linting - # For the side effect of validating that we are run from a debian directory. - context.binary_packages() + context.must_be_called_in_source_root() perform_linting(context) diff --git a/src/debputy/commands/debputy_cmd/plugin_cmds.py b/src/debputy/commands/debputy_cmd/plugin_cmds.py index 54acdc5..1343b2e 100644 --- a/src/debputy/commands/debputy_cmd/plugin_cmds.py +++ b/src/debputy/commands/debputy_cmd/plugin_cmds.py @@ -679,7 +679,7 @@ def _render_rule( "PS: If you want to know more about a non-trivial type of an attribute such as `FileSystemMatchRule`," ) print( - "you can use `debputy plugin show type-mapping FileSystemMatchRule` to look it up " + "you can use `debputy plugin show type-mappings FileSystemMatchRule` to look it up " ) diff --git a/src/debputy/linting/lint_util.py b/src/debputy/linting/lint_util.py index 7cdb8b6..de74217 100644 --- a/src/debputy/linting/lint_util.py +++ b/src/debputy/linting/lint_util.py @@ -160,7 +160,7 @@ def report_diagnostic( lint_report.fixed += 1 return lines_to_print = _lines_to_print(diagnostic.range) - if diagnostic.range.end.line >= len(lines) or diagnostic.range.start.line < 1: + if diagnostic.range.end.line > len(lines) or diagnostic.range.start.line < 0: lint_report.diagnostic_errors += 1 _warn( "Bug in the underlying linter: The line numbers of the warning does not fit in the file..." diff --git a/src/debputy/lsp/lsp_debian_control.py b/src/debputy/lsp/lsp_debian_control.py index 8b6238c..6a7ed6a 100644 --- a/src/debputy/lsp/lsp_debian_control.py +++ b/src/debputy/lsp/lsp_debian_control.py @@ -198,25 +198,6 @@ def _binary_package_checks( lines: List[str], diagnostics: List[Diagnostic], ) -> None: - ma_kvpair = stanza.get_kvpair_element("Multi-Arch", use_get=True) - arch = stanza.get("Architecture", "any") - if arch == "all" and ma_kvpair is not None: - ma_value, ma_value_range = _extract_first_value_and_position( - ma_kvpair, - stanza_position, - position_codec, - lines, - ) - if ma_value == "same": - diagnostics.append( - Diagnostic( - ma_value_range, - "Multi-Arch: same is not valid for Architecture: all packages. Maybe you want foreign?", - severity=DiagnosticSeverity.Error, - source="debputy", - ) - ) - package_name = stanza.get("Package", "") source_section = source_stanza.get("Section") section_kvpair = stanza.get_kvpair_element("Section", use_get=True) @@ -415,6 +396,7 @@ def _diagnostics_for_paragraph( diagnostics.extend( known_field.field_diagnostics( kvpair, + stanza, stanza_position, position_codec, lines, @@ -522,60 +504,6 @@ def _diagnostics_for_paragraph( ) -def _diagnostics_for_field_name( - token: Deb822FieldNameToken, - token_position: "TEPosition", - known_field: DctrlKnownField, - typo_detected: bool, - position_codec: "LintCapablePositionCodec", - lines: List[str], - diagnostics: List[Diagnostic], -) -> None: - field_name = token.text - # Defeat the case-insensitivity from python-debian - field_name_cased = str(field_name) - token_range_server_units = te_range_to_lsp( - TERange.from_position_and_size(token_position, token.size()) - ) - token_range = position_codec.range_to_client_units( - lines, - token_range_server_units, - ) - if known_field.deprecated_with_no_replacement: - diagnostics.append( - Diagnostic( - token_range, - f"{field_name_cased} is deprecated and no longer used", - severity=DiagnosticSeverity.Warning, - source="debputy", - tags=[DiagnosticTag.Deprecated], - data=propose_remove_line_quick_fix(), - ) - ) - elif known_field.replaced_by is not None: - diagnostics.append( - Diagnostic( - token_range, - f"{field_name_cased} is a deprecated name for {known_field.replaced_by}", - severity=DiagnosticSeverity.Warning, - source="debputy", - tags=[DiagnosticTag.Deprecated], - data=propose_correct_text_quick_fix(known_field.replaced_by), - ) - ) - - if not typo_detected and field_name_cased != known_field.name: - diagnostics.append( - Diagnostic( - token_range, - f"Non-canonical spelling of {known_field.name}", - severity=DiagnosticSeverity.Information, - source="debputy", - data=propose_correct_text_quick_fix(known_field.name), - ) - ) - - def _scan_for_syntax_errors_and_token_level_diagnostics( deb822_file: Deb822FileElement, position_codec: LintCapablePositionCodec, diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py index 2cc85bb..689866f 100644 --- a/src/debputy/lsp/lsp_debian_control_reference_data.py +++ b/src/debputy/lsp/lsp_debian_control_reference_data.py @@ -1,6 +1,7 @@ import dataclasses import functools import itertools +import re import textwrap from abc import ABC from enum import Enum, auto @@ -14,10 +15,12 @@ from typing import ( Generic, TypeVar, Union, + Callable, + Tuple, ) from debian.debian_support import DpkgArchTable -from lsprotocol.types import DiagnosticSeverity, Diagnostic, DiagnosticTag +from lsprotocol.types import DiagnosticSeverity, Diagnostic, DiagnosticTag, Range from debputy.lsp.quickfixes import ( propose_correct_text_quick_fix, @@ -36,6 +39,7 @@ from debputy.lsp.vendoring._deb822_repro.parsing import ( Deb822FileElement, ) from debputy.lsp.vendoring._deb822_repro.tokens import Deb822FieldNameToken +from debputy.util import PKGNAME_REGEX try: from debputy.lsp.vendoring._deb822_repro.locatable import ( @@ -51,6 +55,20 @@ F = TypeVar("F", bound="Deb822KnownField") S = TypeVar("S", bound="StanzaMetadata") +CustomFieldCheck = Callable[ + [ + "F", + Deb822KeyValuePairElement, + "TERange", + Deb822ParagraphElement, + "TEPosition", + "LintCapablePositionCodec", + List[str], + ], + Iterable[Diagnostic], +] + + ALL_SECTIONS_WITHOUT_COMPONENT = frozenset( [ "admin", @@ -198,6 +216,176 @@ def dpkg_arch_and_wildcards() -> FrozenSet[str]: return frozenset(all_architectures_and_wildcards(dpkg_arch_table._arch2table)) +def _extract_first_value_and_position( + kvpair: Deb822KeyValuePairElement, + stanza_pos: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], +) -> Tuple[Optional[str], Optional[Range]]: + kvpair_pos = kvpair.position_in_parent().relative_to(stanza_pos) + value_element_pos = kvpair.value_element.position_in_parent().relative_to( + kvpair_pos + ) + for value_ref in kvpair.interpret_as( + LIST_SPACE_SEPARATED_INTERPRETATION + ).iter_value_references(): + v = value_ref.value + section_value_loc = value_ref.locatable + value_range_te = section_value_loc.range_in_parent().relative_to( + value_element_pos + ) + value_range_server_units = te_range_to_lsp(value_range_te) + value_range = position_codec.range_to_client_units( + lines, value_range_server_units + ) + return v, value_range + return None, None + + +def _dctrl_ma_field_validation( + _known_field: "F", + _kvpair: Deb822KeyValuePairElement, + _field_range: "TERange", + stanza: Deb822ParagraphElement, + stanza_position: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], +) -> Iterable[Diagnostic]: + ma_kvpair = stanza.get_kvpair_element("Multi-Arch", use_get=True) + arch = stanza.get("Architecture", "any") + if arch == "all" and ma_kvpair is not None: + ma_value, ma_value_range = _extract_first_value_and_position( + ma_kvpair, + stanza_position, + position_codec, + lines, + ) + if ma_value == "same": + yield Diagnostic( + ma_value_range, + "Multi-Arch: same is not valid for Architecture: all packages. Maybe you want foreign?", + severity=DiagnosticSeverity.Error, + source="debputy", + ) + + +def _udeb_only_field_validation( + known_field: "F", + _kvpair: Deb822KeyValuePairElement, + field_range_te: "TERange", + stanza: Deb822ParagraphElement, + _stanza_position: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], +) -> 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 = position_codec.range_to_client_units( + lines, + field_range_server_units, + ) + yield Diagnostic( + field_range, + f"The {known_field.name} field is only applicable to udeb packages (`Package-Type: udeb`)", + severity=DiagnosticSeverity.Warning, + source="debputy", + ) + + +def _arch_not_all_only_field_validation( + known_field: "F", + _kvpair: Deb822KeyValuePairElement, + field_range_te: "TERange", + stanza: Deb822ParagraphElement, + _stanza_position: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], +) -> Iterable[Diagnostic]: + architecture = stanza.get("Architecture") + if architecture == "all": + field_range_server_units = te_range_to_lsp(field_range_te) + field_range = position_codec.range_to_client_units( + lines, + field_range_server_units, + ) + yield Diagnostic( + field_range, + f"The {known_field.name} field is not applicable to arch:all packages (`Architecture: all`)", + severity=DiagnosticSeverity.Warning, + source="debputy", + ) + + +def _each_value_match_regex_validation( + regex: re.Pattern, + *, + diagnostic_severity: DiagnosticSeverity = DiagnosticSeverity.Error, +) -> CustomFieldCheck: + + def _validator( + _known_field: "F", + kvpair: Deb822KeyValuePairElement, + field_range_te: "TERange", + _stanza: Deb822ParagraphElement, + _stanza_position: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], + ) -> Iterable[Diagnostic]: + + value_element_pos = kvpair.value_element.position_in_parent().relative_to( + field_range_te.start_pos + ) + for value_ref in kvpair.interpret_as( + LIST_SPACE_SEPARATED_INTERPRETATION + ).iter_value_references(): + v = value_ref.value + m = regex.fullmatch(v) + if m is not None: + continue + + section_value_loc = value_ref.locatable + value_range_te = section_value_loc.range_in_parent().relative_to( + value_element_pos + ) + value_range_server_units = te_range_to_lsp(value_range_te) + value_range = position_codec.range_to_client_units( + lines, value_range_server_units + ) + yield Diagnostic( + value_range, + f'The value "{v}" does not match the regex {regex.pattern}.', + severity=diagnostic_severity, + source="debputy", + ) + + return _validator + + +def _combined_custom_field_check(*checks: CustomFieldCheck) -> CustomFieldCheck: + def _validator( + known_field: "F", + kvpair: Deb822KeyValuePairElement, + field_range_te: "TERange", + stanza: Deb822ParagraphElement, + stanza_position: "TEPosition", + position_codec: "LintCapablePositionCodec", + lines: List[str], + ) -> Iterable[Diagnostic]: + for check in checks: + yield from check( + known_field, + kvpair, + field_range_te, + stanza, + stanza_position, + position_codec, + lines, + ) + + return _validator + + class FieldValueClass(Enum): SINGLE_VALUE = auto() SPACE_SEPARATED_LIST = auto() @@ -225,10 +413,12 @@ class Deb822KnownField: spellcheck_value: bool = False is_stanza_name: bool = False is_single_value_field: bool = True + custom_field_check: Optional[CustomFieldCheck] = None def field_diagnostics( self, kvpair: Deb822KeyValuePairElement, + stanza: Deb822ParagraphElement, stanza_position: "TEPosition", position_codec: "LintCapablePositionCodec", lines: List[str], @@ -245,6 +435,16 @@ class Deb822KnownField: position_codec, lines, ) + if self.custom_field_check is not None: + yield from self.custom_field_check( + self, + kvpair, + field_range_te, + stanza, + stanza_position, + position_codec, + lines, + ) if not self.spellcheck_value: yield from self._known_value_diagnostics( kvpair, field_position_te, position_codec, lines @@ -409,6 +609,7 @@ SOURCE_FIELDS = _fields( DctrlKnownField( "Source", FieldValueClass.SINGLE_VALUE, + custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), missing_field_severity=DiagnosticSeverity.Error, is_stanza_name=True, hover_text=textwrap.dedent( @@ -624,7 +825,6 @@ SOURCE_FIELDS = _fields( sources *without* requiring any login. This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. - ``` """ ), ), @@ -637,7 +837,6 @@ SOURCE_FIELDS = _fields( sources *without* requiring any login. This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. - ``` """ ), ), @@ -650,7 +849,6 @@ SOURCE_FIELDS = _fields( sources *without* requiring any login. This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. - ``` """ ), ), @@ -663,7 +861,6 @@ SOURCE_FIELDS = _fields( sources *without* requiring any login. This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. - ``` """ ), ), @@ -676,7 +873,6 @@ SOURCE_FIELDS = _fields( sources *without* requiring any login. This should be used together with the **Vcs-Browser** field provided there is a web UI for the repo. - ``` """ ), ), @@ -978,10 +1174,12 @@ SOURCE_FIELDS = _fields( ), ) + BINARY_FIELDS = _fields( DctrlKnownField( "Package", FieldValueClass.SINGLE_VALUE, + custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), is_stanza_name=True, missing_field_severity=DiagnosticSeverity.Error, hover_text="Declares the name of a binary package", @@ -1518,6 +1716,7 @@ BINARY_FIELDS = _fields( # not warn about it being explicitly "no". warn_if_default=False, default_value="no", + custom_field_check=_dctrl_ma_field_validation, known_values=_allowed_values( Keyword( "no", @@ -1691,7 +1890,10 @@ BINARY_FIELDS = _fields( DctrlKnownField( "XB-Installer-Menu-Item", FieldValueClass.SINGLE_VALUE, - # TODO: udeb only + custom_field_check=_combined_custom_field_check( + _udeb_only_field_validation, + _each_value_match_regex_validation(re.compile(r"^[1-9]\d{3,4}$")), + ), hover_text=textwrap.dedent( """\ This field is only relevant for `udeb` packages (debian-installer). @@ -1713,6 +1915,7 @@ BINARY_FIELDS = _fields( DctrlKnownField( "X-DH-Build-For-Type", FieldValueClass.SINGLE_VALUE, + custom_field_check=_arch_not_all_only_field_validation, default_value="host", known_values=_allowed_values( Keyword( @@ -1753,6 +1956,7 @@ BINARY_FIELDS = _fields( DctrlKnownField( "X-Time64-Compat", FieldValueClass.SINGLE_VALUE, + custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), hover_text=textwrap.dedent( """\ Special purpose field renamed to the 64-bit time transition. @@ -1826,6 +2030,7 @@ BINARY_FIELDS = _fields( DctrlKnownField( "XB-Cnf-Visible-Pkgname", FieldValueClass.SINGLE_VALUE, + custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), hover_text=textwrap.dedent( """\ **Special-case field**: *This field is only useful in very special circumstances.* diff --git a/src/debputy/lsp/lsp_debian_copyright.py b/src/debputy/lsp/lsp_debian_copyright.py index 995d93f..a1bf8e6 100644 --- a/src/debputy/lsp/lsp_debian_copyright.py +++ b/src/debputy/lsp/lsp_debian_copyright.py @@ -160,32 +160,6 @@ def _paragraph_representation_field( return next(iter(paragraph.iter_parts_of_type(Deb822KeyValuePairElement))) -def _extract_first_value_and_position( - kvpair: Deb822KeyValuePairElement, - stanza_pos: "TEPosition", - position_codec: "LintCapablePositionCodec", - lines: List[str], -) -> Tuple[Optional[str], Optional[Range]]: - kvpair_pos = kvpair.position_in_parent().relative_to(stanza_pos) - value_element_pos = kvpair.value_element.position_in_parent().relative_to( - kvpair_pos - ) - for value_ref in kvpair.interpret_as( - LIST_SPACE_SEPARATED_INTERPRETATION - ).iter_value_references(): - v = value_ref.value - section_value_loc = value_ref.locatable - value_range_te = section_value_loc.range_in_parent().relative_to( - value_element_pos - ) - section_range_server_units = te_range_to_lsp(value_range_te) - section_range = position_codec.range_to_client_units( - lines, section_range_server_units - ) - return v, section_range - return None, None - - def _diagnostics_for_paragraph( stanza: Deb822ParagraphElement, stanza_position: "TEPosition", @@ -309,6 +283,7 @@ def _diagnostics_for_paragraph( diagnostics.extend( known_field.field_diagnostics( kvpair, + stanza, stanza_position, position_codec, lines, @@ -400,60 +375,6 @@ def _diagnostics_for_paragraph( ) -def _diagnostics_for_field_name( - token: Deb822FieldNameToken, - token_position: "TEPosition", - known_field: Deb822KnownField, - typo_detected: bool, - position_codec: "LintCapablePositionCodec", - lines: List[str], - diagnostics: List[Diagnostic], -) -> None: - field_name = token.text - # Defeat the case-insensitivity from python-debian - field_name_cased = str(field_name) - token_range_server_units = te_range_to_lsp( - TERange.from_position_and_size(token_position, token.size()) - ) - token_range = position_codec.range_to_client_units( - lines, - token_range_server_units, - ) - if known_field.deprecated_with_no_replacement: - diagnostics.append( - Diagnostic( - token_range, - f"{field_name_cased} is deprecated and no longer used", - severity=DiagnosticSeverity.Warning, - source="debputy", - tags=[DiagnosticTag.Deprecated], - data=propose_remove_line_quick_fix(), - ) - ) - elif known_field.replaced_by is not None: - diagnostics.append( - Diagnostic( - token_range, - f"{field_name_cased} is a deprecated name for {known_field.replaced_by}", - severity=DiagnosticSeverity.Warning, - source="debputy", - tags=[DiagnosticTag.Deprecated], - data=propose_correct_text_quick_fix(known_field.replaced_by), - ) - ) - - if not typo_detected and field_name_cased != known_field.name: - diagnostics.append( - Diagnostic( - token_range, - f"Non-canonical spelling of {known_field.name}", - severity=DiagnosticSeverity.Information, - source="debputy", - data=propose_correct_text_quick_fix(known_field.name), - ) - ) - - def _scan_for_syntax_errors_and_token_level_diagnostics( deb822_file: Deb822FileElement, position_codec: LintCapablePositionCodec, |