summaryrefslogtreecommitdiffstats
path: root/src/debputy/lsp/lsp_debian_debputy_manifest.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/debputy/lsp/lsp_debian_debputy_manifest.py')
-rw-r--r--src/debputy/lsp/lsp_debian_debputy_manifest.py337
1 files changed, 272 insertions, 65 deletions
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