Coverage for src/debputy/commands/debputy_cmd/lint_and_lsp_cmds.py: 18%
130 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-04-19 20:37 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-04-19 20:37 +0000
1import asyncio
2import os.path
3import random
4import textwrap
5from argparse import BooleanOptionalAction
6from typing import TYPE_CHECKING
8from debputy.commands.debputy_cmd.context import ROOT_COMMAND, CommandContext, add_arg
9from debputy.lsp.named_styles import ALL_PUBLIC_NAMED_STYLES
10from debputy.util import _error, _info
12if TYPE_CHECKING:
13 from debputy.lsp.debputy_ls import DebputyLanguageServer
16_EDITOR_SNIPPETS = {
17 "emacs": "emacs+eglot",
18 "emacs+eglot": textwrap.dedent("""\
19 ;; `deputy lsp server` glue for emacs eglot (eglot is built-in these days)
20 ;;
21 ;; Add to ~/.emacs or ~/.emacs.d/init.el and then activate via `M-x eglot`.
22 ;;
23 ;; Requires: apt install elpa-dpkg-dev-el elpa-yaml-mode
24 ;; Recommends: apt install elpa-markdown-mode
26 ;; Make emacs recognize debian/debputy.manifest as a YAML file
27 (add-to-list 'auto-mode-alist '("/debian/debputy.manifest\\'" . yaml-mode))
28 ;; Inform eglot about the debputy LSP
29 (with-eval-after-load 'eglot
30 (add-to-list 'eglot-server-programs
31 '(
32 (
33 ;; Requires elpa-dpkg-dev-el (>= 37.12)
34 (debian-autopkgtest-control-mode :language-id "debian/tests/control")
35 ;; Requires elpa-dpkg-dev-el
36 (debian-control-mode :language-id "debian/control")
37 (debian-changelog-mode :language-id "debian/changelog")
38 (debian-copyright-mode :language-id "debian/copyright")
39 ;; No language id for these atm.
40 makefile-gmake-mode
41 ;; Requires elpa-yaml-mode
42 yaml-mode
43 )
44 . ("debputy" "lsp" "server")
45 )))
47 ;; Auto-start eglot for the relevant modes.
48 (add-hook 'debian-control-mode-hook 'eglot-ensure)
49 ;; Requires elpa-dpkg-dev-el (>= 37.12)
50 ;; Technically, the `eglot-ensure` works before then, but it causes a
51 ;; visible and very annoying long delay on opening the first changelog.
52 ;; It still has a minor delay in 37.12, which may still be too long for
53 ;; for your preference. In that case, comment it out.
54 (add-hook 'debian-changelog-mode-hook 'eglot-ensure)
55 (add-hook 'debian-copyright-mode-hook 'eglot-ensure)
56 ;; Requires elpa-dpkg-dev-el (>= 37.12)
57 (add-hook 'debian-autopkgtest-control-mode-hook 'eglot-ensure)
58 (add-hook 'makefile-gmake-mode-hook 'eglot-ensure)
59 (add-hook 'yaml-mode-hook 'eglot-ensure)
60 """),
61 "vim": "vim+youcompleteme",
62 "vim+youcompleteme": textwrap.dedent("""\
63 # debputy lsp server glue for vim with vim-youcompleteme. Add to ~/.vimrc
64 #
65 # Requires: apt install vim-youcompleteme
66 # - Your vim **MUST** be a provider of `vim-python3` (`vim-nox`/`vim-gtk`, etc.)
68 # Make vim recognize debputy.manifest as YAML file
69 au BufNewFile,BufRead debputy.manifest setf yaml
70 # Inform vim/ycm about the debputy LSP
71 # - NB: No known support for debian/tests/control that we can hook into.
72 # Feel free to provide one :)
73 let g:ycm_language_server = [
74 \\ { 'name': 'debputy',
75 \\ 'filetypes': [ 'debcontrol', 'debcopyright', 'debchangelog', 'autopkgtest', 'make', 'yaml'],
76 \\ 'cmdline': [ 'debputy', 'lsp', 'server', '--ignore-language-ids' ]
77 \\ },
78 \\ ]
80 packadd! youcompleteme
81 # Add relevant ycm keybinding such as:
82 # nmap <leader>d <plug>(YCMHover)
83 """),
84 "vim+vim9lsp": textwrap.dedent("""\
85 # debputy lsp server glue for vim with vim9 lsp. Add to ~/.vimrc
86 #
87 # Requires https://github.com/yegappan/lsp to be in your packages path
89 vim9script
91 # Make vim recognize debputy.manifest as YAML file
92 autocmd BufNewFile,BufRead debputy.manifest setfiletype yaml
94 packadd! lsp
96 final lspServers: list<dict<any>> = []
98 if executable('debputy')
99 lspServers->add({
100 filetype: ['debcontrol', 'debcopyright', 'debchangelog', 'autopkgtest', 'make', 'yaml'],
101 path: 'debputy',
102 args: ['lsp', 'server', '--ignore-language-ids']
103 })
104 endif
106 autocmd User LspSetup g:LspOptionsSet({semanticHighlight: true})
107 autocmd User LspSetup g:LspAddServer(lspServers)
108 """),
109 "neovim": "neovim+nvim-lspconfig",
110 "neovim+nvim-lspconfig": textwrap.dedent("""\
111 # debputy lsp server glue for neovim with nvim-lspconfig. Add to ~/.config/nvim/init.lua
112 #
113 # Requires https://github.com/neovim/nvim-lspconfig to be in your packages path
115 require("lspconfig").debputy.setup {capabilities = capabilities}
117 # Make vim recognize debputy.manifest as YAML file
118 vim.filetype.add({ filename = {["debputy.manifest"] = "yaml"} })
119 """),
120}
123lsp_command = ROOT_COMMAND.add_dispatching_subcommand(
124 "lsp",
125 dest="lsp_command",
126 help_description="Language server related subcommands",
127)
129lsp_internal_commands = lsp_command.add_dispatching_subcommand(
130 "internal-command",
131 dest="internal_command",
132 metavar="command",
133 help_description="Commands used for internal purposes. These are implementation details and subject to change",
134)
137@lsp_command.register_subcommand(
138 "server",
139 log_only_to_stderr=True,
140 help_description="Start the language server",
141 argparser=[
142 add_arg(
143 "--tcp",
144 action="store_true",
145 help="Use TCP server",
146 ),
147 add_arg(
148 "--ws",
149 action="store_true",
150 help="Use WebSocket server",
151 ),
152 add_arg(
153 "--host",
154 default="127.0.0.1",
155 help="Bind to this address (Use with --tcp / --ws)",
156 ),
157 add_arg(
158 "--port",
159 type=int,
160 default=2087,
161 help="Bind to this port (Use with --tcp / --ws)",
162 ),
163 add_arg(
164 "--ignore-language-ids",
165 dest="trust_language_ids",
166 default=True,
167 action="store_false",
168 help="Disregard language IDs from the editor (rely solely on filename instead)",
169 ),
170 add_arg(
171 "--force-locale",
172 dest="forced_locale",
173 default=None,
174 action="store",
175 help="Disregard locale from editor and always use provided locale code (translations)",
176 ),
177 ],
178)
179def lsp_server_cmd(context: CommandContext) -> None:
180 parsed_args = context.parsed_args
182 feature_set = context.load_plugins()
184 from debputy.lsp.lsp_self_check import assert_can_start_lsp
186 assert_can_start_lsp()
188 from debputy.lsp.lsp_features import (
189 ensure_cli_lsp_features_are_loaded,
190 )
191 from debputy.lsp.lsp_dispatch import DEBPUTY_LANGUAGE_SERVER
193 ensure_cli_lsp_features_are_loaded()
194 debputy_language_server = DEBPUTY_LANGUAGE_SERVER
195 debputy_language_server.plugin_feature_set = feature_set
196 debputy_language_server.dctrl_parser = context.dctrl_parser
197 debputy_language_server.trust_language_ids = parsed_args.trust_language_ids
198 debputy_language_server.forced_locale = parsed_args.forced_locale
200 debputy_language_server.finish_startup_initialization()
202 if parsed_args.tcp and parsed_args.ws:
203 _error("Sorry, --tcp and --ws are mutually exclusive")
205 if parsed_args.tcp:
206 debputy_language_server.start_tcp(parsed_args.host, parsed_args.port)
207 elif parsed_args.ws:
208 debputy_language_server.start_ws(parsed_args.host, parsed_args.port)
209 else:
210 debputy_language_server.start_io()
213@lsp_command.register_subcommand(
214 "editor-config",
215 help_description="Provide editor configuration snippets",
216 argparser=[
217 add_arg(
218 "editor_name",
219 metavar="editor",
220 choices=_EDITOR_SNIPPETS,
221 default=None,
222 nargs="?",
223 help="The editor to provide a snippet for",
224 ),
225 ],
226)
227def lsp_editor_glue(context: CommandContext) -> None:
228 editor_name = context.parsed_args.editor_name
230 if editor_name is None:
231 content = []
232 for editor_name, payload in _EDITOR_SNIPPETS.items():
233 alias_of = ""
234 if payload in _EDITOR_SNIPPETS:
235 alias_of = f" (short for: {payload})"
236 content.append((editor_name, alias_of))
237 max_name = max(len(c[0]) for c in content)
238 print(
239 "This version of debputy has instructions or editor config snippets for the following editors: "
240 )
241 print()
242 for editor_name, alias_of in content:
243 print(f" * {editor_name:<{max_name}}{alias_of}")
244 print()
245 choice = random.Random().choice(list(_EDITOR_SNIPPETS))
246 print(
247 f"Use `debputy lsp editor-config {choice}` (as an example) to see the instructions for a concrete editor."
248 )
249 return
250 result = _EDITOR_SNIPPETS[editor_name]
251 while result in _EDITOR_SNIPPETS:
252 result = _EDITOR_SNIPPETS[result]
253 print(result)
256@lsp_command.register_subcommand(
257 "features",
258 help_description="Describe language ids and features",
259)
260def lsp_describe_features(context: CommandContext) -> None:
262 from debputy.lsp.lsp_self_check import assert_can_start_lsp
264 try:
265 from debputy.lsp.lsp_features import describe_lsp_features
266 except ImportError:
267 assert_can_start_lsp()
268 raise AssertionError(
269 "Cannot load the language server features but `assert_can_start_lsp` did not fail"
270 )
272 describe_lsp_features(context)
275@ROOT_COMMAND.register_subcommand(
276 "lint",
277 log_only_to_stderr=True,
278 help_description="Provide diagnostics for the packaging (like `lsp server` except no editor is needed)",
279 argparser=[
280 add_arg(
281 "--spellcheck",
282 dest="spellcheck",
283 action="store_true",
284 help="Enable spellchecking",
285 ),
286 add_arg(
287 "--auto-fix",
288 dest="auto_fix",
289 action="store_true",
290 help="Automatically fix problems with trivial or obvious corrections.",
291 ),
292 add_arg(
293 "--linter-exit-code",
294 dest="linter_exit_code",
295 default=True,
296 action=BooleanOptionalAction,
297 help='Enable or disable the "linter" convention of exiting with an error if severe issues were found',
298 ),
299 add_arg(
300 "--lint-report-format",
301 dest="lint_report_format",
302 default="term",
303 choices=["term", "junit4-xml"],
304 help="The report output format",
305 ),
306 add_arg(
307 "--report-output",
308 dest="report_output",
309 default=None,
310 action="store",
311 help="Where to place the report (for report formats that generate files/directory reports)",
312 ),
313 add_arg(
314 "--warn-about-check-manifest",
315 dest="warn_about_check_manifest",
316 default=True,
317 action=BooleanOptionalAction,
318 help="Warn about limitations that check-manifest would cover if d/debputy.manifest is present",
319 ),
320 ],
321)
322def lint_cmd(context: CommandContext) -> None:
323 try:
324 import lsprotocol
325 except ImportError:
326 _error("This feature requires lsprotocol (apt-get install python3-lsprotocol)")
328 from debputy.lsp.lsp_features import (
329 ensure_cli_lsp_features_are_loaded,
330 )
331 from debputy.linting.lint_impl import perform_linting
333 context.must_be_called_in_source_root()
334 ensure_cli_lsp_features_are_loaded()
335 asyncio.run(perform_linting(context))
338@ROOT_COMMAND.register_subcommand(
339 "reformat",
340 help_description="Reformat the packaging files based on the packaging/maintainer rules",
341 argparser=[
342 add_arg(
343 "--style",
344 dest="named_style",
345 choices=ALL_PUBLIC_NAMED_STYLES,
346 default=None,
347 help="The formatting style to use (overrides packaging style).",
348 ),
349 add_arg(
350 "--write-style",
351 dest="write_style",
352 action="store_true",
353 default=False,
354 help="Add the chosen style (from --style) into the reformatted debian/control.",
355 ),
356 add_arg(
357 "--auto-fix",
358 dest="auto_fix",
359 default=True,
360 action=BooleanOptionalAction,
361 help="Whether to automatically apply any style changes.",
362 ),
363 add_arg(
364 "--linter-exit-code",
365 dest="linter_exit_code",
366 default=True,
367 action=BooleanOptionalAction,
368 help='Enable or disable the "linter" convention of exiting with an error if issues were found',
369 ),
370 add_arg(
371 "--supported-style-is-required",
372 dest="supported_style_required",
373 default=True,
374 action="store_true",
375 help="Fail with an error if a supported style cannot be identified.",
376 ),
377 add_arg(
378 "--unknown-or-unsupported-style-is-ok",
379 dest="supported_style_required",
380 action="store_false",
381 help="Do not exit with an error if no supported style can be identified. Useful for general"
382 ' pipelines to implement "reformat if possible"',
383 ),
384 add_arg(
385 "--missing-style-is-ok",
386 dest="supported_style_required",
387 action="store_false",
388 help="[Deprecated] Use --unknown-or-unsupported-style-is-ok instead",
389 ),
390 ],
391)
392def reformat_cmd(context: CommandContext) -> None:
393 try:
394 import lsprotocol
395 except ImportError:
396 _error("This feature requires lsprotocol (apt-get install python3-lsprotocol)")
398 from debputy.lsp.lsp_features import (
399 ensure_cli_lsp_features_are_loaded,
400 )
401 from debputy.linting.lint_impl import perform_reformat
403 context.must_be_called_in_source_root()
404 ensure_cli_lsp_features_are_loaded()
405 perform_reformat(context, named_style=context.parsed_args.named_style)
408@lsp_internal_commands.register_subcommand(
409 "semantic-tokens",
410 log_only_to_stderr=True,
411 help_description="Test semantic tokens",
412 argparser=[
413 add_arg(
414 "filename",
415 help="Filename to annotate",
416 ),
417 add_arg(
418 "--language-id",
419 action="store",
420 type=str,
421 default="",
422 help="Choose the language id",
423 ),
424 ],
425)
426def lsp_test_semantic_tokens_cmd(context: CommandContext) -> None:
427 parsed_args = context.parsed_args
428 filename = parsed_args.filename
430 debputy_ls = _load_test_lsp(context)
432 with open(filename) as fd:
433 data = fd.read()
435 doc_uri = f"file://{os.path.abspath(filename)}"
437 _fake_initialize(
438 debputy_ls,
439 doc_uri,
440 data,
441 language_id=parsed_args.language_id,
442 )
444 from debputy.lsp.lsp_dispatch import semantic_tokens_full
445 from debputy.lsp.lsp_test_support import resolve_semantic_tokens
446 import lsprotocol.types as types
447 import asyncio
449 result = asyncio.get_event_loop().run_until_complete(
450 semantic_tokens_full(
451 debputy_ls,
452 types.SemanticTokensParams(types.TextDocumentIdentifier(doc_uri)),
453 )
454 )
456 if result is None:
457 _info("No semantic were tokens provided")
458 return
460 lines = data.splitlines(keepends=True)
461 tokens = resolve_semantic_tokens(
462 lines,
463 result,
464 )
465 _info(f"Identified {len(tokens)}")
466 for token in tokens:
467 print(f"{filename}:{token.range}: {token.token_name} {token.value!r}")
470def _load_test_lsp(
471 context: CommandContext,
472) -> "DebputyLanguageServer":
473 from debputy.lsp.lsp_self_check import assert_can_start_lsp
475 assert_can_start_lsp()
477 from debputy.lsp.lsp_features import (
478 ensure_cli_lsp_features_are_loaded,
479 )
480 from debputy.lsp.lsp_dispatch import DEBPUTY_LANGUAGE_SERVER
482 ensure_cli_lsp_features_are_loaded()
483 debputy_language_server = DEBPUTY_LANGUAGE_SERVER
484 debputy_language_server.plugin_feature_set = context.load_plugins()
485 debputy_language_server.dctrl_parser = context.dctrl_parser
486 debputy_language_server.trust_language_ids = True
487 debputy_language_server.forced_locale = None
489 debputy_language_server.finish_startup_initialization()
491 return debputy_language_server
494def _fake_initialize(
495 debputy_ls: "DebputyLanguageServer",
496 doc_uri: str,
497 text: str,
498 *,
499 language_id: str = "",
500 version: int = 1,
501) -> None:
502 import lsprotocol.types as types
504 debputy_ls.lsp.lsp_initialize(
505 types.InitializeParams(
506 types.ClientCapabilities(
507 general=types.GeneralClientCapabilities(
508 position_encodings=[types.PositionEncodingKind.Utf32],
509 )
510 )
511 )
512 )
513 debputy_ls.workspace.put_text_document(
514 types.TextDocumentItem(doc_uri, language_id, version, text)
515 )
518def ensure_lint_and_lsp_commands_are_loaded() -> None:
519 # Loading the module does the heavy lifting
520 # However, having this function means that we do not have an "unused" import that some tool
521 # gets tempted to remove
522 assert ROOT_COMMAND.has_command("lsp")
523 assert ROOT_COMMAND.has_command("lint")
524 assert ROOT_COMMAND.has_command("reformat")