diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-07 04:46:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-07 04:46:50 +0000 |
commit | bc89ba559341c8db00321e79f04789056c092436 (patch) | |
tree | 6bbf70d90e4b04d0cebf8ef460e08ee1f16f86f0 | |
parent | Releasing progress-linux version 0.1.31-0.0~progress7.99u1. (diff) | |
download | debputy-bc89ba559341c8db00321e79f04789056c092436.tar.xz debputy-bc89ba559341c8db00321e79f04789056c092436.zip |
Merging upstream version 0.1.32.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | debputy.pod | 13 | ||||
-rw-r--r-- | src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py | 20 | ||||
-rw-r--r-- | src/debputy/filesystem_scan.py | 3 | ||||
-rw-r--r-- | src/debputy/linting/lint_impl.py | 94 | ||||
-rw-r--r-- | src/debputy/linting/lint_util.py | 12 | ||||
-rw-r--r-- | src/debputy/lsp/debputy_ls.py | 41 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_debian_control_reference_data.py | 123 | ||||
-rw-r--r-- | src/debputy/lsp/lsp_generic_deb822.py | 141 | ||||
-rw-r--r-- | src/debputy/lsp/style_prefs.py | 33 | ||||
-rw-r--r-- | tests/lint_tests/lint_tutil.py | 2 | ||||
-rw-r--r-- | tests/test_style.py | 18 |
11 files changed, 393 insertions, 107 deletions
diff --git a/debputy.pod b/debputy.pod index c53fa23..3687398 100644 --- a/debputy.pod +++ b/debputy.pod @@ -171,7 +171,7 @@ providing the results. If you rely on the exit code, you are recommended to explicitly pass the relevant variant of the flag even if the current default matches your wishes. -=item B<--missing-style-is-ok> +=item B<--unknown-or-unsupported-style-is-ok>, B<--missing-style-is-ok> By default, B<debputy reformat> will exit with an error when it cannot determine which style to use. This is generally what you want for "per package" CI or other lint checks to inform you that @@ -186,6 +186,17 @@ It can also be useful for scripts or automated mass-edits where you want B<debpu the changes you did if there is a known style without being hampered by the packages that have no known style. +The B<--missing-style-is-ok> is a deprecated name since it does not correctly imply that +unsupported styles are also considered ok. + +=item B<--supported-style-is-required> + +Exit with an error if no supported style can be found. This is the default behaviour but +this option can be used to override settings to disable it. The error does not distinguish +between no style found or an unsupported style found (both lead to an error). + +If you rely on the exit code, please set this option explicitly. + =back =item lint diff --git a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py index a5e1142..8189535 100644 --- a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py +++ b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py @@ -314,12 +314,24 @@ def lint_cmd(context: CommandContext) -> None: help='Enable or disable the "linter" convention of exiting with an error if issues were found', ), add_arg( - "--missing-style-is-ok", - dest="declared_style_required", + "--supported-style-is-required", + dest="supported_style_required", default=True, + action="store_true", + help="Fail with an error if a supported style cannot be identified.", + ), + add_arg( + "--unknown-or-unsupported-style-is-ok", + dest="supported_style_required", + action="store_false", + help="Do not exit with an error if no supported style can be identified. Useful for general" + ' pipelines to implement "reformat if possible"', + ), + add_arg( + "--missing-style-is-ok", + dest="supported_style_required", action="store_false", - help="Do not exit with an error if no style can be identified. Useful for general pipelines to implement" - ' "reformat if possible"', + help="[Deprecated] Use --unknown-or-unsupported-style-is-ok instead", ), ], ) diff --git a/src/debputy/filesystem_scan.py b/src/debputy/filesystem_scan.py index 0a18899..7b20040 100644 --- a/src/debputy/filesystem_scan.py +++ b/src/debputy/filesystem_scan.py @@ -1565,7 +1565,8 @@ class FSROOverlay(VirtualPathBase): parent: Optional["FSROOverlay"], ) -> None: self._path: str = path - self._fs_path: str = _normalize_path(fs_path, with_prefix=False) + prefix = "/" if fs_path.startswith("/") else "" + self._fs_path: str = prefix + _normalize_path(fs_path, with_prefix=False) self._parent: Optional[ReferenceType[FSROOverlay]] = ( ref(parent) if parent is not None else None ) diff --git a/src/debputy/linting/lint_impl.py b/src/debputy/linting/lint_impl.py index 8ce78c1..81ce0e9 100644 --- a/src/debputy/linting/lint_impl.py +++ b/src/debputy/linting/lint_impl.py @@ -6,7 +6,9 @@ import sys import textwrap from typing import Optional, List, Union, NoReturn, Mapping +from debputy.filesystem_scan import FSROOverlay from debputy.lsp.vendoring._deb822_repro import Deb822FileElement +from debputy.plugin.api import VirtualPath from debputy.yaml import MANIFEST_YAML, YAMLError from lsprotocol.types import ( CodeAction, @@ -83,16 +85,21 @@ REFORMAT_FORMATS = { class LintContext: plugin_feature_set: PluginProvidedFeatureSet style_preference_table: StylePreferenceTable + source_root: Optional[VirtualPath] + debian_dir: Optional[VirtualPath] parsed_deb822_file_content: Optional[Deb822FileElement] = None source_package: Optional[SourcePackage] = None binary_packages: Optional[Mapping[str, BinaryPackage]] = None effective_preference: Optional[EffectivePreference] = None + unsupported_preference_reason: Optional[str] = None salsa_ci: Optional[CommentedMap] = None def state_for(self, path: str, content: str, lines: List[str]) -> LintStateImpl: return LintStateImpl( self.plugin_feature_set, self.style_preference_table, + self.source_root, + self.debian_dir, path, content, lines, @@ -103,9 +110,15 @@ class LintContext: def gather_lint_info(context: CommandContext) -> LintContext: + source_root = FSROOverlay.create_root_dir(".", ".") + debian_dir = source_root.get("debian") + if debian_dir is not None and not debian_dir.is_dir: + debian_dir = None lint_context = LintContext( context.load_plugins(), StylePreferenceTable.load_styles(), + source_root, + debian_dir, ) try: with open("debian/control") as fd: @@ -131,11 +144,13 @@ def gather_lint_info(context: CommandContext) -> LintContext: except YAMLError: break if source_package is not None or salsa_ci_map is not None: - lint_context.effective_preference = determine_effective_style( + pref, pref_reason = determine_effective_style( lint_context.style_preference_table, source_package, salsa_ci_map, ) + lint_context.effective_preference = pref + lint_context.unsupported_preference_reason = pref_reason return lint_context @@ -207,44 +222,57 @@ def perform_reformat( lint_context.effective_preference = style if lint_context.effective_preference is None: - print( - textwrap.dedent( - """\ - You can enable set a style by doing either of: - - * You can set `X-Style: black` in the source stanza of `debian/control` to pick - `black` as the preferred style for this package. - - Note: `black` is an opinionated style that follows the spirit of the `black` code formatter - for Python. - - If you use `pre-commit`, then there is a formatting hook at - 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. - - 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`). - - * The `debputy` code also comes with a built-in style database. This may be interesting for - packaging teams, so set a default team style that applies to all packages maintained by - that packaging team. - - Individuals can also add their style, which can useful for ad-hoc packaging teams, where - `debputy` will automatically apply a style if *all* co-maintainers agree to it. - - Note the above list is an ordered list of how `debputy` determines which style to use in case - multiple options are available. - """ + if lint_context.unsupported_preference_reason is not None: + _warn( + "While `debputy` could identify a formatting for this package, it does not support it." ) - ) - if parsed_args.declared_style_required: - _error( - "Sorry; `debputy` does not know which style to use for this package." + _warn(f"{lint_context.unsupported_preference_reason}") + if parsed_args.supported_style_required: + _error( + "Sorry; `debputy` does not support the style. Use --unknown-or-unsupported-style-is-ok to make" + " this a non-error." + ) + else: + print( + textwrap.dedent( + """\ + You can enable set a style by doing either of: + + * You can set `X-Style: black` in the source stanza of `debian/control` to pick + `black` as the preferred style for this package. + - Note: `black` is an opinionated style that follows the spirit of the `black` code formatter + for Python. + - If you use `pre-commit`, then there is a formatting hook at + 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. + - 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`). + + * The `debputy` code also comes with a built-in style database. This may be interesting for + packaging teams, so set a default team style that applies to all packages maintained by + that packaging team. + - Individuals can also add their style, which can useful for ad-hoc packaging teams, where + `debputy` will automatically apply a style if *all* co-maintainers agree to it. + + Note the above list is an ordered list of how `debputy` determines which style to use in case + multiple options are available. + """ + ) ) + if parsed_args.supported_style_required: + _error( + "Sorry; `debputy` does not know which style to use for this package. Please either set a" + "style or use --unknown-or-unsupported-style-is-ok to make this a non-error" + ) _info("") _info( - "Doing nothing since no style could be identified as requested." + "Doing nothing since no supported style could be identified as requested." " See above how to set a style." ) + _info("Use --supported-style-is-required if this should be an error instead.") sys.exit(0) changes = False diff --git a/src/debputy/linting/lint_util.py b/src/debputy/linting/lint_util.py index 02b8804..78e9f9a 100644 --- a/src/debputy/linting/lint_util.py +++ b/src/debputy/linting/lint_util.py @@ -5,8 +5,10 @@ from typing import List, Optional, Callable, Counter, TYPE_CHECKING, Mapping, Se from lsprotocol.types import Position, Range, Diagnostic, DiagnosticSeverity, TextEdit from debputy.commands.debputy_cmd.output import OutputStylingBase +from debputy.filesystem_scan import VirtualPathBase from debputy.lsp.vendoring._deb822_repro import Deb822FileElement, parse_deb822_file from debputy.packages import SourcePackage, BinaryPackage +from debputy.plugin.api import VirtualPath from debputy.plugin.api.feature_set import PluginProvidedFeatureSet from debputy.util import _DEFAULT_LOGGER, _warn @@ -33,6 +35,14 @@ class LintState: raise NotImplementedError @property + def source_root(self) -> Optional[VirtualPathBase]: + raise NotImplementedError + + @property + def debian_dir(self) -> Optional[VirtualPathBase]: + raise NotImplementedError + + @property def path(self) -> str: raise NotImplementedError @@ -73,6 +83,8 @@ class LintState: class LintStateImpl(LintState): plugin_feature_set: PluginProvidedFeatureSet = dataclasses.field(repr=False) style_preference_table: "StylePreferenceTable" = dataclasses.field(repr=False) + source_root: Optional[VirtualPathBase] + debian_dir: Optional[VirtualPathBase] path: str content: str lines: List[str] diff --git a/src/debputy/lsp/debputy_ls.py b/src/debputy/lsp/debputy_ls.py index c08fd18..7365491 100644 --- a/src/debputy/lsp/debputy_ls.py +++ b/src/debputy/lsp/debputy_ls.py @@ -13,6 +13,7 @@ from typing import ( from lsprotocol.types import MarkupKind +from debputy.filesystem_scan import FSROOverlay, VirtualPathBase from debputy.linting.lint_util import ( LintState, ) @@ -28,6 +29,7 @@ from debputy.packages import ( BinaryPackage, DctrlParser, ) +from debputy.plugin.api import VirtualPath from debputy.plugin.api.feature_set import PluginProvidedFeatureSet from debputy.util import _info from debputy.yaml import MANIFEST_YAML, YAMLError @@ -167,6 +169,7 @@ class LSProvidedLintState(LintState): self, ls: "DebputyLanguageServer", doc: "TextDocument", + source_root: str, debian_dir_path: str, dctrl_parser: DctrlParser, ) -> None: @@ -174,18 +177,29 @@ class LSProvidedLintState(LintState): self._doc = doc # Cache lines (doc.lines re-splits everytime) self._lines = doc.lines + self._source_root = FSROOverlay.create_root_dir(".", source_root) + debian_dir = self._source_root.get("debian") + if debian_dir is not None and not debian_dir.is_dir: + debian_dir = None + self._debian_dir = debian_dir dctrl_file = os.path.join(debian_dir_path, "control") - self._dctrl_cache: DctrlFileCache = DctrlFileCache( - from_fs_path(dctrl_file), - dctrl_file, - dctrl_parser=dctrl_parser, - ) + if dctrl_file != doc.path: - self._deb822_file: Deb822FileCache = Deb822FileCache( + self._dctrl_cache: DctrlFileCache = DctrlFileCache( from_fs_path(dctrl_file), dctrl_file, + dctrl_parser=dctrl_parser, + ) + self._deb822_file: Deb822FileCache = Deb822FileCache( + doc.uri, + doc.path, ) else: + self._dctrl_cache: DctrlFileCache = DctrlFileCache( + doc.uri, + doc.path, + dctrl_parser=dctrl_parser, + ) self._deb822_file = self._dctrl_cache self._salsa_ci_caches = [ @@ -205,6 +219,14 @@ class LSProvidedLintState(LintState): return self._doc.uri @property + def source_root(self) -> Optional[VirtualPathBase]: + return self._source_root + + @property + def debian_dir(self) -> Optional[VirtualPathBase]: + return self._debian_dir + + @property def path(self) -> str: return self._doc.path @@ -251,11 +273,12 @@ class LSProvidedLintState(LintState): salsa_ci = self._resolve_salsa_ci() if source_package is None and salsa_ci is None: return None - return determine_effective_style( + style, _ = determine_effective_style( self.style_preference_table, source_package, salsa_ci, ) + return style @property def style_preference_table(self) -> StylePreferenceTable: @@ -343,7 +366,9 @@ class DebputyLanguageServer(LanguageServer): while dir_path and dir_path != "/" and os.path.basename(dir_path) != "debian": dir_path = os.path.dirname(dir_path) - return LSProvidedLintState(self, doc, dir_path, self.dctrl_parser) + source_root = os.path.dirname(dir_path) + + return LSProvidedLintState(self, doc, source_root, dir_path, self.dctrl_parser) @property def _client_hover_markup_formats(self) -> Optional[List[MarkupKind]]: diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py index 1d4628c..bd2a43d 100644 --- a/src/debputy/lsp/lsp_debian_control_reference_data.py +++ b/src/debputy/lsp/lsp_debian_control_reference_data.py @@ -25,6 +25,8 @@ from typing import ( Sequence, ) +from debputy.filesystem_scan import VirtualPathBase +from debputy.linting.lint_util import LintState from debputy.lsp.vendoring._deb822_repro.types import TE from debian.debian_support import DpkgArchTable from lsprotocol.types import ( @@ -38,6 +40,7 @@ from lsprotocol.types import ( MarkupContent, CompletionItemTag, MarkupKind, + CompletionItemKind, ) from debputy.lsp.diagnostics import DiagnosticData @@ -71,6 +74,7 @@ from debputy.lsp.vendoring._deb822_repro.parsing import ( LIST_UPLOADERS_INTERPRETATION, _parse_whitespace_list_value, parse_deb822_file, + Deb822ParsedTokenList, ) from debputy.lsp.vendoring._deb822_repro.tokens import ( Deb822FieldNameToken, @@ -82,7 +86,8 @@ from debputy.lsp.vendoring._deb822_repro.tokens import ( ) from debputy.lsp.vendoring._deb822_repro.types import FormatterCallback from debputy.lsp.vendoring.wrap_and_sort import _sort_packages_key -from debputy.util import PKGNAME_REGEX +from debputy.plugin.api import VirtualPath +from debputy.util import PKGNAME_REGEX, _info try: from debputy.lsp.vendoring._deb822_repro.locatable import ( @@ -759,9 +764,9 @@ class FieldValueClass(Enum): COMMA_SEPARATED_EMAIL_LIST = auto(), LIST_UPLOADERS_INTERPRETATION COMMA_OR_SPACE_SEPARATED_LIST = auto(), LIST_COMMA_OR_SPACE_SEPARATED_INTERPRETATION FREE_TEXT_FIELD = auto(), None - DEP5_FILE_LIST = auto(), None # TODO + DEP5_FILE_LIST = auto(), LIST_SPACE_SEPARATED_INTERPRETATION - def interpreter(self) -> Optional[Interpretation[Any]]: + def interpreter(self) -> Optional[Interpretation[Deb822ParsedTokenList[Any, Any]]]: return self.value[1] @@ -809,6 +814,36 @@ def _unknown_value_check( return known_value, message, severity, fix_data +def _dep5_escape_path(path: str) -> str: + return path.replace(" ", "?") + + +def _noop_escape_path(path: str) -> str: + return path + + +def _should_ignore_dir( + path: VirtualPath, + *, + supports_dir_match: bool = False, + match_non_persistent_paths: bool = False, +) -> bool: + if not supports_dir_match and not any(path.iterdir): + return True + cachedir_tag = path.get("CACHEDIR.TAG") + if ( + not match_non_persistent_paths + and cachedir_tag is not None + and cachedir_tag.is_file + ): + # https://bford.info/cachedir/ + with cachedir_tag.open(byte_io=True, buffering=64) as fd: + start = fd.read(43) + if start == b"Signature: 8a477f597d28d172789f06886806bc55": + return True + return False + + @dataclasses.dataclass(slots=True, frozen=True) class Deb822KnownField: name: str @@ -844,6 +879,7 @@ class Deb822KnownField: def complete_field( self, + lint_state: LintState, stanza_parts: Sequence[Deb822ParagraphElement], markdown_kind: MarkupKind, ) -> Optional[CompletionItem]: @@ -852,7 +888,9 @@ class Deb822KnownField: name = self.name complete_as = name + ": " options = self.value_options_for_completer( + lint_state, stanza_parts, + "", is_completion_for_field=True, ) if options is not None and len(options) == 1: @@ -883,13 +921,92 @@ class Deb822KnownField: documentation=doc, ) + def _complete_files( + self, + base_dir: Optional[VirtualPathBase], + value_being_completed: str, + *, + is_dep5_file_list: bool = False, + supports_dir_match: bool = False, + supports_spaces_in_filename: bool = False, + match_non_persistent_paths: bool = False, + ) -> Optional[Sequence[CompletionItem]]: + _info(f"_complete_files: {base_dir.fs_path} - {value_being_completed!r}") + if base_dir is None or not base_dir.is_dir: + return None + + if is_dep5_file_list: + supports_spaces_in_filename = True + supports_dir_match = False + match_non_persistent_paths = False + + if value_being_completed == "": + current_dir = base_dir + unmatched_parts: Sequence[str] = tuple() + else: + current_dir, unmatched_parts = base_dir.attempt_lookup( + value_being_completed + ) + + if len(unmatched_parts) > 1: + # Unknown directory part / glob, and we currently do not deal with that. + return None + if len(unmatched_parts) == 1 and unmatched_parts[0] == "*": + # Avoid convincing the client to remove the star (seen with emacs) + return None + items = [] + + path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path + + for child in current_dir.iterdir: + if child.is_symlink and is_dep5_file_list: + continue + if not supports_spaces_in_filename and ( + " " in child.name or "\t" in child.name + ): + continue + if child.is_dir: + if _should_ignore_dir( + child, + supports_dir_match=supports_dir_match, + match_non_persistent_paths=match_non_persistent_paths, + ): + continue + items.append( + CompletionItem( + f"{child.path}/", + insert_text=path_escaper(f"{child.path}/"), + kind=CompletionItemKind.Folder, + ) + ) + else: + items.append( + CompletionItem( + child.path, + insert_text=path_escaper(child.path), + kind=CompletionItemKind.File, + ) + ) + return items + def value_options_for_completer( self, + lint_state: LintState, stanza_parts: Sequence[Deb822ParagraphElement], + value_being_completed: str, *, is_completion_for_field: bool = False, ) -> Optional[Sequence[CompletionItem]]: known_values = self.known_values + if self.field_value_class == FieldValueClass.DEP5_FILE_LIST: + if is_completion_for_field: + return None + return self._complete_files( + lint_state.source_root, + value_being_completed, + is_dep5_file_list=True, + ) + if known_values is None: return None if is_completion_for_field and ( diff --git a/src/debputy/lsp/lsp_generic_deb822.py b/src/debputy/lsp/lsp_generic_deb822.py index 6d44d1a..5b1a22a 100644 --- a/src/debputy/lsp/lsp_generic_deb822.py +++ b/src/debputy/lsp/lsp_generic_deb822.py @@ -6,7 +6,6 @@ from typing import ( Union, Sequence, Tuple, - Set, Any, Container, List, @@ -21,7 +20,6 @@ from lsprotocol.types import ( CompletionList, CompletionItem, Position, - CompletionItemTag, MarkupContent, Hover, MarkupKind, @@ -72,27 +70,41 @@ except ImportError: _CONTAINS_SPACE_OR_COLON = re.compile(r"[\s:]") -def in_range(te_range: TERange, cursor_position: Position) -> bool: +def in_range( + te_range: TERange, + cursor_position: Position, + *, + inclusive_end: bool = False, +) -> bool: + cursor_line = cursor_position.line start_pos = te_range.start_pos end_pos = te_range.end_pos - if start_pos.line_position < cursor_position.line < end_pos.line_position: - return True - if ( - cursor_position.line == start_pos.line_position - and cursor_position.character >= start_pos.cursor_position - ): - return True + if cursor_line < start_pos.line_position or cursor_line > end_pos.line_position: + return False + + if start_pos.line_position == end_pos.line_position: + start_col = start_pos.cursor_position + cursor_col = cursor_position.character + end_col = end_pos.cursor_position + if inclusive_end: + return start_col <= cursor_col <= end_col + return start_col <= cursor_col < end_col + + if cursor_line == end_pos.line_position: + return cursor_position.character < end_pos.cursor_position + return ( - cursor_position.line == end_pos.line_position - and cursor_position.character < end_pos.cursor_position + cursor_line > start_pos.line_position + or start_pos.cursor_position <= cursor_position.character ) def _field_at_position( stanza: Deb822ParagraphElement, + stanza_metadata: S, stanza_range: TERange, position: Position, -) -> Tuple[Optional[Deb822KeyValuePairElement], bool]: +) -> Tuple[Optional[Deb822KeyValuePairElement], Optional[F], str, bool]: te_range = TERange(stanza_range.start_pos, stanza_range.start_pos) for token_or_element in stanza.iter_parts(): te_range = token_or_element.size().relative_to(te_range.end_pos) @@ -102,9 +114,27 @@ def _field_at_position( value_range = token_or_element.value_element.range_in_parent().relative_to( te_range.start_pos ) + known_field = stanza_metadata.get(token_or_element.field_name) in_value = in_range(value_range, position) - return token_or_element, in_value - return None, False + interpreter = ( + known_field.field_value_class.interpreter() + if known_field is not None + else None + ) + matched_value = "" + if in_value and interpreter is not None: + interpreted = token_or_element.interpret_as(interpreter) + for value_ref in interpreted.iter_value_references(): + value_token_range = ( + value_ref.locatable.range_in_parent().relative_to( + value_range.start_pos + ) + ) + if in_range(value_token_range, position, inclusive_end=True): + matched_value = value_ref.value + break + return token_or_element, known_field, matched_value, in_value + return None, None, "", False def _allow_stanza_continuation( @@ -123,11 +153,20 @@ def _allow_stanza_continuation( def _at_cursor( deb822_file: Deb822FileElement, + file_metadata: Deb822FileMetadata[S], doc: "TextDocument", lines: List[str], client_position: Position, is_completion: bool = False, -) -> Tuple[Position, Optional[str], str, bool, int, Iterable[Deb822ParagraphElement]]: +) -> Tuple[ + Position, + Optional[str], + str, + bool, + Optional[S], + Optional[F], + Iterable[Deb822ParagraphElement], +]: server_position = doc.position_codec.position_from_client_units( lines, client_position, @@ -144,6 +183,9 @@ def _at_cursor( file_iter = iter(deb822_file.iter_parts()) matched_token: Optional[TokenOrElement] = None matched_field: Optional[str] = None + stanza_metadata: Optional[S] = None + known_field: Optional[F] = None + for token_or_element in file_iter: te_range = token_or_element.size().relative_to(te_range.end_pos) if isinstance(token_or_element, Deb822ParagraphElement): @@ -155,8 +197,14 @@ def _at_cursor( continue matched_token = token_or_element if isinstance(token_or_element, Deb822ParagraphElement): - kvpair, in_value = _field_at_position( - token_or_element, te_range, server_position + stanza_metadata = file_metadata.guess_stanza_classification_by_idx( + paragraph_no + ) + kvpair, known_field, current_word, in_value = _field_at_position( + token_or_element, + stanza_metadata, + te_range, + server_position, ) if kvpair is not None: matched_field = kvpair.field_name @@ -172,12 +220,18 @@ def _at_cursor( stanza_parts = (p for p in (previous_stanza, next_stanza) if p is not None) + if stanza_metadata is None and is_completion: + if paragraph_no < 0: + paragraph_no = 0 + stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) + return ( server_position, matched_field, current_word, in_value, - paragraph_no, + stanza_metadata, + known_field, stanza_parts, ) @@ -189,7 +243,8 @@ def deb822_completer( ) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: doc = ls.workspace.get_text_document(params.text_document.uri) lines = doc.lines - deb822_file = ls.lint_state(doc).parsed_deb822_file_content + lint_state = ls.lint_state(doc) + deb822_file = lint_state.parsed_deb822_file_content if deb822_file is None: _warn("The deb822 result missing failed!?") ls.show_message_log( @@ -197,29 +252,40 @@ def deb822_completer( ) return None - _a, current_field, _b, in_value, paragraph_no, matched_stanzas = _at_cursor( + ( + _a, + current_field, + word_at_position, + in_value, + stanza_metadata, + known_field, + matched_stanzas, + ) = _at_cursor( deb822_file, + file_metadata, doc, lines, params.position, is_completion=True, ) - stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) - items: Optional[Sequence[CompletionItem]] if in_value: - _info(f"Completion for field value {current_field}") - if current_field is None: - return None - known_field: Deb822KnownField = stanza_metadata.get(current_field) + _info(f"Completion for field value {current_field} -- {word_at_position}") if known_field is None: return None - items = known_field.value_options_for_completer(list(matched_stanzas)) + value_being_completed = word_at_position + items = known_field.value_options_for_completer( + lint_state, + list(matched_stanzas), + value_being_completed, + ) else: _info("Completing field name") + assert stanza_metadata is not None items = _complete_field_name( ls, + lint_state, stanza_metadata, matched_stanzas, ) @@ -261,17 +327,21 @@ def deb822_hover( ) return None - server_pos, current_field, word_at_position, in_value, paragraph_no, _ = _at_cursor( + ( + server_pos, + current_field, + word_at_position, + in_value, + _, + known_field, + _, + ) = _at_cursor( deb822_file, + file_metadata, doc, lines, params.position, ) - stanza_metadata = file_metadata.guess_stanza_classification_by_idx(paragraph_no) - - known_field = ( - stanza_metadata.get(current_field) if current_field is not None else None - ) hover_text = None if custom_handler is not None: res = custom_handler( @@ -579,6 +649,7 @@ def deb822_semantic_tokens_full( def _complete_field_name( ls: "DebputyLanguageServer", + lint_state: LintState, fields: StanzaMetadata[Any], matched_stanzas: Iterable[Deb822ParagraphElement], ) -> Sequence[CompletionItem]: @@ -603,7 +674,7 @@ def _complete_field_name( for cand_key, cand in fields.items(): if cand_key.lower() in seen_fields: continue - item = cand.complete_field(matched_stanzas, markdown_kind) + item = cand.complete_field(lint_state, matched_stanzas, markdown_kind) if item is not None: items.append(item) return items diff --git a/src/debputy/lsp/style_prefs.py b/src/debputy/lsp/style_prefs.py index 40d850b..755e67c 100644 --- a/src/debputy/lsp/style_prefs.py +++ b/src/debputy/lsp/style_prefs.py @@ -16,6 +16,7 @@ from typing import ( Dict, Iterable, Any, + Tuple, ) from debputy.lsp.lsp_reference_keyword import ALL_PUBLIC_NAMED_STYLES @@ -568,12 +569,12 @@ def determine_effective_style( style_preference_table: StylePreferenceTable, source_package: Optional[SourcePackage], salsa_ci: Optional[CommentedMap], -) -> Optional[EffectivePreference]: +) -> Tuple[Optional[EffectivePreference], Optional[str]]: style = source_package.fields.get("X-Style") if source_package is not None else None if style is not None: if style not in ALL_PUBLIC_NAMED_STYLES: - return None - return style_preference_table.named_styles.get(style) + return None, "X-Style contained an unknown/unsupported style" + return style_preference_table.named_styles.get(style), None if salsa_ci: disable_wrap_and_sort = salsa_ci.mlget( @@ -599,29 +600,35 @@ def determine_effective_style( if wrap_and_sort_options is None: wrap_and_sort_options = "" elif not isinstance(wrap_and_sort_options, str): - return None - return parse_salsa_ci_wrap_and_sort_args(wrap_and_sort_options) + return None, "The salsa-ci had a non-string option for wrap-and-sort" + detected_style = parse_salsa_ci_wrap_and_sort_args(wrap_and_sort_options) + if detected_style is None: + msg = "One or more of the wrap-and-sort options in the salsa-ci file was not supported" + else: + msg = None + return detected_style, msg if source_package is None: - return None + return None, None maint = source_package.fields.get("Maintainer") if maint is None: - return None + return None, None maint_email = extract_maint_email(maint) maint_style = style_preference_table.maintainer_preferences.get(maint_email) # Special-case "@packages.debian.org" when missing, since they are likely to be "ad-hoc" # teams that will not be registered. In that case, we fall back to looking at the uploader # preferences as-if the maintainer had not been listed at all. if maint_style is None and not maint_email.endswith("@packages.debian.org"): - return None + return None, None if maint_style is not None and maint_style.is_packaging_team: # When the maintainer is registered as a packaging team, then we assume the packaging # team's style applies unconditionally. - return maint_style.as_effective_pref() + return maint_style.as_effective_pref(), None uploaders = source_package.fields.get("Uploaders") if uploaders is None: - return maint_style.as_effective_pref() if maint_style is not None else None + detected_style = maint_style.as_effective_pref() if maint_style is not None else None + return detected_style, None all_styles: List[Optional[EffectivePreference]] = [] if maint_style is not None: all_styles.append(maint_style) @@ -633,11 +640,11 @@ def determine_effective_style( all_styles.append(uploader_style) if not all_styles: - return None + return None, None r = functools.reduce(EffectivePreference.aligned_preference, all_styles) if isinstance(r, MaintainerPreference): - return r.as_effective_pref() - return r + return r.as_effective_pref(), None + return r, None def _split_options(args: Iterable[str]) -> Iterable[str]: diff --git a/tests/lint_tests/lint_tutil.py b/tests/lint_tests/lint_tutil.py index 08120ee..26df505 100644 --- a/tests/lint_tests/lint_tutil.py +++ b/tests/lint_tests/lint_tutil.py @@ -60,6 +60,8 @@ class LintWrapper: state = LintStateImpl( self._debputy_plugin_feature_set, self.lint_style_preference_table, + None, + None, self.path, "".join(dctrl_lines) if dctrl_lines is not None else "", lines, diff --git a/tests/test_style.py b/tests/test_style.py index cae4510..0e3f5d7 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -71,7 +71,7 @@ def test_compat_styles() -> None: ) src = SourcePackage(fields) - effective_style = determine_effective_style(styles, src, None) + effective_style, _ = determine_effective_style(styles, src, None) assert effective_style == nt_pref fields["Uploaders"] = ( @@ -79,7 +79,7 @@ def test_compat_styles() -> None: ) src = SourcePackage(fields) - effective_style = determine_effective_style(styles, src, None) + effective_style, _ = determine_effective_style(styles, src, None) assert effective_style == nt_pref assert effective_style == zeha_pref @@ -88,7 +88,7 @@ def test_compat_styles() -> None: ) src = SourcePackage(fields) - effective_style = determine_effective_style(styles, src, None) + effective_style, _ = determine_effective_style(styles, src, None) assert effective_style is None @@ -108,7 +108,7 @@ def test_compat_styles_team_maint() -> None: assert "random@example.org" not in styles.maintainer_preferences team_style = styles.maintainer_preferences["team@lists.debian.org"] assert team_style.is_packaging_team - effective_style = determine_effective_style(styles, src, None) + effective_style, _ = determine_effective_style(styles, src, None) assert effective_style == team_style.as_effective_pref() @@ -125,7 +125,7 @@ def test_x_style() -> None: assert "random@example.org" not in styles.maintainer_preferences assert "black" in styles.named_styles black_style = styles.named_styles["black"] - effective_style = determine_effective_style(styles, src, None) + effective_style, _ = determine_effective_style(styles, src, None) assert effective_style == black_style @@ -139,18 +139,18 @@ def test_was_from_salsa_ci_style() -> None: ) src = SourcePackage(fields) assert "random@example.org" not in styles.maintainer_preferences - effective_style = determine_effective_style(styles, src, None) + effective_style, _ = determine_effective_style(styles, src, None) assert effective_style is None salsa_ci = CommentedMap( {"variables": CommentedMap({"SALSA_CI_DISABLE_WRAP_AND_SORT": "yes"})} ) - effective_style = determine_effective_style(styles, src, salsa_ci) + effective_style, _ = determine_effective_style(styles, src, salsa_ci) assert effective_style is None salsa_ci = CommentedMap( {"variables": CommentedMap({"SALSA_CI_DISABLE_WRAP_AND_SORT": "no"})} ) - effective_style = determine_effective_style(styles, src, salsa_ci) + effective_style, _ = determine_effective_style(styles, src, salsa_ci) was_style = EffectivePreference(**_WAS_DEFAULTS) assert effective_style == was_style @@ -218,7 +218,7 @@ def test_was_from_salsa_ci_style_args( ) } ) - effective_style = determine_effective_style(styles, src, salsa_ci) + effective_style, _ = determine_effective_style(styles, src, salsa_ci) if style_delta is None: assert effective_style is None else: |