summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-08-26 10:21:34 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-08-26 10:21:34 +0000
commitdac850b8d49fb462193d06b76b0737d7cb2484a3 (patch)
tree3f303fe3c57227f3006ab4250cde68d3153b4250
parentAdding upstream version 0.1.40. (diff)
downloaddebputy-dac850b8d49fb462193d06b76b0737d7cb2484a3.tar.xz
debputy-dac850b8d49fb462193d06b76b0737d7cb2484a3.zip
Adding upstream version 0.1.41.upstream/0.1.41
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--debputy/plugins/debputy-documentation.json7
-rw-r--r--src/debputy/analysis/debian_dir.py33
-rw-r--r--src/debputy/dh/dh_assistant.py23
-rw-r--r--src/debputy/linting/lint_impl.py2
-rw-r--r--src/debputy/lsp/lsp_debian_control.py130
-rw-r--r--src/debputy/packager_provided_files.py28
-rw-r--r--tests/lint_tests/test_lint_dctrl.py131
7 files changed, 295 insertions, 59 deletions
diff --git a/debputy/plugins/debputy-documentation.json b/debputy/plugins/debputy-documentation.json
index 4017e2c..2cfde53 100644
--- a/debputy/plugins/debputy-documentation.json
+++ b/debputy/plugins/debputy-documentation.json
@@ -46,6 +46,13 @@
"file-categories": ["maint-config"]
},
{
+ "path": "debian/lintian-brush.conf",
+ "documentation-uris": [
+ "man:lintian-brush.conf(5)"
+ ],
+ "file-categories": ["maint-config"]
+ },
+ {
"path": "debian/source/options",
"documentation-uris": [
"man:dpkg-source(1)"
diff --git a/src/debputy/analysis/debian_dir.py b/src/debputy/analysis/debian_dir.py
index 1e88b14..8ada2a0 100644
--- a/src/debputy/analysis/debian_dir.py
+++ b/src/debputy/analysis/debian_dir.py
@@ -16,6 +16,7 @@ from typing import (
Iterator,
TypedDict,
NotRequired,
+ Container,
)
from debputy.analysis import REFERENCE_DATA_TABLE
@@ -23,6 +24,7 @@ from debputy.analysis.analysis_util import flatten_ppfs
from debputy.dh.dh_assistant import (
resolve_active_and_inactive_dh_commands,
read_dh_addon_sequences,
+ extract_dh_compat_level,
)
from debputy.packager_provided_files import (
PackagerProvidedFile,
@@ -105,7 +107,7 @@ def scan_debian_dir(
or "zz_debputy" in dh_sequences
or "zz-debputy-rrr" in dh_sequences
)
- dh_compat_level, dh_assistant_exit_code = _extract_dh_compat_level()
+ dh_compat_level, dh_assistant_exit_code = extract_dh_compat_level()
dh_issues = []
static_packaging_files = {
@@ -128,6 +130,7 @@ def scan_debian_dir(
binary_packages,
allow_fuzzy_matches=True,
detect_typos=True,
+ ignore_paths=static_packaging_files,
)
)
)
@@ -139,7 +142,7 @@ def scan_debian_dir(
all_dh_ppfs,
dh_issues,
dh_assistant_exit_code,
- ) = _resolve_debhelper_config_files(
+ ) = resolve_debhelper_config_files(
debian_dir,
binary_packages,
debputy_plugin_metadata,
@@ -147,6 +150,7 @@ def scan_debian_dir(
dh_sequences,
dh_compat_level,
uses_dh_sequencer,
+ ignore_paths=static_packaging_files,
)
else:
@@ -285,7 +289,7 @@ def _kpf_install_pattern(
return ppkpf.info.get("install_pattern")
-def _resolve_debhelper_config_files(
+def resolve_debhelper_config_files(
debian_dir: VirtualPath,
binary_packages: Mapping[str, BinaryPackage],
debputy_plugin_metadata: DebputyPluginMetadata,
@@ -293,6 +297,7 @@ def _resolve_debhelper_config_files(
dh_rules_addons: AbstractSet[str],
dh_compat_level: int,
saw_dh: bool,
+ ignore_paths: Container[str] = frozenset(),
) -> Tuple[List[PackagerProvidedFile], Optional[object], int]:
dh_ppfs = {}
commands, exit_code = _relevant_dh_commands(dh_rules_addons)
@@ -434,6 +439,7 @@ def _resolve_debhelper_config_files(
binary_packages,
allow_fuzzy_matches=True,
detect_typos=True,
+ ignore_paths=ignore_paths,
)
)
)
@@ -532,27 +538,6 @@ def _merge_ppfs(
_merge_list(details, "documentation-uris", documentation_uris)
-def _extract_dh_compat_level() -> Tuple[Optional[int], int]:
- try:
- output = subprocess.check_output(
- ["dh_assistant", "active-compat-level"],
- stderr=subprocess.DEVNULL,
- )
- except (FileNotFoundError, subprocess.CalledProcessError) as e:
- exit_code = 127
- if isinstance(e, subprocess.CalledProcessError):
- exit_code = e.returncode
- return None, exit_code
- else:
- data = json.loads(output)
- active_compat_level = data.get("active-compat-level")
- exit_code = 0
- if not isinstance(active_compat_level, int) or active_compat_level < 1:
- active_compat_level = None
- exit_code = 255
- return active_compat_level, exit_code
-
-
def _relevant_dh_commands(dh_rules_addons: Iterable[str]) -> Tuple[List[str], int]:
cmd = ["dh_assistant", "list-commands", "--output-format=json"]
if dh_rules_addons:
diff --git a/src/debputy/dh/dh_assistant.py b/src/debputy/dh/dh_assistant.py
index ba8c14f..836b1b5 100644
--- a/src/debputy/dh/dh_assistant.py
+++ b/src/debputy/dh/dh_assistant.py
@@ -7,7 +7,6 @@ from typing import Iterable, FrozenSet, Optional, List, Union, Mapping, Any, Set
from debian.deb822 import Deb822
from debputy.plugin.api import VirtualPath
-from debputy.util import _info
_FIND_DH_WITH = re.compile(r"--with(?:\s+|=)(\S+)")
_DEP_REGEX = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII)
@@ -123,3 +122,25 @@ def read_dh_addon_sequences(
extract_dh_addons_from_control(source_paragraph, bd_sequences)
return bd_sequences, dr_sequences, saw_dh
return None
+
+
+def extract_dh_compat_level(*, cwd=None) -> Tuple[Optional[int], int]:
+ try:
+ output = subprocess.check_output(
+ ["dh_assistant", "active-compat-level"],
+ stderr=subprocess.DEVNULL,
+ cwd=cwd,
+ )
+ except (FileNotFoundError, subprocess.CalledProcessError) as e:
+ exit_code = 127
+ if isinstance(e, subprocess.CalledProcessError):
+ exit_code = e.returncode
+ return None, exit_code
+ else:
+ data = json.loads(output)
+ active_compat_level = data.get("active-compat-level")
+ exit_code = 0
+ if not isinstance(active_compat_level, int) or active_compat_level < 1:
+ active_compat_level = None
+ exit_code = 255
+ return active_compat_level, exit_code
diff --git a/src/debputy/linting/lint_impl.py b/src/debputy/linting/lint_impl.py
index b424460..ddafd4c 100644
--- a/src/debputy/linting/lint_impl.py
+++ b/src/debputy/linting/lint_impl.py
@@ -276,7 +276,7 @@ def perform_reformat(
https://salsa.debian.org/debian/debputy-pre-commit-hooks
* If you use the Debian Salsa CI pipeline, then you can set SALSA_CI_DISABLE_WRAP_AND_SORT
- to a truth value and `debputy` will pick up the configuration from there.
+ to a "no" or 0 and `debputy` will pick up the configuration from there.
- Note: The option must be in `.gitlab-ci.yml` or `debian/salsa-ci.yml` to work. The Salsa CI
pipeline will use `wrap-and-sort` while `debputy` uses its own emulation of `wrap-and-sort`
(`debputy` also needs to apply the style via `debputy lsp server`).
diff --git a/src/debputy/lsp/lsp_debian_control.py b/src/debputy/lsp/lsp_debian_control.py
index 5bb6265..05cd943 100644
--- a/src/debputy/lsp/lsp_debian_control.py
+++ b/src/debputy/lsp/lsp_debian_control.py
@@ -11,9 +11,12 @@ from typing import (
List,
Dict,
Iterable,
+ Container,
)
-from debputy.analysis.debian_dir import scan_debian_dir
+from debputy.analysis.analysis_util import flatten_ppfs
+from debputy.analysis.debian_dir import scan_debian_dir, 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
from debputy.lsp.diagnostics import DiagnosticData
@@ -92,6 +95,12 @@ from debputy.lsprotocol.types import (
InlayHint,
InlayHintLabelPart,
)
+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
try:
@@ -1042,43 +1051,108 @@ def _package_range_of_stanza(
yield stanza["Package"], stanza.get("Architecture"), representation_field_range
-def _detect_misspelled_packaging_files(
+def _packaging_files(
lint_state: LintState,
- binary_stanzas_w_pos: List[Tuple[Deb822ParagraphElement, TEPosition]],
- diagnostics: List[Diagnostic],
-) -> None:
+) -> Iterable[PackagerProvidedFile]:
+ source_root = lint_state.source_root
debian_dir = lint_state.debian_dir
binary_packages = lint_state.binary_packages
- if debian_dir is None or binary_packages is None:
+ if (
+ source_root is None
+ or not source_root.has_fs_path
+ or debian_dir is None
+ or binary_packages is None
+ ):
return
dh_sequencer_data = lint_state.dh_sequencer_data
-
- all_pkg_file_data, _, _, _ = scan_debian_dir(
- lint_state.plugin_feature_set,
- binary_packages,
- debian_dir,
- uses_dh_sequencer=dh_sequencer_data.uses_dh_sequencer,
- dh_sequences=dh_sequencer_data.sequences,
+ dh_sequences = dh_sequencer_data.sequences
+ is_debputy_package = (
+ "debputy" in dh_sequences
+ or "zz-debputy" in dh_sequences
+ or "zz_debputy" in dh_sequences
+ or "zz-debputy-rrr" in dh_sequences
)
+ feature_set = lint_state.plugin_feature_set
+ known_packaging_files = feature_set.known_packaging_files
+ static_packaging_files = {
+ kpf.detection_value: kpf
+ for kpf in known_packaging_files.values()
+ if kpf.detection_method == "path"
+ }
+ ignored_path = set(static_packaging_files)
+
+ if is_debputy_package:
+ all_debputy_ppfs = list(
+ flatten_ppfs(
+ detect_all_packager_provided_files(
+ feature_set.packager_provided_files,
+ debian_dir,
+ binary_packages,
+ allow_fuzzy_matches=True,
+ detect_typos=True,
+ ignore_paths=ignored_path,
+ )
+ )
+ )
+ for ppf in all_debputy_ppfs:
+ if ppf.path.path in ignored_path:
+ continue
+ ignored_path.add(ppf.path.path)
+ yield ppf
+
+ # FIXME: This should read the editor data, but dh_assistant does not support that.
+ dh_compat_level, _ = extract_dh_compat_level(cwd=source_root.fs_path)
+ if dh_compat_level is not None:
+ debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin()
+ dh_pkgfile_docs = {
+ kpf.detection_value: kpf
+ for kpf in known_packaging_files.values()
+ if kpf.detection_method == "dh.pkgfile"
+ }
+ (
+ all_dh_ppfs,
+ _,
+ _,
+ ) = resolve_debhelper_config_files(
+ debian_dir,
+ binary_packages,
+ debputy_plugin_metadata,
+ dh_pkgfile_docs,
+ dh_sequences,
+ dh_compat_level,
+ saw_dh=dh_sequencer_data.uses_dh_sequencer,
+ ignore_paths=ignored_path,
+ )
+ for ppf in all_dh_ppfs:
+ if ppf.path.path in ignored_path:
+ continue
+ ignored_path.add(ppf.path.path)
+ yield ppf
+
+
+def _detect_misspelled_packaging_files(
+ lint_state: LintState,
+ binary_stanzas_w_pos: List[Tuple[Deb822ParagraphElement, TEPosition]],
+ diagnostics: List[Diagnostic],
+) -> None:
stanza_ranges = {
p: (a, r)
for p, a, r in _package_range_of_stanza(lint_state, binary_stanzas_w_pos)
}
-
- for pkg_file_data in all_pkg_file_data:
- binary_package = pkg_file_data.get("binary-package")
- explicit_package = pkg_file_data.get("pkgfile-explicit-package-name", True)
- name_segment = pkg_file_data.get("pkgfile-name-segment")
- stem = pkg_file_data.get("pkgfile-stem")
+ for ppf in _packaging_files(lint_state):
+ binary_package = ppf.package_name
+ explicit_package = ppf.uses_explicit_package_name
+ name_segment = ppf.name_segment is not None
+ stem = ppf.definition.stem
if binary_package is None or stem is None:
continue
declared_arch, diag_range = stanza_ranges.get(binary_package)
if diag_range is None:
continue
- path = pkg_file_data["path"]
- likely_typo_of = pkg_file_data.get("likely-typo-of")
- arch_restriction = pkg_file_data.get("pkgfile-architecture-restriction")
+ path = ppf.path.path
+ likely_typo_of = ppf.expected_path
+ arch_restriction = ppf.architecture_restriction
if likely_typo_of is not None:
# Handles arch_restriction == 'all' at the same time due to how
# the `likely-typo-of` is created
@@ -1120,7 +1194,7 @@ def _detect_misspelled_packaging_files(
)
)
- if not pkg_file_data.get("pkgfile-is-active-in-build", True):
+ if not ppf.definition.has_active_command:
diagnostics.append(
Diagnostic(
diag_range,
@@ -1137,14 +1211,20 @@ def _detect_misspelled_packaging_files(
if not explicit_package and name_segment is not None:
basename = os.path.basename(path)
+ if basename == ppf.definition.stem:
+ continue
alt_name = f"{binary_package}.{stem}"
if arch_restriction is not None:
alt_name = f"{alt_name}.{arch_restriction}"
+ if ppf.definition.allow_name_segment:
+ or_alt_name = f' (or maybe "debian/{binary_package}.{basename}")'
+ else:
+ or_alt_name = ""
diagnostics.append(
Diagnostic(
diag_range,
- f'Possible typo in "{path}". Consider renaming the file to "debian/{binary_package}.{basename}"'
- f' or "debian/{alt_name} if it is intended for {binary_package}',
+ f'Possible typo in "{path}". Consider renaming the file to "debian/{alt_name}"'
+ f"{or_alt_name} if it is intended for {binary_package}",
severity=DiagnosticSeverity.Warning,
source="debputy",
data=DiagnosticData(
diff --git a/src/debputy/packager_provided_files.py b/src/debputy/packager_provided_files.py
index 8e0e0af..d2512f4 100644
--- a/src/debputy/packager_provided_files.py
+++ b/src/debputy/packager_provided_files.py
@@ -1,6 +1,6 @@
import collections
import dataclasses
-from typing import Mapping, Iterable, Dict, List, Optional, Tuple, Sequence
+from typing import Mapping, Iterable, Dict, List, Optional, Tuple, Sequence, Container
from debputy.packages import BinaryPackage
from debputy.plugin.api import VirtualPath
@@ -10,7 +10,11 @@ from debputy.util import _error, CAN_DETECT_TYPOS, detect_possible_typo
_KNOWN_NON_PPFS = frozenset(
{
+ # Some of these overlap with the _KNOWN_NON_TYPO_EXTENSIONS below
+ # This one is a quicker check. The _KNOWN_NON_TYPO_EXTENSIONS is a general (but more
+ # expensive check).
"gbp.conf", # Typo matches with `gbp.config` (dh_installdebconf) in two edits steps
+ "salsa-ci.yml", # Typo matches with `salsa-ci.wm` (dh_installwm) in two edits steps
# No reason to check any of these as they are never PPFs
"clean",
"control",
@@ -21,6 +25,19 @@ _KNOWN_NON_PPFS = frozenset(
}
)
+_KNOWN_NON_TYPO_EXTENSIONS = frozenset(
+ {
+ "conf",
+ "sh",
+ "yml",
+ "yaml",
+ "json",
+ "bash",
+ "pl",
+ "py",
+ }
+)
+
@dataclasses.dataclass(frozen=True, slots=True)
class PackagerProvidedFile:
@@ -107,6 +124,7 @@ def _find_definition(
basename: str,
*,
period2stems: Optional[Mapping[int, Sequence[str]]] = None,
+ had_arch: bool = False,
) -> Tuple[Optional[str], Optional[PackagerProvidedFileClassSpec], Optional[str]]:
for stem, install_as_name, period_count in _iterate_stem_splits(basename):
definition = packager_provided_files.get(stem)
@@ -118,6 +136,10 @@ def _find_definition(
if not stems:
continue
+ # If the stem is also the extension and a known one at that, then
+ # we do not consider it a typo match (to avoid false positives).
+ if not had_arch and stem in _KNOWN_NON_TYPO_EXTENSIONS:
+ continue
matches = detect_possible_typo(stem, stems)
if matches is not None and len(matches) == 1:
definition = packager_provided_files[matches[0]]
@@ -252,6 +274,7 @@ def _split_path(
packager_provided_files,
basename,
period2stems=period2stems,
+ had_arch=bool(arch_restriction),
)
if definition is None:
continue
@@ -365,6 +388,7 @@ def detect_all_packager_provided_files(
*,
allow_fuzzy_matches: bool = False,
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
@@ -381,7 +405,7 @@ def detect_all_packager_provided_files(
for entry in debian_dir.iterdir:
if entry.is_dir:
continue
- if entry.name in _KNOWN_NON_PPFS:
+ if entry.path in ignore_paths or entry.name in _KNOWN_NON_PPFS:
continue
matching_ppfs = _split_path(
packager_provided_files,
diff --git a/tests/lint_tests/test_lint_dctrl.py b/tests/lint_tests/test_lint_dctrl.py
index 745c323..840fabe 100644
--- a/tests/lint_tests/test_lint_dctrl.py
+++ b/tests/lint_tests/test_lint_dctrl.py
@@ -732,7 +732,12 @@ def test_dctrl_lint_ambiguous_pkgfile(line_linter: LintWrapper) -> None:
# FIXME: This relies on "cwd" being a valid debian directory using debhelper. Fix and
# remove the `build_time_only` restriction
- line_linter.source_root = build_virtual_file_system(["./debian/bar.install"])
+ line_linter.source_root = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="."),
+ "./debian/bar.service",
+ ]
+ )
diagnostics = line_linter(lines)
print(diagnostics)
@@ -740,8 +745,8 @@ def test_dctrl_lint_ambiguous_pkgfile(line_linter: LintWrapper) -> None:
issue = diagnostics[0]
msg = (
- 'Possible typo in "./debian/bar.install". Consider renaming the file to "debian/foo.bar.install"'
- ' or "debian/foo.install if it is intended for foo'
+ 'Possible typo in "./debian/bar.service". Consider renaming the file to "debian/foo.service"'
+ ' (or maybe "debian/foo.bar.service") if it is intended for foo'
)
assert issue.message == msg
assert f"{issue.range}" == "7:0-8:0"
@@ -749,8 +754,59 @@ def test_dctrl_lint_ambiguous_pkgfile(line_linter: LintWrapper) -> None:
diag_data = issue.data
assert isinstance(diag_data, dict)
assert diag_data.get("report_for_related_file") in (
- "./debian/bar.install",
- "debian/bar.install",
+ "./debian/bar.service",
+ "debian/bar.service",
+ )
+
+
+@build_time_only
+def test_dctrl_lint_ambiguous_pkgfile_no_name_segment(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), dh-sequence-zz-debputy,
+
+ Package: foo
+ Architecture: all
+ Depends: bar, baz
+ Description: some short synopsis
+ A very interesting description
+ with a valid synopsis
+ .
+ Just so be clear, this is for a test.
+ """
+ ).splitlines(keepends=True)
+
+ # FIXME: This relies on "cwd" being a valid debian directory using debhelper. Fix and
+ # remove the `build_time_only` restriction
+ line_linter.source_root = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="."),
+ "./debian/bar.alternatives",
+ ]
+ )
+
+ diagnostics = line_linter(lines)
+ print(diagnostics)
+ assert diagnostics and len(diagnostics) == 1
+ issue = diagnostics[0]
+
+ msg = (
+ 'Possible typo in "./debian/bar.alternatives". Consider renaming the file to "debian/foo.alternatives"'
+ " if it is intended for foo"
+ )
+ assert issue.message == msg
+ assert f"{issue.range}" == "7:0-8:0"
+ assert issue.severity == DiagnosticSeverity.Warning
+ diag_data = issue.data
+ assert isinstance(diag_data, dict)
+ assert diag_data.get("report_for_related_file") in (
+ "./debian/bar.alternatives",
+ "debian/bar.alternatives",
)
@@ -779,7 +835,12 @@ def test_dctrl_lint_stem_typo_pkgfile(line_linter: LintWrapper) -> None:
# FIXME: This relies on "cwd" being a valid debian directory using debhelper. Fix and
# remove the `build_time_only` restriction
- line_linter.source_root = build_virtual_file_system(["./debian/foo.intsall"])
+ line_linter.source_root = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="."),
+ "./debian/foo.intsall",
+ ]
+ )
diagnostics = line_linter(lines)
print(diagnostics)
@@ -827,6 +888,7 @@ def test_dctrl_lint_stem_inactive_pkgfile_fp(line_linter: LintWrapper) -> None:
# load the `zz-debputy` sequence.
line_linter.source_root = build_virtual_file_system(
[
+ virtual_path_def(".", fs_path="."),
"./debian/foo.install",
virtual_path_def(
"./debian/rules",
@@ -846,3 +908,60 @@ def test_dctrl_lint_stem_inactive_pkgfile_fp(line_linter: LintWrapper) -> None:
print(diagnostics)
# We should not emit diagnostics when the package is not using dh!
assert not diagnostics
+
+
+@requires_levenshtein
+@build_time_only
+def test_dctrl_lint_stem_typo_pkgfile_ignored_exts_or_files(
+ 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
+ Description: some short synopsis
+ A very interesting description
+ with a valid synopsis
+ .
+ Just so be clear, this is for a test.
+ """
+ ).splitlines(keepends=True)
+
+ # FIXME: This relies on "cwd" being a valid debian directory using debhelper. Fix and
+ # remove the `build_time_only` restriction
+ line_linter.source_root = build_virtual_file_system(
+ [
+ virtual_path_def(".", fs_path="."),
+ "debian/salsa-ci.yml",
+ "debian/gbp.conf",
+ "debian/foo.conf",
+ "debian/foo.sh",
+ "debian/foo.yml",
+ # One wrong one to ensure the test works.
+ "debian/foo.intsall",
+ ]
+ )
+
+ diagnostics = line_linter(lines)
+ print(diagnostics)
+ assert diagnostics and len(diagnostics) == 1
+ issue = diagnostics[0]
+
+ msg = 'The file "./debian/foo.intsall" is likely a typo of "./debian/foo.install"'
+ assert issue.message == msg
+ assert f"{issue.range}" == "7:0-8:0"
+ assert issue.severity == DiagnosticSeverity.Warning
+ diag_data = issue.data
+ assert isinstance(diag_data, dict)
+ assert diag_data.get("report_for_related_file") in (
+ "./debian/foo.intsall",
+ "debian/foo.intsall",
+ )