Coverage for src/debputy/lsp/lsp_debian_control.py: 64%

412 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-03-24 16:38 +0000

1import asyncio 

2import dataclasses 

3import importlib.resources 

4import os.path 

5import textwrap 

6from functools import lru_cache 

7from itertools import chain 

8from typing import ( 

9 Union, 

10 Sequence, 

11 Tuple, 

12 Optional, 

13 Mapping, 

14 List, 

15 Iterable, 

16 Self, 

17 TYPE_CHECKING, 

18) 

19 

20import debputy.lsp.data.deb822_data as deb822_ref_data_dir 

21from debputy.analysis.analysis_util import flatten_ppfs 

22from debputy.analysis.debian_dir import resolve_debhelper_config_files 

23from debputy.dh.dh_assistant import extract_dh_compat_level 

24from debputy.linting.lint_util import ( 

25 LintState, 

26 te_range_to_lsp, 

27 te_position_to_lsp, 

28) 

29from debputy.lsp.apt_cache import PackageLookup 

30from debputy.lsp.debputy_ls import DebputyLanguageServer 

31from debputy.lsp.lsp_debian_control_reference_data import ( 

32 DctrlKnownField, 

33 DctrlFileMetadata, 

34 package_name_to_section, 

35 all_package_relationship_fields, 

36 extract_first_value_and_position, 

37 all_source_relationship_fields, 

38 StanzaMetadata, 

39 SUBSTVAR_RE, 

40) 

41from debputy.lsp.lsp_features import ( 

42 lint_diagnostics, 

43 lsp_completer, 

44 lsp_hover, 

45 lsp_standard_handler, 

46 lsp_folding_ranges, 

47 lsp_semantic_tokens_full, 

48 lsp_will_save_wait_until, 

49 lsp_format_document, 

50 lsp_text_doc_inlay_hints, 

51 LanguageDispatchRule, 

52 SecondaryLanguage, 

53) 

54from debputy.lsp.lsp_generic_deb822 import ( 

55 deb822_completer, 

56 deb822_hover, 

57 deb822_folding_ranges, 

58 deb822_semantic_tokens_full, 

59 deb822_format_file, 

60 scan_for_syntax_errors_and_token_level_diagnostics, 

61) 

62from debputy.lsp.lsp_reference_keyword import LSP_DATA_DOMAIN 

63from debputy.lsp.quickfixes import ( 

64 propose_correct_text_quick_fix, 

65 propose_insert_text_on_line_after_diagnostic_quick_fix, 

66 propose_remove_range_quick_fix, 

67) 

68from debputy.lsp.ref_models.deb822_reference_parse_models import ( 

69 DCTRL_SUBSTVARS_REFERENCE_DATA_PARSER, 

70 DCtrlSubstvar, 

71) 

72from debputy.lsp.text_util import markdown_urlify 

73from debputy.lsp.vendoring._deb822_repro import ( 

74 Deb822ParagraphElement, 

75) 

76from debputy.lsp.vendoring._deb822_repro.parsing import ( 

77 Deb822KeyValuePairElement, 

78) 

79from debputy.lsprotocol.types import ( 

80 Position, 

81 FoldingRange, 

82 FoldingRangeParams, 

83 CompletionItem, 

84 CompletionList, 

85 CompletionParams, 

86 HoverParams, 

87 Hover, 

88 TEXT_DOCUMENT_CODE_ACTION, 

89 SemanticTokens, 

90 SemanticTokensParams, 

91 WillSaveTextDocumentParams, 

92 TextEdit, 

93 DocumentFormattingParams, 

94 InlayHint, 

95) 

96from debputy.manifest_parser.util import AttributePath 

97from debputy.packager_provided_files import ( 

98 PackagerProvidedFile, 

99 detect_all_packager_provided_files, 

100) 

101from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin 

102from debputy.util import PKGNAME_REGEX, _info, _trace_log, _is_trace_log_enabled 

103from debputy.yaml import MANIFEST_YAML 

104 

105if TYPE_CHECKING: 

106 import lsprotocol.types as types 

107else: 

108 import debputy.lsprotocol.types as types 

109 

110try: 

111 from debputy.lsp.vendoring._deb822_repro.locatable import ( 

112 Position as TEPosition, 

113 Range as TERange, 

114 START_POSITION, 

115 ) 

116 

117 from pygls.workspace import TextDocument 

118except ImportError: 

119 pass 

120 

121 

122_DISPATCH_RULE = LanguageDispatchRule.new_rule( 

123 "debian/control", 

124 "debian/control", 

125 [ 

126 # emacs's name 

127 SecondaryLanguage("debian-control"), 

128 # vim's name 

129 SecondaryLanguage("debcontrol"), 

130 ], 

131) 

132 

133 

134@dataclasses.dataclass(slots=True, frozen=True) 

135class SubstvarMetadata: 

136 name: str 

137 defined_by: str 

138 dh_sequence: Optional[str] 

139 doc_uris: Sequence[str] 

140 synopsis: str 

141 description: str 

142 

143 def render_metadata_fields(self) -> str: 

144 def_by = f"Defined by: {self.defined_by}" 

