diff options
Diffstat (limited to 'src/debputy/lsp')
-rw-r--r-- | src/debputy/lsp/debian-wordlist.dic | 2 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_control_reference_data.py | 14 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_debputy_manifest.py | 337 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_rules.py | 8 | ||||
-rw-r--r-- | src/debputy/lsp/vendoring/_deb822_repro/__init__.py | 2 | ||||
-rw-r--r-- | src/debputy/lsp/vendoring/_deb822_repro/parsing.py | 2 | ||||
-rw-r--r-- | src/debputy/lsp/vendoring/_deb822_repro/tokens.py | 2 | ||||
-rw-r--r-- | src/debputy/lsp/vendoring/_deb822_repro/types.py | 2 |
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: |