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

420 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1import dataclasses 

2import importlib.resources 

3import os.path 

4import textwrap 

5 

6import debputy.lsp.data.deb822_data as deb822_ref_data_dir 

7 

8from functools import lru_cache 

9from itertools import chain 

10from typing import ( 

11 Union, 

12 Sequence, 

13 Tuple, 

14 Optional, 

15 Mapping, 

16 List, 

17 Iterable, 

18 Self, 

19) 

20 

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_token_iter, 

60 deb822_format_file, 

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 Deb822FileElement, 

75 Deb822ParagraphElement, 

76) 

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

78 Deb822KeyValuePairElement, 

79) 

80from debputy.lsprotocol.types import ( 

81 Position, 

82 FoldingRange, 

83 FoldingRangeParams, 

84 CompletionItem, 

85 CompletionList, 

86 CompletionParams, 

87 Location, 

88 HoverParams, 

89 Hover, 

90 TEXT_DOCUMENT_CODE_ACTION, 

91 SemanticTokens, 

92 SemanticTokensParams, 

93 WillSaveTextDocumentParams, 

94 TextEdit, 

95 DocumentFormattingParams, 

96 InlayHintParams, 

97 InlayHint, 

98 InlayHintLabelPart, 

99) 

100from debputy.manifest_parser.util import AttributePath 

101from debputy.packager_provided_files import ( 

102 PackagerProvidedFile, 

103 detect_all_packager_provided_files, 

104) 

105from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin 

106from debputy.util import PKGNAME_REGEX, _info 

107from debputy.yaml import MANIFEST_YAML 

108 

109try: 

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

111 Position as TEPosition, 

112 Range as TERange, 

113 START_POSITION, 

114 ) 

115 

116 from pygls.workspace import TextDocument 

117except ImportError: 

118 pass 

119 

120 

121_DISPATCH_RULE = LanguageDispatchRule.new_rule( 

122 "debian/control", 

123 "debian/control", 

124 [ 

125 # emacs's name 

126 SecondaryLanguage("debian-control"), 

127 # vim's name 

128 SecondaryLanguage("debcontrol"), 

129 ], 

130) 

131 

132 

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

134class SubstvarMetadata: 

135 name: str 

136 defined_by: str 

137 dh_sequence: Optional[str] 

138 doc_uris: Sequence[str] 

139 synopsis: str 

140 description: str 

141 

142 def render_metadata_fields(self) -> str: 

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

144 dh_seq = ( 

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

146 ) 

147 doc_uris = self.doc_uris 

148 parts = [def_by, dh_seq] 

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

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

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

152 else: 

153 parts.append("Documentation:") 

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

155 return "\n".join(parts) 

156 

157 @classmethod 

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

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

160 return cls( 

161 x["name"], 

162 x["defined_by"], 

163 x.get("dh_sequence"), 

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

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

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

167 ) 

168 

169 

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

171 relationship_fields = all_package_relationship_fields() 

172 try: 

173 col_idx = substvar.rindex(":") 

174 except ValueError: 

175 return None 

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

177 

178 

179def _as_substvars_metadata( 

180 args: List[SubstvarMetadata], 

181) -> Mapping[str, SubstvarMetadata]: 

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

183 assert len(r) == len(args) 

184 return r 

185 

186 

187def substvars_metadata_basename() -> str: 

188 return "debian_control_substvars_data.yaml" 

189 

190 

191@lru_cache 

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

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

194 substvars_metadata_basename() 

195 ) 

196 

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

198 raw = MANIFEST_YAML.load(fd) 

199 

200 attr_path = AttributePath.root_path(p) 

201 ref = DCTRL_SUBSTVARS_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

202 return _as_substvars_metadata( 

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

204 ) 

205 

206 

207_DCTRL_FILE_METADATA = DctrlFileMetadata() 

208 

209 

210lsp_standard_handler(_DISPATCH_RULE, TEXT_DOCUMENT_CODE_ACTION) 

211 

212 

213@lsp_hover(_DISPATCH_RULE) 

214def _debian_control_hover( 

215 ls: "DebputyLanguageServer", 

216 params: HoverParams, 

217) -> Optional[Hover]: 

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