145 dh_seq = ( 

146 f"DH Sequence: {self.dh_sequence}" if self.dh_sequence is not None else None 

147 ) 

148 doc_uris = self.doc_uris 

149 parts = [def_by, dh_seq] 

150 if doc_uris: 150 ↛ 156line 150 didn't jump to line 156 because the condition on line 150 was always true

151 if len(doc_uris) == 1: 151 ↛ 154line 151 didn't jump to line 154 because the condition on line 151 was always true

152 parts.append(f"Documentation: {markdown_urlify(doc_uris[0])}") 

153 else: 

154 parts.append("Documentation:") 

155 parts.extend(f" - {markdown_urlify(uri)}" for uri in doc_uris) 

156 return "\n".join(parts) 

157 

158 @classmethod 

159 def from_ref_data(cls, x: DCtrlSubstvar) -> "Self": 

160 doc = x.get("documentation", {}) 

161 return cls( 

162 x["name"], 

163 x["defined_by"], 

164 x.get("dh_sequence"), 

165 doc.get("uris", []), 

166 doc.get("synopsis", ""), 

167 doc.get("long_description", ""), 

168 ) 

169 

170 

171def relationship_substvar_for_field(substvar: str) -> Optional[str]: 

172 relationship_fields = all_package_relationship_fields() 

173 try: 

174 col_idx = substvar.rindex(":") 

175 except ValueError: 

176 return None 

177 return relationship_fields.get(substvar[col_idx + 1 : -1].lower()) 

178 

179 

180def _as_substvars_metadata( 

181 args: List[SubstvarMetadata], 

182) -> Mapping[str, SubstvarMetadata]: 

183 r = {s.name: s for s in args} 

184 assert len(r) == len(args) 

185 return r 

186 

187 

188def substvars_metadata_basename() -> str: 

189 return "debian_control_substvars_data.yaml" 

190 

191 

192@lru_cache 

193def substvars_metadata() -> Mapping[str, SubstvarMetadata]: 

194 p = importlib.resources.files(deb822_ref_data_dir.__name__).joinpath( 

195 substvars_metadata_basename() 

196 ) 

197 

198 with p.open("r", encoding="utf-8") as fd: 

199 raw = MANIFEST_YAML.load(fd) 

200 

201 attr_path = AttributePath.root_path(p) 

202 ref = DCTRL_SUBSTVARS_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

203 return _as_substvars_metadata( 

204 [SubstvarMetadata.from_ref_data(x) for x in ref["substvars"]] 

205 ) 

206 

207 

208_DCTRL_FILE_METADATA = DctrlFileMetadata() 

209 

210 

211lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION) 

212 

213 

214@lsp_hover(_DISPATCH_RULE) 

215def _debian_control_hover( 

216 ls: "DebputyLanguageServer", 

217 params: HoverParams, 

218) -> Optional[Hover]: 

219 return deb822_hover(ls, params, _DCTRL_FILE_METADATA, custom_handler=_custom_hover) 

220 

221 

222def _custom_hover_description( 

223 _ls: "DebputyLanguageServer", 

224 _known_field: DctrlKnownField, 

225 line: str, 

226 _word_at_position: str, 

227) -> Optional[Union[Hover, str]]: 

228 if line[0].isspace(): 228 ↛ 229line 228 didn't jump to line 229 because the condition on line 228 was never true

229 return None 

230 try: 

231 col_idx = line.index(":") 

232 except ValueError: 

233 return None 

234 

235 content = line[col_idx + 1 :].strip() 

236 

237 # Synopsis 

238 return textwrap.dedent( 

239 f"""\ 

240 \ 

241 # Package synopsis 

242 

243 The synopsis functions as a phrase describing the package, not a 

244 complete sentence, so sentential punctuation is inappropriate: it 

245 does not need extra capital letters or a final period (full stop). 

246 It should also omit any initial indefinite or definite article 

247 - "a", "an", or "the". Thus for instance: 

248 

249 ``` 

250 Package: libeg0 

251 Description: exemplification support library 

252 ``` 

253 

254 Technically this is a noun phrase minus articles, as opposed to a 

255 verb phrase. A good heuristic is that it should be possible to 

256 substitute the package name and synopsis into this formula: 

257 

258 ``` 

259 # Generic 

260 The package provides { a,an,the,some} synopsis. 

261 

262 # The current package for comparison 

263 The package provides { a,an,the,some} {content}. 

264 ``` 

265 

266 Other advice for writing synopsis: 

267 * Avoid using the package name. Any software would display the 

268 package name already and it generally does not help the user 

269 understand what they are looking at. 

270 * In many situations, the user will only see the package name 

271 and its synopsis. The synopsis must be able to stand alone. 

272 

273 **Example renderings in various terminal UIs**: 

274 ``` 

275 # apt search TERM 

276 package/stable,now 1.0-1 all: 

277 {content} 

278 

279 # apt-get search TERM 

280 package - {content} 

281 ``` 

282 

283 ## Reference example 

284 

285 An reference example for comparison: The Sphinx package 

286 (python3-sphinx/7.2.6-6) had the following synopsis: 

287 

288 ``` 

289 Description: documentation generator for Python projects 

290 ``` 

291 

292 In the test sentence, it would read as: 

293 

294 ``` 

295 The python3-sphinx package provides a documentation generator for Python projects. 

296 ``` 

297 

298 **Side-by-side comparison in the terminal UIs**: 

299 ``` 

300 # apt search TERM 

301 python3-sphinx/stable,now 7.2.6-6 all: 

302 documentation generator for Python projects 

303 

304 package/stable,now 1.0-1 all: 

305 {content} 

306 

307 

308 # apt-get search TERM 

309 package - {content} 

310 python3-sphinx - documentation generator for Python projects 

311 ``` 

312 """ 

313 ) 

