summaryrefslogtreecommitdiffstats
path: root/src/debputy/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/lsp')
-rw-r--r--src/debputy/lsp/debian-wordlist.dic2
-rw-r--r--src/debputy/lsp/lsp_debian_control_reference_data.py14
-rw-r--r--src/debputy/lsp/lsp_debian_debputy_manifest.py337
-rw-r--r--src/debputy/lsp/lsp_debian_rules.py8
-rw-r--r--src/debputy/lsp/vendoring/_deb822_repro/__init__.py2
-rw-r--r--src/debputy/lsp/vendoring/_deb822_repro/parsing.py2
-rw-r--r--src/debputy/lsp/vendoring/_deb822_repro/tokens.py2
-rw-r--r--src/debputy/lsp/vendoring/_deb822_repro/types.py2
8 files changed, 291 insertions, 78 deletions
diff --git a/src/debputy/lsp/debian-wordlist.dic b/src/debputy/lsp/debian-wordlist.dic
index 11e0438..5d75eaa 100644
--- a/src/debputy/lsp/debian-wordlist.dic
+++ b/src/debputy/lsp/debian-wordlist.dic
@@ -171,8 +171,6 @@ maintscript
maintscripts
makefile
makefiles
-manpage
-manpages
md5sum
md5sums
menutest
diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py
index 3e16f3c..feed858 100644
--- a/src/debputy/lsp/lsp_debian_control_reference_data.py
+++ b/src/debputy/lsp/lsp_debian_control_reference_data.py
@@ -233,7 +233,7 @@ def all_architectures_and_wildcards(arch2table) -> Iterable[Union[str, Keyword]]
The package is an architecture dependent package and need to be compiled for each and every
architecture it.
- The name `any` refers to the fact that this is an architecture *wildcard* matching
+ The name `any` refers to the fact that this is an architecture *wildcard* matching
*any machine architecture* supported by dpkg.
"""
),
@@ -1313,7 +1313,7 @@ BINARY_FIELDS = _fields(
"no",
hover_text=textwrap.dedent(
"""\
- The package is a regular package. This is the default and recommended.</p>
+ The package is a regular package. This is the default and recommended.
Note that declaring a package to be "Essential: no" is the same as not having the field except omitting
the field wastes fewer bytes on everyone's hard disk.
@@ -1358,7 +1358,7 @@ BINARY_FIELDS = _fields(
"no",
hover_text=textwrap.dedent(
"""\
- The package is a regular package. This is the default and recommended.</p>
+ The package is a regular package. This is the default and recommended.
Note that declaring a package to be `XB-Important: no` is the same as not having the field
except omitting the field wastes fewer bytes on everyone's hard-disk.
@@ -1381,7 +1381,7 @@ BINARY_FIELDS = _fields(
"no",
hover_text=textwrap.dedent(
"""\
- The package is a regular package. This is the default and recommended.</p>
+ The package is a regular package. This is the default and recommended.
Note that declaring a package to be `Protected: no` is the same as not having the field
except omitting the field wastes fewer bytes on everyone's hard-disk.
@@ -1450,7 +1450,7 @@ BINARY_FIELDS = _fields(
hover_text=textwrap.dedent(
"""\
Lists the packages that *should* be installed when this package is installed in all but
- *unusual installations*.</p>
+ *unusual installations*.
**Example**:
```
@@ -2017,7 +2017,7 @@ BINARY_FIELDS = _fields(
custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
hover_text=textwrap.dedent(
"""\
- Special purpose field renamed to the 64-bit time transition.
+ Special purpose field related to the 64-bit time transition.
It is used to inform packaging helpers what the original (non-transitioned) package name
was when the auto-detection is inadequate. The non-transitioned package name is then
@@ -2644,7 +2644,7 @@ _DTESTSCTRL_FIELDS = _fields(
hover_text=textwrap.dedent(
"""\
If your test only contains a shell command or two, or you want to
- re-use an existing upstream test executable and just need to wrap it
+ reuse an existing upstream test executable and just need to wrap it
with some command like `dbus-launch` or `env`, you can use this
field to specify the shell command directly. It will be run under
`bash -e`. This is mutually exclusive with the `Tests:` field.
diff --git a/src/debputy/lsp/lsp_debian_debputy_manifest.py b/src/debputy/lsp/lsp_debian_debputy_manifest.py
index d24d441..ba30c75 100644
--- a/src/debputy/lsp/lsp_debian_debputy_manifest.py
+++ b/src/debputy/lsp/lsp_debian_debputy_manifest.py
@@ -25,6 +25,8 @@ from lsprotocol.types import (
CompletionParams,
CompletionList,
CompletionItem,
+ DiagnosticRelatedInformation,
+ Location,
)
from debputy.lsp.quickfixes import propose_correct_text_quick_fix
from debputy.manifest_parser.base_types import DebputyDispatchableType
@@ -123,7 +125,7 @@ def _word_range_at_position(
@lint_diagnostics(_LANGUAGE_IDS)
def _lint_debian_debputy_manifest(
- _doc_reference: str,
+ doc_reference: str,
path: str,
lines: List[str],
position_codec: LintCapablePositionCodec,
@@ -172,94 +174,299 @@ def _lint_debian_debputy_manifest(
)
else:
feature_set = lsp_get_plugin_features()
- root_parser = feature_set.manifest_parser_generator.dispatchable_object_parsers[
- OPARSER_MANIFEST_ROOT
- ]
- diagnostics.extend(_lint_content(root_parser, content, lines, position_codec))
+ pg = feature_set.manifest_parser_generator
+ root_parser = pg.dispatchable_object_parsers[OPARSER_MANIFEST_ROOT]
+ diagnostics.extend(
+ _lint_content(
+ doc_reference,
+ pg,
+ root_parser,
+ content,
+ lines,
+ position_codec,
+ )
+ )
return diagnostics
+def _unknown_key(
+ key: str,
+ expected_keys: Iterable[str],
+ line: int,
+ col: int,
+ lines: List[str],
+ position_codec: LintCapablePositionCodec,
+) -> Tuple["Diagnostic", Optional[str]]:
+ key_range = position_codec.range_to_client_units(
+ lines,
+ Range(
+ Position(
+ line,
+ col,
+ ),
+ Position(
+ line,
+ col + len(key),
+ ),
+ ),
+ )
+
+ candidates = detect_possible_typo(key, expected_keys)
+ extra = ""
+ corrected_key = None
+ if candidates:
+ extra = f' It looks like a typo of "{candidates[0]}".'
+ # TODO: We should be able to tell that `install-doc` and `install-docs` are the same.
+ # That would enable this to work in more cases.
+ corrected_key = candidates[0] if len(candidates) == 1 else None
+
+ diagnostic = Diagnostic(
+ key_range,
+ f'Unknown or unsupported key "{key}".{extra}',
+ DiagnosticSeverity.Error,
+ source="debputy",
+ data=[propose_correct_text_quick_fix(n) for n in candidates],
+ )
+ return diagnostic, corrected_key
+
+
+def _conflicting_key(
+ uri: str,
+ key_a: str,
+ key_b: str,
+ key_a_line: int,
+ key_a_col: int,
+ key_b_line: int,
+ key_b_col: int,
+ lines: List[str],
+ position_codec: LintCapablePositionCodec,
+) -> Iterable["Diagnostic"]:
+ key_a_range = position_codec.range_to_client_units(
+ lines,
+ Range(
+ Position(
+ key_a_line,
+ key_a_col,
+ ),
+ Position(
+ key_a_line,
+ key_a_col + len(key_a),
+ ),
+ ),
+ )
+ key_b_range = position_codec.range_to_client_units(
+ lines,
+ Range(
+ Position(
+ key_b_line,
+ key_b_col,
+ ),
+ Position(
+ key_b_line,
+ key_b_col + len(key_b),
+ ),
+ ),
+ )
+ yield Diagnostic(
+ key_a_range,
+ f'The "{key_a}" cannot be used with "{key_b}".',
+ DiagnosticSeverity.Error,
+ source="debputy",
+ related_information=[
+ DiagnosticRelatedInformation(
+ location=Location(
+ uri,
+ key_b_range,
+ ),
+ message=f'The attribute "{key_b}" is used here.',
+ )
+ ],
+ )
+
+ yield Diagnostic(
+ key_b_range,
+ f'The "{key_b}" cannot be used with "{key_a}".',
+ DiagnosticSeverity.Error,
+ source="debputy",
+ related_information=[
+ DiagnosticRelatedInformation(
+ location=Location(
+ uri,
+ key_a_range,
+ ),
+ message=f'The attribute "{key_a}" is used here.',
+ )
+ ],
+ )
+
+
+def _lint_attr_value(
+ uri: str,
+ attr: AttributeDescription,
+ pg: ParserGenerator,
+ value: Any,
+ lines: List[str],
+ position_codec: LintCapablePositionCodec,
+) -> Iterable["Diagnostic"]:
+ attr_type = attr.attribute_type
+ orig = get_origin(attr_type)
+ valid_values: Sequence[Any] = tuple()
+ if orig == Literal:
+ valid_values = get_args(attr.attribute_type)
+ elif orig == bool or attr.attribute_type == bool:
+ valid_values = ("true", "false")
+ elif isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType):
+ parser = pg.dispatch_parser_table_for(attr_type)
+ yield from _lint_content(
+ uri,
+ pg,
+ parser,
+ value,
+ lines,
+ position_codec,
+ )
+ return
+
+ if value in valid_values:
+ return
+ # TODO: Emit diagnostic for broken values
+ return
+
+
+def _lint_declarative_mapping_input_parser(
+ uri: str,
+ pg: ParserGenerator,
+ parser: DeclarativeMappingInputParser,
+ content: Any,
+ lines: List[str],
+ position_codec: LintCapablePositionCodec,
+) -> Iterable["Diagnostic"]:
+ if not isinstance(content, CommentedMap):
+ return
+ lc = content.lc
+ for key, value in content.items():
+ attr = parser.manifest_attributes.get(key)
+ line, col = lc.key(key)
+ if attr is None:
+ diag, corrected_key = _unknown_key(
+ key,
+ parser.manifest_attributes,
+ line,
+ col,
+ lines,
+ position_codec,
+ )
+ yield diag
+ if corrected_key:
+ key = corrected_key
+ attr = parser.manifest_attributes.get(corrected_key)
+ if attr is None:
+ continue
+
+ yield from _lint_attr_value(
+ uri,
+ attr,
+ pg,
+ value,
+ lines,
+ position_codec,
+ )
+
+ for forbidden_key in attr.conflicting_attributes:
+ if forbidden_key in content:
+ con_line, con_col = lc.key(forbidden_key)
+ yield from _conflicting_key(
+ uri,
+ key,
+ forbidden_key,
+ line,
+ col,
+ con_line,
+ con_col,
+ lines,
+ position_codec,
+ )
+ for mx in parser.mutually_exclusive_attributes:
+ matches = content.keys() & mx
+ if len(matches) < 2:
+ continue
+ key, *others = list(matches)
+ line, col = lc.key(key)
+ for other in others:
+ con_line, con_col = lc.key(other)
+ yield from _conflicting_key(
+ uri,
+ key,
+ other,
+ line,
+ col,
+ con_line,
+ con_col,
+ lines,
+ position_codec,
+ )
+
+
def _lint_content(
+ uri: str,
+ pg: ParserGenerator,
parser: DeclarativeInputParser[Any],
content: Any,
lines: List[str],
position_codec: LintCapablePositionCodec,
-) -> Iterable[Diagnostic]:
+) -> Iterable["Diagnostic"]:
if isinstance(parser, DispatchingParserBase):
if not isinstance(content, CommentedMap):
return
lc = content.lc
for key, value in content.items():
- if not parser.is_known_keyword(key):
+ is_known = parser.is_known_keyword(key)
+ if not is_known:
line, col = lc.key(key)
- key_range = position_codec.range_to_client_units(
+ diag, corrected_key = _unknown_key(
+ key,
+ parser.registered_keywords(),
+ line,
+ col,
lines,
- Range(
- Position(
- line,
- col,
- ),
- Position(
- line,
- col + len(key),
- ),
- ),
+ position_codec,
)
+ yield diag
+ if corrected_key is not None:
+ key = corrected_key
+ is_known = True
- candidates = detect_possible_typo(key, parser.registered_keywords())
-
- yield Diagnostic(
- key_range,
- f"Unknown or unsupported key {key}",
- DiagnosticSeverity.Error,
- source="debputy",
- data=[propose_correct_text_quick_fix(n) for n in candidates],
- )
- else:
+ if is_known:
subparser = parser.parser_for(key)
assert subparser is not None
- yield from _lint_content(subparser.parser, value, lines, position_codec)
+ yield from _lint_content(
+ uri,
+ pg,
+ subparser.parser,
+ value,
+ lines,
+ position_codec,
+ )
elif isinstance(parser, ListWrappedDeclarativeInputParser):
if not isinstance(content, CommentedSeq):
return
subparser = parser.delegate
for value in content:
- yield from _lint_content(subparser, value, lines, position_codec)
+ yield from _lint_content(uri, pg, subparser, value, lines, position_codec)
elif isinstance(parser, InPackageContextParser):
if not isinstance(content, CommentedMap):
return
for v in content.values():
- yield from _lint_content(parser.delegate, v, lines, position_codec)
+ yield from _lint_content(uri, pg, parser.delegate, v, lines, position_codec)
elif isinstance(parser, DeclarativeMappingInputParser):
- if not isinstance(content, CommentedMap):
- return
- lc = content.lc
- for key, value in content.items():
- attr = parser.manifest_attributes.get(key)
- if attr is None:
- line, col = lc.key(key)
- key_range = position_codec.range_to_client_units(
- lines,
- Range(
- Position(
- line,
- col,
- ),
- Position(
- line,
- col + len(key),
- ),
- ),
- )
-
- candidates = detect_possible_typo(key, parser.manifest_attributes)
- yield Diagnostic(
- key_range,
- f"Unknown or unsupported key {key}",
- DiagnosticSeverity.Error,
- source="debputy",
- data=[propose_correct_text_quick_fix(n) for n in candidates],
- )
+ yield from _lint_declarative_mapping_input_parser(
+ uri,
+ pg,
+ parser,
+ content,
+ lines,
+ position_codec,
+ )
def is_at(position: Position, lc_pos: Tuple[int, int]) -> bool:
@@ -469,7 +676,7 @@ def _guess_rule_name(segments: List[Union[str, int]], idx: int) -> str:
return "<Bug: unknown rule name>"
-def _ecsape(v: str) -> str:
+def _escape(v: str) -> str:
return '"' + v.replace("\n", "\\n") + '"'
@@ -479,7 +686,7 @@ def _insert_snippet(lines: List[str], server_position: Position) -> bool:
line = lines[line_no]
pos_rhs = line[server_position.character :]
if pos_rhs and not pos_rhs.isspace():
- _info(f"No insertion: {_ecsape(line[server_position.character:])}")
+ _info(f"No insertion: {_escape(line[server_position.character:])}")
return False
lhs_ws = line[: server_position.character]
lhs = lhs_ws.strip()
@@ -492,17 +699,17 @@ def _insert_snippet(lines: List[str], server_position: Position) -> bool:
snippet = _COMPLETION_HINT_KEY if ":" not in lhs else _COMPLETION_HINT_VALUE
new_line = line[: server_position.character] + snippet
elif not lhs or (lhs_ws and not lhs_ws[0].isspace()):
- _info(f"Insertion of key or value: {_ecsape(line[server_position.character:])}")
+ _info(f"Insertion of key or value: {_escape(line[server_position.character:])}")
# Respect the provided indentation
snippet = _COMPLETION_HINT_KEY if ":" not in lhs else _COMPLETION_HINT_VALUE
new_line = line[: server_position.character] + snippet
elif lhs.isalpha() and ":" not in lhs:
- _info(f"Expanding value to a key: {_ecsape(line[server_position.character:])}")
+ _info(f"Expanding value to a key: {_escape(line[server_position.character:])}")
# Respect the provided indentation
new_line = line[: server_position.character] + _COMPLETION_HINT_KEY
else:
c = line[server_position.character]
- _info(f"Not touching line: {_ecsape(line)} -- {_ecsape(c)}")
+ _info(f"Not touching line: {_escape(line)} -- {_escape(c)}")
return False
_info(f'Evaluating complete on synthetic line: "{new_line}"')
lines[line_no] = new_line
diff --git a/src/debputy/lsp/lsp_debian_rules.py b/src/debputy/lsp/lsp_debian_rules.py
index 86b114c..f05099d 100644
--- a/src/debputy/lsp/lsp_debian_rules.py
+++ b/src/debputy/lsp/lsp_debian_rules.py
@@ -1,3 +1,4 @@
+import functools
import itertools
import json
import os
@@ -152,10 +153,17 @@ def _lint_debian_rules(
)
+@functools.lru_cache
+def _is_project_trusted(source_root: str) -> bool:
+ return os.environ.get("DEBPUTY_TRUST_PROJECT", "0") == "1"
+
+
def _run_make_dryrun(
source_root: str,
lines: List[str],
) -> Optional[Diagnostic]:
+ if not _is_project_trusted(source_root):
+ return None
try:
make_res = subprocess.run(
["make", "--dry-run", "-f", "-", "debhelper-fail-me"],
diff --git a/src/debputy/lsp/vendoring/_deb822_repro/__init__.py b/src/debputy/lsp/vendoring/_deb822_repro/__init__.py
index cc2b1de..0736189 100644
--- a/src/debputy/lsp/vendoring/_deb822_repro/__init__.py
+++ b/src/debputy/lsp/vendoring/_deb822_repro/__init__.py
@@ -150,7 +150,7 @@ Deb822ParagraphElement.as_interpreted_dict_view method.
Stability of this API
---------------------
-The API is subject to change based on feedback from early adoptors and beta
+The API is subject to change based on feedback from early adopters and beta
testers. That said, the code for valid files is unlikely to change in
a backwards incompatible way.
diff --git a/src/debputy/lsp/vendoring/_deb822_repro/parsing.py b/src/debputy/lsp/vendoring/_deb822_repro/parsing.py
index 1a2da25..e2c638a 100644
--- a/src/debputy/lsp/vendoring/_deb822_repro/parsing.py
+++ b/src/debputy/lsp/vendoring/_deb822_repro/parsing.py
@@ -3008,7 +3008,7 @@ class Deb822FileElement(Deb822Element):
"""Inserts a paragraph into the file at the given "index" of paragraphs
Note that if the index is between two paragraphs containing a "free
- floating" comment (e.g. paragrah/start-of-file, empty line, comment,
+ floating" comment (e.g. paragraph/start-of-file, empty line, comment,
empty line, paragraph) then it is unspecified which "side" of the
comment the new paragraph will appear and this may change between
versions of python-debian.
diff --git a/src/debputy/lsp/vendoring/_deb822_repro/tokens.py b/src/debputy/lsp/vendoring/_deb822_repro/tokens.py
index 5db991a..6697a2c 100644
--- a/src/debputy/lsp/vendoring/_deb822_repro/tokens.py
+++ b/src/debputy/lsp/vendoring/_deb822_repro/tokens.py
@@ -171,7 +171,7 @@ class Deb822Token(Locatable):
return self._text
def size(self, *, skip_leading_comments: bool = False) -> Range:
- # As tokens are an atomtic unit
+ # As tokens are an atomic unit
token_size = self._token_size
if token_size is not None:
return token_size
diff --git a/src/debputy/lsp/vendoring/_deb822_repro/types.py b/src/debputy/lsp/vendoring/_deb822_repro/types.py
index 7b78024..181f5c9 100644
--- a/src/debputy/lsp/vendoring/_deb822_repro/types.py
+++ b/src/debputy/lsp/vendoring/_deb822_repro/types.py
@@ -61,7 +61,7 @@ try:
"""
FormatterCallback.__doc__ = """\
Formatter callback used with the round-trip safe parser
-
+
See debian._repro_deb822.formatter.format_field for details
"""
except AttributeError: