summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/debputy/commands/debputy_cmd/context.py9
-rw-r--r--src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py3
-rw-r--r--src/debputy/commands/debputy_cmd/plugin_cmds.py2
-rw-r--r--src/debputy/linting/lint_util.py2
-rw-r--r--src/debputy/lsp/lsp_debian_control.py74
-rw-r--r--src/debputy/lsp/lsp_debian_control_reference_data.py219
-rw-r--r--src/debputy/lsp/lsp_debian_copyright.py81
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,