summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/debputy/commands/debputy_cmd/__main__.py22
-rw-r--r--src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py40
-rw-r--r--src/debputy/commands/debputy_cmd/output.py12
-rw-r--r--src/debputy/commands/debputy_cmd/plugin_cmds.py2
-rw-r--r--src/debputy/deb_packaging_support.py9
-rw-r--r--src/debputy/debhelper_emulation.py4
-rw-r--r--src/debputy/dh_migration/migrators_impl.py4
-rw-r--r--src/debputy/filesystem_scan.py4
-rw-r--r--src/debputy/highlevel_manifest.py2
-rw-r--r--src/debputy/highlevel_manifest_parser.py11
-rw-r--r--src/debputy/installations.py1
-rw-r--r--src/debputy/interpreter.py4
-rw-r--r--src/debputy/linting/lint_impl.py5
-rw-r--r--src/debputy/lsp/debputy_ls.py133
-rw-r--r--src/debputy/lsp/lsp_debian_changelog.py2
-rw-r--r--src/debputy/lsp/lsp_debian_control.py404
-rw-r--r--src/debputy/lsp/lsp_debian_control_reference_data.py285
-rw-r--r--src/debputy/lsp/lsp_debian_copyright.py48
-rw-r--r--src/debputy/lsp/lsp_debian_debputy_manifest.py17
-rw-r--r--src/debputy/lsp/lsp_debian_rules.py7
-rw-r--r--src/debputy/lsp/lsp_debian_tests_control.py45
-rw-r--r--src/debputy/lsp/lsp_dispatch.py30
-rw-r--r--src/debputy/lsp/lsp_features.py24
-rw-r--r--src/debputy/lsp/lsp_generic_deb822.py275
-rw-r--r--src/debputy/lsp/lsp_self_check.py2
-rw-r--r--src/debputy/lsp/quickfixes.py114
-rw-r--r--src/debputy/lsp/text_util.py15
-rw-r--r--src/debputy/lsp/vendoring/_deb822_repro/parsing.py25
-rw-r--r--src/debputy/lsp/vendoring/_deb822_repro/tokens.py14
-rw-r--r--src/debputy/path_matcher.py22
-rw-r--r--src/debputy/plugin/api/impl_types.py2
-rw-r--r--src/debputy/plugin/api/spec.py8
-rw-r--r--src/debputy/plugin/debputy/metadata_detectors.py4
-rw-r--r--src/debputy/plugin/debputy/private_api.py8
-rw-r--r--src/debputy/plugin/debputy/strip_non_determinism.py4
-rw-r--r--src/debputy/util.py8
-rw-r--r--src/debputy/yaml/compat.py10
37 files changed, 1150 insertions, 476 deletions
diff --git a/src/debputy/commands/debputy_cmd/__main__.py b/src/debputy/commands/debputy_cmd/__main__.py
index 27edf49..1a7a737 100644
--- a/src/debputy/commands/debputy_cmd/__main__.py
+++ b/src/debputy/commands/debputy_cmd/__main__.py
@@ -71,6 +71,7 @@ except ImportError:
from debputy.version import __version__
from debputy.filesystem_scan import (
FSROOverlay,
+ FSRootDir,
)
from debputy.plugin.api.impl_types import (
PackagerProvidedFileClassSpec,
@@ -754,7 +755,8 @@ def _dh_integration_generate_debs(context: CommandContext) -> None:
continue
# Ensure all fs's are read-only before we enable cross package checks.
# This ensures that no metadata detector will never see a read-write FS
- cast("FSRootDir", binary_data.fs_root).is_read_write = False
+ pkg_fs_root: "FSRootDir" = cast("FSRootDir", binary_data.fs_root)
+ pkg_fs_root.is_read_write = False
package_data_table.enable_cross_package_checks = True
assemble_debs(
@@ -799,7 +801,7 @@ _POST_FORMATTING_REWRITE = {
def _fake_PPFClassSpec(
debputy_plugin_metadata: DebputyPluginMetadata,
stem: str,
- doc_uris: Sequence[str],
+ doc_uris: Optional[Sequence[str]],
install_pattern: Optional[str],
*,
default_priority: Optional[int] = None,
@@ -978,7 +980,7 @@ def _resolve_debhelper_config_files(
post_formatting_rewrite=post_formatting_rewrite,
packageless_is_fallback_for_all_packages=packageless_is_fallback_for_all_packages,
)
- dh_ppfs = list(
+ all_dh_ppfs = list(
flatten_ppfs(
detect_all_packager_provided_files(
dh_ppfs,
@@ -988,13 +990,13 @@ def _resolve_debhelper_config_files(
)
)
)
- return dh_ppfs, issues, exit_code
+ return all_dh_ppfs, issues, exit_code
def _merge_list(
existing_table: Dict[str, Any],
key: str,
- new_data: Optional[List[str]],
+ new_data: Optional[Sequence[str]],
) -> None:
if not new_data:
return
@@ -1368,13 +1370,11 @@ def _annotate_debian_directory(context: CommandContext) -> None:
def _json_output(data: Any) -> None:
- format_options = {}
if sys.stdout.isatty():
- format_options = {
- "indent": 4,
- # sort_keys might be tempting but generally insert order makes more sense in practice.
- }
- json.dump(data, sys.stdout, **format_options)
+ # sort_keys might be tempting but generally insert order makes more sense in practice.
+ json.dump(data, sys.stdout, indent=4)
+ else:
+ json.dump(data, sys.stdout)
if sys.stdout.isatty():
# Looks better with a final newline.
print()
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 3eecb14..2f283e8 100644
--- a/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py
+++ b/src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py
@@ -21,19 +21,19 @@ _EDITOR_SNIPPETS = {
;; Inform eglot about the debputy LSP
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
- '(debian-control-mode . ("debputy" "lsp" "server")))
+ '(debian-control-mode . ("debputy" "lsp" "server" "--ignore-language-ids")))
(add-to-list 'eglot-server-programs
- '(debian-changelog-mode . ("debputy" "lsp" "server")))
+ '(debian-changelog-mode . ("debputy" "lsp" "server" "--ignore-language-ids")))
(add-to-list 'eglot-server-programs
- '(debian-copyright-mode . ("debputy" "lsp" "server")))
+ '(debian-copyright-mode . ("debputy" "lsp" "server" "--ignore-language-ids")))
;; Requires elpa-dpkg-dev-el (>> 37.11)
;; (add-to-list 'eglot-server-programs
- ;; '(debian-autopkgtest-control-mode . ("debputy" "lsp" "server")))
+ ;; '(debian-autopkgtest-control-mode . ("debputy" "lsp" "server" "--ignore-language-ids")))
;; The debian/rules file uses the qmake mode.
(add-to-list 'eglot-server-programs
- '(makefile-gmake-mode . ("debputy" "lsp" "server")))
+ '(makefile-gmake-mode . ("debputy" "lsp" "server" "--ignore-language-ids")))
(add-to-list 'eglot-server-programs
- '(yaml-mode . ("debputy" "lsp" "server")))
+ '(yaml-mode . ("debputy" "lsp" "server" "--ignore-language-ids")))
)
;; Auto-start eglot for the relevant modes.
@@ -64,7 +64,7 @@ _EDITOR_SNIPPETS = {
let g:ycm_language_server = [
\\ { 'name': 'debputy',
\\ 'filetypes': [ 'debcontrol', 'debcopyright', 'debchangelog', 'make', 'yaml'],
- \\ 'cmdline': [ 'debputy', 'lsp', 'server' ]
+ \\ 'cmdline': [ 'debputy', 'lsp', 'server', '--ignore-language-ids' ]
\\ },
\\ ]
@@ -92,7 +92,7 @@ _EDITOR_SNIPPETS = {
lspServers->add({
filetype: ['debcontrol', 'debcopyright', 'debchangelog', 'make', 'yaml'],
path: 'debputy',
- args: ['lsp', 'server']
+ args: ['lsp', 'server', '--ignore-language-ids']
})
endif
@@ -100,6 +100,19 @@ _EDITOR_SNIPPETS = {
autocmd User LspSetup g:LspAddServer(lspServers)
"""
),
+ "neovim": "neovim+nvim-lspconfig",
+ "neovim+nvim-lspconfig": textwrap.dedent(
+ """\
+ # debputy lsp server glue for neovim with nvim-lspconfig. Add to ~/.config/nvim/init.lua
+ #
+ # Requires https://github.com/neovim/nvim-lspconfig to be in your packages path
+
+ require("lspconfig").debputy.setup {capabilities = capabilities}
+
+ # Make vim recognize debputy.manifest as YAML file
+ vim.filetype.add({filename = {["debputy.manifest"] = "yaml"})
+ """
+ ),
}
@@ -136,6 +149,13 @@ lsp_command = ROOT_COMMAND.add_dispatching_subcommand(
default=2087,
help="Bind to this port (Use with --tcp / --ws)",
),
+ add_arg(
+ "--ignore-language-ids",
+ dest="trust_language_ids",
+ default=True,
+ action="store_false",
+ help="Disregard language IDs from the editor (rely solely on filename instead)",
+ ),
],
)
def lsp_server_cmd(context: CommandContext) -> None:
@@ -156,6 +176,10 @@ def lsp_server_cmd(context: CommandContext) -> None:
debputy_language_server = DEBPUTY_LANGUAGE_SERVER
debputy_language_server.plugin_feature_set = feature_set
debputy_language_server.dctrl_parser = context.dctrl_parser
+ debputy_language_server.trust_language_ids = parsed_args.trust_language_ids
+
+ if parsed_args.tcp and parsed_args.ws:
+ _error("Sorry, --tcp and --ws are mutually exclusive")
if parsed_args.tcp:
debputy_language_server.start_tcp(parsed_args.host, parsed_args.port)
diff --git a/src/debputy/commands/debputy_cmd/output.py b/src/debputy/commands/debputy_cmd/output.py
index df8e6eb..2e117ba 100644
--- a/src/debputy/commands/debputy_cmd/output.py
+++ b/src/debputy/commands/debputy_cmd/output.py
@@ -133,10 +133,10 @@ class OutputStylingBase:
row_format = f"| {row_format_inner} |"
if self.supports_colors:
- c = self._color_support
- assert c is not None
- header_color = c.Style.bold
- header_color_reset = c.Style.reset
+ cs = self._color_support
+ assert cs is not None
+ header_color = cs.Style.bold
+ header_color_reset = cs.Style.reset
else:
header_color = ""
header_color_reset = ""
@@ -218,9 +218,9 @@ class ANSIOutputStylingBase(OutputStylingBase):
self._check_color(fg)
self._check_color(bg)
self._check_text_style(style)
- if not self.supports_colors:
- return text
_colored = self._color_support
+ if not self.supports_colors or _colored is None:
+ return text
codes = []
if style is not None:
code = getattr(_colored.Style, style)
diff --git a/src/debputy/commands/debputy_cmd/plugin_cmds.py b/src/debputy/commands/debputy_cmd/plugin_cmds.py
index a8103fb..60d3c70 100644
--- a/src/debputy/commands/debputy_cmd/plugin_cmds.py
+++ b/src/debputy/commands/debputy_cmd/plugin_cmds.py
@@ -1189,7 +1189,7 @@ def _render_value(v: Any) -> str:
return str(v)
-def ensure_plugin_commands_are_loaded():
+def ensure_plugin_commands_are_loaded() -> None:
# Loading the module does the heavy lifting
# However, having this function means that we do not have an "unused" import that some tool
# gets tempted to remove
diff --git a/src/debputy/deb_packaging_support.py b/src/debputy/deb_packaging_support.py
index b38cbc2..6de61b4 100644
--- a/src/debputy/deb_packaging_support.py
+++ b/src/debputy/deb_packaging_support.py
@@ -930,7 +930,7 @@ def _relevant_service_definitions(
if key in by_service_manager_key
and service_rule.applies_to_service_manager(key[-1])
}
- relevant_names = {}
+ relevant_names: Dict[Tuple[str, str, str, str], ServiceDefinition[Any]] = {}
seen_keys = set()
if not pending_queue:
@@ -954,7 +954,7 @@ def _relevant_service_definitions(
):
pending_queue.add(target_key)
- return relevant_names
+ return relevant_names.items()
def handle_service_management(
@@ -982,7 +982,9 @@ def handle_service_management(
)
for service_manager_details in feature_set.service_managers.values():
- service_registry = ServiceRegistryImpl(service_manager_details)
+ service_registry: ServiceRegistryImpl = ServiceRegistryImpl(
+ service_manager_details
+ )
service_manager_details.service_detector(
fs_root,
service_registry,
@@ -1652,6 +1654,7 @@ def _generate_control_files(
dctrl_file = "debian/control"
if has_dbgsym:
+ assert dbgsym_root_fs is not None # mypy hint
_generate_dbgsym_control_file_if_relevant(
binary_package,
dbgsym_root_fs,
diff --git a/src/debputy/debhelper_emulation.py b/src/debputy/debhelper_emulation.py
index 38d9a15..65a26f8 100644
--- a/src/debputy/debhelper_emulation.py
+++ b/src/debputy/debhelper_emulation.py
@@ -17,6 +17,8 @@ from typing import (
List,
)
+from debian.deb822 import Deb822
+
from debputy.packages import BinaryPackage
from debputy.plugin.api import VirtualPath
from debputy.substitution import Substitution
@@ -251,7 +253,7 @@ def parse_drules_for_addons(lines: Iterable[str], sequences: Set[str]) -> None:
def extract_dh_addons_from_control(
- source_paragraph: Mapping[str, str],
+ source_paragraph: Union[Mapping[str, str], Deb822],
sequences: Set[str],
) -> None:
for f in ("Build-Depends", "Build-Depends-Indep", "Build-Depends-Arch"):
diff --git a/src/debputy/dh_migration/migrators_impl.py b/src/debputy/dh_migration/migrators_impl.py
index d7aa252..2ceefd5 100644
--- a/src/debputy/dh_migration/migrators_impl.py
+++ b/src/debputy/dh_migration/migrators_impl.py
@@ -432,7 +432,7 @@ def migrate_bash_completion(
install_as_rules.append((source, dest_basename))
if install_dest_sources:
- sources = (
+ sources: Union[List[str], str] = (
install_dest_sources
if len(install_dest_sources) > 1
else install_dest_sources[0]
@@ -1502,7 +1502,7 @@ def read_dh_addon_sequences(
ctrl_file = debian_dir.get("control")
if ctrl_file:
dr_sequences: Set[str] = set()
- bd_sequences = set()
+ bd_sequences: Set[str] = set()
drules = debian_dir.get("rules")
if drules and drules.is_file:
diff --git a/src/debputy/filesystem_scan.py b/src/debputy/filesystem_scan.py
index dec123c..0a18899 100644
--- a/src/debputy/filesystem_scan.py
+++ b/src/debputy/filesystem_scan.py
@@ -1603,9 +1603,9 @@ class FSROOverlay(VirtualPathBase):
continue
if dir_part == "..":
p = current.parent_dir
- if current is None:
+ if p is None:
raise ValueError(f'The path "{path}" escapes the root dir')
- current = p
+ current = cast("FSROOverlay", p)
continue
try:
current = current[dir_part]
diff --git a/src/debputy/highlevel_manifest.py b/src/debputy/highlevel_manifest.py
index 30440f1..1fea1a2 100644
--- a/src/debputy/highlevel_manifest.py
+++ b/src/debputy/highlevel_manifest.py
@@ -1199,7 +1199,7 @@ class HighLevelManifest:
dtmp_dir = None
search_dirs = install_request_context.search_dirs
into = frozenset(self._binary_packages.values())
- seen = set()
+ seen: Set[BinaryPackage] = set()
for search_dir in search_dirs:
seen.update(search_dir.applies_to)
diff --git a/src/debputy/highlevel_manifest_parser.py b/src/debputy/highlevel_manifest_parser.py
index 28a3f80..c5fb410 100644
--- a/src/debputy/highlevel_manifest_parser.py
+++ b/src/debputy/highlevel_manifest_parser.py
@@ -444,13 +444,10 @@ class YAMLManifestParser(HighLevelManifestParser):
parser_generator = self._plugin_provided_feature_set.manifest_parser_generator
dispatchable_object_parsers = parser_generator.dispatchable_object_parsers
manifest_root_parser = dispatchable_object_parsers[OPARSER_MANIFEST_ROOT]
- parsed_data = cast(
- "ManifestRootRule",
- manifest_root_parser.parse_input(
- yaml_data,
- attribute_path,
- parser_context=self,
- ),
+ parsed_data = manifest_root_parser.parse_input(
+ yaml_data,
+ attribute_path,
+ parser_context=self,
)
packages_dict: Mapping[str, PackageContextData[Mapping[str, Any]]] = cast(
diff --git a/src/debputy/installations.py b/src/debputy/installations.py
index e1e8f3a..b781757 100644
--- a/src/debputy/installations.py
+++ b/src/debputy/installations.py
@@ -546,6 +546,7 @@ def _resolve_matches(
dest_paths: Union[Sequence[Tuple[str, bool]], Callable[[PathMatch], str]],
install_context: "InstallRuleContext",
) -> Iterator[Tuple[PathMatch, Sequence[Tuple[str, "FSPath"]]]]:
+ dest_and_roots: Sequence[Tuple[str, "FSPath"]]
if callable(dest_paths):
compute_dest_path = dest_paths
for match in matches:
diff --git a/src/debputy/interpreter.py b/src/debputy/interpreter.py
index 0d986e1..5a933fc 100644
--- a/src/debputy/interpreter.py
+++ b/src/debputy/interpreter.py
@@ -147,6 +147,10 @@ class DetectedInterpreter(Interpreter):
def replace_shebang_line(self, path: "VirtualPath") -> None:
new_shebang_line = self.corrected_shebang_line
+ if new_shebang_line is None:
+ raise RuntimeError(
+ "Please do not call replace_shebang_line when fixup_needed returns False"
+ )
assert new_shebang_line.startswith("#!")
if not new_shebang_line.endswith("\n"):
new_shebang_line += "\n"
diff --git a/src/debputy/linting/lint_impl.py b/src/debputy/linting/lint_impl.py
index a6f493e..ec13d53 100644
--- a/src/debputy/linting/lint_impl.py
+++ b/src/debputy/linting/lint_impl.py
@@ -13,6 +13,7 @@ from lsprotocol.types import (
TextEdit,
Position,
DiagnosticSeverity,
+ Diagnostic,
)
from debputy.commands.debputy_cmd.context import CommandContext
@@ -185,9 +186,9 @@ def _auto_fix_run(
lint_report: LintReport,
) -> None:
another_round = True
- unfixed_diagnostics = []
+ unfixed_diagnostics: List[Diagnostic] = []
remaining_rounds = 10
- fixed_count = False
+ fixed_count = 0
too_many_rounds = False
lines = text.splitlines(keepends=True)
lint_state = lint_context.state_for(
diff --git a/src/debputy/lsp/debputy_ls.py b/src/debputy/lsp/debputy_ls.py
index f375992..cc3f00e 100644
--- a/src/debputy/lsp/debputy_ls.py
+++ b/src/debputy/lsp/debputy_ls.py
@@ -1,6 +1,17 @@
import dataclasses
import os
-from typing import Optional, List, Any, Mapping
+from typing import (
+ Optional,
+ List,
+ Any,
+ Mapping,
+ Container,
+ TYPE_CHECKING,
+ Tuple,
+ Literal,
+)
+
+from lsprotocol.types import MarkupKind
from debputy.linting.lint_util import LintState
from debputy.lsp.text_util import LintCapablePositionCodec
@@ -11,17 +22,23 @@ from debputy.packages import (
)
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
-try:
+if TYPE_CHECKING:
from pygls.server import LanguageServer
from pygls.workspace import TextDocument
from pygls.uris import from_fs_path
-except ImportError as e:
- class LanguageServer:
- def __init__(self, *args, **kwargs) -> None:
- """Placeholder to work if pygls is not installed"""
- # Should not be called
- raise e # pragma: no cover
+else:
+ try:
+ from pygls.server import LanguageServer
+ from pygls.workspace import TextDocument
+ from pygls.uris import from_fs_path
+ except ImportError as e:
+
+ class LanguageServer:
+ def __init__(self, *args, **kwargs) -> None:
+ """Placeholder to work if pygls is not installed"""
+ # Should not be called
+ raise e # pragma: no cover
@dataclasses.dataclass(slots=True)
@@ -86,10 +103,13 @@ class LSProvidedLintState(LintState):
dctrl_doc = self._ls.workspace.get_text_document(dctrl_cache.doc_uri)
re_parse_lines: Optional[List[str]] = None
if is_open:
+ last_doc_version = dctrl_cache.last_doc_version
+ dctrl_doc_version = dctrl_doc.version
if (
not dctrl_cache.is_open_in_editor
- or dctrl_cache.last_doc_version is None
- or dctrl_cache.last_doc_version < dctrl_doc.version
+ or last_doc_version is None
+ or dctrl_doc_version is None
+ or last_doc_version < dctrl_doc_version
):
re_parse_lines = doc.lines
@@ -127,6 +147,19 @@ class LSProvidedLintState(LintState):
return dctrl.binary_packages if dctrl is not None else None
+def _preference(
+ client_preference: Optional[List[MarkupKind]],
+ options: Container[MarkupKind],
+ fallback_kind: MarkupKind,
+) -> MarkupKind:
+ if not client_preference:
+ return fallback_kind
+ for markdown_kind in client_preference:
+ if markdown_kind in options:
+ return markdown_kind
+ return fallback_kind
+
+
class DebputyLanguageServer(LanguageServer):
def __init__(
@@ -137,6 +170,7 @@ class DebputyLanguageServer(LanguageServer):
super().__init__(*args, **kwargs)
self._dctrl_parser: Optional[DctrlParser] = None
self._plugin_feature_set: Optional[PluginProvidedFeatureSet] = None
+ self._trust_language_ids: Optional[bool] = None
@property
def plugin_feature_set(self) -> PluginProvidedFeatureSet:
@@ -177,3 +211,82 @@ class DebputyLanguageServer(LanguageServer):
dir_path = os.path.dirname(dir_path)
return LSProvidedLintState(self, doc, dir_path, self.dctrl_parser)
+
+ @property
+ def _client_hover_markup_formats(self) -> Optional[List[MarkupKind]]:
+ try:
+ return (
+ self.client_capabilities.text_document.hover.content_format
+ ) # type : ignore
+ except AttributeError:
+ return None
+
+ def hover_markup_format(
+ self,
+ *options: MarkupKind,
+ fallback_kind: MarkupKind = MarkupKind.PlainText,
+ ) -> MarkupKind:
+ """Pick the client preferred hover markup format from a set of options
+
+ :param options: The markup kinds possible.
+ :param fallback_kind: If no overlapping option was found in the client preferences
+ (or client did not announce a value at all), this parameter is returned instead.
+ :returns: The client's preferred markup format from the provided options, or,
+ (if there is no overlap), the `fallback_kind` value is returned.
+ """
+ client_preference = self._client_hover_markup_formats
+ return _preference(client_preference, frozenset(options), fallback_kind)
+
+ @property
+ def _client_completion_item_document_markup_formats(
+ self,
+ ) -> Optional[List[MarkupKind]]:
+ try:
+ return (
+ self.client_capabilities.text_document.completion.completion_item.documentation_format # type : ignore
+ )
+ except AttributeError:
+ return None
+
+ def completion_item_document_markup(
+ self,
+ *options: MarkupKind,
+ fallback_kind: MarkupKind = MarkupKind.PlainText,
+ ) -> MarkupKind:
+ """Pick the client preferred completion item documentation markup format from a set of options
+
+ :param options: The markup kinds possible.
+ :param fallback_kind: If no overlapping option was found in the client preferences
+ (or client did not announce a value at all), this parameter is returned instead.
+ :returns: The client's preferred markup format from the provided options, or,
+ (if there is no overlap), the `fallback_kind` value is returned.
+ """
+
+ client_preference = self._client_completion_item_document_markup_formats
+ return _preference(client_preference, frozenset(options), fallback_kind)
+
+ @property
+ def trust_language_ids(self) -> bool:
+ v = self._trust_language_ids
+ if v is None:
+ return True
+ return v
+
+ @trust_language_ids.setter
+ def trust_language_ids(self, new_value: bool) -> None:
+ self._trust_language_ids = new_value
+
+ def determine_language_id(
+ self,
+ doc: "TextDocument",
+ ) -> Tuple[Literal["editor-provided", "filename"], str]:
+ lang_id = doc.language_id
+ if self.trust_language_ids and lang_id and not lang_id.isspace():
+ return "editor-provided", lang_id
+ path = doc.path
+ try:
+ last_idx = path.rindex("debian/")
+ except ValueError:
+ return "filename", os.path.basename(path)
+ guess_language_id = path[last_idx:]
+ return "filename", guess_language_id
diff --git a/src/debputy/lsp/lsp_debian_changelog.py b/src/debputy/lsp/lsp_debian_changelog.py
index 89604e4..ecff192 100644
--- a/src/debputy/lsp/lsp_debian_changelog.py
+++ b/src/debputy/lsp/lsp_debian_changelog.py
@@ -262,7 +262,7 @@ def _scan_debian_changelog_for_diagnostics(
*,
max_line_length: int = _MAXIMUM_WIDTH,
) -> Iterator[List[Diagnostic]]:
- diagnostics = []
+ diagnostics: List[Diagnostic] = []
diagnostics_at_last_update = 0
lines_since_last_update = 0
lines = lint_state.lines
diff --git a/src/debputy/lsp/lsp_debian_control.py b/src/debputy/lsp/lsp_debian_control.py
index 8c246d8..b44e8f9 100644
--- a/src/debputy/lsp/lsp_debian_control.py
+++ b/src/debputy/lsp/lsp_debian_control.py
@@ -1,5 +1,7 @@
+import dataclasses
import re
import textwrap
+from functools import lru_cache
from typing import (
Union,
Sequence,
@@ -9,15 +11,16 @@ from typing import (
Iterable,
Mapping,
List,
+ FrozenSet,
+ Dict,
)
+from debputy.lsp.debputy_ls import DebputyLanguageServer
from lsprotocol.types import (
DiagnosticSeverity,
Range,
Diagnostic,
Position,
- DidOpenTextDocumentParams,
- DidChangeTextDocumentParams,
FoldingRange,
FoldingRangeParams,
CompletionItem,
@@ -39,6 +42,7 @@ from debputy.lsp.lsp_debian_control_reference_data import (
BINARY_FIELDS,
SOURCE_FIELDS,
DctrlFileMetadata,
+ package_name_to_section,
)
from debputy.lsp.lsp_features import (
lint_diagnostics,
@@ -53,11 +57,13 @@ from debputy.lsp.lsp_generic_deb822 import (
deb822_hover,
deb822_folding_ranges,
deb822_semantic_tokens_full,
+ deb822_token_iter,
)
from debputy.lsp.quickfixes import (
propose_remove_line_quick_fix,
range_compatible_with_remove_line_fix,
propose_correct_text_quick_fix,
+ propose_insert_text_on_line_after_diagnostic_quick_fix,
)
from debputy.lsp.spellchecking import default_spellchecker
from debputy.lsp.text_util import (
@@ -100,123 +106,182 @@ _LANGUAGE_IDS = [
# vim's name
"debcontrol",
]
-_SUBSTVAR_RE = re.compile(r"[$][{][a-zA-Z0-9][a-zA-Z0-9-:]*[}]")
-_SUBSTVARS_DOC = {
- "${}": textwrap.dedent(
- """\
- This is a substvar for a literal `$`. This form will never recurse
- into another substvar. As an example, `${}{binary:Version}` will result
- literal `${binary:Version}` (which will not be replaced).
-
- Defined by: `dpkg-gencontrol`
- DH Sequence: <default>
- Source: <https://manpages.debian.org/deb-substvars.5>
- """
- ),
- "${binary:Version}": textwrap.dedent(
- """\
- The version of the current binary package including binNMU version.
- Often used with `Depends: dep (= ${binary:Version})` relations
- where:
- * The `dep` package is from the same source (listed in the same
- `debian/control` file)
- * The current package and `dep` are both `arch:any` (or both `arch:all`)
- packages.
+@dataclasses.dataclass(slots=True, frozen=True)
+class SubstvarMetadata:
+ name: str
+ defined_by: str
+ dh_sequence: Optional[str]
+ source: Optional[str]
+ description: str
- Defined by: `dpkg-gencontrol`
- DH Sequence: <default>
- Source: <https://manpages.debian.org/deb-substvars.5>
- """
- ),
- "${source:Version}": textwrap.dedent(
- """\
- The version of the current source package excluding binNMU version.
+ def render_metadata_fields(self) -> str:
+ def_by = f"Defined by: {self.defined_by}"
+ dh_seq = (
+ f"DH Sequence: {self.dh_sequence}" if self.dh_sequence is not None else None
+ )
+ source = f"Source: {self.source}" if self.source is not None else None
+ return "\n".join(filter(None, (def_by, dh_seq, source)))
+
+
+def relationship_substvar_for_field(substvar: str) -> Optional[str]:
+ relationship_fields = _relationship_fields()
+ try:
+ col_idx = substvar.rindex(":")
+ except ValueError:
+ return None
+ return relationship_fields.get(substvar[col_idx + 1 : -1].lower())
- Often used with `Depends: dep (= ${source:Version})` relations
- where:
- * The `dep` package is from the same source (listed in the same
- `debian/control` file)
- * The `dep` is `arch:all`.
+def _substvars_metadata(*args: SubstvarMetadata) -> Mapping[str, SubstvarMetadata]:
+ r = {s.name: s for s in args}
+ assert len(r) == len(args)
+ return r
- Defined by: `dpkg-gencontrol`
- DH Sequence: <default>
- Source: <https://manpages.debian.org/deb-substvars.5>
+
+_SUBSTVAR_RE = re.compile(r"[$][{][a-zA-Z0-9][a-zA-Z0-9-:]*[}]")
+_SUBSTVARS_DOC = _substvars_metadata(
+ SubstvarMetadata(
+ "${}",
+ "`dpkg-gencontrol`",
+ "(default)",
+ "<https://manpages.debian.org/deb-substvars.5>",
+ textwrap.dedent(
+ """\
+ This is a substvar for a literal `$`. This form will never recurse
+ into another substvar. As an example, `${}{binary:Version}` will result
+ literal `${binary:Version}` (which will not be replaced).
+ """
+ ),
+ ),
+ SubstvarMetadata(
+ "${binary:Version}",
+ "`dpkg-gencontrol`",
+ "(default)",
+ "<https://manpages.debian.org/deb-substvars.5>",
+ textwrap.dedent(
+ """\
+ The version of the current binary package including binNMU version.
+
+ Often used with `Depends: dep (= ${binary:Version})` relations
+ where:
+
+ * The `dep` package is from the same source (listed in the same
+ `debian/control` file)
+ * The current package and `dep` are both `arch:any` (or both `arch:all`)
+ packages.
"""
+ ),
),
- "${misc:Depends}": textwrap.dedent(
- """\
- Some debhelper commands may make the generated package need to depend on some other packages.
- For example, if you use `dh_installdebconf(1)`, your package will generally need to depend on
- debconf. Or if you use `dh_installxfonts(1)`, your package will generally need to depend on a
- particular version of xutils. Keeping track of these miscellaneous dependencies can be
- annoying since they are dependent on how debhelper does things, so debhelper offers a way to
- automate it.
-
- All commands of this type, besides documenting what dependencies may be needed on their man
- pages, will automatically generate a substvar called ${misc:Depends}. If you put that token
- into your `debian/control` file, it will be expanded to the dependencies debhelper figures
- you need.
-
- This is entirely independent of the standard `${shlibs:Depends}` generated by `dh_makeshlibs(1)`,
- and the `${perl:Depends}` generated by `dh_perl(1)`.
-
- Defined by: `debhelper`
- DH Sequence: <default>
- Source: <https://manpages.debian.org/debhelper.7>
+ SubstvarMetadata(
+ "${source:Version}",
+ "`dpkg-gencontrol`",
+ "(default)",
+ "<https://manpages.debian.org/deb-substvars.5>",
+ textwrap.dedent(
+ """\
+ The version of the current source package excluding binNMU version.
+
+ Often used with `Depends: dep (= ${source:Version})` relations
+ where:
+
+ * The `dep` package is from the same source (listed in the same
+ `debian/control` file)
+ * The `dep` is `arch:all`.
"""
+ ),
),
- "${misc:Pre-Depends}": textwrap.dedent(
- """\
- This is the moral equivalent to `${misc:Depends}` but for `Pre-Depends`.
-
- Defined by: `debhelper`
- DH Sequence: <default>
+ SubstvarMetadata(
+ "${misc:Depends}",
+ "`debhelper`",
+ "(default)",
+ "<https://manpages.debian.org/debhelper.7>",
+ textwrap.dedent(
+ """\
+ Some debhelper commands may make the generated package need to depend on some other packages.
+ For example, if you use `dh_installdebconf(1)`, your package will generally need to depend on
+ debconf. Or if you use `dh_installxfonts(1)`, your package will generally need to depend on a
+ particular version of xutils. Keeping track of these miscellaneous dependencies can be
+ annoying since they are dependent on how debhelper does things, so debhelper offers a way to
+ automate it.
+
+ All commands of this type, besides documenting what dependencies may be needed on their man
+ pages, will automatically generate a substvar called ${misc:Depends}. If you put that token
+ into your `debian/control` file, it will be expanded to the dependencies debhelper figures
+ you need.
+
+ This is entirely independent of the standard `${shlibs:Depends}` generated by `dh_makeshlibs(1)`,
+ and the `${perl:Depends}` generated by `dh_perl(1)`.
"""
+ ),
),
- "${perl:Depends}": textwrap.dedent(
- """\
- The dependency on perl as determined by `dh_perl`. Note this only covers the relationship
- with the Perl interpreter and not perl modules.
-
- Defined by: `dh_perl`
- DH Sequence: <default>
- Source: <https://manpages.debian.org/dh_perl.1>
+ SubstvarMetadata(
+ "${misc:Pre-Depends}",
+ "`debhelper`",
+ "(default)",
+ None,
+ textwrap.dedent(
+ """\
+ This is the moral equivalent to `${misc:Depends}` but for `Pre-Depends`.
"""
+ ),
),
- "${gir:Depends}": textwrap.dedent(
- """\
- Dependencies related to GObject introspection data.
+ SubstvarMetadata(
+ "${perl:Depends}",
+ "`dh_perl`",
+ "(default)",
+ "<https://manpages.debian.org/dh_perl.1>",
+ textwrap.dedent(
+ """\
+ The dependency on perl as determined by `dh_perl`. Note this only covers the relationship
+ with the Perl interpreter and not perl modules.
- Defined by: `dh_girepository`
- DH Sequence: `gir`
- Source: <https://manpages.debian.org/dh_girepository.1>
"""
+ ),
),
- "${shlibs:Depends}": textwrap.dedent(
- """\
- Dependencies related to ELF dependencies.
-
- Defined by: `dpkg-shlibdeps` (often via `dh_shlibdeps`)
- DH Sequence: <default>
- Source: <https://manpages.debian.org/dpkg-shlibdeps.1>
+ SubstvarMetadata(
+ "${gir:Depends}",
+ "`dh_girepository`",
+ "gir",
+ "<https://manpages.debian.org/dh_girepository.1>",
+ textwrap.dedent(
+ """\
+ Dependencies related to GObject introspection data.
"""
+ ),
),
- "${shlibs:Pre-Depends}": textwrap.dedent(
- """\
- Dependencies related to ELF dependencies. The `Pre-Depends`
- version is often only seen in `Essential: yes` packages
- or packages that manually request the `Pre-Depends`
- relation via `dpkg-shlibdeps`.
-
- Defined by: `dpkg-shlibdeps` (often via `dh_shlibdeps`)
- DH Sequence: <default>
- Source: <https://manpages.debian.org/dpkg-shlibdeps.1>
+ SubstvarMetadata(
+ "${shlibs:Depends}",
+ "`dpkg-shlibdeps` (often via `dh_shlibdeps`)",
+ "(default)",
+ "<https://manpages.debian.org/dpkg-shlibdeps.1>",
+ textwrap.dedent(
+ """\
+ Dependencies related to ELF dependencies.
"""
+ ),
),
-}
+ SubstvarMetadata(
+ "${shlibs:Pre-Depends}",
+ "`dpkg-shlibdeps` (often via `dh_shlibdeps`)",
+ "(default)",
+ "<https://manpages.debian.org/dpkg-shlibdeps.1>",
+ textwrap.dedent(
+ """\
+ Dependencies related to ELF dependencies. The `Pre-Depends`
+ version is often only seen in `Essential: yes` packages
+ or packages that manually request the `Pre-Depends`
+ relation via `dpkg-shlibdeps`.
+
+ Note: This substvar only appears in `debhelper-compat (= 14)`, or
+ with use of `debputy` (at an integration level, where `debputy`
+ runs `dpkg-shlibdeps`), or when passing relevant options to
+ `dpkg-shlibdeps` (often via `dh_shlibdeps`) such as `-dPre-Depends`.
+ """
+ ),
+ ),
+)
_DCTRL_FILE_METADATA = DctrlFileMetadata()
@@ -225,9 +290,30 @@ lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_CODE_ACTION)
lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)
+@lru_cache
+def _relationship_fields() -> Mapping[str, str]:
+ # TODO: Pull from `dpkg-dev` when possible fallback only to the static list.
+ return {
+ f.lower(): f
+ for f in (
+ "Pre-Depends",
+ "Depends",
+ "Recommends",
+ "Suggests",
+ "Enhances",
+ "Conflicts",
+ "Breaks",
+ "Replaces",
+ "Provides",
+ "Built-Using",
+ "Static-Built-Using",
+ )
+ }
+
+
@lsp_hover(_LANGUAGE_IDS)
def _debian_control_hover(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: HoverParams,
) -> Optional[Hover]:
return deb822_hover(ls, params, _DCTRL_FILE_METADATA, custom_handler=_custom_hover)
@@ -248,26 +334,40 @@ def _custom_hover(
line_no = server_position.line
line = lines[line_no]
substvar_search_ref = server_position.character
- if line[substvar_search_ref] in ("$", "{"):
- substvar_search_ref += 2
substvar = ""
try:
+ if line and line[substvar_search_ref] in ("$", "{"):
+ substvar_search_ref += 2
substvar_start = line.rindex("${", 0, substvar_search_ref)
substvar_end = line.index("}", substvar_start)
if server_position.character <= substvar_end:
- _info(
- f"Range {substvar_start} <= {server_position.character} <= {substvar_end}"
- )
substvar = line[substvar_start : substvar_end + 1]
- except ValueError:
+ except (ValueError, IndexError):
pass
if substvar == "${}" or _SUBSTVAR_RE.fullmatch(substvar):
- doc = _SUBSTVARS_DOC.get(substvar)
+ substvar_md = _SUBSTVARS_DOC.get(substvar)
+
+ computed_doc = ""
+ for_field = relationship_substvar_for_field(substvar)
+ if for_field:
+ # Leading empty line is intentional!
+ computed_doc = textwrap.dedent(
+ f"""
+ This substvar is a relationship substvar for the field {for_field}.
+ Relationship substvars are automatically added in the field they
+ are named after in `debhelper-compat (= 14)` or later, or with
+ `debputy` (any integration mode after 0.1.21).
+ """
+ )
- if doc is None:
- doc = "No documentation for {substvar}."
- return f"# Substvar `{substvar}`\n\n{doc}"
+ if substvar_md is None:
+ doc = f"No documentation for {substvar}.\n"
+ md_fields = ""
+ else:
+ doc = substvar_md.description
+ md_fields = "\n" + substvar_md.render_metadata_fields()
+ return f"# Substvar `{substvar}`\n\n{doc}{computed_doc}{md_fields}"
if known_field is None or known_field.name != "Description":
return None
@@ -318,7 +418,7 @@ def _custom_hover(
@lsp_completer(_LANGUAGE_IDS)
def _debian_control_completions(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: CompletionParams,
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
return deb822_completer(ls, params, _DCTRL_FILE_METADATA)
@@ -326,37 +426,12 @@ def _debian_control_completions(
@lsp_folding_ranges(_LANGUAGE_IDS)
def _debian_control_folding_ranges(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: FoldingRangeParams,
) -> Optional[Sequence[FoldingRange]]:
return deb822_folding_ranges(ls, params, _DCTRL_FILE_METADATA)
-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 _paragraph_representation_field(
paragraph: Deb822ParagraphElement,
) -> Deb822KeyValuePairElement:
@@ -441,23 +516,32 @@ def _binary_package_checks(
source="debputy",
)
)
- if effective_section != "debian-installer":
- quickfix_data = None
- if section is not None:
- quickfix_data = [
- propose_correct_text_quick_fix(
- f"{component_prefix}debian-installer"
- )
- ]
- diagnostics.append(
- Diagnostic(
- section_range,
- f'The Section should be "{component_prefix}debian-installer" for udebs',
- severity=DiagnosticSeverity.Warning,
- source="debputy",
- data=quickfix_data,
+ guessed_section = "debian-installer"
+ section_diagnostic_rationale = " since it is an udeb"
+ else:
+ guessed_section = package_name_to_section(package_name)
+ section_diagnostic_rationale = " based on the package name"
+ if guessed_section is not None and guessed_section != effective_section:
+ if section is not None:
+ quickfix_data = [
+ propose_correct_text_quick_fix(f"{component_prefix}{guessed_section}")
+ ]
+ else:
+ quickfix_data = [
+ propose_insert_text_on_line_after_diagnostic_quick_fix(
+ f"Section: {component_prefix}{guessed_section}\n"
)
+ ]
+ assert section_range is not None # mypy hint
+ diagnostics.append(
+ Diagnostic(
+ section_range,
+ f'The Section should be "{component_prefix}{guessed_section}"{section_diagnostic_rationale}',
+ severity=DiagnosticSeverity.Warning,
+ source="debputy",
+ data=quickfix_data,
)
+ )
def _diagnostics_for_paragraph(
@@ -513,7 +597,7 @@ def _diagnostics_for_paragraph(
diagnostics,
)
- seen_fields = {}
+ seen_fields: Dict[str, Tuple[str, str, Range, List[Range]]] = {}
for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement):
field_name_token = kvpair.field_token
@@ -621,12 +705,12 @@ def _diagnostics_for_paragraph(
)
if pos:
word_pos_te = TEPosition(0, pos).relative_to(word_pos_te)
- word_range = TERange(
+ word_range_te = TERange(
START_POSITION,
TEPosition(0, endpos - pos),
)
word_range_server_units = te_range_to_lsp(
- TERange.from_position_and_size(word_pos_te, word_range)
+ TERange.from_position_and_size(word_pos_te, word_range_te)
)
word_range = position_codec.range_to_client_units(
lines,
@@ -718,7 +802,7 @@ def _scan_for_syntax_errors_and_token_level_diagnostics(
start_offset,
end_line,
end_offset,
- ) in _deb822_token_iter(deb822_file.iter_tokens()):
+ ) in deb822_token_iter(deb822_file.iter_tokens()):
if token.is_error:
first_error = min(first_error, start_line)
start_pos = Position(
@@ -741,17 +825,17 @@ def _scan_for_syntax_errors_and_token_level_diagnostics(
)
)
elif token.is_comment:
- for word, pos, end_pos in spell_checker.iter_words(token.text):
+ for word, col_pos, end_col_pos in spell_checker.iter_words(token.text):
corrections = spell_checker.provide_corrections_for(word)
if not corrections:
continue
start_pos = Position(
start_line,
- pos,
+ col_pos,
)
end_pos = Position(
start_line,
- end_pos,
+ end_col_pos,
)
word_range = position_codec.range_to_client_units(
lines, Range(start_pos, end_pos)
@@ -820,8 +904,8 @@ def _lint_debian_control(
@lsp_semantic_tokens_full(_LANGUAGE_IDS)
-def _semantic_tokens_full(
- ls: "LanguageServer",
+def _debian_control_semantic_tokens_full(
+ ls: "DebputyLanguageServer",
request: SemanticTokensParams,
) -> Optional[SemanticTokens]:
return deb822_semantic_tokens_full(
diff --git a/src/debputy/lsp/lsp_debian_control_reference_data.py b/src/debputy/lsp/lsp_debian_control_reference_data.py
index e65ab86..898faab 100644
--- a/src/debputy/lsp/lsp_debian_control_reference_data.py
+++ b/src/debputy/lsp/lsp_debian_control_reference_data.py
@@ -22,8 +22,6 @@ from typing import (
)
from debian.debian_support import DpkgArchTable
-from lsprotocol.types import DiagnosticSeverity, Diagnostic, DiagnosticTag, Range
-
from debputy.lsp.quickfixes import (
propose_correct_text_quick_fix,
propose_remove_line_quick_fix,
@@ -56,6 +54,7 @@ from debputy.lsp.vendoring._deb822_repro.tokens import (
Deb822SpaceSeparatorToken,
)
from debputy.util import PKGNAME_REGEX
+from lsprotocol.types import DiagnosticSeverity, Diagnostic, DiagnosticTag, Range
try:
from debputy.lsp.vendoring._deb822_repro.locatable import (
@@ -330,7 +329,7 @@ def all_architectures_and_wildcards(arch2table) -> Iterable[Union[str, Keyword]]
@functools.lru_cache
-def dpkg_arch_and_wildcards() -> FrozenSet[str]:
+def dpkg_arch_and_wildcards() -> FrozenSet[Union[str, Keyword]]:
dpkg_arch_table = DpkgArchTable.load_arch_table()
return frozenset(all_architectures_and_wildcards(dpkg_arch_table._arch2table))
@@ -505,6 +504,180 @@ def _combined_custom_field_check(*checks: CustomFieldCheck) -> CustomFieldCheck:
return _validator
+@dataclasses.dataclass(slots=True, frozen=True)
+class PackageNameSectionRule:
+ section: str
+ check: Callable[[str], bool]
+
+
+def _package_name_section_rule(
+ section: str,
+ check: Union[Callable[[str], bool], re.Pattern],
+ *,
+ confirm_re: Optional[re.Pattern] = None,
+) -> PackageNameSectionRule:
+ if confirm_re is not None:
+ assert callable(check)
+
+ def _impl(v: str) -> bool:
+ return check(v) and confirm_re.search(v)
+
+ elif isinstance(check, re.Pattern):
+
+ def _impl(v: str) -> bool:
+ return check.search(v) is not None
+
+ else:
+ _impl = check
+
+ return PackageNameSectionRule(section, _impl)
+
+
+# rules: order is important (first match wins in case of a conflict)
+_PKGNAME_VS_SECTION_RULES = [
+ _package_name_section_rule("debian-installer", lambda n: n.endswith("-udeb")),
+ _package_name_section_rule("doc", lambda n: n.endswith(("-doc", "-docs"))),
+ _package_name_section_rule("debug", lambda n: n.endswith(("-dbg", "-dbgsym"))),
+ _package_name_section_rule(
+ "httpd",
+ lambda n: n.startswith(("lighttpd-mod", "libapache2-mod-", "libnginx-mod-")),
+ ),
+ _package_name_section_rule("gnustep", lambda n: n.startswith("gnustep-")),
+ _package_name_section_rule(
+ "gnustep",
+ lambda n: n.endswith(
+ (
+ ".framework",
+ ".framework-common",
+ ".tool",
+ ".tool-common",
+ ".app",
+ ".app-common",
+ )
+ ),
+ ),
+ _package_name_section_rule("embedded", lambda n: n.startswith("moblin-")),
+ _package_name_section_rule("javascript", lambda n: n.startswith("node-")),
+ _package_name_section_rule("zope", lambda n: n.startswith(("python-zope", "zope"))),
+ _package_name_section_rule(
+ "python",
+ lambda n: n.startswith(("python-", "python3-")),
+ ),
+ _package_name_section_rule(
+ "gnu-r",
+ lambda n: n.startswith(("r-cran-", "r-bioc-", "r-other-")),
+ ),
+ _package_name_section_rule("editors", lambda n: n.startswith("elpa-")),
+ _package_name_section_rule("lisp", lambda n: n.startswith("cl-")),
+ _package_name_section_rule(
+ "lisp",
+ lambda n: "-elisp-" in n or n.endswith("-elisp"),
+ ),
+ _package_name_section_rule(
+ "lisp",
+ lambda n: n.startswith("lib") and n.endswith("-guile"),
+ ),
+ _package_name_section_rule("lisp", lambda n: n.startswith("guile-")),
+ _package_name_section_rule("golang", lambda n: n.startswith("golang-")),
+ _package_name_section_rule(
+ "perl",
+ lambda n: n.startswith("lib") and n.endswith("-perl"),
+ ),
+ _package_name_section_rule(
+ "cli-mono",
+ lambda n: n.startswith("lib") and n.endswith(("-cil", "-cil-dev")),
+ ),
+ _package_name_section_rule(
+ "java",
+ lambda n: n.startswith("lib") and n.endswith(("-java", "-gcj", "-jni")),
+ ),
+ _package_name_section_rule(
+ "php",
+ lambda n: n.startswith(("libphp", "php")),
+ confirm_re=re.compile(r"^(?:lib)?php(?:\d(?:\.\d)?)?-"),
+ ),
+ _package_name_section_rule(
+ "php", lambda n: n.startswith("lib-") and n.endswith("-php")
+ ),
+ _package_name_section_rule(
+ "haskell",
+ lambda n: n.startswith(("haskell-", "libhugs-", "libghc-", "libghc6-")),
+ ),
+ _package_name_section_rule(
+ "ruby",
+ lambda n: "-ruby" in n,
+ confirm_re=re.compile(r"^lib.*-ruby(?:1\.\d)?$"),
+ ),
+ _package_name_section_rule("ruby", lambda n: n.startswith("ruby-")),
+ _package_name_section_rule(
+ "rust",
+ lambda n: n.startswith("librust-") and n.endswith("-dev"),
+ ),
+ _package_name_section_rule("rust", lambda n: n.startswith("rust-")),
+ _package_name_section_rule(
+ "ocaml",
+ lambda n: n.startswith("lib-") and n.endswith(("-ocaml-dev", "-camlp4-dev")),
+ ),
+ _package_name_section_rule("javascript", lambda n: n.startswith("libjs-")),
+ _package_name_section_rule(
+ "interpreters",
+ lambda n: n.startswith("lib-") and n.endswith(("-tcl", "-lua", "-gst")),
+ ),
+ _package_name_section_rule(
+ "introspection",
+ lambda n: n.startswith("gir-"),
+ confirm_re=re.compile(r"^gir\d+\.\d+-.*-\d+\.\d+$"),
+ ),
+ _package_name_section_rule(
+ "fonts",
+ lambda n: n.startswith(("xfonts-", "fonts-", "ttf-")),
+ ),
+ _package_name_section_rule("admin", lambda n: n.startswith(("libnss-", "libpam-"))),
+ _package_name_section_rule(
+ "localization",
+ lambda n: n.startswith(
+ (
+ "aspell-",
+ "hunspell-",
+ "myspell-",
+ "mythes-",
+ "dict-freedict-",
+ "gcompris-sound-",
+ )
+ ),
+ ),
+ _package_name_section_rule(
+ "localization",
+ lambda n: n.startswith("hypen-"),
+ confirm_re=re.compile(r"^hyphen-[a-z]{2}(?:-[a-z]{2})?$"),
+ ),
+ _package_name_section_rule(
+ "localization",
+ lambda n: "-l10n-" in n or n.endswith("-l10n"),
+ ),
+ _package_name_section_rule("kernel", lambda n: n.endswith(("-dkms", "-firmware"))),
+ _package_name_section_rule(
+ "libdevel",
+ lambda n: n.startswith("lib") and n.endswith(("-dev", "-headers")),
+ ),
+ _package_name_section_rule(
+ "libs",
+ lambda n: n.startswith("lib"),
+ confirm_re=re.compile(r"^lib.*\d[ad]?$"),
+ ),
+]
+
+
+# Fiddling with the package name can cause a lot of changes (diagnostic scans), so we have an upper bound
+# on the cache. The number is currently just taken out of a hat.
+@functools.lru_cache(64)
+def package_name_to_section(name: str) -> Optional[str]:
+ for rule in _PKGNAME_VS_SECTION_RULES:
+ if rule.check(name):
+ return rule.section
+ return None
+
+
class FieldValueClass(Enum):
SINGLE_VALUE = auto(), LIST_SPACE_SEPARATED_INTERPRETATION
SPACE_SEPARATED_LIST = auto(), LIST_SPACE_SEPARATED_INTERPRETATION
@@ -576,6 +749,8 @@ class Deb822KnownField:
unknown_value_diagnostic_severity: Optional[DiagnosticSeverity] = (
DiagnosticSeverity.Error
)
+ # One-line description for space-constrained docs (such as completion docs)
+ synopsis_doc: Optional[str] = None
hover_text: Optional[str] = None
spellcheck_value: bool = False
is_stanza_name: bool = False
@@ -812,6 +987,7 @@ SOURCE_FIELDS = _fields(
custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
missing_field_severity=DiagnosticSeverity.Error,
is_stanza_name=True,
+ synopsis_doc="Name of source package",
hover_text=textwrap.dedent(
"""\
Declares the name of the source package.
@@ -824,6 +1000,7 @@ SOURCE_FIELDS = _fields(
"Standards-Version",
FieldValueClass.SINGLE_VALUE,
missing_field_severity=DiagnosticSeverity.Error,
+ synopsis_doc="Debian Policy version this package complies with",
hover_text=textwrap.dedent(
"""\
Declares the last semantic version of the Debian Policy this package as last checked against.
@@ -843,6 +1020,7 @@ SOURCE_FIELDS = _fields(
FieldValueClass.SINGLE_VALUE,
known_values=ALL_SECTIONS,
unknown_value_diagnostic_severity=DiagnosticSeverity.Warning,
+ synopsis_doc="Default section",
hover_text=textwrap.dedent(
"""\
Define the default section for packages in this source package.
@@ -862,6 +1040,7 @@ SOURCE_FIELDS = _fields(
default_value="optional",
warn_if_default=False,
known_values=ALL_PRIORITIES,
+ synopsis_doc="Default priority",
hover_text=textwrap.dedent(
"""\
Define the default priority for packages in this source package.
@@ -881,6 +1060,7 @@ SOURCE_FIELDS = _fields(
"Maintainer",
FieldValueClass.SINGLE_VALUE,
missing_field_severity=DiagnosticSeverity.Error,
+ synopsis_doc="Name and email of maintainer / maintenance team",
hover_text=textwrap.dedent(
"""\
The maintainer of the package.
@@ -897,6 +1077,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Uploaders",
FieldValueClass.COMMA_SEPARATED_EMAIL_LIST,
+ synopsis_doc="Names and emails of co-maintainers",
hover_text=textwrap.dedent(
"""\
Comma separated list of uploaders associated with the package.
@@ -922,6 +1103,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Browser",
FieldValueClass.SINGLE_VALUE,
+ synopsis_doc="URL for browsers to interact with packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the Version control system repo used for the packaging. The URL should be usable with a
@@ -934,6 +1116,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Git",
FieldValueClass.SPACE_SEPARATED_LIST,
+ synopsis_doc="URL and options for cloning the packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the git repo used for the packaging. The URL should be usable with `git clone`
@@ -952,6 +1135,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Svn",
FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value
+ synopsis_doc="URL for checking out the packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the git repo used for the packaging. The URL should be usable with `svn checkout`
@@ -965,6 +1149,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Arch",
FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value
+ synopsis_doc="URL for checking out the packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the git repo used for the packaging. The URL should be usable for getting a copy of the
@@ -977,6 +1162,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Cvs",
FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value
+ synopsis_doc="URL for checking out the packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the git repo used for the packaging. The URL should be usable for getting a copy of the
@@ -989,6 +1175,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Darcs",
FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value
+ synopsis_doc="URL for checking out the packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the git repo used for the packaging. The URL should be usable for getting a copy of the
@@ -1001,6 +1188,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Hg",
FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value
+ synopsis_doc="URL for checking out the packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the git repo used for the packaging. The URL should be usable for getting a copy of the
@@ -1013,6 +1201,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Vcs-Mtn",
FieldValueClass.SPACE_SEPARATED_LIST, # TODO: Might be a single value
+ synopsis_doc="URL for checking out the packaging VCS",
hover_text=textwrap.dedent(
"""\
URL to the git repo used for the packaging. The URL should be usable for getting a copy of the
@@ -1028,6 +1217,7 @@ SOURCE_FIELDS = _fields(
deprecated_with_no_replacement=True,
default_value="no",
known_values=_allowed_values("yes", "no"),
+ synopsis_doc="**Obsolete**: Old ACL mechanism for Debian Managers",
hover_text=textwrap.dedent(
"""\
Obsolete field
@@ -1044,6 +1234,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Build-Depends",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Dependencies requires for clean and full build actions",
hover_text=textwrap.dedent(
"""\
All minimum build-dependencies for this source package. Needed for any target including **clean**.
@@ -1053,6 +1244,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Build-Depends-Arch",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Dependencies requires for arch:any action (build-arch/binary-arch)",
hover_text=textwrap.dedent(
"""\
Build-dependencies required for building the architecture dependent binary packages of this source
@@ -1068,6 +1260,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Build-Depends-Indep",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Dependencies requires for arch:all action (build-indep/binary-indep)",
hover_text=textwrap.dedent(
"""\
Build-dependencies required for building the architecture independent binary packages of this source
@@ -1083,6 +1276,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Build-Conflicts",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Package versions that will break the build or the clean target (use sparingly)",
hover_text=textwrap.dedent(
"""\
Packages that must **not** be installed during **any** part of the build, including the **clean**
@@ -1097,6 +1291,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Build-Conflicts-Arch",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Package versions that will break an arch:any build (use sparingly)",
hover_text=textwrap.dedent(
"""\
Packages that must **not** be installed during the **build-arch** or **binary-arch** targets.
@@ -1111,6 +1306,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Build-Conflicts-Indep",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Package versions that will break an arch:all build (use sparingly)",
hover_text=textwrap.dedent(
"""\
Packages that must **not** be installed during the **build-indep** or **binary-indep** targets.
@@ -1125,6 +1321,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Testsuite",
FieldValueClass.SPACE_SEPARATED_LIST,
+ synopsis_doc="Announce **autodep8** tests",
hover_text=textwrap.dedent(
"""\
Declares that this package provides or should run install time tests via `autopkgtest`.
@@ -1142,6 +1339,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Homepage",
FieldValueClass.SINGLE_VALUE,
+ synopsis_doc="Upstream homepage",
hover_text=textwrap.dedent(
"""\
Link to the upstream homepage for this source package.
@@ -1196,6 +1394,7 @@ SOURCE_FIELDS = _fields(
),
),
),
+ synopsis_doc="Declare (fake)root requirements for the package",
hover_text=textwrap.dedent(
"""\
Declare if and when the package build assumes it is run as root or fakeroot.
@@ -1225,6 +1424,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Bugs",
FieldValueClass.SINGLE_VALUE,
+ synopsis_doc="Custom bugtracker URL (for third-party packages)",
hover_text=textwrap.dedent(
"""\
Provide a custom bug tracker URL
@@ -1238,6 +1438,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"Origin",
FieldValueClass.SINGLE_VALUE,
+ synopsis_doc="Custom origin (for third-party packages)",
hover_text=textwrap.dedent(
"""\
Declare the origin of the package.
@@ -1252,6 +1453,7 @@ SOURCE_FIELDS = _fields(
"X-Python-Version",
FieldValueClass.COMMA_SEPARATED_LIST,
replaced_by="X-Python3-Version",
+ synopsis_doc="**Obsolete**: Supported Python2 versions (`dh-python` specific)",
hover_text=textwrap.dedent(
"""\
Obsolete field for declaring the supported Python2 versions
@@ -1264,6 +1466,7 @@ SOURCE_FIELDS = _fields(
DctrlKnownField(
"X-Python3-Version",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Supported Python3 versions (`dh-python` specific)",
hover_text=textwrap.dedent(
# Too lazy to provide a better description
"""\
@@ -1278,6 +1481,7 @@ SOURCE_FIELDS = _fields(
"XS-Autobuild",
FieldValueClass.SINGLE_VALUE,
known_values=_allowed_values("yes"),
+ synopsis_doc="Whether this non-free is auto-buildable on buildds",
hover_text=textwrap.dedent(
"""\
Used for non-free packages to denote that they may be auto-build on the Debian build infrastructure
@@ -1291,6 +1495,7 @@ SOURCE_FIELDS = _fields(
"Description",
FieldValueClass.FREE_TEXT_FIELD,
spellcheck_value=True,
+ synopsis_doc="Common base description for all packages via substvar",
hover_text=textwrap.dedent(
"""\
This field contains a human-readable description of the package. However, it is not used directly.
@@ -1343,6 +1548,7 @@ BINARY_FIELDS = _fields(
custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
is_stanza_name=True,
missing_field_severity=DiagnosticSeverity.Error,
+ synopsis_doc="Declares the name of a binary package",
hover_text="Declares the name of a binary package",
),
DctrlKnownField(
@@ -1356,6 +1562,7 @@ BINARY_FIELDS = _fields(
hover_text="The package will be built as a micro-deb (also known as a udeb). These are solely used by the debian-installer.",
),
),
+ synopsis_doc="Non-standard package type (such as `udeb`)",
hover_text=textwrap.dedent(
"""\
**Special-purpose only**. *This field is a special purpose field and is rarely needed.*
@@ -1373,6 +1580,7 @@ BINARY_FIELDS = _fields(
missing_field_severity=DiagnosticSeverity.Error,
unknown_value_diagnostic_severity=None,
known_values=_allowed_values(*dpkg_arch_and_wildcards()),
+ synopsis_doc="Architecture of the package",
hover_text=textwrap.dedent(
"""\
Determines which architectures this package can be compiled for or if it is an architecture-independent
@@ -1424,6 +1632,7 @@ BINARY_FIELDS = _fields(
),
),
),
+ synopsis_doc="Whether the package is essential (Policy term)",
hover_text=textwrap.dedent(
"""\
**Special-purpose only**. *This field is a special purpose field and is rarely needed.*
@@ -1451,6 +1660,7 @@ BINARY_FIELDS = _fields(
FieldValueClass.SINGLE_VALUE,
replaced_by="Protected",
default_value="no",
+ synopsis_doc="**Deprecated**: Use Protected instead",
known_values=_allowed_values(
Keyword(
"yes",
@@ -1469,6 +1679,13 @@ BINARY_FIELDS = _fields(
),
),
),
+ hover_text=textwrap.dedent(
+ """\
+ This is the prototype field that lead to `Protected`, which should be used instead.
+
+ It makes `apt` (but not `dpkg`) require extra confirmation before removing the package.
+ """
+ ),
),
DctrlKnownField(
"Protected",
@@ -1492,10 +1709,24 @@ BINARY_FIELDS = _fields(
),
),
),
+ synopsis_doc="Mark as protected (uninstall protection)",
+ hover_text=textwrap.dedent(
+ """\
+ Declare this package as a potential system critical package. When set to `yes`, both `apt`
+ and `dpkg` will assume that removing the package *may* break the system. As a consequence,
+ they will require extra confirmation (or "force" options) before removing the package.
+
+ This field basically provides a "uninstall" protection similar to that of `Essential` packages
+ without the other benefits and requirements that comes with `Essential` packages. This option
+ is generally applicable to packages like bootloaders, kernels, and other packages that might
+ be necessary for booting the system.
+ """
+ ),
),
DctrlKnownField(
"Pre-Depends",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Very strong dependencies; prefer Depends when applicable",
hover_text=textwrap.dedent(
"""\
**Advanced field**. *This field covers an advanced topic. If you are new to packaging, you are*
@@ -1522,6 +1753,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Depends",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Dependencies required to install and use this package",
hover_text=textwrap.dedent(
"""\
Lists the packages that must be installed, before this package is installed.
@@ -1550,6 +1782,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Recommends",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Optional dependencies **most** people should have",
hover_text=textwrap.dedent(
"""\
Lists the packages that *should* be installed when this package is installed in all but
@@ -1573,6 +1806,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Suggests",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Optional dependencies that some people might want",
hover_text=textwrap.dedent(
"""\
Lists the packages that may make this package more useful but not installing them is perfectly
@@ -1589,6 +1823,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Enhances",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Packages enhanced by installing this package",
hover_text=textwrap.dedent(
"""\
This field is similar to Suggests but works in the opposite direction. It is used to declare that
@@ -1606,6 +1841,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Provides",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Additional packages/versions this package dependency-wise satisfy",
hover_text=textwrap.dedent(
"""\
Declare this package also provide one or more other packages. This means that this package can
@@ -1648,6 +1884,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Conflicts",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Packages that this package is not co-installable with",
hover_text=textwrap.dedent(
"""\
**Warning**: *You may be looking for Breaks instead of Conflicts*.
@@ -1675,6 +1912,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Breaks",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="Package/versions that does not work with this package",
hover_text=textwrap.dedent(
"""\
This package cannot be installed together with the packages listed in the `Breaks` field.
@@ -1719,6 +1957,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Replaces",
FieldValueClass.COMMA_SEPARATED_LIST,
+ synopsis_doc="This package replaces content from these packages/versions",
hover_text=textwrap.dedent(
"""\
This package either replaces another package or overwrites files that used to be provided by
@@ -1745,6 +1984,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Build-Profiles",
FieldValueClass.BUILD_PROFILES_LIST,
+ synopsis_doc="Conditionally build this package",
hover_text=textwrap.dedent(
"""\
**Advanced field**. *This field covers an advanced topic. If you are new to packaging, you are*
@@ -1780,6 +2020,7 @@ BINARY_FIELDS = _fields(
inherits_from_source=True,
known_values=ALL_SECTIONS,
unknown_value_diagnostic_severity=DiagnosticSeverity.Warning,
+ synopsis_doc="Which section this package should be in",
hover_text=textwrap.dedent(
"""\
Define the section for this package.
@@ -1801,6 +2042,7 @@ BINARY_FIELDS = _fields(
missing_field_severity=DiagnosticSeverity.Error,
inherits_from_source=True,
known_values=ALL_PRIORITIES,
+ synopsis_doc="The package's priority (Policy term)",
hover_text=textwrap.dedent(
"""\
Define the priority this package.
@@ -1892,6 +2134,7 @@ BINARY_FIELDS = _fields(
),
),
),
+ synopsis_doc="**Advanced field**: How this package interacts with multi arch",
hover_text=textwrap.dedent(
"""\
**Advanced field**. *This field covers an advanced topic. If you are new to packaging, you are*
@@ -1921,7 +2164,8 @@ BINARY_FIELDS = _fields(
* If you have an architecture dependent package, where everything is installed in
`/usr/lib/${DEB_HOST_MULTIARCH}` (plus a bit of standard documentation in `/usr/share/doc`), then
- you *probably* want `Multi-Arch: same`
+ you *probably* want `Multi-Arch: same`. Note that `debputy` automatically detects the most common
+ variants of this case and sets the field for you.
* If none of the above applies, then omit the field unless you know what you are doing or you are
receiving advice from a Multi-Arch expert.
@@ -2001,6 +2245,7 @@ BINARY_FIELDS = _fields(
_udeb_only_field_validation,
_each_value_match_regex_validation(re.compile(r"^[1-9]\d{3,4}$")),
),
+ synopsis_doc="(udeb-only) Package's order in the d-i menu",
hover_text=textwrap.dedent(
"""\
This field is only relevant for `udeb` packages (debian-installer).
@@ -2034,6 +2279,7 @@ BINARY_FIELDS = _fields(
hover_text="The package should be compiled for `DEB_TARGET_ARCH`.",
),
),
+ synopsis_doc="(Special purpose) For cross-compiling cross-compilers",
hover_text=textwrap.dedent(
"""\
**Special-purpose only**. *This field is a special purpose field and is rarely needed.*
@@ -2064,6 +2310,7 @@ BINARY_FIELDS = _fields(
"X-Time64-Compat",
FieldValueClass.SINGLE_VALUE,
custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
+ synopsis_doc="(Special purpose) Compat name for time64_t transition",
hover_text=textwrap.dedent(
"""\
Special purpose field related to the 64-bit time transition.
@@ -2077,6 +2324,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"Homepage",
FieldValueClass.SINGLE_VALUE,
+ synopsis_doc="(Special purpose) Upstream homepage URL for this binary package",
hover_text=textwrap.dedent(
"""\
Link to the upstream homepage for this binary package.
@@ -2095,6 +2343,7 @@ BINARY_FIELDS = _fields(
spellcheck_value=True,
# It will build just fine. But no one will know what it is for, so it probably won't be installed
missing_field_severity=DiagnosticSeverity.Warning,
+ synopsis_doc="Package synopsis and description",
hover_text=textwrap.dedent(
"""\
A human-readable description of the package. This field consists of two related but distinct parts.
@@ -2140,6 +2389,7 @@ BINARY_FIELDS = _fields(
"XB-Cnf-Visible-Pkgname",
FieldValueClass.SINGLE_VALUE,
custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX),
+ synopsis_doc="(Special purpose) Hint for `command-not-found`",
hover_text=textwrap.dedent(
"""\
**Special-case field**: *This field is only useful in very special circumstances.*
@@ -2168,6 +2418,7 @@ BINARY_FIELDS = _fields(
DctrlKnownField(
"X-DhRuby-Root",
FieldValueClass.SINGLE_VALUE,
+ synopsis_doc="For multi-binary layout with `dh_ruby`",
hover_text=textwrap.dedent(
"""\
Used by `dh_ruby` to request "multi-binary" layout and where the root for the given
@@ -2624,6 +2875,7 @@ _DTESTSCTRL_FIELDS = _fields(
FieldValueClass.SPACE_SEPARATED_LIST,
unknown_value_diagnostic_severity=None,
known_values=_allowed_values(*dpkg_arch_and_wildcards()),
+ synopsis_doc="Only run these tests on specific architectures",
hover_text=textwrap.dedent(
"""\
When package tests are only supported on a limited set of
@@ -2641,6 +2893,7 @@ _DTESTSCTRL_FIELDS = _fields(
Deb822KnownField(
"Classes",
FieldValueClass.FREE_TEXT_FIELD,
+ synopsis_doc="Hardware related tagging",
hover_text=textwrap.dedent(
"""\
Most package tests should work in a minimal environment and are
@@ -2663,6 +2916,7 @@ _DTESTSCTRL_FIELDS = _fields(
"Depends",
FieldValueClass.COMMA_SEPARATED_LIST,
default_value="@",
+ synopsis_doc="Dependencies for running the tests",
hover_text="""\
Declares that the specified packages must be installed for the test
to go ahead. This supports all features of dpkg dependencies, including
@@ -3019,6 +3273,7 @@ _DTESTSCTRL_FIELDS = _fields(
),
),
),
+ synopsis_doc="Test restrictions and requirements",
hover_text=textwrap.dedent(
"""\
Declares some restrictions or problems with the tests defined in
@@ -3035,6 +3290,7 @@ _DTESTSCTRL_FIELDS = _fields(
Deb822KnownField(
"Tests",
FieldValueClass.COMMA_OR_SPACE_SEPARATED_LIST,
+ synopsis_doc="List of test scripts to run",
hover_text=textwrap.dedent(
"""\
This field names the tests which are defined by this stanza, and map
@@ -3051,6 +3307,7 @@ _DTESTSCTRL_FIELDS = _fields(
Deb822KnownField(
"Test-Command",
FieldValueClass.FREE_TEXT_FIELD,
+ synopsis_doc="Single test command",
hover_text=textwrap.dedent(
"""\
If your test only contains a shell command or two, or you want to
@@ -3069,6 +3326,8 @@ _DTESTSCTRL_FIELDS = _fields(
Deb822KnownField(
"Test-Directory",
FieldValueClass.FREE_TEXT_FIELD, # TODO: Single path
+ default_value="debian/tests",
+ synopsis_doc="The directory containing the tests listed in from `Tests`",
hover_text=textwrap.dedent(
"""\
Replaces the path segment `debian/tests` in the filenames of the
@@ -3190,7 +3449,9 @@ _DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata("Tests", _DTESTSCTRL_FIELDS)
class Dep5FileMetadata(Deb822FileMetadata[Dep5StanzaMetadata]):
- def classify_stanza(self, stanza: Deb822ParagraphElement, stanza_idx: int) -> S:
+ def classify_stanza(
+ self, stanza: Deb822ParagraphElement, stanza_idx: int
+ ) -> Dep5StanzaMetadata:
if stanza_idx == 0:
return _DEP5_HEADER_STANZA
if stanza_idx > 0:
@@ -3199,19 +3460,19 @@ class Dep5FileMetadata(Deb822FileMetadata[Dep5StanzaMetadata]):
return _DEP5_LICENSE_STANZA
raise ValueError("The stanza_idx must be 0 or greater")
- def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S:
+ def guess_stanza_classification_by_idx(self, stanza_idx: int) -> Dep5StanzaMetadata:
if stanza_idx == 0:
return _DEP5_HEADER_STANZA
if stanza_idx > 0:
return _DEP5_FILES_STANZA
raise ValueError("The stanza_idx must be 0 or greater")
- def stanza_types(self) -> Iterable[S]:
+ def stanza_types(self) -> Iterable[Dep5StanzaMetadata]:
yield _DEP5_HEADER_STANZA
yield _DEP5_FILES_STANZA
yield _DEP5_LICENSE_STANZA
- def __getitem__(self, item: str) -> S:
+ def __getitem__(self, item: str) -> Dep5StanzaMetadata:
if item == "Header":
return _DEP5_FILES_STANZA
if item == "Files":
@@ -3222,18 +3483,20 @@ class Dep5FileMetadata(Deb822FileMetadata[Dep5StanzaMetadata]):
class DctrlFileMetadata(Deb822FileMetadata[DctrlStanzaMetadata]):
- def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S:
+ def guess_stanza_classification_by_idx(
+ self, stanza_idx: int
+ ) -> DctrlStanzaMetadata:
if stanza_idx == 0:
return _DCTRL_SOURCE_STANZA
if stanza_idx > 0:
return _DCTRL_PACKAGE_STANZA
raise ValueError("The stanza_idx must be 0 or greater")
- def stanza_types(self) -> Iterable[S]:
+ def stanza_types(self) -> Iterable[DctrlStanzaMetadata]:
yield _DCTRL_SOURCE_STANZA
yield _DCTRL_PACKAGE_STANZA
- def __getitem__(self, item: str) -> S:
+ def __getitem__(self, item: str) -> DctrlStanzaMetadata:
if item == "Source":
return _DCTRL_SOURCE_STANZA
if item == "Package":
diff --git a/src/debputy/lsp/lsp_debian_copyright.py b/src/debputy/lsp/lsp_debian_copyright.py
index b21cc79..b037792 100644
--- a/src/debputy/lsp/lsp_debian_copyright.py
+++ b/src/debputy/lsp/lsp_debian_copyright.py
@@ -8,8 +8,10 @@ from typing import (
Iterable,
Mapping,
List,
+ Dict,
)
+from debputy.lsp.debputy_ls import DebputyLanguageServer
from lsprotocol.types import (
DiagnosticSeverity,
Range,
@@ -51,6 +53,7 @@ from debputy.lsp.lsp_generic_deb822 import (
deb822_hover,
deb822_folding_ranges,
deb822_semantic_tokens_full,
+ deb822_token_iter,
)
from debputy.lsp.quickfixes import (
propose_correct_text_quick_fix,
@@ -105,7 +108,7 @@ lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)
@lsp_hover(_LANGUAGE_IDS)
def _debian_copyright_hover(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: HoverParams,
) -> Optional[Hover]:
return deb822_hover(ls, params, _DEP5_FILE_METADATA)
@@ -113,7 +116,7 @@ def _debian_copyright_hover(
@lsp_completer(_LANGUAGE_IDS)
def _debian_copyright_completions(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: CompletionParams,
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
return deb822_completer(ls, params, _DEP5_FILE_METADATA)
@@ -121,37 +124,12 @@ def _debian_copyright_completions(
@lsp_folding_ranges(_LANGUAGE_IDS)
def _debian_copyright_folding_ranges(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: FoldingRangeParams,
) -> Optional[Sequence[FoldingRange]]:
return deb822_folding_ranges(ls, params, _DEP5_FILE_METADATA)
-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 _paragraph_representation_field(
paragraph: Deb822ParagraphElement,
) -> Deb822KeyValuePairElement:
@@ -196,7 +174,7 @@ def _diagnostics_for_paragraph(
)
)
- seen_fields = {}
+ seen_fields: Dict[str, Tuple[str, str, Range, List[Range]]] = {}
for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement):
field_name_token = kvpair.field_token
@@ -306,12 +284,12 @@ def _diagnostics_for_paragraph(
)
if pos:
word_pos_te = TEPosition(0, pos).relative_to(word_pos_te)
- word_range = TERange(
+ word_range_te = TERange(
START_POSITION,
TEPosition(0, endpos - pos),
)
word_range_server_units = te_range_to_lsp(
- TERange.from_position_and_size(word_pos_te, word_range)
+ TERange.from_position_and_size(word_pos_te, word_range_te)
)
word_range = position_codec.range_to_client_units(
lines,
@@ -387,7 +365,7 @@ def _scan_for_syntax_errors_and_token_level_diagnostics(
start_offset,
end_line,
end_offset,
- ) in _deb822_token_iter(deb822_file.iter_tokens()):
+ ) in deb822_token_iter(deb822_file.iter_tokens()):
if token.is_error:
first_error = min(first_error, start_line)
start_pos = Position(
@@ -444,7 +422,7 @@ def _lint_debian_copyright(
lines = lint_state.lines
position_codec = lint_state.position_codec
doc_reference = lint_state.doc_uri
- diagnostics = []
+ diagnostics: List[Diagnostic] = []
deb822_file = parse_deb822_file(
lines,
accept_files_with_duplicated_fields=True,
@@ -494,8 +472,8 @@ def _lint_debian_copyright(
@lsp_semantic_tokens_full(_LANGUAGE_IDS)
-def _semantic_tokens_full(
- ls: "LanguageServer",
+def _debian_copyright_semantic_tokens_full(
+ ls: "DebputyLanguageServer",
request: SemanticTokensParams,
) -> Optional[SemanticTokens]:
return deb822_semantic_tokens_full(
diff --git a/src/debputy/lsp/lsp_debian_debputy_manifest.py b/src/debputy/lsp/lsp_debian_debputy_manifest.py
index 03581be..74b5d7b 100644
--- a/src/debputy/lsp/lsp_debian_debputy_manifest.py
+++ b/src/debputy/lsp/lsp_debian_debputy_manifest.py
@@ -134,7 +134,7 @@ def _lint_debian_debputy_manifest(
path = lint_state.path
if not is_valid_file(path):
return None
- diagnostics = []
+ diagnostics: List[Diagnostic] = []
try:
content = MANIFEST_YAML.load("".join(lines))
except MarkedYAMLError as e:
@@ -922,7 +922,7 @@ def debputy_manifest_hover(
)
if km is None:
_info("No keyword match")
- return
+ return None
parser, plugin_metadata, at_depth_idx = km
_info(f"Match leaf parser {at_depth_idx}/{len(segments)} -- {parser.__class__}")
hover_doc_text = resolve_hover_text(
@@ -1020,19 +1020,14 @@ def resolve_hover_text(
return hover_doc_text
-def _hover_doc(ls: "LanguageServer", hover_doc_text: Optional[str]) -> Optional[Hover]:
+def _hover_doc(
+ ls: "DebputyLanguageServer", hover_doc_text: Optional[str]
+) -> Optional[Hover]:
if hover_doc_text is None:
return None
- try:
- supported_formats = ls.client_capabilities.text_document.hover.content_format
- except AttributeError:
- supported_formats = []
- markup_kind = MarkupKind.Markdown
- if markup_kind not in supported_formats:
- markup_kind = MarkupKind.PlainText
return Hover(
contents=MarkupContent(
- kind=markup_kind,
+ kind=ls.hover_markup_format(MarkupKind.Markdown, MarkupKind.PlainText),
value=hover_doc_text,
),
)
diff --git a/src/debputy/lsp/lsp_debian_rules.py b/src/debputy/lsp/lsp_debian_rules.py
index b44fad4..7f5aef9 100644
--- a/src/debputy/lsp/lsp_debian_rules.py
+++ b/src/debputy/lsp/lsp_debian_rules.py
@@ -12,6 +12,7 @@ from typing import (
List,
Iterator,
Tuple,
+ Set,
)
from lsprotocol.types import (
@@ -238,7 +239,7 @@ def _lint_debian_rules_impl(
source_root = os.path.dirname(os.path.dirname(path))
if source_root == "":
source_root = "."
- diagnostics = []
+ diagnostics: List[Diagnostic] = []
make_error = _run_make_dryrun(source_root, lines)
if make_error is not None:
@@ -316,7 +317,7 @@ def _lint_debian_rules_impl(
def _all_dh_commands(source_root: str, lines: List[str]) -> Optional[Sequence[str]]:
- drules_sequences = set()
+ drules_sequences: Set[str] = set()
parse_drules_for_addons(lines, drules_sequences)
cmd = ["dh_assistant", "list-commands", "--output-format=json"]
if drules_sequences:
@@ -369,6 +370,8 @@ def _debian_rules_completions(
source_root = os.path.dirname(os.path.dirname(doc.path))
all_commands = _all_dh_commands(source_root, lines)
+ if all_commands is None:
+ return None
items = [CompletionItem(ht) for c in all_commands for ht in _as_hook_targets(c)]
return items
diff --git a/src/debputy/lsp/lsp_debian_tests_control.py b/src/debputy/lsp/lsp_debian_tests_control.py
index 27221f6..cc27579 100644
--- a/src/debputy/lsp/lsp_debian_tests_control.py
+++ b/src/debputy/lsp/lsp_debian_tests_control.py
@@ -8,8 +8,11 @@ from typing import (
Iterable,
Mapping,
List,
+ Set,
+ Dict,
)
+from debputy.lsp.debputy_ls import DebputyLanguageServer
from lsprotocol.types import (
DiagnosticSeverity,
Range,
@@ -49,6 +52,7 @@ from debputy.lsp.lsp_generic_deb822 import (
deb822_hover,
deb822_folding_ranges,
deb822_semantic_tokens_full,
+ deb822_token_iter,
)
from debputy.lsp.quickfixes import (
propose_correct_text_quick_fix,
@@ -103,7 +107,7 @@ lsp_standard_handler(_LANGUAGE_IDS, TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL)
@lsp_hover(_LANGUAGE_IDS)
def debian_tests_control_hover(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: HoverParams,
) -> Optional[Hover]:
return deb822_hover(ls, params, _DEP5_FILE_METADATA)
@@ -111,7 +115,7 @@ def debian_tests_control_hover(
@lsp_completer(_LANGUAGE_IDS)
def debian_tests_control_completions(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: CompletionParams,
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
return deb822_completer(ls, params, _DEP5_FILE_METADATA)
@@ -119,37 +123,12 @@ def debian_tests_control_completions(
@lsp_folding_ranges(_LANGUAGE_IDS)
def debian_tests_control_folding_ranges(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: FoldingRangeParams,
) -> Optional[Sequence[FoldingRange]]:
return deb822_folding_ranges(ls, params, _DEP5_FILE_METADATA)
-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 _paragraph_representation_field(
paragraph: Deb822ParagraphElement,
) -> Deb822KeyValuePairElement:
@@ -211,7 +190,7 @@ def _diagnostics_for_paragraph(
)
)
- seen_fields = {}
+ seen_fields: Dict[str, Tuple[str, str, Range, List[Range]]] = {}
for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement):
field_name_token = kvpair.field_token
@@ -384,7 +363,7 @@ def _scan_for_syntax_errors_and_token_level_diagnostics(
start_offset,
end_line,
end_offset,
- ) in _deb822_token_iter(deb822_file.iter_tokens()):
+ ) in deb822_token_iter(deb822_file.iter_tokens()):
if token.is_error:
first_error = min(first_error, start_line)
start_pos = Position(
@@ -441,7 +420,7 @@ def _lint_debian_tests_control(
lines = lint_state.lines
position_codec = lint_state.position_codec
doc_reference = lint_state.doc_uri
- diagnostics = []
+ diagnostics: List[Diagnostic] = []
deb822_file = parse_deb822_file(
lines,
accept_files_with_duplicated_fields=True,
@@ -475,8 +454,8 @@ def _lint_debian_tests_control(
@lsp_semantic_tokens_full(_LANGUAGE_IDS)
-def _semantic_tokens_full(
- ls: "LanguageServer",
+def _debian_tests_control_semantic_tokens_full(
+ ls: "DebputyLanguageServer",
request: SemanticTokensParams,
) -> Optional[SemanticTokens]:
return deb822_semantic_tokens_full(
diff --git a/src/debputy/lsp/lsp_dispatch.py b/src/debputy/lsp/lsp_dispatch.py
index b63f30c..5d09a44 100644
--- a/src/debputy/lsp/lsp_dispatch.py
+++ b/src/debputy/lsp/lsp_dispatch.py
@@ -10,6 +10,7 @@ from typing import (
Mapping,
List,
Tuple,
+ Literal,
)
from lsprotocol.types import (
@@ -75,21 +76,22 @@ def is_doc_at_version(uri: str, version: int) -> bool:
return dv == version
-def determine_language_id(doc: "TextDocument") -> Tuple[str, str]:
- lang_id = doc.language_id
- if lang_id and not lang_id.isspace():
- return "declared", lang_id
- path = doc.path
- try:
- last_idx = path.rindex("debian/")
- except ValueError:
- return "filename", os.path.basename(path)
- guess_language_id = path[last_idx:]
- return "filename", guess_language_id
+@DEBPUTY_LANGUAGE_SERVER.feature(TEXT_DOCUMENT_DID_OPEN)
+async def _open_document(
+ ls: "DebputyLanguageServer",
+ params: DidChangeTextDocumentParams,
+) -> None:
+ await _open_or_changed_document(ls, params)
-@DEBPUTY_LANGUAGE_SERVER.feature(TEXT_DOCUMENT_DID_OPEN)
@DEBPUTY_LANGUAGE_SERVER.feature(TEXT_DOCUMENT_DID_CHANGE)
+async def _changed_document(
+ ls: "DebputyLanguageServer",
+ params: DidChangeTextDocumentParams,
+) -> None:
+ await _open_or_changed_document(ls, params)
+
+
async def _open_or_changed_document(
ls: "DebputyLanguageServer",
params: Union[DidOpenTextDocumentParams, DidChangeTextDocumentParams],
@@ -99,7 +101,7 @@ async def _open_or_changed_document(
doc = ls.workspace.get_text_document(doc_uri)
_DOCUMENT_VERSION_TABLE[doc_uri] = version
- id_source, language_id = determine_language_id(doc)
+ id_source, language_id = ls.determine_language_id(doc)
handler = DIAGNOSTIC_HANDLERS.get(language_id)
if handler is None:
_info(
@@ -214,7 +216,7 @@ def _dispatch_standard_handler(
) -> R:
doc = ls.workspace.get_text_document(doc_uri)
- id_source, language_id = determine_language_id(doc)
+ id_source, language_id = ls.determine_language_id(doc)
handler = handler_table.get(language_id)
if handler is None:
_info(
diff --git a/src/debputy/lsp/lsp_features.py b/src/debputy/lsp/lsp_features.py
index 7a1110d..e7b4445 100644
--- a/src/debputy/lsp/lsp_features.py
+++ b/src/debputy/lsp/lsp_features.py
@@ -1,7 +1,16 @@
import collections
import inspect
import sys
-from typing import Callable, TypeVar, Sequence, Union, Dict, List, Optional
+from typing import (
+ Callable,
+ TypeVar,
+ Sequence,
+ Union,
+ Dict,
+ List,
+ Optional,
+ AsyncIterator,
+)
from lsprotocol.types import (
TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
@@ -29,14 +38,23 @@ 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_types=["keyword", "enumMember", "comment"],
token_modifiers=[],
)
SEMANTIC_TOKEN_TYPES_IDS = {
t: idx for idx, t in enumerate(SEMANTIC_TOKENS_LEGEND.token_types)
}
-DIAGNOSTIC_HANDLERS = {}
+DIAGNOSTIC_HANDLERS: Dict[
+ str,
+ Callable[
+ [
+ "DebputyLanguageServer",
+ Union["DidOpenTextDocumentParams", "DidChangeTextDocumentParams"],
+ ],
+ AsyncIterator[Optional[List[Diagnostic]]],
+ ],
+] = {}
COMPLETER_HANDLERS = {}
HOVER_HANDLERS = {}
CODE_ACTION_HANDLERS = {}
diff --git a/src/debputy/lsp/lsp_generic_deb822.py b/src/debputy/lsp/lsp_generic_deb822.py
index ec7b979..e2124e4 100644
--- a/src/debputy/lsp/lsp_generic_deb822.py
+++ b/src/debputy/lsp/lsp_generic_deb822.py
@@ -1,3 +1,4 @@
+import dataclasses
import re
from typing import (
Optional,
@@ -13,6 +14,7 @@ from typing import (
Callable,
)
+from debputy.lsp.debputy_ls import DebputyLanguageServer
from debputy.lsp.lsp_debian_control_reference_data import (
Deb822FileMetadata,
Deb822KnownField,
@@ -22,11 +24,13 @@ from debputy.lsp.lsp_debian_control_reference_data import (
S,
)
from debputy.lsp.lsp_features import SEMANTIC_TOKEN_TYPES_IDS
-from debputy.lsp.text_util import normalize_dctrl_field_name
+from debputy.lsp.text_util import normalize_dctrl_field_name, te_position_to_lsp
from debputy.lsp.vendoring._deb822_repro import parse_deb822_file
from debputy.lsp.vendoring._deb822_repro.parsing import (
Deb822KeyValuePairElement,
LIST_SPACE_SEPARATED_INTERPRETATION,
+ Deb822ParagraphElement,
+ Deb822ValueLineElement,
)
from debputy.lsp.vendoring._deb822_repro.tokens import tokenize_deb822_file, Deb822Token
from debputy.util import _info
@@ -64,7 +68,7 @@ def _at_cursor(
) -> Tuple[Position, Optional[str], str, bool, int, Set[str]]:
paragraph_no = -1
paragraph_started = False
- seen_fields = set()
+ seen_fields: Set[str] = set()
last_field_seen: Optional[str] = None
current_field: Optional[str] = None
server_position = doc.position_codec.position_from_client_units(
@@ -116,7 +120,7 @@ def _at_cursor(
def deb822_completer(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: CompletionParams,
file_metadata: Deb822FileMetadata[Any],
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
@@ -142,6 +146,7 @@ def deb822_completer(
else:
_info("Completing field name")
items = _complete_field_name(
+ ls,
stanza_metadata,
seen_fields,
)
@@ -152,7 +157,7 @@ def deb822_completer(
def deb822_hover(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: HoverParams,
file_metadata: Deb822FileMetadata[S],
*,
@@ -170,7 +175,7 @@ def deb822_hover(
Optional[Hover],
]
] = None,
-) -> Optional[Union[Hover, str]]:
+) -> Optional[Hover]:
doc = ls.workspace.get_text_document(params.text_document.uri)
lines = doc.lines
server_pos, current_field, word_at_position, in_value, paragraph_no, _ = _at_cursor(
@@ -220,27 +225,17 @@ def deb822_hover(
if hover_text is None:
return None
-
- try:
- supported_formats = ls.client_capabilities.text_document.hover.content_format
- except AttributeError:
- supported_formats = []
-
- _info(f"Supported formats {supported_formats}")
- markup_kind = MarkupKind.Markdown
- if markup_kind not in supported_formats:
- markup_kind = MarkupKind.PlainText
return Hover(
contents=MarkupContent(
- kind=markup_kind,
+ kind=ls.hover_markup_format(MarkupKind.Markdown, MarkupKind.PlainText),
value=hover_text,
)
)
-def _deb822_token_iter(
+def deb822_token_iter(
tokens: Iterable[Deb822Token],
-) -> Iterator[Tuple[Deb822Token, int, int, int, int, int]]:
+) -> Iterator[Tuple[Deb822Token, int, int, int, int]]:
line_no = 0
line_offset = 0
@@ -264,7 +259,7 @@ def _deb822_token_iter(
def deb822_folding_ranges(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
params: FoldingRangeParams,
# Unused for now: might be relevant for supporting folding for some fields
_file_metadata: Deb822FileMetadata[Any],
@@ -278,7 +273,7 @@ def deb822_folding_ranges(
start_offset,
end_line,
end_offset,
- ) in _deb822_token_iter(tokenize_deb822_file(doc.lines)):
+ ) in deb822_token_iter(tokenize_deb822_file(doc.lines)):
if token.is_comment:
if comment_start < 0:
comment_start = start_line
@@ -295,90 +290,170 @@ def deb822_folding_ranges(
return folding_ranges
+@dataclasses.dataclass(slots=True)
+class SemanticTokenState:
+ ls: "DebputyLanguageServer"
+ file_metadata: Deb822FileMetadata[Any]
+ doc: "TextDocument"
+ lines: List[str]
+ tokens: List[int]
+ keyword_token_code: int
+ known_value_token_code: int
+ comment_token_code: int
+ _previous_line: int = 0
+ _previous_col: int = 0
+
+ def emit_token(
+ self,
+ start_pos: Position,
+ len_client_units: int,
+ token_code: int,
+ *,
+ token_modifiers: int = 0,
+ ) -> None:
+ line_delta = start_pos.line - self._previous_line
+ self._previous_line = start_pos.line
+ previous_col = self._previous_col
+
+ if line_delta:
+ previous_col = 0
+
+ column_delta = start_pos.character - previous_col
+ self._previous_col = start_pos.character
+
+ tokens = self.tokens
+ tokens.append(line_delta) # Line delta
+ tokens.append(column_delta) # Token column delta
+ tokens.append(len_client_units) # Token length
+ tokens.append(token_code)
+ tokens.append(token_modifiers)
+
+
+def _deb822_paragraph_semantic_tokens_full(
+ sem_token_state: SemanticTokenState,
+ stanza: Deb822ParagraphElement,
+ stanza_idx: int,
+) -> None:
+ doc = sem_token_state.doc
+ keyword_token_code = sem_token_state.keyword_token_code
+ known_value_token_code = sem_token_state.known_value_token_code
+ comment_token_code = sem_token_state.comment_token_code
+
+ stanza_position = stanza.position_in_file()
+ stanza_metadata = sem_token_state.file_metadata.classify_stanza(
+ stanza,
+ stanza_idx=stanza_idx,
+ )
+ for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement):
+ field_start = kvpair.key_position_in_stanza().relative_to(stanza_position)
+ comment = kvpair.comment_element
+ if comment:
+ comment_start_line = field_start.line_position - len(comment)
+ for comment_line_no, comment_token in enumerate(
+ comment.iter_parts(),
+ start=comment_start_line,
+ ):
+ assert comment_token.is_comment
+ assert isinstance(comment_token, Deb822Token)
+ sem_token_state.emit_token(
+ Position(comment_line_no, 0),
+ len(comment_token.text.rstrip()),
+ comment_token_code,
+ )
+ field_size = doc.position_codec.client_num_units(kvpair.field_name)
+
+ sem_token_state.emit_token(
+ te_position_to_lsp(field_start),
+ field_size,
+ keyword_token_code,
+ )
+
+ known_field: Optional[Deb822KnownField] = stanza_metadata.get(kvpair.field_name)
+ if known_field is not None:
+ if known_field.spellcheck_value:
+ continue
+ known_values: Container[str] = known_field.known_values or frozenset()
+ interpretation = known_field.field_value_class.interpreter()
+ else:
+ known_values = frozenset()
+ interpretation = None
+
+ value_element_pos = kvpair.value_position_in_stanza().relative_to(
+ stanza_position
+ )
+ if interpretation is None:
+ # TODO: Emit tokens for value comments of unknown fields.
+ continue
+ else:
+ parts = kvpair.interpret_as(interpretation).iter_parts()
+ for te in parts:
+ if te.is_whitespace:
+ continue
+ if te.is_separator:
+ continue
+ value_range_in_parent_te = te.range_in_parent()
+ value_range_te = value_range_in_parent_te.relative_to(value_element_pos)
+ value = te.convert_to_text()
+ if te.is_comment:
+ token_type = comment_token_code
+ value = value.rstrip()
+ elif value in known_values:
+ token_type = known_value_token_code
+ else:
+ continue
+ value_len = doc.position_codec.client_num_units(value)
+
+ sem_token_state.emit_token(
+ te_position_to_lsp(value_range_te.start_pos),
+ value_len,
+ token_type,
+ )
+
+
def deb822_semantic_tokens_full(
- ls: "LanguageServer",
+ ls: "DebputyLanguageServer",
request: SemanticTokensParams,
file_metadata: Deb822FileMetadata[Any],
) -> Optional[SemanticTokens]:
doc = ls.workspace.get_text_document(request.text_document.uri)
+ position_codec = doc.position_codec
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
+ tokens: List[int] = []
+ comment_token_code = SEMANTIC_TOKEN_TYPES_IDS["comment"]
+ sem_token_state = SemanticTokenState(
+ ls,
+ file_metadata,
+ doc,
+ lines,
+ tokens,
+ SEMANTIC_TOKEN_TYPES_IDS["keyword"],
+ SEMANTIC_TOKEN_TYPES_IDS["enumMember"],
+ comment_token_code,
+ )
- 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)
+ stanza_idx = 0
+ for part in deb822_file.iter_parts():
+ if part.is_comment:
+ pos = part.position_in_file()
+ sem_token_state.emit_token(
+ te_position_to_lsp(pos),
+ # Avoid trailing newline
+ position_codec.client_num_units(part.convert_to_text().rstrip()),
+ comment_token_code,
+ )
+ elif isinstance(part, Deb822ParagraphElement):
+ _deb822_paragraph_semantic_tokens_full(
+ sem_token_state,
+ part,
+ stanza_idx,
+ )
+ stanza_idx += 1
if not tokens:
return None
return SemanticTokens(tokens)
@@ -396,10 +471,14 @@ def _should_complete_field_with_value(cand: Deb822KnownField) -> bool:
def _complete_field_name(
+ ls: "DebputyLanguageServer",
fields: StanzaMetadata[Any],
seen_fields: Container[str],
) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]:
items = []
+ markdown_kind = ls.completion_item_document_markup(
+ MarkupKind.Markdown, MarkupKind.PlainText
+ )
for cand_key, cand in fields.items():
if cand_key.lower() in seen_fields:
continue
@@ -409,14 +488,28 @@ def _complete_field_name(
value = next(iter(v for v in cand.known_values if v != cand.default_value))
complete_as += value
tags = []
+ is_deprecated = False
if cand.replaced_by or cand.deprecated_with_no_replacement:
+ is_deprecated = True
tags.append(CompletionItemTag.Deprecated)
+ doc = cand.hover_text
+ if doc:
+ doc = MarkupContent(
+ value=doc,
+ kind=markdown_kind,
+ )
+ else:
+ doc = None
+
items.append(
CompletionItem(
name,
insert_text=complete_as,
+ deprecated=is_deprecated,
tags=tags,
+ detail=cand.synopsis_doc,
+ documentation=doc,
)
)
return items
diff --git a/src/debputy/lsp/lsp_self_check.py b/src/debputy/lsp/lsp_self_check.py
index 61a5733..3c7d2e4 100644
--- a/src/debputy/lsp/lsp_self_check.py
+++ b/src/debputy/lsp/lsp_self_check.py
@@ -83,7 +83,7 @@ def spell_checking() -> bool:
)
-def assert_can_start_lsp():
+def assert_can_start_lsp() -> None:
for self_check in LSP_CHECKS:
if self_check.is_mandatory and not self_check.test():
_error(
diff --git a/src/debputy/lsp/quickfixes.py b/src/debputy/lsp/quickfixes.py
index d911961..2d564f4 100644
--- a/src/debputy/lsp/quickfixes.py
+++ b/src/debputy/lsp/quickfixes.py
@@ -17,7 +17,6 @@ from lsprotocol.types import (
Command,
CodeActionParams,
Diagnostic,
- CodeActionDisabledType,
TextEdit,
WorkspaceEdit,
TextDocumentEdit,
@@ -30,7 +29,10 @@ from lsprotocol.types import (
from debputy.util import _warn
try:
- from debian._deb822_repro.locatable import Position as TEPosition, Range as TERange
+ from debputy.lsp.vendoring._deb822_repro.locatable import (
+ Position as TEPosition,
+ Range as TERange,
+ )
from pygls.server import LanguageServer
from pygls.workspace import TextDocument
@@ -38,7 +40,11 @@ except ImportError:
pass
-CodeActionName = Literal["correct-text", "remove-line"]
+CodeActionName = Literal[
+ "correct-text",
+ "remove-line",
+ "insert-text-on-line-after-diagnostic",
+]
class CorrectTextCodeAction(TypedDict):
@@ -46,6 +52,11 @@ class CorrectTextCodeAction(TypedDict):
correct_value: str
+class InsertTextOnLineAfterDiagnosticCodeAction(TypedDict):
+ code_action: Literal["insert-text-on-line-after-diagnostic"]
+ text_to_insert: str
+
+
class RemoveLineCodeAction(TypedDict):
code_action: Literal["remove-line"]
@@ -57,6 +68,15 @@ def propose_correct_text_quick_fix(correct_value: str) -> CorrectTextCodeAction:
}
+def propose_insert_text_on_line_after_diagnostic_quick_fix(
+ text_to_insert: str,
+) -> InsertTextOnLineAfterDiagnosticCodeAction:
+ return {
+ "code_action": "insert-text-on-line-after-diagnostic",
+ "text_to_insert": text_to_insert,
+ }
+
+
def propose_remove_line_quick_fix() -> RemoveLineCodeAction:
return {
"code_action": "remove-line",
@@ -93,24 +113,64 @@ def _correct_value_code_action(
diagnostic: Diagnostic,
) -> Iterable[Union[CodeAction, Command]]:
corrected_value = code_action_data["correct_value"]
- edits = [
- TextEdit(
- diagnostic.range,
- corrected_value,
- ),
- ]
+ edit = TextEdit(
+ diagnostic.range,
+ corrected_value,
+ )
yield CodeAction(
title=f'Replace with "{corrected_value}"',
kind=CodeActionKind.QuickFix,
diagnostics=[diagnostic],
edit=WorkspaceEdit(
- changes={code_action_params.text_document.uri: edits},
+ changes={code_action_params.text_document.uri: [edit]},
+ document_changes=[
+ TextDocumentEdit(
+ text_document=OptionalVersionedTextDocumentIdentifier(
+ uri=code_action_params.text_document.uri,
+ ),
+ edits=[edit],
+ )
+ ],
+ ),
+ )
+
+
+@_code_handler_for("insert-text-on-line-after-diagnostic")
+def _correct_value_code_action(
+ code_action_data: InsertTextOnLineAfterDiagnosticCodeAction,
+ code_action_params: CodeActionParams,
+ diagnostic: Diagnostic,
+) -> Iterable[Union[CodeAction, Command]]:
+ corrected_value = code_action_data["text_to_insert"]
+ line_no = diagnostic.range.end.line
+ if diagnostic.range.end.character > 0:
+ line_no += 1
+ insert_range = Range(
+ Position(
+ line_no,
+ 0,
+ ),
+ Position(
+ line_no,
+ 0,
+ ),
+ )
+ edit = TextEdit(
+ insert_range,
+ corrected_value,
+ )
+ yield CodeAction(
+ title=f'Insert "{corrected_value}"',
+ kind=CodeActionKind.QuickFix,
+ diagnostics=[diagnostic],
+ edit=WorkspaceEdit(
+ changes={code_action_params.text_document.uri: [edit]},
document_changes=[
TextDocumentEdit(
text_document=OptionalVersionedTextDocumentIdentifier(
uri=code_action_params.text_document.uri,
),
- edits=edits,
+ edits=[edit],
)
],
),
@@ -126,7 +186,7 @@ def range_compatible_with_remove_line_fix(range_: Range) -> bool:
@_code_handler_for("remove-line")
-def _correct_value_code_action(
+def _remove_line_code_action(
_code_action_data: RemoveLineCodeAction,
code_action_params: CodeActionParams,
diagnostic: Diagnostic,
@@ -138,33 +198,31 @@ def _correct_value_code_action(
)
return
- edits = [
- TextEdit(
- Range(
- start=Position(
- line=start.line,
- character=0,
- ),
- end=Position(
- line=start.line + 1,
- character=0,
- ),
+ edit = TextEdit(
+ Range(
+ start=Position(
+ line=start.line,
+ character=0,
+ ),
+ end=Position(
+ line=start.line + 1,
+ character=0,
),
- "",
),
- ]
+ "",
+ )
yield CodeAction(
title="Remove the line",
kind=CodeActionKind.QuickFix,
diagnostics=[diagnostic],
edit=WorkspaceEdit(
- changes={code_action_params.text_document.uri: edits},
+ changes={code_action_params.text_document.uri: [edit]},
document_changes=[
TextDocumentEdit(
text_document=OptionalVersionedTextDocumentIdentifier(
uri=code_action_params.text_document.uri,
),
- edits=edits,
+ edits=[edit],
)
],
),
@@ -174,7 +232,7 @@ def _correct_value_code_action(
def provide_standard_quickfixes_from_diagnostics(
code_action_params: CodeActionParams,
) -> Optional[List[Union[Command, CodeAction]]]:
- actions = []
+ actions: List[Union[Command, CodeAction]] = []
for diagnostic in code_action_params.context.diagnostics:
data = diagnostic.data
if not isinstance(data, list):
diff --git a/src/debputy/lsp/text_util.py b/src/debputy/lsp/text_util.py
index d66cb28..ef4cd0a 100644
--- a/src/debputy/lsp/text_util.py
+++ b/src/debputy/lsp/text_util.py
@@ -1,4 +1,4 @@
-from typing import List, Optional, Sequence, Union, Iterable
+from typing import List, Optional, Sequence, Union, Iterable, TYPE_CHECKING
from lsprotocol.types import (
TextEdit,
@@ -10,15 +10,22 @@ from lsprotocol.types import (
from debputy.linting.lint_util import LinterPositionCodec
try:
- from debian._deb822_repro.locatable import Position as TEPosition, Range as TERange
+ from debputy.lsp.vendoring._deb822_repro.locatable import (
+ Position as TEPosition,
+ Range as TERange,
+ )
except ImportError:
pass
try:
- from pygls.workspace import LanguageServer, TextDocument, PositionCodec
+ from pygls.server import LanguageServer
+ from pygls.workspace import TextDocument, PositionCodec
+except ImportError:
+ pass
+if TYPE_CHECKING:
LintCapablePositionCodec = Union[LinterPositionCodec, PositionCodec]
-except ImportError:
+else:
LintCapablePositionCodec = LinterPositionCodec
diff --git a/src/debputy/lsp/vendoring/_deb822_repro/parsing.py b/src/debputy/lsp/vendoring/_deb822_repro/parsing.py
index e2c638a..c5753e2 100644
--- a/src/debputy/lsp/vendoring/_deb822_repro/parsing.py
+++ b/src/debputy/lsp/vendoring/_deb822_repro/parsing.py
@@ -280,6 +280,9 @@ class Deb822ParsedTokenList(
# type: () -> Iterator[VE]
yield from (v for v in self._token_list if isinstance(v, self._vtype))
+ def iter_parts(self) -> Iterable[TokenOrElement]:
+ yield from self._token_list
+
def _mark_changed(self):
# type: () -> None
self._changed = True
@@ -1082,6 +1085,14 @@ class Deb822Element(Locatable):
return False
@property
+ def is_whitespace(self) -> bool:
+ return False
+
+ @property
+ def is_separator(self) -> bool:
+ return False
+
+ @property
def parent_element(self):
# type: () -> Optional[Deb822Element]
return resolve_ref(self._parent_element)
@@ -1492,6 +1503,20 @@ class Deb822KeyValuePairElement(Deb822Element):
yield self._separator_token
yield self._value_element
+ def key_position_in_stanza(self) -> Position:
+ position = super().position_in_parent(skip_leading_comments=False)
+ if self._comment_element:
+ field_pos = self._field_token.position_in_parent()
+ position = field_pos.relative_to(position)
+ return position
+
+ def value_position_in_stanza(self) -> Position:
+ position = super().position_in_parent(skip_leading_comments=False)
+ if self._comment_element:
+ value_pos = self._value_element.position_in_parent()
+ position = value_pos.relative_to(position)
+ return position
+
def position_in_parent(
self,
*,
diff --git a/src/debputy/lsp/vendoring/_deb822_repro/tokens.py b/src/debputy/lsp/vendoring/_deb822_repro/tokens.py
index 6697a2c..88d2058 100644
--- a/src/debputy/lsp/vendoring/_deb822_repro/tokens.py
+++ b/src/debputy/lsp/vendoring/_deb822_repro/tokens.py
@@ -161,6 +161,10 @@ class Deb822Token(Locatable):
return False
@property
+ def is_separator(self) -> bool:
+ return False
+
+ @property
def text(self):
# type: () -> str
return self._text
@@ -253,6 +257,10 @@ class Deb822SpaceSeparatorToken(Deb822SemanticallySignificantWhiteSpace):
__slots__ = ()
+ @property
+ def is_separator(self) -> bool:
+ return True
+
class Deb822ErrorToken(Deb822Token):
"""Token that represents a syntactical error"""
@@ -296,8 +304,12 @@ class Deb822SeparatorToken(Deb822Token):
__slots__ = ()
+ @property
+ def is_separator(self) -> bool:
+ return True
+
-class Deb822FieldSeparatorToken(Deb822Token):
+class Deb822FieldSeparatorToken(Deb822SeparatorToken):
__slots__ = ()
diff --git a/src/debputy/path_matcher.py b/src/debputy/path_matcher.py
index 47e5c91..2917b14 100644
--- a/src/debputy/path_matcher.py
+++ b/src/debputy/path_matcher.py
@@ -229,7 +229,9 @@ class MatchAnything(MatchRule):
def _full_pattern(self) -> str:
return "**/*"
- def finditer(self, fs_root: VP, *, ignore_paths=None) -> Iterable[VP]:
+ def finditer(
+ self, fs_root: VP, *, ignore_paths: Optional[Callable[[VP], bool]] = None
+ ) -> Iterable[VP]:
if ignore_paths is not None:
yield from (p for p in fs_root.all_paths() if not ignore_paths(p))
yield from fs_root.all_paths()
@@ -253,7 +255,9 @@ class ExactFileSystemPath(MatchRule):
def _full_pattern(self) -> str:
return self._path
- def finditer(self, fs_root: VP, *, ignore_paths=None) -> Iterable[VP]:
+ def finditer(
+ self, fs_root: VP, *, ignore_paths: Optional[Callable[[VP], bool]] = None
+ ) -> Iterable[VP]:
p = _lookup_path(fs_root, self._path)
if p is not None and (ignore_paths is None or not ignore_paths(p)):
yield p
@@ -376,7 +380,12 @@ class BasenameGlobMatch(MatchRule):
return f"{self._directory}/{maybe_recursive}{self._basename_glob}"
return self._basename_glob
- def finditer(self, fs_root: VP, *, ignore_paths=None) -> Iterable[VP]:
+ def finditer(
+ self,
+ fs_root: VP,
+ *,
+ ignore_paths: Optional[Callable[[VP], bool]] = None,
+ ) -> Iterable[VP]:
search_root = fs_root
if self._directory is not None:
p = _lookup_path(fs_root, self._directory)
@@ -466,7 +475,12 @@ class GenericGlobImplementation(MatchRule):
def _full_pattern(self) -> str:
return self._glob_pattern
- def finditer(self, fs_root: VP, *, ignore_paths=None) -> Iterable[VP]:
+ def finditer(
+ self,
+ fs_root: VP,
+ *,
+ ignore_paths: Optional[Callable[[VP], bool]] = None,
+ ) -> Iterable[VP]:
search_history = [fs_root]
for part in self._match_parts:
next_layer = itertools.chain.from_iterable(
diff --git a/src/debputy/plugin/api/impl_types.py b/src/debputy/plugin/api/impl_types.py
index 5aca980..9075ac6 100644
--- a/src/debputy/plugin/api/impl_types.py
+++ b/src/debputy/plugin/api/impl_types.py
@@ -420,7 +420,7 @@ class DispatchingParserBase(Generic[TP]):
def _add_parser(
self,
- keyword: Union[str, List[str]],
+ keyword: Union[str, Iterable[str]],
ppp: "PluginProvidedParser[PF, TP]",
) -> None:
ks = [keyword] if isinstance(keyword, str) else keyword
diff --git a/src/debputy/plugin/api/spec.py b/src/debputy/plugin/api/spec.py
index dba4523..07954e6 100644
--- a/src/debputy/plugin/api/spec.py
+++ b/src/debputy/plugin/api/spec.py
@@ -1046,7 +1046,7 @@ class VirtualPath:
self,
*,
byte_io: Literal[False] = False,
- buffering: Optional[int] = ...,
+ buffering: int = -1,
) -> TextIO: ...
@overload
@@ -1054,7 +1054,7 @@ class VirtualPath:
self,
*,
byte_io: Literal[True],
- buffering: Optional[int] = ...,
+ buffering: int = -1,
) -> BinaryIO: ...
@overload
@@ -1062,7 +1062,7 @@ class VirtualPath:
self,
*,
byte_io: bool,
- buffering: Optional[int] = ...,
+ buffering: int = -1,
) -> Union[TextIO, BinaryIO]: ...
def open(
@@ -1085,7 +1085,7 @@ class VirtualPath:
:param byte_io: If True, open the file in binary mode (like `rb` for `open`)
:param buffering: Same as open(..., buffering=...) where supported. Notably during
testing, the content may be purely in memory and use a BytesIO/StringIO
- (which does not accept that parameter, but then is buffered in a different way)
+ (which does not accept that parameter, but then it is buffered in a different way)
:return: The file handle.
"""
diff --git a/src/debputy/plugin/debputy/metadata_detectors.py b/src/debputy/plugin/debputy/metadata_detectors.py
index 4338087..e325500 100644
--- a/src/debputy/plugin/debputy/metadata_detectors.py
+++ b/src/debputy/plugin/debputy/metadata_detectors.py
@@ -520,8 +520,8 @@ def auto_depends_arch_any_solink(
if not roots:
return
- for libdir, target in targets:
- final_path = os.path.join(libdir, target)
+ for libdir_path, target in targets:
+ final_path = os.path.join(libdir_path, target)
matches = []
for opkg, ofs_root in roots:
m = ofs_root.lookup(final_path)
diff --git a/src/debputy/plugin/debputy/private_api.py b/src/debputy/plugin/debputy/private_api.py
index 8428a5f..37c9318 100644
--- a/src/debputy/plugin/debputy/private_api.py
+++ b/src/debputy/plugin/debputy/private_api.py
@@ -2517,21 +2517,20 @@ def _install_docs_rule_handler(
path, package_type="deb", package_attribute="into"
)
]
- into = frozenset(into)
if install_as is not None:
assert len(sources) == 1
assert dest_dir is None
return InstallRule.install_doc_as(
sources[0],
install_as.match_rule.path,
- into,
+ frozenset(into),
path.path,
condition,
)
return InstallRule.install_doc(
sources,
dest_dir,
- into,
+ frozenset(into),
path.path,
condition,
)
@@ -2622,10 +2621,9 @@ def _install_man_rule_handler(
)
]
condition = parsed_data.get("when")
- into = frozenset(into)
return InstallRule.install_man(
sources,
- into,
+ frozenset(into),
section,
language,
attribute_path.path,
diff --git a/src/debputy/plugin/debputy/strip_non_determinism.py b/src/debputy/plugin/debputy/strip_non_determinism.py
index 2f8fd39..a94d348 100644
--- a/src/debputy/plugin/debputy/strip_non_determinism.py
+++ b/src/debputy/plugin/debputy/strip_non_determinism.py
@@ -70,10 +70,10 @@ class ExtensionPlusFileOutputRule(SndDetectionRule):
def file_output_verdict(
self,
path: VirtualPath,
- file_analysis: str,
+ file_analysis: Optional[str],
) -> bool:
file_pattern = self.file_pattern
- assert file_pattern is not None
+ assert file_pattern is not None and file_analysis is not None
m = file_pattern.search(file_analysis)
return m is not None
diff --git a/src/debputy/util.py b/src/debputy/util.py
index 4da2772..d8cfd67 100644
--- a/src/debputy/util.py
+++ b/src/debputy/util.py
@@ -70,8 +70,8 @@ _DOUBLE_ESCAPEES = re.compile(r'([\n`$"\\])')
_REGULAR_ESCAPEES = re.compile(r'([\s!"$()*+#;<>?@\[\]\\`|~])')
_PROFILE_GROUP_SPLIT = re.compile(r">\s+<")
_DEFAULT_LOGGER: Optional[logging.Logger] = None
-_STDOUT_HANDLER: Optional[logging.StreamHandler] = None
-_STDERR_HANDLER: Optional[logging.StreamHandler] = None
+_STDOUT_HANDLER: Optional[logging.StreamHandler[Any]] = None
+_STDERR_HANDLER: Optional[logging.StreamHandler[Any]] = None
def assume_not_none(x: Optional[T]) -> T:
@@ -764,14 +764,14 @@ def setup_logging(
)
logger = logging.getLogger()
if existing_stdout_handler is not None:
- logger.removeHandler(existing_stderr_handler)
+ logger.removeHandler(existing_stdout_handler)
_STDERR_HANDLER = stderr_handler
logger.addHandler(stderr_handler)
else:
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(logging.Formatter(colorless_format, style="{"))
logger = logging.getLogger()
- if existing_stdout_handler is not None:
+ if existing_stderr_handler is not None:
logger.removeHandler(existing_stderr_handler)
_STDERR_HANDLER = stderr_handler
logger.addHandler(stderr_handler)
diff --git a/src/debputy/yaml/compat.py b/src/debputy/yaml/compat.py
index f26af02..f36fc5a 100644
--- a/src/debputy/yaml/compat.py
+++ b/src/debputy/yaml/compat.py
@@ -10,10 +10,10 @@ __all__ = [
]
try:
- from ruyaml import YAMLError, YAML, Node
+ from ruyaml import YAML, Node
from ruyaml.comments import LineCol, CommentedBase, CommentedMap, CommentedSeq
- from ruyaml.error import MarkedYAMLError
+ from ruyaml.error import YAMLError, MarkedYAMLError
except (ImportError, ModuleNotFoundError):
- from ruamel.yaml import YAMLError, YAML, Node
- from ruamel.yaml.comments import LineCol, CommentedBase, CommentedMap, CommentedSeq
- from ruamel.yaml.error import MarkedYAMLError
+ from ruamel.yaml import YAML, Node # type: ignore
+ from ruamel.yaml.comments import LineCol, CommentedBase, CommentedMap, CommentedSeq # type: ignore
+ from ruamel.yaml.error import YAMLError, MarkedYAMLError # type: ignore