219 

220 

221def _custom_hover_description( 

222 _ls: "DebputyLanguageServer", 

223 _known_field: DctrlKnownField, 

224 line: str, 

225 _word_at_position: str, 

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

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

228 return None 

229 try: 

230 col_idx = line.index(":") 

231 except ValueError: 

232 return None 

233 

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

235 

236 # Synopsis 

237 return textwrap.dedent( 

238 f"""\ 

239 \ 

240 # Package synopsis 

241 

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

243 complete sentence, so sentential punctuation is inappropriate: it 

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

245 It should also omit any initial indefinite or definite article 

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

247 

248 ``` 

249 Package: libeg0 

250 Description: exemplification support library 

251 ``` 

252 

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

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

255 substitute the package name and synopsis into this formula: 

256 

257 ``` 

258 # Generic 

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

260 

261 # The current package for comparison 

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

263 ``` 

264 

265 Other advice for writing synopsis: 

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

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

268 understand what they are looking at. 

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

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

271 

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

273 ``` 

274 # apt search TERM 

275 package/stable,now 1.0-1 all: 

276 {content} 

277 

278 # apt-get search TERM 

279 package - {content} 

280 ``` 

281 

282 ## Reference example 

283 

284 An reference example for comparison: The Sphinx package 

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

286 

287 ``` 

288 Description: documentation generator for Python projects 

289 ``` 

290 

291 In the test sentence, it would read as: 

292 

293 ``` 

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

295 ``` 

296 

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

298 ``` 

299 # apt search TERM 

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

301 documentation generator for Python projects 

302 

303 package/stable,now 1.0-1 all: 

304 {content} 

305 

306 

307 # apt-get search TERM 

308 package - {content} 

309 python3-sphinx - documentation generator for Python projects 

310 ``` 

311 """ 

312 ) 

313 

314 

315def _render_package_lookup( 

316 package_lookup: PackageLookup, 

317 known_field: DctrlKnownField, 

318) -> str: 

319 name = package_lookup.name 

320 provider = package_lookup.package 

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

322 provider = package_lookup.provided_by[0] 

323 

324 if provider: 

325 segments = [ 

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

327 "", 

328 ] 

329 

330 if ( 

331 _is_bd_field(known_field) 

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

333 and len(name) > 12 

334 ): 

335 sequence = name[12:] 

336 segments.append( 

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

338 ) 

339 segments.append("") 

340 

341 elif ( 

342 known_field.name == "Build-Depends" 

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

344 and len(name) > 15 

345 ): 

346 plugin_name = name[15:] 

347 segments.append( 

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

349 ) 

350 segments.append("") 

351 

352 segments.extend( 

353 [ 

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

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

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

357 ] 

358 ) 

359 if provider.upstream_homepage is not None: 

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

361 segments.append("") 

362 segments.append( 

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

364 ) 

365 return "\n".join(segments) 

366 

367 segments = [ 

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

369 "", 

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

371 ] 

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

373 segments.append("") 

374 segments.append( 

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

376 ) 

377 return "\n".join(segments) 

378 

379 

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

381 if is_empty: 

382 return textwrap.dedent( 

383 """\ 

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

385 package exist. 

386""" 

387 ) 

388 return textwrap.dedent( 

389 """\ 

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

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

392""" 

393 ) 

394 

395 

396def _render_package_by_name( 

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

398) -> Optional[str]: 

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

400 sequence = name[12:] 

401 return ( 

402 textwrap.dedent( 

403 f"""\ 

404 \ 

405 # {name} 

406 

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

408 

409 """ 

410 ) 

411 + _disclaimer(is_empty) 

412 ) 

413 if ( 

414 known_field.name == "Build-Depends" 

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

416 and len(name) > 15 

417 ): 

418 plugin_name = name[15:] 

419 return ( 

420 textwrap.dedent( 

421 f"""\ 

422 \ 

423 # {name} 

424 

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

426 

427 """ 

428 ) 

429 + _disclaimer(is_empty) 

430 ) 

431 return ( 

432 textwrap.dedent( 

433 f"""\ 

434 \ 

435 # {name} 

436 

437 """ 

438 ) 

439 + _disclaimer(is_empty) 

440 ) 

441 

442 

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

444 return known_field.name in ( 

445 "Build-Depends", 

446 "Build-Depends-Arch", 

447 "Build-Depends-Indep", 

448 ) 

449 

450 

451def _custom_hover_relationship_field( 

452 ls: "DebputyLanguageServer", 

453 known_field: DctrlKnownField, 

454 _line: str, 

455 word_at_position: str, 

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

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

458 state = apt_cache.state 

459 is_empty = False 

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

461 if "|" in word_at_position: 

462 return textwrap.dedent( 

463 f"""\ 

464 \ 

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

466 

467 The relation being matched: `{word_at_position}` 

468 468 ↛ 469line 468 didn't jump to line 469 because the condition on line 468 was never true

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

470 """ 

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

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

473 if match is None: 

474 return 474 ↛ 475line 474 didn't jump to line 475 because the condition on line 474 was never true

475 package = match.group() 

476 if state == "empty-cache": 

477 state = "loaded" 

478 is_empty = True 

479 if state == "loaded": 

480 result = apt_cache.lookup(package) 

481 if result is None: 

482 return _render_package_by_name( 

483 package, 

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

485 is_empty=is_empty, 

486 ) 

487 return _render_package_lookup(result, known_field) 

488 

489 if state in ( 

490 "not-loaded", 

491 "failed", 

492 "tooling-not-available", 

493 ): 

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

495 return textwrap.dedent( 

496 f"""\ 

497 \ 

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

499 

500 Details: {details} 

501 """ 

502 ) 

503 

504 if state == "empty-cache": 

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

506 

507 if state == "loading": 

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

509 return None 

510 

511 

512_CUSTOM_FIELD_HOVER = { 

513 field: _custom_hover_relationship_field 

514 for field in chain( 

515 all_package_relationship_fields().values(), 

516 all_source_relationship_fields().values(), 

517 ) 

518 if field != "Provides" 

519} 

520 

521_CUSTOM_FIELD_HOVER["Description"] = _custom_hover_description 

522 

523 

524def _custom_hover( 

525 ls: "DebputyLanguageServer", 

526 server_position: Position, 

527 _current_field: Optional[str], 

528 word_at_position: str, 

529 known_field: Optional[DctrlKnownField], 

530 in_value: bool, 

531 _doc: "TextDocument", 

532 lines: List[str], 

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

534 if not in_value: 

535 return None 

536 

537 line_no = server_position.line 

538 line = lines[line_no] 

539 substvar_search_ref = server_position.character 

540 substvar = "" 

541 try: 

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

543 substvar_search_ref += 2 

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

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

546 if server_position.character <= substvar_end: 

547 substvar = line[substvar_start : substvar_end + 1] 

548 except (ValueError, IndexError): 

549 pass 

550 550 ↛ 552line 550 didn't jump to line 552 because the condition on line 550 was never true

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

552 substvar_md = substvars_metadata().get(substvar) 

553 

554 computed_doc = "" 

555 for_field = relationship_substvar_for_field(substvar) 

556 if for_field: 

557 # Leading empty line is intentional! 

558 computed_doc = textwrap.dedent( 

559 f""" 

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

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

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

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

564 """ 

565 ) 

566 

567 if substvar_md is None: 

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

569 md_fields = "" 

570 else: 

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

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

573 substvar_md.description, 

574 ) 

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

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

577 

578 if known_field is None: 

579 return None 

580 dispatch = _CUSTOM_FIELD_HOVER.get(known_field.name) 

581 if dispatch is None: 

582 return None 

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

584 

585 

586@lsp_completer(_DISPATCH_RULE) 

587def _debian_control_completions( 

588 ls: "DebputyLanguageServer", 

589 params: CompletionParams, 

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

591 return deb822_completer(ls, params, _DCTRL_FILE_METADATA) 

592 

593 

594@lsp_folding_ranges(_DISPATCH_RULE) 

595def _debian_control_folding_ranges( 

596 ls: "DebputyLanguageServer", 

597 params: FoldingRangeParams, 

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

599 return deb822_folding_ranges(ls, params, _DCTRL_FILE_METADATA) 

600 

601 

602@lsp_text_doc_inlay_hints(_DISPATCH_RULE) 

603def _doc_inlay_hint( 

604 ls: "DebputyLanguageServer", 

605 params: InlayHintParams, 

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

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

608 lint_state = ls.lint_state(doc) 

609 deb822_file = lint_state.parsed_deb822_file_content 

610 if not deb822_file: 

611 return None 

612 inlay_hints = [] 

613 stanzas = list(deb822_file) 

614 if len(stanzas) < 2: 

615 return None 

616 source_stanza = stanzas[0] 

617 source_stanza_pos = source_stanza.position_in_file() 

618 for stanza_no, stanza in enumerate(deb822_file): 

619 stanza_range = stanza.range_in_parent() 

620 if stanza_no < 1: 

621 continue 

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

623 if pkg_kvpair is None: 

624 continue 

625 

626 inlay_hint_pos_te = pkg_kvpair.range_in_parent().end_pos.relative_to( 

627 stanza_range.start_pos 

628 ) 

629 inlay_hint_pos = doc.position_codec.position_to_client_units( 

630 lint_state.lines, 

631 te_position_to_lsp(inlay_hint_pos_te), 

632 ) 

633 stanza_def = _DCTRL_FILE_METADATA.classify_stanza(stanza, stanza_no) 

634 for known_field in stanza_def.stanza_fields.values(): 

635 if ( 

636 not known_field.inheritable_from_other_stanza 

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 inlay_hints.append( 

650 InlayHint( 

651 inlay_hint_pos, 

652 [ 

653 InlayHintLabelPart( 

654 f"{known_field.name}: {inherited_value}\n", 

655 tooltip="Inherited from Source stanza", 

656 location=Location( 

657 params.text_document.uri, 

658 value_range, 

659 ), 

660 ), 

661 ], 

662 ) 

663 ) 

664 

665 return inlay_hints 

666 

667 

668def _source_package_checks( 

669 stanza: Deb822ParagraphElement, 

670 stanza_position: "TEPosition", 

671 stanza_metadata: StanzaMetadata[DctrlKnownField], 

672 lint_state: LintState, 

673) -> None: 

674 vcs_fields = {} 

675 source_fields = _DCTRL_FILE_METADATA["Source"].stanza_fields 

676 for kvpair in stanza.iter_parts_of_type(Deb822KeyValuePairElement): 

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

678 if ( 

679 not name.startswith("vcs-") 

680 or name == "vcs-browser" 

681 or name not in source_fields 

682 ): 

683 continue 

684 vcs_fields[name] = kvpair 

685 

686 if len(vcs_fields) < 2: 

687 return 

688 for kvpair in vcs_fields.values(): 

689 lint_state.emit_diagnostic( 

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

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

692 "warning", 

693 "Policy 5.6.26", 

694 quickfixes=[ 

695 propose_remove_range_quick_fix( 

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

697 ) 

698 ], 

699 ) 

700 

701 

702def _binary_package_checks( 

703 stanza: Deb822ParagraphElement, 

704 stanza_position: "TEPosition", 

705 source_stanza: Deb822ParagraphElement, 

706 representation_field_range: "TERange", 

707 lint_state: LintState, 

708) -> None: 

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

710 source_section = source_stanza.get("Section") 

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

712 section: Optional[str] = None 

713 section_range: Optional["TERange"] = None 

714 if section_kvpair is not None: 

715 section, section_range = extract_first_value_and_position( 

716 section_kvpair, 

717 stanza_position, 

718 ) 

719 

720 if section_range is None: 

721 section_range = representation_field_range 

722 effective_section = section or source_section or "unknown" 

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

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

725 if "/" in effective_section: 

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

727 component_prefix += "/" 

728 

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

730 if package_type != "udeb": 

731 package_type_kvpair = stanza.get_kvpair_element( 

732 "Package-Type", use_get=True 

733 ) 

734 package_type_range: Optional["TERange"] = None 

735 if package_type_kvpair is not None: 

736 _, package_type_range = extract_first_value_and_position( 

737 package_type_kvpair, 

738 stanza_position, 

739 ) 

740 if package_type_range is None: 

741 package_type_range = representation_field_range 

742 lint_state.emit_diagnostic( 

743 package_type_range, 

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

745 "warning", 

746 "debputy", 

747 ) 747 ↛ 748line 747 didn't jump to line 748 because the condition on line 747 was never true

748 guessed_section = "debian-installer" 

749 section_diagnostic_rationale = " since it is an udeb" 

750 else: 

751 guessed_section = package_name_to_section(package_name) 

752 section_diagnostic_rationale = " based on the package name" 

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

754 if section is not None: 

755 quickfix_data = [ 

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

757 ] 

758 else: 

759 quickfix_data = [ 

760 propose_insert_text_on_line_after_diagnostic_quick_fix( 

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

762 ) 

763 ] 

764 assert section_range is not None # mypy hint 

765 lint_state.emit_diagnostic( 

766 section_range, 

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

768 "warning", 

769 "debputy", 

770 quickfixes=quickfix_data, 

771 ) 

772 

773 

774def _scan_for_syntax_errors_and_token_level_diagnostics( 

775 deb822_file: Deb822FileElement, 

776 lint_state: LintState, 

777) -> int: 

778 first_error = len(lint_state.lines) + 1 

779 spell_checker = lint_state.spellchecker() 

780 for ( 

781 token, 781 ↛ 782line 781 didn't jump to line 782 because the condition on line 781 was never true

782 start_line, 

783 start_offset, 

784 end_line, 

785 end_offset, 

786 ) in deb822_token_iter(deb822_file.iter_tokens()): 

787 if token.is_error: 

788 first_error = min(first_error, start_line) 

789 start_pos = TEPosition( 

790 start_line, 

791 start_offset, 

792 ) 

793 end_pos = TEPosition( 

794 end_line, 

795 end_offset, 

796 ) 

797 token_range = TERange.between(start_pos, end_pos) 

798 lint_state.emit_diagnostic( 

799 token_range, 

800 "Syntax error", 

801 "error", 801 ↛ 803line 801 didn't jump to line 803 because the condition on line 801 was always true

802 "debputy", 

803 ) 

804 elif token.is_comment: 

805 for word, col_pos, end_col_pos in spell_checker.iter_words(token.text): 

806 corrections = spell_checker.provide_corrections_for(word) 

807 if not corrections: 

808 continue 

809 start_pos = TEPosition( 

810 start_line, 

811 col_pos, 

812 ) 

813 end_pos = TEPosition( 

814 start_line, 

815 end_col_pos, 

816 ) 

817 word_range = TERange.between(start_pos, end_pos) 

818 lint_state.emit_diagnostic( 

819 word_range, 

820 f'Spelling "{word}"', 

821 "spelling", 

822 "debputy", 

823 quickfixes=[propose_correct_text_quick_fix(c) for c in corrections], 

824 enable_non_interactive_auto_fix=False, 

825 ) 

826 return first_error 

827 

828 

829@lint_diagnostics(_DISPATCH_RULE) 

830def _lint_debian_control(lint_state: LintState) -> None: 

831 doc_reference = lint_state.doc_uri 

832 deb822_file = lint_state.parsed_deb822_file_content 

833 

834 first_error = _scan_for_syntax_errors_and_token_level_diagnostics( 

835 deb822_file, 

836 lint_state, 

837 ) 

838 

839 stanzas = list(deb822_file) 

840 source_stanza = stanzas[0] if stanzas else None 

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

842 

843 source_stanza_metadata, binary_stanza_metadata = _DCTRL_FILE_METADATA.stanza_types() 

844 

845 for stanza_no, stanza in enumerate(stanzas, start=1): 

846 stanza_position = stanza.position_in_file() 

847 if stanza_position.line_position >= first_error: 

848 break 

849 is_binary_stanza = stanza_no != 1 

850 if is_binary_stanza: 

851 stanza_metadata = binary_stanza_metadata 

852 other_stanza_metadata = source_stanza_metadata 

853 other_stanza_name = "Source" 

854 binary_stanzas_w_pos.append((stanza, stanza_position)) 

855 _, representation_field_range = stanza_metadata.stanza_representation( 

856 stanza, stanza_position 

857 ) 

858 _binary_package_checks( 

859 stanza, 

860 stanza_position, 

861 source_stanza, 

862 representation_field_range, 

863 lint_state, 

864 ) 

865 else: 

866 stanza_metadata = source_stanza_metadata 

867 other_stanza_metadata = binary_stanza_metadata 

868 other_stanza_name = "Binary" 

869 _source_package_checks( 

870 stanza, 

871 stanza_position, 

872 stanza_metadata, 

873 lint_state, 

874 ) 

875 

876 stanza_metadata.stanza_diagnostics( 

877 deb822_file, 

878 stanza, 

879 stanza_position, 

880 doc_reference, 

881 lint_state, 

882 confusable_with_stanza_metadata=other_stanza_metadata, 

883 confusable_with_stanza_name=other_stanza_name, 

884 inherit_from_stanza=source_stanza if is_binary_stanza else None, 

885 ) 

886 

887 _detect_misspelled_packaging_files( 

888 lint_state, 

889 binary_stanzas_w_pos, 

890 ) 

891 

892 892 ↛ 893line 892 didn't jump to line 893 because the condition on line 892 was never true

893def _package_range_of_stanza( 

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

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

896 for stanza, stanza_position in binary_stanzas: 

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

898 if kvpair is None: 

899 continue 

900 representation_field_range = kvpair.range_in_parent().relative_to( 

901 stanza_position 

902 ) 

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

904 

905 

906def _packaging_files( 

907 lint_state: LintState, 

908) -> Iterable[PackagerProvidedFile]: 

909 source_root = lint_state.source_root 

910 debian_dir = lint_state.debian_dir 

911 binary_packages = lint_state.binary_packages 

912 if ( 

913 source_root is None 

914 or not source_root.has_fs_path 

915 or debian_dir is None 

916 or binary_packages is None 

917 ): 

918 return 

919 

920 dh_sequencer_data = lint_state.dh_sequencer_data 

921 dh_sequences = dh_sequencer_data.sequences 

922 is_debputy_package = ( 

923 "debputy" in dh_sequences 

924 or "zz-debputy" in dh_sequences 

925 or "zz_debputy" in dh_sequences 

926 or "zz-debputy-rrr" in dh_sequences 

927 ) 

928 feature_set = lint_state.plugin_feature_set 

929 known_packaging_files = feature_set.known_packaging_files 

930 static_packaging_files = { 

931 kpf.detection_value: kpf 

932 for kpf in known_packaging_files.values() 

933 if kpf.detection_method == "path" 

934 } 

935 ignored_path = set(static_packaging_files) 

936 

937 if is_debputy_package: 

938 all_debputy_ppfs = list( 

939 flatten_ppfs( 

940 detect_all_packager_provided_files( 

941 feature_set.packager_provided_files, 

942 debian_dir, 

943 binary_packages, 

944 allow_fuzzy_matches=True, 

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

946 ignore_paths=ignored_path, 

947 ) 

948 ) 

949 ) 

950 for ppf in all_debputy_ppfs: 

951 if ppf.path.path in ignored_path: 

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

953 ignored_path.add(ppf.path.path) 

954 yield ppf 

955 

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

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

958 if dh_compat_level is not None: 

959 debputy_plugin_metadata = plugin_metadata_for_debputys_own_plugin() 

960 dh_pkgfile_docs = { 

961 kpf.detection_value: kpf 

962 for kpf in known_packaging_files.values() 

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

964 } 

965 ( 

966 all_dh_ppfs, 

967 _, 

968 _, 

969 ) = resolve_debhelper_config_files( 

970 debian_dir, 

971 binary_packages, 

972 debputy_plugin_metadata, 

973 dh_pkgfile_docs, 

974 dh_sequences, 974 ↛ 975line 974 didn't jump to line 975 because the condition on line 974 was never true

975 dh_compat_level, 

976 saw_dh=dh_sequencer_data.uses_dh_sequencer, 

977 ignore_paths=ignored_path, 

978 ) 

979 for ppf in all_dh_ppfs: 

980 if ppf.path.path in ignored_path: 

981 continue 

982 ignored_path.add(ppf.path.path) 

983 yield ppf 

984 

985 

986def _detect_misspelled_packaging_files( 

987 lint_state: LintState, 

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

989) -> None: 

990 stanza_ranges = { 

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

992 } 992 ↛ 993line 992 didn't jump to line 993 because the condition on line 992 was never true

993 for ppf in _packaging_files(lint_state): 

994 binary_package = ppf.package_name 

995 explicit_package = ppf.uses_explicit_package_name 995 ↛ 996line 995 didn't jump to line 996 because the condition on line 995 was never true

996 name_segment = ppf.name_segment is not None 

997 stem = ppf.definition.stem 

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

999 continue 

1000 res = stanza_ranges.get(binary_package) 

1001 if res is None: 

1002 continue 

1003 declared_arch, diag_range = res 

1004 if diag_range is None: 

1005 continue 

1006 path = ppf.path.path 

1007 likely_typo_of = ppf.expected_path 

1008 arch_restriction = ppf.architecture_restriction 

1009 if likely_typo_of is not None: 

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

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

1012 lint_state.emit_diagnostic( 

1013 diag_range, 

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

1015 "warning", 

1016 "debputy", 

1017 diagnostic_applies_to_another_file=path, 

1018 ) 

1019 continue 

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

1021 lint_state.emit_diagnostic( 

1022 diag_range, 

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

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

1025 "warning", 

1026 "debputy", 

1027 diagnostic_applies_to_another_file=path, 

1028 ) 

1029 elif arch_restriction == "all": 

1030 lint_state.emit_diagnostic( 

1031 diag_range, 

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

1033 "warning", 

1034 "debputy", 

1035 diagnostic_applies_to_another_file=path, 

1036 ) 

1037 

1038 if not ppf.definition.has_active_command: 

1039 lint_state.emit_diagnostic( 

1040 diag_range, 

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

1042 " with the current addons", 

1043 "warning", 

1044 "debputy", 

1045 diagnostic_applies_to_another_file=path, 

1046 ) 

1047 continue 

1048 1048 ↛ 1049line 1048 didn't jump to line 1049 because the condition on line 1048 was never true

1049 if not explicit_package and name_segment is not None: 

1050 basename = os.path.basename(path) 

1051 if basename == ppf.definition.stem: 

1052 continue 

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

1054 if arch_restriction is not None: 

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

1056 if ppf.definition.allow_name_segment: 

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

1058 else: 

1059 or_alt_name = "" 

1060 

1061 lint_state.emit_diagnostic( 

1062 diag_range, 

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

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

1065 "warning", 

1066 "debputy", 

1067 diagnostic_applies_to_another_file=path, 

1068 ) 

1069 

1070 

1071@lsp_will_save_wait_until(_DISPATCH_RULE) 

1072def _debian_control_on_save_formatting( 

1073 ls: "DebputyLanguageServer", 

1074 params: WillSaveTextDocumentParams, 

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

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

1077 lint_state = ls.lint_state(doc) 

1078 return _reformat_debian_control(lint_state) 

1079 

1080 

1081def _reformat_debian_control( 

1082 lint_state: LintState, 

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

1084 return deb822_format_file(lint_state, _DCTRL_FILE_METADATA) 

1085 

1086 

1087@lsp_format_document(_DISPATCH_RULE) 

1088def _debian_control_format_file( 

1089 ls: "DebputyLanguageServer", 

1090 params: DocumentFormattingParams, 

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

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

1093 lint_state = ls.lint_state(doc) 

1094 return _reformat_debian_control(lint_state) 

1095 

1096 

1097@lsp_semantic_tokens_full(_DISPATCH_RULE) 

1098def _debian_control_semantic_tokens_full( 

1099 ls: "DebputyLanguageServer", 

1100 request: SemanticTokensParams, 

1101) -> Optional[SemanticTokens]: 

1102 return deb822_semantic_tokens_full( 

1103 ls, 

1104 request, 

1105 _DCTRL_FILE_METADATA, 

1106 )