diff options
Diffstat (limited to 'src')
30 files changed, 658 insertions, 388 deletions
diff --git a/src/debputy/commands/debputy_cmd/__main__.py b/src/debputy/commands/debputy_cmd/__main__.py index d894731..27edf49 100644 --- a/src/debputy/commands/debputy_cmd/__main__.py +++ b/src/debputy/commands/debputy_cmd/__main__.py @@ -39,6 +39,7 @@ from debputy.commands.debputy_cmd.context import ( from debputy.commands.debputy_cmd.dc_util import flatten_ppfs from debputy.commands.debputy_cmd.output import _stream_to_pager from debputy.dh_migration.migrators import MIGRATORS +from debputy.dh_migration.migrators_impl import read_dh_addon_sequences from debputy.exceptions import ( DebputyRuntimeError, PluginNotFoundError, @@ -93,8 +94,6 @@ from debputy.dh_migration.models import AcceptableMigrationIssues from debputy.packages import BinaryPackage from debputy.debhelper_emulation import ( dhe_pkgdir, - parse_drules_for_addons, - extract_dh_addons_from_control, ) from debputy.deb_packaging_support import ( @@ -1053,19 +1052,6 @@ def _merge_ppfs( _merge_list(details, "documentation-uris", documentation_uris) -def _is_debputy_package(context: CommandContext, dh_rules_addons: Set[str]) -> bool: - drules = context.debian_dir.get("rules") - sequences = set() - source_package = context.source_package() - if drules is not None and not drules.is_dir: - parse_drules_for_addons(drules, dh_rules_addons) - extract_dh_addons_from_control(source_package.fields, sequences) - sequences.update(dh_rules_addons) - return ( - "debputy" in sequences or "zz-debputy" in sequences or "zz_debputy" in sequences - ) - - def _extract_dh_compat_level() -> Tuple[Optional[int], int]: try: output = subprocess.check_output( @@ -1254,8 +1240,18 @@ def _annotate_debian_directory(context: CommandContext) -> None: annotated: List[PackagingFileInfo] = [] seen_paths = set() - drules_sequences = set() - is_debputy_package = _is_debputy_package(context, drules_sequences) + r = read_dh_addon_sequences(context.debian_dir) + if r is not None: + bd_sequences, dr_sequences = r + drules_sequences = bd_sequences | dr_sequences + else: + drules_sequences = set() + is_debputy_package = ( + "debputy" in drules_sequences + or "zz-debputy" in drules_sequences + or "zz_debputy" in drules_sequences + or "zz-debputy-rrr" in drules_sequences + ) dh_compat_level, dh_assistant_exit_code = _extract_dh_compat_level() dh_issues = [] 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 0f2ae0f..b30b98d 100644 --- a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py +++ b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py @@ -14,6 +14,7 @@ _EDITOR_SNIPPETS = { ;; Add to ~/.emacs or ~/.emacs.d/init.el and then activate via `M-x eglot`. ;; ;; Requires: apt install elpa-dpkg-dev-el + ;; Recommends: apt install elpa-markdown-mode ;; Make emacs recognize debian/debputy.manifest as a YAML file (add-to-list 'auto-mode-alist '("/debian/debputy.manifest\\'" . yaml-mode)) @@ -52,13 +53,14 @@ _EDITOR_SNIPPETS = { # Inform vim/ycm about the debputy LSP let g:ycm_language_server = [ \\ { 'name': 'debputy', - \\ 'filetypes': [ 'debcontrol', 'debcopyright', 'debchangelog', 'make'], + \\ 'filetypes': [ 'debcontrol', 'debcopyright', 'debchangelog', 'make', 'yaml'], \\ 'cmdline': [ 'debputy', 'lsp', 'server' ] \\ }, \\ ] packadd! youcompleteme - nmap <leader>d <plug>(YCMHover) + # Add relevant ycm keybinding such as: + # nmap <leader>d <plug>(YCMHover) """ ), } diff --git a/src/debputy/commands/debputy_cmd/plugin_cmds.py b/src/debputy/commands/debputy_cmd/plugin_cmds.py index 3d8bdcb..54acdc5 100644 --- a/src/debputy/commands/debputy_cmd/plugin_cmds.py +++ b/src/debputy/commands/debputy_cmd/plugin_cmds.py @@ -437,8 +437,8 @@ def _parser_type_name(v: Union[str, Type[Any]]) -> str: @plugin_list_cmds.register_subcommand( - ["plugable-manifest-rules", "p-m-r", "pmr"], - help_description="Plugable manifest rules (such as install rules)", + ["pluggable-manifest-rules", "p-m-r", "pmr"], + help_description="Pluggable manifest rules (such as install rules)", argparser=TEXT_CSV_FORMAT_NO_STABILITY_PROMISE, ) def _plugin_cmd_list_manifest_rules(context: CommandContext) -> None: @@ -953,8 +953,8 @@ def _plugin_cmd_show_ppf(context: CommandContext) -> None: @plugin_show_cmds.register_subcommand( - ["plugable-manifest-rules", "p-m-r", "pmr"], - help_description="Plugable manifest rules (such as install rules)", + ["pluggable-manifest-rules", "p-m-r", "pmr"], + help_description="Pluggable manifest rules (such as install rules)", argparser=add_arg( "pmr_rule_name", metavar="rule-name", @@ -991,8 +991,8 @@ def _plugin_cmd_show_manifest_rule(context: CommandContext) -> None: if len(matched) != 1 and (matched or rule_name != "::"): if not matched: _error( - f"Could not find any plugable manifest rule related to {parsed_args.pmr_rule_name}." - f" Please use `debputy plugin list plugable-manifest-rules` to see the list of rules." + f"Could not find any pluggable manifest rule related to {parsed_args.pmr_rule_name}." + f" Please use `debputy plugin list pluggable-manifest-rules` to see the list of rules." ) match_a = matched[0][0] match_b = matched[1][0] @@ -1000,7 +1000,7 @@ def _plugin_cmd_show_manifest_rule(context: CommandContext) -> None: f"The name {rule_name} was ambiguous and matched multiple rule types. Please use" f" <rule-type>::{rule_name} to clarify which rule to use" f" (such as {_parser_type_name(match_a)}::{rule_name} or {_parser_type_name(match_b)}::{rule_name})." - f" Please use `debputy plugin list plugable-manifest-rules` to see the list of rules." + f" Please use `debputy plugin list pluggable-manifest-rules` to see the list of rules." ) if matched: @@ -1154,7 +1154,7 @@ def _render_discard_rule( @plugin_show_cmds.register_subcommand( ["automatic-discard-rules", "a-d-r"], - help_description="Plugable manifest rules (such as install rules)", + help_description="Pluggable manifest rules (such as install rules)", argparser=add_arg( "discard_rule", metavar="automatic-discard-rule", diff --git a/src/debputy/deb_packaging_support.py b/src/debputy/deb_packaging_support.py index 4cb4e8f..863e394 100644 --- a/src/debputy/deb_packaging_support.py +++ b/src/debputy/deb_packaging_support.py @@ -1148,6 +1148,7 @@ def _generate_dbgsym_control_file_if_relevant( dbgsym_root_dir: str, dbgsym_ids: str, multi_arch: Optional[str], + dctrl: str, extra_common_params: Sequence[str], ) -> None: section = binary_package.archive_section @@ -1170,13 +1171,18 @@ def _generate_dbgsym_control_file_if_relevant( extra_params.append(f"-VInstalled-Size={total_size}") extra_params.extend(extra_common_params) - package = binary_package.name + package = ( + binary_package.name + if dctrl == "debian/control" + else f"{binary_package.name}-dbgsym" + ) dpkg_cmd = [ "dpkg-gencontrol", f"-p{package}", # FIXME: Support d/<pkg>.changelog at some point. "-ldebian/changelog", "-T/dev/null", + f"-c{dctrl}", f"-P{dbgsym_root_dir}", f"-DPackage={package}-dbgsym", "-DDepends=" + package + " (= ${binary:Version})", @@ -1339,14 +1345,22 @@ def dpkg_field_list_pkg_dep() -> Sequence[str]: def _handle_relationship_substvars( source: SourcePackage, - dctrl: BinaryPackage, + dctrl_file: BinaryPackage, substvars: FlushableSubstvars, + has_dbgsym: bool, ) -> Optional[str]: relationship_fields = dpkg_field_list_pkg_dep() relationship_fields_lc = frozenset(x.lower() for x in relationship_fields) substvar_fields = collections.defaultdict(list) + needs_dbgsym_stanza = False for substvar_name, substvar in substvars.as_substvar.items(): - if substvar.assignment_operator == "$=" or ":" not in substvar_name: + if ":" not in substvar_name: + continue + if substvar.assignment_operator in ("$=", "!="): + # Will create incorrect results if there is a dbgsym and we do nothing + needs_dbgsym_stanza = True + + if substvar.assignment_operator == "$=": # Automatically handled; no need for manual merging. continue _, field = substvar_name.rsplit(":", 1) @@ -1354,10 +1368,14 @@ def _handle_relationship_substvars( if field_lc not in relationship_fields_lc: continue substvar_fields[field_lc].append("${" + substvar_name + "}") - if not substvar_fields: + + if not has_dbgsym: + needs_dbgsym_stanza = False + + if not substvar_fields and not needs_dbgsym_stanza: return None - replacement_stanza = debian.deb822.Deb822(dctrl.fields) + replacement_stanza = debian.deb822.Deb822(dctrl_file.fields) for field_name in relationship_fields: field_name_lc = field_name.lower() @@ -1375,7 +1393,7 @@ def _handle_relationship_substvars( final_value = f"{existing_value}, {substvars_part}" replacement_stanza[field_name] = final_value - tmpdir = generated_content_dir(package=dctrl) + tmpdir = generated_content_dir(package=dctrl_file) with tempfile.NamedTemporaryFile( mode="wb", dir=tmpdir, @@ -1388,6 +1406,17 @@ def _handle_relationship_substvars( debian.deb822.Deb822(source.fields).dump(fd) fd.write(b"\n") replacement_stanza.dump(fd) + + if has_dbgsym: + # Minimal stanza to avoid substvars warnings. Most fields are still set + # via -D. + dbgsym_stanza = Deb822() + dbgsym_stanza["Package"] = f"{dctrl_file.name}-dbgsym" + dbgsym_stanza["Architecture"] = dctrl_file.fields["Architecture"] + dbgsym_stanza["Description"] = f"debug symbols for {dctrl_file.name}" + fd.write(b"\n") + dbgsym_stanza.dump(fd) + return fd.name @@ -1424,9 +1453,11 @@ def _generate_control_files( ' accordingly in the binary. If this auto-correction is wrong, please add "Multi-Arch: no" to the' ' relevant part of "debian/control" to disable this feature.' ) - extra_params_specific.append(f"-DMulti-Arch={ma_value}") + # We want this to apply to the `-dbgsym` package as well to avoid + # lintian `debug-package-for-multi-arch-same-pkg-not-coinstallable` + extra_common_params.append(f"-DMulti-Arch={ma_value}") elif ma_value == "no": - extra_params_specific.append("-UMulti-Arch") + extra_common_params.append("-UMulti-Arch") dbgsym_root_dir = dhe_dbgsym_root_dir(binary_package) dbgsym_ids = " ".join(dbgsym_build_ids) if dbgsym_build_ids else "" @@ -1436,15 +1467,26 @@ def _generate_control_files( _t64_migration_substvar(binary_package, control_output_dir, substvars) with substvars.flush() as flushed_substvars: - if dbgsym_root_fs is not None and any( + has_dbgsym = dbgsym_root_fs is not None and any( f for f in dbgsym_root_fs.all_paths() if f.is_file - ): + ) + dctrl_file = _handle_relationship_substvars( + source_package, + binary_package, + substvars, + has_dbgsym, + ) + if dctrl_file is None: + dctrl_file = "debian/control" + + if has_dbgsym: _generate_dbgsym_control_file_if_relevant( binary_package, dbgsym_root_fs, dbgsym_root_dir, dbgsym_ids, ma_value, + dctrl_file, extra_common_params, ) generate_md5sums_file( @@ -1454,21 +1496,13 @@ def _generate_control_files( elif dbgsym_ids: extra_common_params.append(f"-DBuild-Ids={dbgsym_ids}") - dctrl = _handle_relationship_substvars( - source_package, - binary_package, - substvars, - ) - if dctrl is None: - dctrl = "debian/control" - ctrl_file = os.path.join(control_output_dir, "control") dpkg_cmd = [ "dpkg-gencontrol", f"-p{package}", # FIXME: Support d/<pkg>.changelog at some point. "-ldebian/changelog", - f"-c{dctrl}", + f"-c{dctrl_file}", f"-T{flushed_substvars}", f"-O{ctrl_file}", f"-P{control_output_dir}", diff --git a/src/debputy/debhelper_emulation.py b/src/debputy/debhelper_emulation.py index 88352bd..38d9a15 100644 --- a/src/debputy/debhelper_emulation.py +++ b/src/debputy/debhelper_emulation.py @@ -241,14 +241,13 @@ _FIND_DH_WITH = re.compile(r"--with(?:\s+|=)(\S+)") _DEP_REGEX = re.compile("^([a-z0-9][-+.a-z0-9]+)", re.ASCII) -def parse_drules_for_addons(debian_rules: VirtualPath, sequences: Set[str]) -> None: - with debian_rules.open() as fd: - for line in fd: - if not line.startswith("\tdh "): - continue - for match in _FIND_DH_WITH.finditer(line): - sequence_def = match.group(1) - sequences.update(sequence_def.split(",")) +def parse_drules_for_addons(lines: Iterable[str], sequences: Set[str]) -> None: + for line in lines: + if not line.startswith("\tdh "): + continue + for match in _FIND_DH_WITH.finditer(line): + sequence_def = match.group(1) + sequences.update(sequence_def.split(",")) def extract_dh_addons_from_control( diff --git a/src/debputy/dh_migration/migration.py b/src/debputy/dh_migration/migration.py index 1366f22..bcdd3f9 100644 --- a/src/debputy/dh_migration/migration.py +++ b/src/debputy/dh_migration/migration.py @@ -170,7 +170,9 @@ def _check_migration_target( f'Using "{resolved_migration_target}" as migration target based on the packaging' ) else: - _info(f'Using "{resolved_migration_target}" as default migration target.') + _info( + f'Using "{resolved_migration_target}" as default migration target. Use --migration-target to choose!' + ) return resolved_migration_target diff --git a/src/debputy/dh_migration/migrators_impl.py b/src/debputy/dh_migration/migrators_impl.py index 6613c25..7856d27 100644 --- a/src/debputy/dh_migration/migrators_impl.py +++ b/src/debputy/dh_migration/migrators_impl.py @@ -1,5 +1,6 @@ import collections import dataclasses +import functools import json import os import re @@ -675,16 +676,15 @@ def migrate_install_file( current_sources.extend(sources) continue key = (dest_dir, dhe_line.conditional_key()) + ctor = functools.partial( + SourcesAndConditional, + dest_dir=dest_dir, + conditional=dhe_line.conditional(), + ) md = _fetch_or_create( sources_by_destdir, key, - # Use named parameters to avoid warnings about the values possible changing - # in the next iteration. We always resolve the lambda in this iteration, so - # the bug is non-existent. However, that is harder for a linter to prove. - lambda *, dest=dest_dir, dhe=dhe_line: SourcesAndConditional( - dest_dir=dest, - conditional=dhe.conditional(), - ), + ctor, ) md.sources.extend(sources) @@ -908,10 +908,13 @@ def migrate_installinfo_file( info_files_by_condition: Dict[Tuple[str, ...], InfoFilesDefinition] = {} for dhe_line in content: key = dhe_line.conditional_key() + ctr = functools.partial( + InfoFilesDefinition, conditional=dhe_line.conditional() + ) info_def = _fetch_or_create( info_files_by_condition, key, - lambda: InfoFilesDefinition(conditional=dhe_line.conditional()), + ctr, ) info_def.sources.extend( _normalize_path(w, with_prefix=False) for w in dhe_line.tokens @@ -1023,12 +1026,15 @@ def migrate_installman_file( else: language = None key = (language, dhe_line.conditional_key()) + ctor = functools.partial( + ManpageDefinition, + language=language, + conditional=dhe_line.conditional(), + ) manpage_def = _fetch_or_create( complex_definitions, key, - lambda: ManpageDefinition( - language=language, conditional=dhe_line.conditional() - ), + ctor, ) manpage_def.sources.extend(sources) else: @@ -1483,7 +1489,8 @@ def read_dh_addon_sequences( drules = debian_dir.get("rules") if drules and drules.is_file: - parse_drules_for_addons(drules, dr_sequences) + with drules.open() as fd: + parse_drules_for_addons(fd, dr_sequences) with ctrl_file.open() as fd: ctrl = list(Deb822.iter_paragraphs(fd)) diff --git a/src/debputy/filesystem_scan.py b/src/debputy/filesystem_scan.py index f7f97c2..dec123c 100644 --- a/src/debputy/filesystem_scan.py +++ b/src/debputy/filesystem_scan.py @@ -325,7 +325,7 @@ class VirtualPathBase(VirtualPath, ABC): if current.path in link_expansions: # This is our loop detection for now. It might have some false positives where you # could safely resolve the same symlink twice. However, given that this use-case is - # basically none existent in practice for packaging, we just stop here for now. + # basically non-existent in practice for packaging, we just stop here for now. raise SymlinkLoopError( f'The path "{path}" traversed the symlink "{current.path}" multiple' " times. Currently, traversing the same symlink twice is considered" diff --git a/src/debputy/linting/lint_impl.py b/src/debputy/linting/lint_impl.py index 68be9d9..7248022 100644 --- a/src/debputy/linting/lint_impl.py +++ b/src/debputy/linting/lint_impl.py @@ -26,7 +26,7 @@ from debputy.lsp.lsp_debian_changelog import _lint_debian_changelog from debputy.lsp.lsp_debian_control import _lint_debian_control from debputy.lsp.lsp_debian_copyright import _lint_debian_copyright from debputy.lsp.lsp_debian_debputy_manifest import _lint_debian_debputy_manifest -from debputy.lsp.lsp_debian_rules import _lint_debian_rules +from debputy.lsp.lsp_debian_rules import _lint_debian_rules_impl from debputy.lsp.quickfixes import provide_standard_quickfixes_from_diagnostics from debputy.lsp.spellchecking import disable_spellchecking from debputy.lsp.text_edit import ( @@ -40,7 +40,7 @@ LINTER_FORMATS = { "debian/control": _lint_debian_control, "debian/copyright": _lint_debian_copyright, "debian/changelog": _lint_debian_changelog, - "debian/rules": _lint_debian_rules, + "debian/rules": _lint_debian_rules_impl, "debian/debputy.manifest": _lint_debian_debputy_manifest, } diff --git a/src/debputy/lsp/lsp_debian_changelog.py b/src/debputy/lsp/lsp_debian_changelog.py index 3ec0b4d..77df145 100644 --- a/src/debputy/lsp/lsp_debian_changelog.py +++ b/src/debputy/lsp/lsp_debian_changelog.py @@ -1,4 +1,5 @@ import sys +from email.utils import parsedate_to_datetime from typing import ( Union, List, @@ -26,6 +27,7 @@ from lsprotocol.types import ( from debputy.lsp.lsp_features import lsp_diagnostics, lsp_standard_handler from debputy.lsp.quickfixes import ( provide_standard_quickfixes_from_diagnostics, + propose_correct_text_quick_fix, ) from debputy.lsp.spellchecking import spellcheck_line from debputy.lsp.text_util import ( @@ -52,6 +54,17 @@ _LANGUAGE_IDS = [ "debchangelog", ] +_WEEKDAYS_BY_IDX = [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", +] +_KNOWN_WEEK_DAYS = frozenset(_WEEKDAYS_BY_IDX) + DOCUMENT_VERSION_TABLE: Dict[str, int] = {} @@ -106,6 +119,114 @@ def _diagnostics_debian_changelog( yield from scanner +def _check_footer_line( + line: str, + line_no: int, + lines: List[str], + position_codec: LintCapablePositionCodec, +) -> Iterator[Diagnostic]: + try: + end_email_idx = line.rindex("> ") + except ValueError: + # Syntax error; flag later + return + line_len = len(line) + start_date_idx = end_email_idx + 3 + # 3 characters for the day name (Mon), then a comma plus a space followed by the + # actual date. The 6 characters limit is a gross under estimation of the real + # size. + if line_len < start_date_idx + 6: + range_server_units = Range( + Position( + line_no, + start_date_idx, + ), + Position( + line_no, + line_len, + ), + ) + yield Diagnostic( + position_codec.range_to_client_units(lines, range_server_units), + "Expected a date in RFC822 format (Tue, 12 Mar 2024 12:34:56 +0000)", + severity=DiagnosticSeverity.Error, + source="debputy", + ) + return + day_name_range_server_units = Range( + Position( + line_no, + start_date_idx, + ), + Position( + line_no, + start_date_idx + 3, + ), + ) + day_name = line[start_date_idx : start_date_idx + 3] + if day_name not in _KNOWN_WEEK_DAYS: + yield Diagnostic( + position_codec.range_to_client_units(lines, day_name_range_server_units), + "Expected a three letter date here (Mon, Tue, ..., Sun).", + severity=DiagnosticSeverity.Error, + source="debputy", + ) + return + + date_str = line[start_date_idx + 5 :] + + if line[start_date_idx + 3 : start_date_idx + 5] != ", ": + sep = line[start_date_idx + 3 : start_date_idx + 5] + range_server_units = Range( + Position( + line_no, + start_date_idx + 3, + ), + Position( + line_no, + start_date_idx + 4, + ), + ) + yield Diagnostic( + position_codec.range_to_client_units(lines, range_server_units), + f'Improper formatting of date. Expected ", " here, not "{sep}"', + severity=DiagnosticSeverity.Error, + source="debputy", + ) + return + + try: + # FIXME: this parser is too forgiving (it ignores trailing garbage) + date = parsedate_to_datetime(date_str) + except ValueError as e: + range_server_units = Range( + Position( + line_no, + start_date_idx + 5, + ), + Position( + line_no, + line_len, + ), + ) + yield Diagnostic( + position_codec.range_to_client_units(lines, range_server_units), + f"Unable to the date as a valid RFC822 date: {e.args[0]}", + severity=DiagnosticSeverity.Error, + source="debputy", + ) + return + expected_week_day = _WEEKDAYS_BY_IDX[date.weekday()] + if expected_week_day != day_name: + yield Diagnostic( + position_codec.range_to_client_units(lines, day_name_range_server_units), + f"The date was a {expected_week_day}day.", + severity=DiagnosticSeverity.Warning, + source="debputy", + data=[propose_correct_text_quick_fix(expected_week_day)], + ) + + def _scan_debian_changelog_for_diagnostics( lines: List[str], position_codec: LintCapablePositionCodec, @@ -123,6 +244,9 @@ def _scan_debian_changelog_for_diagnostics( line = line.rstrip() if not line: continue + if line.startswith(" --"): + diagnostics.extend(_check_footer_line(line, line_no, lines, position_codec)) + continue if not line.startswith(" "): continue # minus 1 for newline diff --git a/src/debputy/lsp/lsp_debian_control.py b/src/debputy/lsp/lsp_debian_control.py index d00f1c2..f73612f 100644 --- a/src/debputy/lsp/lsp_debian_control.py +++ b/src/debputy/lsp/lsp_debian_control.py @@ -9,20 +9,6 @@ from typing import ( List, ) -from debputy.lsp.vendoring._deb822_repro import ( - parse_deb822_file, - Deb822FileElement, - Deb822ParagraphElement, -) -from debputy.lsp.vendoring._deb822_repro.parsing import ( - Deb822KeyValuePairElement, - LIST_SPACE_SEPARATED_INTERPRETATION, -) -from debputy.lsp.vendoring._deb822_repro.tokens import ( - Deb822Token, - tokenize_deb822_file, - Deb822FieldNameToken, -) from lsprotocol.types import ( DiagnosticSeverity, Range, @@ -30,7 +16,6 @@ from lsprotocol.types import ( Position, DidOpenTextDocumentParams, DidChangeTextDocumentParams, - FoldingRangeKind, FoldingRange, FoldingRangeParams, CompletionItem, @@ -38,7 +23,6 @@ from lsprotocol.types import ( CompletionParams, TEXT_DOCUMENT_DID_OPEN, TEXT_DOCUMENT_DID_CHANGE, - TEXT_DOCUMENT_FOLDING_RANGE, TEXT_DOCUMENT_COMPLETION, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL, DiagnosticRelatedInformation, @@ -58,7 +42,6 @@ from debputy.lsp.lsp_debian_control_reference_data import ( DctrlKnownField, BINARY_FIELDS, SOURCE_FIELDS, - FieldValueClass, DctrlFileMetadata, ) from debputy.lsp.lsp_features import ( @@ -66,8 +49,15 @@ from debputy.lsp.lsp_features import ( lsp_completer, lsp_hover, lsp_standard_handler, + lsp_folding_ranges, + lsp_semantic_tokens_full, +) +from debputy.lsp.lsp_generic_deb822 import ( + deb822_completer, + deb822_hover, + deb822_folding_ranges, + deb822_semantic_tokens_full, ) -from debputy.lsp.lsp_generic_deb822 import deb822_completer, deb822_hover from debputy.lsp.quickfixes import ( propose_remove_line_quick_fix, range_compatible_with_remove_line_fix, @@ -82,6 +72,19 @@ from debputy.lsp.text_util import ( detect_possible_typo, te_range_to_lsp, ) +from debputy.lsp.vendoring._deb822_repro import ( + parse_deb822_file, + Deb822FileElement, + Deb822ParagraphElement, +) +from debputy.lsp.vendoring._deb822_repro.parsing import ( + Deb822KeyValuePairElement, + LIST_SPACE_SEPARATED_INTERPRETATION, +) +from debputy.lsp.vendoring._deb822_repro.tokens import ( + Deb822Token, + Deb822FieldNameToken, +) from debputy.util import _info, _error try: @@ -106,33 +109,9 @@ _LANGUAGE_IDS = [ ] -SEMANTIC_TOKENS_LEGEND = SemanticTokensLegend( - token_types=["keyword"], - token_modifiers=[], -) _DCTRL_FILE_METADATA = DctrlFileMetadata() -def register_dctrl_lsp(ls: "LanguageServer") -> None: - try: - from debputy.lsp.vendoring._deb822_repro.locatable import Locatable - except ImportError: - _error( - 'Sorry; this feature requires a newer version of python-debian (with "Locatable").' - ) - - ls.feature(TEXT_DOCUMENT_DID_OPEN)(_diagnostics_debian_control) - ls.feature(TEXT_DOCUMENT_DID_CHANGE)(_diagnostics_debian_control) - ls.feature(TEXT_DOCUMENT_FOLDING_RANGE)(_detect_folding_ranges_debian_control) - ls.feature(TEXT_DOCUMENT_COMPLETION)(_debian_control_completions) - ls.feature(TEXT_DOCUMENT_CODE_ACTION)(provide_standard_quickfixes_from_diagnostics) - ls.feature(TEXT_DOCUMENT_HOVER)(_debian_control_hover) - ls.feature(TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)(on_save_trim_end_of_line_whitespace) - ls.feature(TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, SEMANTIC_TOKENS_LEGEND)( - _handle_semantic_tokens_full - ) - - lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION) lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) @@ -153,36 +132,12 @@ def _debian_control_completions( return deb822_completer(ls, params, _DCTRL_FILE_METADATA) -def _detect_folding_ranges_debian_control( +@lsp_folding_ranges(_LANGUAGE_IDS) +def _debian_control_folding_ranges( ls: "LanguageServer", params: FoldingRangeParams, ) -> Optional[Sequence[FoldingRange]]: - doc = ls.workspace.get_text_document(params.text_document.uri) - comment_start = -1 - folding_ranges = [] - for ( - token, - start_line, - start_offset, - end_line, - end_offset, - ) in _deb822_token_iter(tokenize_deb822_file(doc.lines)): - if token.is_comment: - if comment_start < 0: - comment_start = start_line - _info(f"Detected new comment: {start_line}") - elif comment_start > -1: - comment_start = -1 - folding_range = FoldingRange( - comment_start, - end_line, - kind=FoldingRangeKind.Comment, - ) - - folding_ranges.append(folding_range) - _info(f"Detected folding range: {folding_range}") - - return folding_ranges + return deb822_folding_ranges(ls, params, _DCTRL_FILE_METADATA) def _deb822_token_iter( @@ -760,38 +715,13 @@ def _lint_debian_control( return diagnostics -def _handle_semantic_tokens_full( +@lsp_semantic_tokens_full(_LANGUAGE_IDS) +def _semantic_tokens_full( ls: "LanguageServer", request: SemanticTokensParams, ) -> Optional[SemanticTokens]: - doc = ls.workspace.get_text_document(request.text_document.uri) - lines = doc.lines - deb822_file = parse_deb822_file( - lines, - accept_files_with_duplicated_fields=True, - accept_files_with_error_tokens=True, + return deb822_semantic_tokens_full( + ls, + request, + _DCTRL_FILE_METADATA, ) - tokens = [] - previous_line = 0 - keyword_token = 0 - no_modifiers = 0 - - for paragraph_no, paragraph in enumerate(deb822_file, start=1): - paragraph_position = paragraph.position_in_file() - for kvpair in paragraph.iter_parts_of_type(Deb822KeyValuePairElement): - field_position_without_comments = kvpair.position_in_parent().relative_to( - paragraph_position - ) - field_size = doc.position_codec.client_num_units(kvpair.field_name) - current_line = field_position_without_comments.line_position - line_delta = current_line - previous_line - previous_line = current_line - tokens.append(line_delta) # Line delta - tokens.append(0) # Token delta - tokens.append(field_size) # Token length - tokens.append(keyword_token) - tokens.append(no_modifiers) - - if not tokens: - return None - return SemanticTokens(tokens) diff --git a/src/debputy/lsp/lsp_debian_copyright.py b/src/debputy/lsp/lsp_debian_copyright.py index 052654a..f22bd0b 100644 --- a/src/debputy/lsp/lsp_debian_copyright.py +++ b/src/debputy/lsp/lsp_debian_copyright.py @@ -10,36 +10,16 @@ from typing import ( List, ) -from debputy.lsp.vendoring._deb822_repro import ( - parse_deb822_file, - Deb822FileElement, - Deb822ParagraphElement, -) -from debputy.lsp.vendoring._deb822_repro.parsing import ( - Deb822KeyValuePairElement, - LIST_SPACE_SEPARATED_INTERPRETATION, -) -from debputy.lsp.vendoring._deb822_repro.tokens import ( - Deb822Token, - tokenize_deb822_file, - Deb822FieldNameToken, -) from lsprotocol.types import ( DiagnosticSeverity, Range, Diagnostic, Position, - DidOpenTextDocumentParams, - DidChangeTextDocumentParams, - FoldingRangeKind, - FoldingRange, - FoldingRangeParams, CompletionItem, CompletionList, CompletionParams, TEXT_DOCUMENT_DID_OPEN, TEXT_DOCUMENT_DID_CHANGE, - TEXT_DOCUMENT_FOLDING_RANGE, TEXT_DOCUMENT_COMPLETION, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL, DiagnosticRelatedInformation, @@ -53,10 +33,11 @@ from lsprotocol.types import ( TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, SemanticTokens, SemanticTokensParams, + FoldingRangeParams, + FoldingRange, ) from debputy.lsp.lsp_debian_control_reference_data import ( - FieldValueClass, _DEP5_HEADER_FIELDS, _DEP5_FILES_FIELDS, Deb822KnownField, @@ -68,8 +49,15 @@ from debputy.lsp.lsp_features import ( lsp_completer, lsp_hover, lsp_standard_handler, + lsp_folding_ranges, + lsp_semantic_tokens_full, +) +from debputy.lsp.lsp_generic_deb822 import ( + deb822_completer, + deb822_hover, + deb822_folding_ranges, + deb822_semantic_tokens_full, ) -from debputy.lsp.lsp_generic_deb822 import deb822_completer, deb822_hover from debputy.lsp.quickfixes import ( propose_remove_line_quick_fix, propose_correct_text_quick_fix, @@ -83,7 +71,20 @@ from debputy.lsp.text_util import ( detect_possible_typo, te_range_to_lsp, ) -from debputy.util import _info, _error +from debputy.lsp.vendoring._deb822_repro import ( + parse_deb822_file, + Deb822FileElement, + Deb822ParagraphElement, +) +from debputy.lsp.vendoring._deb822_repro.parsing import ( + Deb822KeyValuePairElement, + LIST_SPACE_SEPARATED_INTERPRETATION, +) +from debputy.lsp.vendoring._deb822_repro.tokens import ( + Deb822Token, + Deb822FieldNameToken, +) +from debputy.util import _error try: from debputy.lsp.vendoring._deb822_repro.locatable import ( @@ -109,32 +110,6 @@ _LANGUAGE_IDS = [ _DEP5_FILE_METADATA = Dep5FileMetadata() -SEMANTIC_TOKENS_LEGEND = SemanticTokensLegend( - token_types=["keyword"], - token_modifiers=[], -) - - -def register_dcpy_lsp(ls: "LanguageServer") -> None: - try: - from debian._deb822_repro.locatable import Locatable - except ImportError: - _error( - 'Sorry; this feature requires a newer version of python-debian (with "Locatable").' - ) - - ls.feature(TEXT_DOCUMENT_DID_OPEN)(_diagnostics_debian_copyright) - ls.feature(TEXT_DOCUMENT_DID_CHANGE)(_diagnostics_debian_copyright) - ls.feature(TEXT_DOCUMENT_FOLDING_RANGE)(_detect_folding_ranges_debian_copyright) - ls.feature(TEXT_DOCUMENT_COMPLETION)(_debian_copyright_completions) - ls.feature(TEXT_DOCUMENT_CODE_ACTION)(provide_standard_quickfixes_from_diagnostics) - ls.feature(TEXT_DOCUMENT_HOVER)(_debian_copyright_hover) - ls.feature(TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)(on_save_trim_end_of_line_whitespace) - ls.feature(TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, SEMANTIC_TOKENS_LEGEND)( - _handle_semantic_tokens_full - ) - - lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION) lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) @@ -155,36 +130,12 @@ def _debian_copyright_completions( return deb822_completer(ls, params, _DEP5_FILE_METADATA) -def _detect_folding_ranges_debian_copyright( +@lsp_folding_ranges(_LANGUAGE_IDS) +def _debian_copyright_folding_ranges( ls: "LanguageServer", params: FoldingRangeParams, ) -> Optional[Sequence[FoldingRange]]: - doc = ls.workspace.get_text_document(params.text_document.uri) - comment_start = -1 - folding_ranges = [] - for ( - token, - start_line, - start_offset, - end_line, - end_offset, - ) in _deb822_token_iter(tokenize_deb822_file(doc.lines)): - if token.is_comment: - if comment_start < 0: - comment_start = start_line - _info(f"Detected new comment: {start_line}") - elif comment_start > -1: - comment_start = -1 - folding_range = FoldingRange( - comment_start, - end_line, - kind=FoldingRangeKind.Comment, - ) - - folding_ranges.append(folding_range) - _info(f"Detected folding range: {folding_range}") - - return folding_ranges + return deb822_folding_ranges(ls, params, _DEP5_FILE_METADATA) def _deb822_token_iter( @@ -576,22 +527,6 @@ def _scan_for_syntax_errors_and_token_level_diagnostics( return first_error -def _diagnostics_debian_copyright( - ls: "LanguageServer", - params: Union[DidOpenTextDocumentParams, DidChangeTextDocumentParams], -) -> None: - doc = ls.workspace.get_text_document(params.text_document.uri) - _info(f"Opened document: {doc.path} ({doc.language_id})") - lines = doc.lines - position_codec: LintCapablePositionCodec = doc.position_codec - - diagnostics = _lint_debian_copyright(doc.uri, doc.path, lines, position_codec) - ls.publish_diagnostics( - doc.uri, - diagnostics, - ) - - @lint_diagnostics(_LANGUAGE_IDS) def _lint_debian_copyright( doc_reference: str, @@ -648,38 +583,13 @@ def _lint_debian_copyright( return diagnostics -def _handle_semantic_tokens_full( +@lsp_semantic_tokens_full(_LANGUAGE_IDS) +def _semantic_tokens_full( ls: "LanguageServer", request: SemanticTokensParams, ) -> Optional[SemanticTokens]: - doc = ls.workspace.get_text_document(request.text_document.uri) - lines = doc.lines - deb822_file = parse_deb822_file( - lines, - accept_files_with_duplicated_fields=True, - accept_files_with_error_tokens=True, + return deb822_semantic_tokens_full( + ls, + request, + _DEP5_FILE_METADATA, ) - tokens = [] - previous_line = 0 - keyword_token = 0 - no_modifiers = 0 - - for paragraph_no, paragraph in enumerate(deb822_file, start=1): - paragraph_position = paragraph.position_in_file() - for kvpair in paragraph.iter_parts_of_type(Deb822KeyValuePairElement): - field_position_without_comments = kvpair.position_in_parent().relative_to( - paragraph_position - ) - field_size = doc.position_codec.client_num_units(kvpair.field_name) - current_line = field_position_without_comments.line_position - line_delta = current_line - previous_line - previous_line = current_line - tokens.append(line_delta) # Line delta - tokens.append(0) # Token delta - tokens.append(field_size) # Token length - tokens.append(keyword_token) - tokens.append(no_modifiers) - - if not tokens: - return None - return SemanticTokens(tokens) diff --git a/src/debputy/lsp/lsp_debian_debputy_manifest.py b/src/debputy/lsp/lsp_debian_debputy_manifest.py index 2f9920e..97fffcc 100644 --- a/src/debputy/lsp/lsp_debian_debputy_manifest.py +++ b/src/debputy/lsp/lsp_debian_debputy_manifest.py @@ -45,6 +45,12 @@ _LANGUAGE_IDS = [ lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) +def is_valid_file(path: str) -> bool: + # For debian/debputy.manifest, the language ID is often set to makefile meaning we get random + # "non-debian/debputy.manifest" YAML files here. Skip those. + return path.endswith("debian/debputy.manifest") + + def _word_range_at_position( lines: List[str], line_no: int, @@ -69,10 +75,12 @@ def _word_range_at_position( @lint_diagnostics(_LANGUAGE_IDS) def _lint_debian_debputy_manifest( _doc_reference: str, - _path: str, + path: str, lines: List[str], position_codec: LintCapablePositionCodec, ) -> Optional[List[Diagnostic]]: + if not is_valid_file(path): + return None diagnostics = [] try: MANIFEST_YAML.load("".join(lines)) diff --git a/src/debputy/lsp/lsp_debian_rules.py b/src/debputy/lsp/lsp_debian_rules.py index 7f0e5fb..86b114c 100644 --- a/src/debputy/lsp/lsp_debian_rules.py +++ b/src/debputy/lsp/lsp_debian_rules.py @@ -15,8 +15,6 @@ from typing import ( from lsprotocol.types import ( CompletionItem, - DidOpenTextDocumentParams, - DidChangeTextDocumentParams, Diagnostic, Range, Position, @@ -27,6 +25,7 @@ from lsprotocol.types import ( TEXT_DOCUMENT_CODE_ACTION, ) +from debputy.debhelper_emulation import parse_drules_for_addons from debputy.lsp.lsp_features import ( lint_diagnostics, lsp_standard_handler, @@ -126,40 +125,26 @@ def _as_hook_targets(command_name: str) -> Iterable[str]: yield f"{prefix}{command_name}{suffix}" -def _diagnostics_debian_rules( - ls: "LanguageServer", - params: Union[DidOpenTextDocumentParams, DidChangeTextDocumentParams], -) -> None: - doc = ls.workspace.get_text_document(params.text_document.uri) - if not doc.path.endswith("debian/rules"): - return - lines = doc.lines - diagnostics = _lint_debian_rules( - doc.uri, - doc.path, - lines, - doc.position_codec, - ) - ls.publish_diagnostics( - doc.uri, - diagnostics, - ) - - lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION) lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) +def is_valid_file(path: str) -> bool: + # For debian/rules, the language ID is often set to makefile meaning we get random "non-debian/rules" + # makefiles here. Skip those. + return path.endswith("debian/rules") + + @lint_diagnostics(_LANGUAGE_IDS) -def _lint_debian_rules_via_debputy_lsp( +def _lint_debian_rules( doc_reference: str, path: str, lines: List[str], position_codec: LintCapablePositionCodec, ) -> Optional[List[Diagnostic]]: - if not path.endswith("debian/rules"): + if not is_valid_file(path): return None - return _lint_debian_rules( + return _lint_debian_rules_impl( doc_reference, path, lines, @@ -245,7 +230,7 @@ def iter_make_lines( yield line_no, line -def _lint_debian_rules( +def _lint_debian_rules_impl( _doc_reference: str, path: str, lines: List[str], @@ -259,7 +244,8 @@ def _lint_debian_rules( make_error = _run_make_dryrun(source_root, lines) if make_error is not None: diagnostics.append(make_error) - all_dh_commands = _all_dh_commands(source_root) + + all_dh_commands = _all_dh_commands(source_root, lines) if all_dh_commands: all_hook_targets = {ht for c in all_dh_commands for ht in _as_hook_targets(c)} all_hook_targets.update(_KNOWN_TARGETS) @@ -330,10 +316,15 @@ def _lint_debian_rules( return diagnostics -def _all_dh_commands(source_root: str) -> Optional[Sequence[str]]: +def _all_dh_commands(source_root: str, lines: List[str]) -> Optional[Sequence[str]]: + drules_sequences = set() + parse_drules_for_addons(lines, drules_sequences) + cmd = ["dh_assistant", "list-commands", "--output-format=json"] + if drules_sequences: + cmd.append(f"--with={','.join(drules_sequences)}") try: output = subprocess.check_output( - ["dh_assistant", "list-commands", "--output-format=json"], + cmd, stderr=subprocess.DEVNULL, cwd=source_root, ) @@ -364,7 +355,7 @@ def _debian_rules_completions( params: CompletionParams, ) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: doc = ls.workspace.get_text_document(params.text_document.uri) - if not doc.path.endswith("debian/rules"): + if not is_valid_file(doc.path): return None lines = doc.lines server_position = doc.position_codec.position_from_client_units( @@ -378,7 +369,7 @@ def _debian_rules_completions( return None source_root = os.path.dirname(os.path.dirname(doc.path)) - all_commands = _all_dh_commands(source_root) + all_commands = _all_dh_commands(source_root, lines) items = [CompletionItem(ht) for c in all_commands for ht in _as_hook_targets(c)] return items diff --git a/src/debputy/lsp/lsp_dispatch.py b/src/debputy/lsp/lsp_dispatch.py index 41e9111..b7b744c 100644 --- a/src/debputy/lsp/lsp_dispatch.py +++ b/src/debputy/lsp/lsp_dispatch.py @@ -1,5 +1,15 @@ import asyncio -from typing import Dict, Sequence, Union, Optional +from typing import ( + Dict, + Sequence, + Union, + Optional, + Any, + TypeVar, + Callable, + Mapping, + List, +) from lsprotocol.types import ( DidOpenTextDocumentParams, @@ -11,6 +21,19 @@ from lsprotocol.types import ( CompletionItem, CompletionParams, TEXT_DOCUMENT_HOVER, + TEXT_DOCUMENT_FOLDING_RANGE, + FoldingRange, + FoldingRangeParams, + TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, + SemanticTokensParams, + SemanticTokens, + Hover, + TEXT_DOCUMENT_CODE_ACTION, + Command, + CodeAction, + TextDocumentCodeActionRequest, + CodeActionParams, + SemanticTokensRegistrationOptions, ) from debputy import __version__ @@ -18,6 +41,9 @@ from debputy.lsp.lsp_features import ( DIAGNOSTIC_HANDLERS, COMPLETER_HANDLERS, HOVER_HANDLERS, + SEMANTIC_TOKENS_FULL_HANDLERS, + CODE_ACTION_HANDLERS, + SEMANTIC_TOKENS_LEGEND, ) from debputy.util import _info @@ -37,6 +63,10 @@ except ImportError: DEBPUTY_LANGUAGE_SERVER = Mock() +P = TypeVar("P") +R = TypeVar("R") + + def is_doc_at_version(uri: str, version: int) -> bool: dv = _DOCUMENT_VERSION_TABLE.get(uri) return dv == version @@ -88,22 +118,12 @@ def _completions( ls: "LanguageServer", params: CompletionParams, ) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: - doc_uri = params.text_document.uri - doc = ls.workspace.get_text_document(doc_uri) - - handler = COMPLETER_HANDLERS.get(doc.language_id) - if handler is None: - _info( - f"Complete request for document: {doc.path} ({doc.language_id}) - no handler" - ) - return - _info( - f"Complete request for document: {doc.path} ({doc.language_id}) - delegating to handler" - ) - - return handler( + return _dispatch_standard_handler( ls, + params.text_document.uri, params, + COMPLETER_HANDLERS, + "Complete request", ) @@ -111,18 +131,81 @@ def _completions( def _hover( ls: "LanguageServer", params: CompletionParams, -) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: - doc_uri = params.text_document.uri +) -> Optional[Hover]: + return _dispatch_standard_handler( + ls, + params.text_document.uri, + params, + HOVER_HANDLERS, + "Hover doc request", + ) + + +@DEBPUTY_LANGUAGE_SERVER.feature(TEXT_DOCUMENT_CODE_ACTION) +def _code_actions( + ls: "LanguageServer", + params: CodeActionParams, +) -> Optional[List[Union[Command, CodeAction]]]: + return _dispatch_standard_handler( + ls, + params.text_document.uri, + params, + CODE_ACTION_HANDLERS, + "Code action request", + ) + + +@DEBPUTY_LANGUAGE_SERVER.feature(TEXT_DOCUMENT_FOLDING_RANGE) +def _folding_ranges( + ls: "LanguageServer", + params: FoldingRangeParams, +) -> Optional[Sequence[FoldingRange]]: + return _dispatch_standard_handler( + ls, + params.text_document.uri, + params, + HOVER_HANDLERS, + "Folding range request", + ) + + +@DEBPUTY_LANGUAGE_SERVER.feature( + TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL, + SemanticTokensRegistrationOptions( + SEMANTIC_TOKENS_LEGEND, + full=True, + ), +) +def _semantic_tokens_full( + ls: "LanguageServer", + params: SemanticTokensParams, +) -> Optional[SemanticTokens]: + return _dispatch_standard_handler( + ls, + params.text_document.uri, + params, + SEMANTIC_TOKENS_FULL_HANDLERS, + "Semantic tokens request", + ) + + +def _dispatch_standard_handler( + ls: "LanguageServer", + doc_uri: str, + params: P, + handler_table: Mapping[str, Callable[["LanguageServer", P], R]], + request_type: str, +) -> R: doc = ls.workspace.get_text_document(doc_uri) - handler = HOVER_HANDLERS.get(doc.language_id) + handler = handler_table.get(doc.language_id) if handler is None: _info( - f"Hover request for document: {doc.path} ({doc.language_id}) - no handler" + f"{request_type} for document: {doc.path} ({doc.language_id}) - no handler" ) return _info( - f"Hover request for document: {doc.path} ({doc.language_id}) - delegating to handler" + f"{request_type} for document: {doc.path} ({doc.language_id}) - delegating to handler" ) return handler( diff --git a/src/debputy/lsp/lsp_features.py b/src/debputy/lsp/lsp_features.py index b417dd3..8260675 100644 --- a/src/debputy/lsp/lsp_features.py +++ b/src/debputy/lsp/lsp_features.py @@ -8,6 +8,7 @@ from lsprotocol.types import ( DidChangeTextDocumentParams, Diagnostic, DidOpenTextDocumentParams, + SemanticTokensLegend, ) try: @@ -21,11 +22,20 @@ from debputy.lsp.text_util import on_save_trim_end_of_line_whitespace C = TypeVar("C", bound=Callable) +SEMANTIC_TOKENS_LEGEND = SemanticTokensLegend( + token_types=["keyword", "enumMember"], + token_modifiers=[], +) +SEMANTIC_TOKEN_TYPES_IDS = { + t: idx for idx, t in enumerate(SEMANTIC_TOKENS_LEGEND.token_types) +} DIAGNOSTIC_HANDLERS = {} COMPLETER_HANDLERS = {} HOVER_HANDLERS = {} CODE_ACTION_HANDLERS = {} +FOLDING_RANGE_HANDLERS = {} +SEMANTIC_TOKENS_FULL_HANDLERS = {} WILL_SAVE_WAIT_UNTIL_HANDLERS = {} _ALIAS_OF = {} @@ -111,6 +121,16 @@ def lsp_hover(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C]: return _registering_wrapper(file_formats, HOVER_HANDLERS) +def lsp_folding_ranges(file_formats: Union[str, Sequence[str]]) -> Callable[[C], C]: + return _registering_wrapper(file_formats, FOLDING_RANGE_HANDLERS) + + +def lsp_semantic_tokens_full( + file_formats: Union[str, Sequence[str]] +) -> Callable[[C], C]: + return _registering_wrapper(file_formats, SEMANTIC_TOKENS_FULL_HANDLERS) + + def lsp_standard_handler(file_formats: Union[str, Sequence[str]], topic: str) -> None: res = _STANDARD_HANDLERS.get(topic) if res is None: @@ -171,6 +191,8 @@ def describe_lsp_features() -> None: ("code actions/quickfixes", CODE_ACTION_HANDLERS), ("completion suggestions", COMPLETER_HANDLERS), ("hover docs", HOVER_HANDLERS), + ("folding ranges", FOLDING_RANGE_HANDLERS), + ("semantic tokens", SEMANTIC_TOKENS_FULL_HANDLERS), ("on-save handler", WILL_SAVE_WAIT_UNTIL_HANDLERS), ] print("LSP language IDs and their features:") diff --git a/src/debputy/lsp/lsp_generic_deb822.py b/src/debputy/lsp/lsp_generic_deb822.py index 245f3de..7a1f96f 100644 --- a/src/debputy/lsp/lsp_generic_deb822.py +++ b/src/debputy/lsp/lsp_generic_deb822.py @@ -8,6 +8,8 @@ from typing import ( Any, Container, List, + Iterable, + Iterator, ) from lsprotocol.types import ( @@ -20,14 +22,27 @@ from lsprotocol.types import ( Hover, MarkupKind, HoverParams, + FoldingRangeParams, + FoldingRange, + FoldingRangeKind, + SemanticTokensParams, + SemanticTokens, ) from debputy.lsp.lsp_debian_control_reference_data import ( Deb822FileMetadata, Deb822KnownField, StanzaMetadata, + FieldValueClass, ) +from debputy.lsp.lsp_features import SEMANTIC_TOKEN_TYPES_IDS from debputy.lsp.text_util import normalize_dctrl_field_name +from debputy.lsp.vendoring._deb822_repro import parse_deb822_file +from debputy.lsp.vendoring._deb822_repro.parsing import ( + Deb822KeyValuePairElement, + LIST_SPACE_SEPARATED_INTERPRETATION, +) +from debputy.lsp.vendoring._deb822_repro.tokens import tokenize_deb822_file, Deb822Token from debputy.util import _info try: @@ -175,6 +190,152 @@ def deb822_hover( ) +def _deb822_token_iter( + tokens: Iterable[Deb822Token], +) -> Iterator[Tuple[Deb822Token, int, int, int, int, int]]: + line_no = 0 + line_offset = 0 + + for token in tokens: + start_line = line_no + start_line_offset = line_offset + + newlines = token.text.count("\n") + line_no += newlines + text_len = len(token.text) + if newlines: + if token.text.endswith("\n"): + line_offset = 0 + else: + # -2, one to remove the "\n" and one to get 0-offset + line_offset = text_len - token.text.rindex("\n") - 2 + else: + line_offset += text_len + + yield token, start_line, start_line_offset, line_no, line_offset + + +def deb822_folding_ranges( + ls: "LanguageServer", + params: FoldingRangeParams, + # Unused for now: might be relevant for supporting folding for some fields + _file_metadata: Deb822FileMetadata[Any], +) -> Optional[Sequence[FoldingRange]]: + doc = ls.workspace.get_text_document(params.text_document.uri) + comment_start = -1 + folding_ranges = [] + for ( + token, + start_line, + start_offset, + end_line, + end_offset, + ) in _deb822_token_iter(tokenize_deb822_file(doc.lines)): + if token.is_comment: + if comment_start < 0: + comment_start = start_line + elif comment_start > -1: + comment_start = -1 + folding_range = FoldingRange( + comment_start, + end_line, + kind=FoldingRangeKind.Comment, + ) + + folding_ranges.append(folding_range) + + return folding_ranges + + +def deb822_semantic_tokens_full( + ls: "LanguageServer", + request: SemanticTokensParams, + file_metadata: Deb822FileMetadata[Any], +) -> Optional[SemanticTokens]: + doc = ls.workspace.get_text_document(request.text_document.uri) + lines = doc.lines + deb822_file = parse_deb822_file( + lines, + accept_files_with_duplicated_fields=True, + accept_files_with_error_tokens=True, + ) + tokens = [] + previous_line = 0 + keyword_token_code = SEMANTIC_TOKEN_TYPES_IDS["keyword"] + known_value_token_code = SEMANTIC_TOKEN_TYPES_IDS["enumMember"] + no_modifiers = 0 + + # TODO: Add comment support; slightly complicated by how we parse the file. + + for stanza_idx, stanza in enumerate(deb822_file): + stanza_position = stanza.position_in_file() + stanza_metadata = file_metadata.classify_stanza(stanza, stanza_idx=stanza_idx) + for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement): + kvpair_pos = kvpair.position_in_parent().relative_to(stanza_position) + # These two happen to be the same; the indirection is to make it explicit that the two + # positions for different tokens are the same. + field_position_without_comments = kvpair_pos + field_size = doc.position_codec.client_num_units(kvpair.field_name) + current_line = field_position_without_comments.line_position + line_delta = current_line - previous_line + previous_line = current_line + tokens.append(line_delta) # Line delta + tokens.append(0) # Token column delta + tokens.append(field_size) # Token length + tokens.append(keyword_token_code) + tokens.append(no_modifiers) + + known_field: Optional[Deb822KnownField] = stanza_metadata.get( + kvpair.field_name + ) + if ( + known_field is None + or not known_field.known_values + or known_field.spellcheck_value + ): + continue + + if known_field.field_value_class not in ( + FieldValueClass.SINGLE_VALUE, + FieldValueClass.SPACE_SEPARATED_LIST, + ): + continue + value_element_pos = kvpair.value_element.position_in_parent().relative_to( + kvpair_pos + ) + + last_token_start_column = 0 + + for value_ref in kvpair.interpret_as( + LIST_SPACE_SEPARATED_INTERPRETATION + ).iter_value_references(): + if value_ref.value not in known_field.known_values: + continue + value_loc = value_ref.locatable + value_range_te = value_loc.range_in_parent().relative_to( + value_element_pos + ) + start_line = value_range_te.start_pos.line_position + line_delta = start_line - current_line + current_line = start_line + if line_delta: + last_token_start_column = 0 + + value_start_column = value_range_te.start_pos.cursor_position + column_delta = value_start_column - last_token_start_column + last_token_start_column = value_start_column + + tokens.append(line_delta) # Line delta + tokens.append(column_delta) # Token column delta + tokens.append(field_size) # Token length + tokens.append(known_value_token_code) + tokens.append(no_modifiers) + + if not tokens: + return None + return SemanticTokens(tokens) + + def _should_complete_field_with_value(cand: Deb822KnownField) -> bool: return cand.known_values is not None and ( len(cand.known_values) == 1 diff --git a/src/debputy/lsp/quickfixes.py b/src/debputy/lsp/quickfixes.py index d911961..7ae0324 100644 --- a/src/debputy/lsp/quickfixes.py +++ b/src/debputy/lsp/quickfixes.py @@ -172,6 +172,7 @@ def _correct_value_code_action( def provide_standard_quickfixes_from_diagnostics( + ls: "LanguageServer", code_action_params: CodeActionParams, ) -> Optional[List[Union[Command, CodeAction]]]: actions = [] diff --git a/src/debputy/lsp/spellchecking.py b/src/debputy/lsp/spellchecking.py index 69dd119..f9027af 100644 --- a/src/debputy/lsp/spellchecking.py +++ b/src/debputy/lsp/spellchecking.py @@ -45,7 +45,7 @@ _LOOKS_LIKE_PROGRAMMING_TERM = re.compile( # SCREAMING_SNAKE_CASE (environment variables plus -DVAR=B or $FOO) | [-$%&*_]{0,2}[A-Z][A-Z0-9]*(_[A-Z0-9]+)+(?:=\S+)? | \#[A-Z][A-Z0-9]*(_[A-Z0-9]+)+\# - # Subcommand names. Require at least two "-" to avoid skipping hypenated words + # Subcommand names. Require at least two "-" to avoid skipping hyphenated words | [a-z][a-z0-9]*(-[a-z0-9]+){2,} # Short args | -[a-z0-9]+ diff --git a/src/debputy/lsp/vendoring/_deb822_repro/__init__.py b/src/debputy/lsp/vendoring/_deb822_repro/__init__.py index 72fe6dc..cc2b1de 100644 --- a/src/debputy/lsp/vendoring/_deb822_repro/__init__.py +++ b/src/debputy/lsp/vendoring/_deb822_repro/__init__.py @@ -53,7 +53,7 @@ Compared to debian.deb822 ------------------------- The round-trip safe API is primarily useful when your program is editing files -and the file in question is (likely) to be hand-edited or formated directly by +and the file in question is (likely) to be hand-edited or formatted directly by human maintainers. This includes files like debian/control and the debian/copyright using the "DEP-5" format. diff --git a/src/debputy/lsp/vendoring/_deb822_repro/locatable.py b/src/debputy/lsp/vendoring/_deb822_repro/locatable.py index 90bfa1c..afebf14 100644 --- a/src/debputy/lsp/vendoring/_deb822_repro/locatable.py +++ b/src/debputy/lsp/vendoring/_deb822_repro/locatable.py @@ -270,7 +270,7 @@ class Range: :param base: The desired starting position :param sizes: All the ranges that combined makes up the size of the desired position. Note that order can affect the end result. Particularly - the end character offset gets reset everytime a size spans a line. + the end character offset gets reset every time a size spans a line. :returns: A range at the provided base position that has the size of the provided range. """ diff --git a/src/debputy/lsp/vendoring/_deb822_repro/parsing.py b/src/debputy/lsp/vendoring/_deb822_repro/parsing.py index 13e59b1..1a2da25 100644 --- a/src/debputy/lsp/vendoring/_deb822_repro/parsing.py +++ b/src/debputy/lsp/vendoring/_deb822_repro/parsing.py @@ -2076,7 +2076,7 @@ class Deb822ParagraphElement(Deb822Element, Deb822ParagraphToStrWrapperMixin, AB :param discard_comments_on_read: When getting a field value from the dict, this parameter decides how in-line comments are handled. When setting the value, inline comments are still allowed and will be retained. - However, keep in mind that this option makes getter and setter assymetric + However, keep in mind that this option makes getter and setter asymmetric as a "get" following a "set" with inline comments will omit the comments even if they are there (see the code example). :param auto_map_initial_line_whitespace: Special-case the first value line diff --git a/src/debputy/lsp/vendoring/_deb822_repro/tokens.py b/src/debputy/lsp/vendoring/_deb822_repro/tokens.py index 4e5fa16..5db991a 100644 --- a/src/debputy/lsp/vendoring/_deb822_repro/tokens.py +++ b/src/debputy/lsp/vendoring/_deb822_repro/tokens.py @@ -45,7 +45,7 @@ _RE_WHITESPACE_SEPARATED_WORD_LIST = re.compile( _RE_COMMA_SEPARATED_WORD_LIST = re.compile( r""" # This regex is slightly complicated by the fact that it should work with - # finditer and comsume the entire value. + # finditer and consume the entire value. # # To do this, we structure the regex so it always starts on a comma (except # for the first iteration, where we permit the absence of a comma) diff --git a/src/debputy/manifest_parser/declarative_parser.py b/src/debputy/manifest_parser/declarative_parser.py index 32e93fe..84e0230 100644 --- a/src/debputy/manifest_parser/declarative_parser.py +++ b/src/debputy/manifest_parser/declarative_parser.py @@ -720,7 +720,7 @@ class ParserGenerator: ``` While this is sufficient for programmers, it is a bit ridig for the packager writing the manifest. Therefore, - you can also provide a TypedDict descriping the input, enabling more flexibility: + you can also provide a TypedDict describing the input, enabling more flexibility: >>> class InstallDocsRule(DebputyParsedContent): ... sources: List[str] @@ -737,7 +737,7 @@ class ParserGenerator: In this case, the `sources` field can either come from a single `source` in the manifest (which must be a string) or `sources` (which must be a list of strings). The parser also ensures that only one of `source` or `sources` - is used to ensure the input is not ambigious. For the `into` parameter, the parser will accept it being a str + is used to ensure the input is not ambiguous. For the `into` parameter, the parser will accept it being a str or a list of strings. Regardless of how the input was provided, the parser will normalize the input such that both `sources` and `into` in the result is a list of strings. As an example, this parser can accept both the previous input but also the following input: diff --git a/src/debputy/plugin/api/impl.py b/src/debputy/plugin/api/impl.py index e25713f..8b75322 100644 --- a/src/debputy/plugin/api/impl.py +++ b/src/debputy/plugin/api/impl.py @@ -783,7 +783,7 @@ class DebputyPluginInitializerProvider(DebputyPluginInitializer): self._unloaders.append(_unload) - def plugable_object_parser( + def pluggable_object_parser( self, rule_type: str, rule_name: str, @@ -821,12 +821,12 @@ class DebputyPluginInitializerProvider(DebputyPluginInitializer): def _unload() -> None: raise PluginInitializationError( - "Cannot unload plugable_object_parser (not implemented)" + "Cannot unload pluggable_object_parser (not implemented)" ) self._unloaders.append(_unload) - def plugable_manifest_rule( + def pluggable_manifest_rule( self, rule_type: Union[TTP, str], rule_name: Union[str, List[str]], @@ -869,7 +869,7 @@ class DebputyPluginInitializerProvider(DebputyPluginInitializer): def _unload() -> None: raise PluginInitializationError( - "Cannot unload plugable_manifest_rule (not implemented)" + "Cannot unload pluggable_manifest_rule (not implemented)" ) self._unloaders.append(_unload) diff --git a/src/debputy/plugin/api/test_api/test_spec.py b/src/debputy/plugin/api/test_api/test_spec.py index b05f7ed..0c0c6bc 100644 --- a/src/debputy/plugin/api/test_api/test_spec.py +++ b/src/debputy/plugin/api/test_api/test_spec.py @@ -61,7 +61,7 @@ def build_virtual_file_system( ... ) True - Any string provided will be pased to `virtual_path` using all defaults for other parameters, making `str` + Any string provided will be passed to `virtual_path` using all defaults for other parameters, making `str` arguments a nice easy shorthand if you just want a path to exist, but do not really care about it otherwise (or `virtual_path_def` defaults happens to work for you). @@ -158,7 +158,7 @@ class RegisteredPackagerProvidedFile(metaclass=ABCMeta): installed_path: str """The mode that debputy will give these files when installed (unless overridden)""" default_mode: int - """The default priority assigned to files unless overriden (if priories are assigned at all)""" + """The default priority assigned to files unless overridden (if priories are assigned at all)""" default_priority: Optional[int] """The filename format to be used""" filename_format: Optional[str] diff --git a/src/debputy/plugin/debputy/binary_package_rules.py b/src/debputy/plugin/debputy/binary_package_rules.py index 04a0fa1..686d71a 100644 --- a/src/debputy/plugin/debputy/binary_package_rules.py +++ b/src/debputy/plugin/debputy/binary_package_rules.py @@ -59,7 +59,7 @@ ACCEPTABLE_CLEAN_ON_REMOVAL_IF_EXACT_MATCH_OR_SUBDIR_OF = frozenset( def register_binary_package_rules(api: DebputyPluginInitializerProvider) -> None: - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_PACKAGES, "binary-version", BinaryVersionParsedFormat, @@ -93,7 +93,7 @@ def register_binary_package_rules(api: DebputyPluginInitializerProvider) -> None ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_PACKAGES, "transformations", ListOfTransformationRulesFormat, @@ -131,7 +131,7 @@ def register_binary_package_rules(api: DebputyPluginInitializerProvider) -> None ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_PACKAGES, "conffile-management", ListOfDpkgMaintscriptHelperCommandFormat, @@ -139,7 +139,7 @@ def register_binary_package_rules(api: DebputyPluginInitializerProvider) -> None source_format=List[DpkgMaintscriptHelperCommand], ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_PACKAGES, "clean-after-removal", ListParsedFormat, @@ -203,7 +203,7 @@ def register_binary_package_rules(api: DebputyPluginInitializerProvider) -> None ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_PACKAGES, "installation-search-dirs", InstallationSearchDirsParsedFormat, diff --git a/src/debputy/plugin/debputy/manifest_root_rules.py b/src/debputy/plugin/debputy/manifest_root_rules.py index cc2b1d4..86a1c27 100644 --- a/src/debputy/plugin/debputy/manifest_root_rules.py +++ b/src/debputy/plugin/debputy/manifest_root_rules.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: def register_manifest_root_rules(api: DebputyPluginInitializerProvider) -> None: # Registration order matters. Notably, definitions must come before anything that can # use definitions (variables), which is why it is second only to the manifest version. - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_MANIFEST_ROOT, MK_MANIFEST_VERSION, ManifestVersionFormat, @@ -56,13 +56,13 @@ def register_manifest_root_rules(api: DebputyPluginInitializerProvider) -> None: ), ), ) - api.plugable_object_parser( + api.pluggable_object_parser( OPARSER_MANIFEST_ROOT, MK_MANIFEST_DEFINITIONS, object_parser_key=OPARSER_MANIFEST_DEFINITIONS, on_end_parse_step=lambda _a, _b, _c, mp: mp._ensure_package_states_is_initialized(), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_MANIFEST_DEFINITIONS, MK_MANIFEST_VARIABLES, ManifestVariablesParsedFormat, @@ -105,7 +105,7 @@ def register_manifest_root_rules(api: DebputyPluginInitializerProvider) -> None: ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_MANIFEST_ROOT, MK_INSTALLATIONS, ListOfInstallRulesFormat, @@ -156,7 +156,7 @@ def register_manifest_root_rules(api: DebputyPluginInitializerProvider) -> None: ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( OPARSER_MANIFEST_ROOT, MK_PACKAGES, DictFormat, diff --git a/src/debputy/plugin/debputy/private_api.py b/src/debputy/plugin/debputy/private_api.py index 2db2b56..b9aa043 100644 --- a/src/debputy/plugin/debputy/private_api.py +++ b/src/debputy/plugin/debputy/private_api.py @@ -781,7 +781,7 @@ def register_special_ppfs(api: DebputyPluginInitializerProvider) -> None: def register_install_rules(api: DebputyPluginInitializerProvider) -> None: - api.plugable_manifest_rule( + api.pluggable_manifest_rule( InstallRule, MK_INSTALLATIONS_INSTALL, ParsedInstallRule, @@ -868,7 +868,7 @@ def register_install_rules(api: DebputyPluginInitializerProvider) -> None: reference_documentation_url=_manifest_format_doc("generic-install-install"), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( InstallRule, [ MK_INSTALLATIONS_INSTALL_DOCS, @@ -977,7 +977,7 @@ def register_install_rules(api: DebputyPluginInitializerProvider) -> None: ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( InstallRule, [ MK_INSTALLATIONS_INSTALL_EXAMPLES, @@ -1059,7 +1059,7 @@ def register_install_rules(api: DebputyPluginInitializerProvider) -> None: ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( InstallRule, MK_INSTALLATIONS_INSTALL_MAN, ParsedInstallManpageRule, @@ -1166,7 +1166,7 @@ def register_install_rules(api: DebputyPluginInitializerProvider) -> None: ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( InstallRule, MK_INSTALLATIONS_DISCARD, ParsedInstallDiscardRule, @@ -1242,7 +1242,7 @@ def register_install_rules(api: DebputyPluginInitializerProvider) -> None: ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( InstallRule, MK_INSTALLATIONS_MULTI_DEST_INSTALL, ParsedMultiDestInstallRule, @@ -1329,7 +1329,7 @@ def register_install_rules(api: DebputyPluginInitializerProvider) -> None: def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None: - api.plugable_manifest_rule( + api.pluggable_manifest_rule( TransformationRule, "move", TransformationMoveRuleSpec, @@ -1381,7 +1381,7 @@ def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( TransformationRule, "remove", TransformationRemoveRuleSpec, @@ -1451,7 +1451,7 @@ def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( TransformationRule, "create-symlink", CreateSymlinkRule, @@ -1537,7 +1537,7 @@ def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( TransformationRule, "path-metadata", PathManifestRule, @@ -1653,7 +1653,7 @@ def register_transformation_rules(api: DebputyPluginInitializerProvider) -> None ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( TransformationRule, "create-directories", EnsureDirectoryRule, @@ -1824,7 +1824,7 @@ def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( ManifestCondition, "not", MCNot, @@ -1868,7 +1868,7 @@ def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( ManifestCondition, ["any-of", "all-of"], MCAnyOfAllOf, @@ -1887,7 +1887,7 @@ def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> ), ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( ManifestCondition, "arch-matches", MCArchMatches, @@ -1997,7 +1997,7 @@ def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> ), ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( ManifestCondition, "source-context-arch-matches", MCArchMatches, @@ -2005,7 +2005,7 @@ def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> source_format=str, inline_reference_documentation=context_arch_doc, ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( ManifestCondition, "package-context-arch-matches", MCArchMatches, @@ -2013,7 +2013,7 @@ def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> source_format=str, inline_reference_documentation=context_arch_doc, ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( ManifestCondition, "build-profiles-matches", MCBuildProfileMatches, @@ -2043,7 +2043,7 @@ def register_manifest_condition_rules(api: DebputyPluginInitializerProvider) -> def register_dpkg_conffile_rules(api: DebputyPluginInitializerProvider) -> None: - api.plugable_manifest_rule( + api.pluggable_manifest_rule( DpkgMaintscriptHelperCommand, "remove", DpkgRemoveConffileRule, @@ -2051,7 +2051,7 @@ def register_dpkg_conffile_rules(api: DebputyPluginInitializerProvider) -> None: inline_reference_documentation=None, # TODO: write and add ) - api.plugable_manifest_rule( + api.pluggable_manifest_rule( DpkgMaintscriptHelperCommand, "rename", DpkgRenameConffileRule, diff --git a/src/debputy/transformation_rules.py b/src/debputy/transformation_rules.py index 8d9caae..fdf9528 100644 --- a/src/debputy/transformation_rules.py +++ b/src/debputy/transformation_rules.py @@ -217,7 +217,7 @@ class MoveTransformationRule(TransformationRule): self._error( f"Could not rename {self._match_rule.describe_match_short()} to {self._dest_path}" f" (from: {self._definition_source}). Multiple paths matched the pattern and the" - " destination was not a directory. Either correct the pattern to only match ony source" + " destination was not a directory. Either correct the pattern to only match only source" " OR define the destination to be a directory (E.g., add a trailing slash - example:" f' "{self._dest_path}/")' ) |