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