summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-08-26 10:21:50 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-08-26 10:21:50 +0000
commite4fd260f1696922f28424a967296183b0142e833 (patch)
tree2b2d1ee74820ba297dc876dbd3d52e4392d2f839
parentAdding debian version 0.1.41. (diff)
downloaddebputy-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.py10
-rw-r--r--src/debputy/lsp/lsp_debian_control.py17
-rw-r--r--src/debputy/lsp/lsp_debian_control_reference_data.py375
-rw-r--r--src/debputy/lsp/lsp_debian_copyright.py6
-rw-r--r--src/debputy/lsp/lsp_debian_tests_control.py6
-rw-r--r--src/debputy/packager_provided_files.py14
-rw-r--r--tests/lint_tests/lint_tutil.py19
-rw-r--r--tests/lint_tests/test_lint_dctrl.py244
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