314 

315 

316def _render_package_lookup( 

317 package_lookup: PackageLookup, 

318 known_field: DctrlKnownField, 

319) -> str: 

320 name = package_lookup.name 

321 provider = package_lookup.package 

322 if package_lookup.package is None and len(package_lookup.provided_by) == 1: 

323 provider = package_lookup.provided_by[0] 

324 

325 if provider: 

326 segments = [ 

327 f"# {name} ({provider.version}, {provider.architecture}) ", 

328 "", 

329 ] 

330 

331 if ( 

332 _is_bd_field(known_field) 

333 and name.startswith("dh-sequence-") 

334 and len(name) > 12 

335 ): 

336 sequence = name[12:] 

337 segments.append( 

338 f"This build-dependency will activate the `dh` sequence called `{sequence}`." 

339 ) 

340 segments.append("") 

341 

342 elif ( 

343 known_field.name == "Build-Depends" 

344 and name.startswith("debputy-plugin-") 

345 and len(name) > 15 

346 ): 

347 plugin_name = name[15:] 

348 segments.append( 

349 f"This build-dependency will activate the `debputy` plugin called `{plugin_name}`." 

350 ) 

351 segments.append("") 

352 

353 segments.extend( 

354 [ 

355 f"Synopsis: {provider.synopsis}", 

356 "", 

357 f"Multi-Arch: {provider.multi_arch}", 

358 "", 

359 f"Section: {provider.section}", 

360 ] 

361 ) 

362 if provider.upstream_homepage is not None: 

363 segments.append("") 

364 segments.append(f"Upstream homepage: {provider.upstream_homepage}") 

365 segments.append("") 

366 segments.append( 

367 "Data is from the system's APT cache, which may not match the target distribution." 

368 ) 

369 return "\n".join(segments) 

370 

371 segments = [ 

372 f"# {name} [virtual]", 

373 "", 

374 "The package {name} is a virtual package provided by one of:", 

375 ] 

376 segments.extend(f" * {p.name}" for p in package_lookup.provided_by) 

377 segments.append("") 

378 segments.append( 

379 "Data is from the system's APT cache, which may not match the target distribution." 

380 ) 

381 return "\n".join(segments) 

382 

383 

384def _disclaimer(is_empty: bool) -> str: 

385 if is_empty: 

386 return textwrap.dedent( 

387 """\ 

388 The system's APT cache is empty, so it was not possible to verify that the 

389 package exist. 

390""" 

391 ) 

392 return textwrap.dedent( 

393 """\ 

394 The package is not known by the APT cache on this system, so there may be typo 

395 or the package may not be available in the version of your distribution. 

396""" 

397 ) 

398 

399 

400def _render_package_by_name( 

401 name: str, known_field: DctrlKnownField, is_empty: bool 

402) -> Optional[str]: 

403 if _is_bd_field(known_field) and name.startswith("dh-sequence-") and len(name) > 12: 

404 sequence = name[12:] 

405 return ( 

406 textwrap.dedent( 

407 f"""\ 

408 \ 

409 # {name} 

410 

411 This build-dependency will activate the `dh` sequence called `{sequence}`. 

412 

413 """ 

414 ) 

415 + _disclaimer(is_empty) 

416 ) 

417 if ( 

418 known_field.name == "Build-Depends" 

419 and name.startswith("debputy-plugin-") 

420 and len(name) > 15 

421 ): 

422 plugin_name = name[15:] 

423 return ( 

424 textwrap.dedent( 

425 f"""\ 

426 \ 

427 # {name} 

428 

429 This build-dependency will activate the `debputy` plugin called `{plugin_name}`. 

430 

431 """ 

432 ) 

433 + _disclaimer(is_empty) 

434 ) 

435 return ( 

436 textwrap.dedent( 

437 f"""\ 

438 \ 

439 # {name} 

440 

441 """ 

442 ) 

443 + _disclaimer(is_empty) 

444 ) 

445 

446 

447def _is_bd_field(known_field: DctrlKnownField) -> bool: 

448 return known_field.name in ( 

449 "Build-Depends", 

450 "Build-Depends-Arch", 

451 "Build-Depends-Indep", 

452 ) 

453 

454 

455def _custom_hover_relationship_field( 

456 ls: "DebputyLanguageServer", 

457 known_field: DctrlKnownField, 

458 _line: str, 

459 word_at_position: str, 

460) -> Optional[Union[Hover, str]]: 

461 apt_cache = ls.apt_cache 461 ↛ 462line 461 didn't jump to line 462 because the condition on line 461 was never true

462 state = apt_cache.state 

463 is_empty = False 

464 _info(f"Rel field: {known_field.name} - {word_at_position} - {state}") 

465 if "|" in word_at_position: 

466 return textwrap.dedent( 

467 f"""\ 

468 \ 

469 Sorry, no hover docs for OR relations at the moment. 

470 

471 The relation being matched: `{word_at_position}` 

472 472 ↛ 473line 472 didn't jump to line 473 because the condition on line 472 was never true

473 The code is missing logic to determine which side of the OR the lookup is happening. 

474 """ 

475 ) 475 ↛ 476line 475 didn't jump to line 476 because the condition on line 475 was never true

476 match = next(iter(PKGNAME_REGEX.finditer(word_at_position)), None) 

477 if match is None: 

478 return 478 ↛ 479line 478 didn't jump to line 479 because the condition on line 478 was never true

479 package = match.group() 

480 if state == "empty-cache": 

481 state = "loaded" 

482 is_empty = True 

483 if state == "loaded": 

484 result = apt_cache.lookup(package) 

485 if result is None: 

486 return _render_package_by_name( 

487 package, 

488 known_field, 488 ↛ 502line 488 didn't jump to line 502 because the condition on line 488 was always true

489 is_empty=is_empty, 

490 ) 

491 return _render_package_lookup(result, known_field) 

492 

493 if state in ( 

494 "not-loaded", 

495 "failed", 

496 "tooling-not-available", 

497 ): 

498 details = apt_cache.load_error if apt_cache.load_error else "N/A" 

499 return textwrap.dedent( 

500 f"""\ 

501 \ 

502 Sorry, the APT cache data is not available due to an error or missing tool. 

503 

504 Details: {details} 

505 """ 

506 ) 

507 

508 if state == "empty-cache": 

509 return f"Cannot lookup {package}: APT cache data was empty" 

510 

511 if state == "loading": 

512 return f"Cannot lookup {package}: APT cache data is still being indexed. Please try again in a moment." 

513 return None 

514 

515 

516_CUSTOM_FIELD_HOVER = { 

517 field: _custom_hover_relationship_field 

518 for field in chain( 

519 all_package_relationship_fields().values(), 

520 all_source_relationship_fields().values(), 

521 ) 

522 if field != "Provides" 

523} 

524 

525_CUSTOM_FIELD_HOVER["Description"] = _custom_hover_description 

526 

527 

528def _custom_hover( 

529 ls: "DebputyLanguageServer", 

530 server_position: Position, 

531 _current_field: Optional[str], 

532 word_at_position: str, 

533 known_field: Optional[DctrlKnownField], 

534 in_value: bool, 

535 _doc: "TextDocument", 

536 lines: List[str], 

537) -> Optional[Union[Hover, str]]: 

538 if not in_value: 

539 return None 

540 

541 line_no = server_position.line 

542 line = lines[line_no] 

543 substvar_search_ref = server_position.character 

544 substvar = "" 

545 try: 

546 if line and line[substvar_search_ref] in ("$", "{"): 

547 substvar_search_ref += 2 

548 substvar_start = line.rindex("${", 0, substvar_search_ref) 

549 substvar_end = line.index("}", substvar_start) 

550 if server_position.character <= substvar_end: 

551 substvar = line[substvar_start : substvar_end + 1] 

552 except (ValueError, IndexError): 

553 pass 

554 554 ↛ 556line 554 didn't jump to line 556 because the condition on line 554 was never true

555 if substvar == "${}" or SUBSTVAR_RE.fullmatch(substvar): 

556 substvar_md = substvars_metadata().get(substvar) 

557 

558 computed_doc = "" 

559 for_field = relationship_substvar_for_field(substvar) 

560 if for_field: 

561 # Leading empty line is intentional! 

562 computed_doc = textwrap.dedent( 

563 f""" 

564 This substvar is a relationship substvar for the field {for_field}. 

565 Relationship substvars are automatically added in the field they 565 ↛ 566line 565 didn't jump to line 566 because the condition on line 565 was never true

566 are named after in `debhelper-compat (= 14)` or later, or with 

567 `debputy` (any integration mode after 0.1.21). 

568 """ 

569 ) 

570 

571 if substvar_md is None: 

572 doc = f"No documentation for {substvar}.\n" 

573 md_fields = "" 

574 else: 

575 doc = ls.translation(LSP_DATA_DOMAIN).pgettext( 

576 f"Substvars:{substvar_md.name}", 576 ↛ 577line 576 didn't jump to line 577 because the condition on line 576 was never true

577 substvar_md.description, 

578 ) 

579 md_fields = "\n" + substvar_md.render_metadata_fields() 579 ↛ 580line 579 didn't jump to line 580 because the condition on line 579 was never true

580 return f"# Substvar `{substvar}`\n\n{doc}{computed_doc}{md_fields}" 

581 

582 if known_field is None: 

583 return None 

584 dispatch = _CUSTOM_FIELD_HOVER.get(known_field.name) 

585 if dispatch is None: 

586 return None 

587 return dispatch(ls, known_field, line, word_at_position) 

588 

589 

590@lsp_completer(_DISPATCH_RULE) 

591def _debian_control_completions( 

592 ls: "DebputyLanguageServer", 

593 params: CompletionParams, 

594) -> Optional[Union[CompletionList, Sequence[CompletionItem]]]: 

595 return deb822_completer(ls, params, _DCTRL_FILE_METADATA) 

596 

597 

598@lsp_folding_ranges(_DISPATCH_RULE) 

599def _debian_control_folding_ranges( 

600 ls: "DebputyLanguageServer", 

601 params: FoldingRangeParams, 

602) -> Optional[Sequence[FoldingRange]]: 

603 return deb822_folding_ranges(ls, params, _DCTRL_FILE_METADATA) 

604 

605 

606@lsp_text_doc_inlay_hints(_DISPATCH_RULE) 

607async def _doc_inlay_hint( 

608 ls: "DebputyLanguageServer", 

609 params: types.InlayHintParams, 

610) -> Optional[List[InlayHint]]: 

611 doc = ls.workspace.get_text_document(params.text_document.uri) 

612 lint_state = ls.lint_state(doc) 

613 deb822_file = lint_state.parsed_deb822_file_content 

614 if not deb822_file: 

615 return None 

616 inlay_hints = [] 

617 stanzas = list(deb822_file) 

618 if len(stanzas) < 2: 

619 return None 

620 source_stanza = stanzas[0] 

621 source_stanza_pos = source_stanza.position_in_file() 

622 async for stanza_no, stanza in ls.slow_iter(enumerate(deb822_file), yield_every=20): 

623 stanza_range = stanza.range_in_parent() 

624 if stanza_no < 1: 

625 continue 

626 pkg_kvpair = stanza.get_kvpair_element(("Package", 0), use_get=True) 

627 if pkg_kvpair is None: 

628 continue 

629 stanza_def = _DCTRL_FILE_METADATA.classify_stanza(stanza, stanza_no) 

630 parts = [] 

631 async for known_field in ls.slow_iter( 

632 stanza_def.stanza_fields.values(), yield_every=25 

633 ): 

634 if ( 

635 not known_field.inheritable_from_other_stanza 

636 or not known_field.show_as_inherited 

637 or known_field.name in stanza 

638 ): 

639 continue 

640 

641 inherited_value = source_stanza.get(known_field.name) 

642 if inherited_value is not None: 

643 kvpair = source_stanza.get_kvpair_element(known_field.name) 

644 value_range_te = kvpair.range_in_parent().relative_to(source_stanza_pos) 

645 value_range = doc.position_codec.range_to_client_units( 

646 lint_state.lines, 

647 te_range_to_lsp(value_range_te), 

648 ) 

649 parts.append( 

650 types.InlayHintLabelPart( 

651 f" ({known_field.name}: {inherited_value})", 

652 tooltip="Inherited from Source stanza", 

653 location=types.Location( 

654 params.text_document.uri, 

655 value_range, 

656 ), 

657 ), 

658 ) 

659 

660 if parts: 

661 known_field = stanza_def["Package"] 

662 values = known_field.field_value_class.interpreter().interpret(pkg_kvpair) 

663 assert values is not None 

664 anchor_value = list(values.iter_value_references())[-1] 

665 anchor_position = ( 

666 anchor_value.locatable.range_in_parent().end_pos.relative_to( 

667 pkg_kvpair.value_element.position_in_parent().relative_to( 

668 stanza_range.start_pos 

669 ) 

670 ) 

671 ) 

672 anchor_position_client_units = doc.position_codec.position_to_client_units( 

673 lint_state.lines, 

674 te_position_to_lsp(anchor_position), 

675 ) 

676 inlay_hints.append( 

677 types.InlayHint( 

678 anchor_position_client_units, 

679 parts, 

680 padding_left=True, 

681 padding_right=False, 

682 ) 

683 ) 

684 return inlay_hints 

685 

686 

687def _source_package_checks( 

688 stanza: Deb822ParagraphElement, 

689 stanza_position: "TEPosition", 

690 stanza_metadata: StanzaMetadata[DctrlKnownField], 

691 lint_state: LintState, 

692) -> None: 

693 vcs_fields = {} 

694 source_fields = _DCTRL_FILE_METADATA["Source"].stanza_fields 

695 for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement): 

696 name = stanza_metadata.normalize_field_name(kvpair.field_name.lower()) 

697 if ( 

698 not name.startswith("vcs-") 

699 or name == "vcs-browser" 

700 or name not in source_fields 

701 ): 

702 continue 

703 vcs_fields[name] = kvpair 

704 

705 if len(vcs_fields) < 2: 

706 return 

707 for kvpair in vcs_fields.values(): 

708 lint_state.emit_diagnostic( 

709 kvpair.range_in_parent().relative_to(stanza_position), 

710 f'Multiple Version Control fields defined ("{kvpair.field_name}")', 

711 "warning", 

712 "Policy 5.6.26", 

713 quickfixes=[ 

714 propose_remove_range_quick_fix( 

715 proposed_title=f'Remove "{kvpair.field_name}"' 

716 ) 

717 ], 

718 ) 

719 

720 

721def _binary_package_checks( 

722 stanza: Deb822ParagraphElement, 

723 stanza_position: "TEPosition", 

724 source_stanza: Deb822ParagraphElement, 

725 representation_field_range: "TERange", 

726 lint_state: LintState, 

727) -> None: 

728 package_name = stanza.get("Package", "") 

729 source_section = source_stanza.get("Section") 

730 section_kvpair = stanza.get_kvpair_element(("Section", 0), use_get=True) 

731 section: Optional[str] = None 

732 section_range: Optional["TERange"] = None 

733 if section_kvpair is not None: 

734 section, section_range = extract_first_value_and_position( 

735 section_kvpair, 

736 stanza_position, 

737 ) 

738 

739 if section_range is None: 

740 section_range = representation_field_range 

741 effective_section = section or source_section or "unknown" 

742 package_type = stanza.get("Package-Type", "") 

743 component_prefix = "" 743 ↛ 744line 743 didn't jump to line 744 because the condition on line 743 was never true

744 if "/" in effective_section: 

745 component_prefix, effective_section = effective_section.split("/", maxsplit=1) 

746 component_prefix += "/" 

747 

748 if package_name.endswith("-udeb") or package_type == "udeb": 

749 if package_type != "udeb": 

750 package_type_kvpair = stanza.get_kvpair_element( 

751 "Package-Type", use_get=True 

752 ) 

753 package_type_range: Optional["TERange"] = None 

754 if package_type_kvpair is not None: 

755 _, package_type_range = extract_first_value_and_position( 

756 package_type_kvpair, 

757 stanza_position, 

758 ) 

759 if package_type_range is None: 

760 package_type_range = representation_field_range 

761 lint_state.emit_diagnostic( 

762 package_type_range, 

763 'The Package-Type should be "udeb" given the package name', 

764 "warning", 

765 "debputy", 

766 ) 766 ↛ 767line 766 didn't jump to line 767 because the condition on line 766 was never true

767 guessed_section = "debian-installer" 

768 section_diagnostic_rationale = " since it is an udeb" 

769 else: 

770 guessed_section = package_name_to_section(package_name) 

771 section_diagnostic_rationale = " based on the package name" 

772 if guessed_section is not None and guessed_section != effective_section: 

773 if section is not None: 

774 quickfix_data = [ 

775 propose_correct_text_quick_fix(f"{component_prefix}{guessed_section}") 

776 ] 

777 else: 

778 quickfix_data = [ 

779 propose_insert_text_on_line_after_diagnostic_quick_fix( 

780 f"Section: {component_prefix}{guessed_section}\n" 

781 ) 

782 ] 

783 assert section_range is not None # mypy hint 

784 lint_state.emit_diagnostic( 

785 section_range, 

786 f'The Section should be "{component_prefix}{guessed_section}"{section_diagnostic_rationale}', 

787 "warning", 

788 "debputy", 

789 quickfixes=quickfix_data, 

790 ) 

791 

792 

793@lint_diagnostics(_DISPATCH_RULE) 

794async def _lint_debian_control(lint_state: LintState) -> None: 

795 deb822_file = lint_state.parsed_deb822_file_content 

796 

797 first_error = await scan_for_syntax_errors_and_token_level_diagnostics( 

798 deb822_file, 

799 lint_state, 

800 ) 

801 

802 stanzas = list(deb822_file) 

803 source_stanza = stanzas[0] if stanzas else None 

804 binary_stanzas_w_pos = [] 804 ↛ 805line 804 didn't jump to line 805 because the condition on line 804 was never true

805 

806 source_stanza_metadata, binary_stanza_metadata = _DCTRL_FILE_METADATA.stanza_types() 

807 

808 async for stanza_no, stanza in lint_state.slow_iter(enumerate(stanzas, start=1)): 

809 stanza_position = stanza.position_in_file() 

810 if stanza_position.line_position >= first_error: 

811 break 

812 is_binary_stanza = stanza_no != 1 

813 if is_binary_stanza: 

814 stanza_metadata = binary_stanza_metadata 

815 other_stanza_metadata = source_stanza_metadata 

816 other_stanza_name = "Source" 

817 binary_stanzas_w_pos.append((stanza, stanza_position)) 

818 _, representation_field_range = stanza_metadata.stanza_representation( 

819 stanza, stanza_position 

820 ) 

821 _binary_package_checks( 

822 stanza, 

823 stanza_position, 

824 source_stanza, 

825 representation_field_range, 

826 lint_state, 

827 ) 

828 else: 

829 stanza_metadata = source_stanza_metadata 

830 other_stanza_metadata = binary_stanza_metadata 

831 other_stanza_name = "Binary" 

832 _source_package_checks( 

833 stanza, 

834 stanza_position, 

835 stanza_metadata, 

836 lint_state, 

837 ) 

838 

839 await stanza_metadata.stanza_diagnostics( 

840 deb822_file, 

841 stanza, 

842 stanza_position, 

843 lint_state, 

844 confusable_with_stanza_metadata=other_stanza_metadata, 

845 confusable_with_stanza_name=other_stanza_name, 

846 inherit_from_stanza=source_stanza if is_binary_stanza else None, 

847 ) 

848 

849 _detect_misspelled_packaging_files( 

850 lint_state, 

851 binary_stanzas_w_pos, 

852 ) 

853 

854 854 ↛ 855line 854 didn't jump to line 855 because the condition on line 854 was never true

855def _package_range_of_stanza( 

856 binary_stanzas: List[Tuple[Deb822ParagraphElement, TEPosition]], 

857) -> Iterable[Tuple[str, Optional[str], "TERange"]]: 

858 for stanza, stanza_position in binary_stanzas: 

859 kvpair = stanza.get_kvpair_element(("Package", 0), use_get=True) 

860 if kvpair is None: 

861 continue 

862 representation_field_range = kvpair.range_in_parent().relative_to( 

863 stanza_position 

864 ) 

865 yield stanza["Package"], stanza.get("Architecture"), representation_field_range 

866 

867 

868def _packaging_files( 

869 lint_state: LintState, 

870) -> Iterable[PackagerProvidedFile]: 

871 source_root = lint_state.source_root 

872 debian_dir = lint_state.debian_dir 

873 binary_packages = lint_state.binary_packages 

874 if ( 

875 source_root is None 

876 or not source_root.has_fs_path 

877 or debian_dir is None 

878 or binary_packages is None 

879 ): 

880 return 

881 

882 debputy_integration_mode = lint_state.debputy_metadata.debputy_integration_mode 

883 dh_sequencer_data = lint_state.dh_sequencer_data 

884 dh_sequences = dh_sequencer_data.sequences 

885 is_debputy_package = debputy_integration_mode is not None 

886 feature_set = lint_state.plugin_feature_set 

887 known_packaging_files = feature_set.known_packaging_files 

888 static_packaging_files = { 

889 kpf.detection_value: kpf 

890 for kpf in known_packaging_files.values() 

891 if kpf.detection_method == "path" 

892 } 

893 ignored_path = set(static_packaging_files) 

894 

895 if is_debputy_package: 

896 all_debputy_ppfs = list( 

897 flatten_ppfs( 

898 detect_all_packager_provided_files( 

899 feature_set.packager_provided_files, 

900 debian_dir, 

901 binary_packages, 

902 allow_fuzzy_matches=True, 

903 detect_typos=True, 903 ↛ 904line 903 didn't jump to line 904 because the condition on line 903 was never true

904 ignore_paths=ignored_path, 

905 ) 

906 ) 

907 ) 

908 for ppf in all_debputy_ppfs: 

909 if ppf.path.path in ignored_path: 

910 continue 910 ↛ exitline 910 didn't return from function '_packaging_files' because the condition on line 910 was always true

911 ignored_path.add(ppf.path.path) 

912 yield ppf 

913 

914 # FIXME: This should read the editor data, but dh_assistant does not support that. 

915 dh_compat_level, _ = extract_dh_compat_level(cwd=source_root.fs_path) 

916 if dh_compat_level is not None: 

917 debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin() 

918 dh_pkgfile_docs = { 

919 kpf.detection_value: kpf 

920 for kpf in known_packaging_files.values() 

921 if kpf.detection_method == "dh.pkgfile" 

922 } 

923 ( 

924 all_dh_ppfs, 

925 _, 

926 _, 

927 ) = resolve_debhelper_config_files( 

928 debian_dir, 

929 binary_packages, 

930 debputy_plugin_metadata, 

931 dh_pkgfile_docs, 

932 dh_sequences, 

933 dh_compat_level, 

934 saw_dh=dh_sequencer_data.uses_dh_sequencer, 934 ↛ 935line 934 didn't jump to line 935 because the condition on line 934 was never true

935 ignore_paths=ignored_path, 

936 debputy_integration_mode=debputy_integration_mode, 

937 cwd=source_root.fs_path, 

938 ) 

939 for ppf in all_dh_ppfs: 

940 if ppf.path.path in ignored_path: 

941 continue 

942 ignored_path.add(ppf.path.path) 

943 yield ppf 

944 

945 

946def _detect_misspelled_packaging_files( 

947 lint_state: LintState, 

948 binary_stanzas_w_pos: List[Tuple[Deb822ParagraphElement, TEPosition]], 

949) -> None: 

950 stanza_ranges = { 

951 p: (a, r) for p, a, r in _package_range_of_stanza(binary_stanzas_w_pos) 

952 } 952 ↛ 953line 952 didn't jump to line 953 because the condition on line 952 was never true

953 for ppf in _packaging_files(lint_state): 

954 binary_package = ppf.package_name 

955 explicit_package = ppf.uses_explicit_package_name 

956 name_segment = ppf.name_segment is not None 956 ↛ 957line 956 didn't jump to line 957 because the condition on line 956 was never true

957 stem = ppf.definition.stem 

958 if _is_trace_log_enabled(): 

959 _trace_log( 959 ↛ 960line 959 didn't jump to line 960 because the condition on line 959 was never true

960 f"PPF check: {binary_package} {stem=} {explicit_package=} {name_segment=} {ppf.expected_path=} {ppf.definition.has_active_command=}" 

961 ) 

962 if binary_package is None or stem is None: 962 ↛ 963line 962 didn't jump to line 963 because the condition on line 962 was never true

963 continue 

964 res = stanza_ranges.get(binary_package) 

965 if res is None: 

966 continue 

967 declared_arch, diag_range = res 

968 if diag_range is None: 

969 continue 

970 path = ppf.path.path 

971 likely_typo_of = ppf.expected_path 

972 arch_restriction = ppf.architecture_restriction 

973 if likely_typo_of is not None: 

974 # Handles arch_restriction == 'all' at the same time due to how 

975 # the `likely-typo-of` is created 

976 lint_state.emit_diagnostic( 

977 diag_range, 

978 f'The file "{path}" is likely a typo of "{likely_typo_of}"', 978 ↛ 979line 978 didn't jump to line 979 because the condition on line 978 was never true

979 "warning", 

980 "debputy", 

981 diagnostic_applies_to_another_file=path, 

982 ) 

983 continue 

984 if declared_arch == "all" and arch_restriction is not None: 

985 lint_state.emit_diagnostic( 

986 diag_range, 

987 f'The file "{path}" has an architecture restriction but is for an `arch:all` package, so' 987 ↛ 988line 987 didn't jump to line 988 because the condition on line 987 was never true

988 f" the restriction does not make sense.", 

989 "warning", 

990 "debputy", 

991 diagnostic_applies_to_another_file=path, 

992 ) 

993 elif arch_restriction == "all": 

994 lint_state.emit_diagnostic( 

995 diag_range, 

996 f'The file "{path}" has an architecture restriction of `all` rather than a real architecture', 996 ↛ 997line 996 didn't jump to line 997 because the condition on line 996 was never true

997 "warning", 

998 "debputy", 

999 diagnostic_applies_to_another_file=path, 

1000 ) 

1001 

1002 if not ppf.definition.has_active_command: 

1003 lint_state.emit_diagnostic( 

1004 diag_range, 

1005 f"The file {path} is related to a command that is not active in the dh sequence" 

1006 " with the current addons", 

1007 "warning", 

1008 "debputy", 

1009 diagnostic_applies_to_another_file=path, 

1010 ) 

1011 continue 

1012 1012 ↛ 1013line 1012 didn't jump to line 1013 because the condition on line 1012 was never true

1013 if not explicit_package and name_segment is not None: 

1014 basename = os.path.basename(path) 

1015 if basename == ppf.definition.stem: 

1016 continue 

1017 alt_name = f"{binary_package}.{stem}" 

1018 if arch_restriction is not None: 

1019 alt_name = f"{alt_name}.{arch_restriction}" 

1020 if ppf.definition.allow_name_segment: 

1021 or_alt_name = f' (or maybe "debian/{binary_package}.{basename}")' 

1022 else: 

1023 or_alt_name = "" 

1024 

1025 lint_state.emit_diagnostic( 

1026 diag_range, 

1027 f'Possible typo in "{path}". Consider renaming the file to "debian/{alt_name}"' 

1028 f"{or_alt_name} if it is intended for {binary_package}", 

1029 "warning", 

1030 "debputy", 

1031 diagnostic_applies_to_another_file=path, 

1032 ) 

1033 

1034 

1035@lsp_will_save_wait_until(_DISPATCH_RULE) 

1036def _debian_control_on_save_formatting( 

1037 ls: "DebputyLanguageServer", 

1038 params: WillSaveTextDocumentParams, 

1039) -> Optional[Sequence[TextEdit]]: 

1040 doc = ls.workspace.get_text_document(params.text_document.uri) 

1041 lint_state = ls.lint_state(doc) 

1042 return _reformat_debian_control(lint_state) 

1043 

1044 

1045def _reformat_debian_control( 

1046 lint_state: LintState, 

1047) -> Optional[Sequence[TextEdit]]: 

1048 return deb822_format_file(lint_state, _DCTRL_FILE_METADATA) 

1049 

1050 

1051@lsp_format_document(_DISPATCH_RULE) 

1052def _debian_control_format_file( 

1053 ls: "DebputyLanguageServer", 

1054 params: DocumentFormattingParams, 

1055) -> Optional[Sequence[TextEdit]]: 

1056 doc = ls.workspace.get_text_document(params.text_document.uri) 

1057 lint_state = ls.lint_state(doc) 

1058 return _reformat_debian_control(lint_state) 

1059 

1060 

1061@lsp_semantic_tokens_full(_DISPATCH_RULE) 

1062async def _debian_control_semantic_tokens_full( 

1063 ls: "DebputyLanguageServer", 

1064 request: SemanticTokensParams, 

1065) -> Optional[SemanticTokens]: 

1066 return await deb822_semantic_tokens_full( 

1067 ls, 

1068 request, 

1069 _DCTRL_FILE_METADATA, 

1070 )