Coverage for src/debputy/lsp/lsp_debian_upstream_metadata.py: 30%

233 statements  

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

1import textwrap 

2from functools import lru_cache 

3from typing import ( 

4 Optional, 

5 Any, 

6 Union, 

7 Sequence, 

8 TYPE_CHECKING, 

9) 

10 

11from debputy.highlevel_manifest import MANIFEST_YAML 

12from debputy.linting.lint_util import LintState, te_range_to_lsp 

13from debputy.lsp.lsp_features import ( 

14 lint_diagnostics, 

15 lsp_standard_handler, 

16 lsp_hover, 

17 lsp_completer, 

18 LanguageDispatchRule, 

19 SecondaryLanguage, 

20) 

21from debputy.lsp.lsp_generic_yaml import ( 

22 error_range_at_position, 

23 insert_complete_marker_snippet, 

24 YAML_COMPLETION_HINT_KEY, 

25 yaml_flag_unknown_key, 

26 _trace_cursor, 

27 DEBPUTY_PLUGIN_METADATA, 

28 resolve_keyword, 

29 generic_yaml_hover, 

30 completion_from_attr, 

31) 

32from debputy.manifest_parser.base_types import ( 

33 DebputyParsedContent, 

34) 

35from debputy.manifest_parser.declarative_parser import ( 

36 AttributeDescription, 

37 ParserGenerator, 

38 DeclarativeNonMappingInputParser, 

39) 

40from debputy.manifest_parser.declarative_parser import DeclarativeMappingInputParser 

41from debputy.manifest_parser.parser_data import ParserContextData 

42from debputy.manifest_parser.tagging_types import DebputyDispatchableType 

43from debputy.manifest_parser.util import AttributePath 

44from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin 

45from debputy.plugin.api.impl_types import ( 

46 DeclarativeInputParser, 

47 DispatchingParserBase, 

48 DebputyPluginMetadata, 

49 ListWrappedDeclarativeInputParser, 

50 InPackageContextParser, 

51 DeclarativeValuelessKeywordInputParser, 

52 DispatchingObjectParser, 

53) 

54from debputy.plugin.api.spec import ParserDocumentation, reference_documentation 

55from debputy.util import _info 

56from debputy.yaml.compat import ( 

57 CommentedMap, 

58 CommentedSeq, 

59 MarkedYAMLError, 

60 YAMLError, 

61) 

62 

63try: 

64 from debputy.lsp.debputy_ls import DebputyLanguageServer 

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

66 Position as TEPosition, 

67 Range as TERange, 

68 ) 

69except ImportError: 

70 pass 

71 

72if TYPE_CHECKING: 

73 import lsprotocol.types as types 

74else: 

75 import debputy.lsprotocol.types as types 

76 

77 

78_DISPATCH_RULE = LanguageDispatchRule.new_rule( 

79 "debian/upstream/metadata", 

80 "debian/upstream/metadata", 

81 [SecondaryLanguage("yaml", filename_based_lookup=True)], 

82) 

83 

84 

85lsp_standard_handler(_DISPATCH_RULE, types.TEXT_DOCUMENT_CODE_ACTION) 

86lsp_standard_handler(_DISPATCH_RULE, types.TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL) 

87 

88 

89class StrDebputyParsedContent(DebputyParsedContent): 

90 content: str 

91 

92 

93def _parser_handler( 

94 _key: str, 

95 value: Any, 

96 _attr_path: AttributePath, 

97 _context: Optional["ParserContextData"], 

98) -> Any: 

99 return value 

100 

101 

102def add_keyword( 

103 pg: ParserGenerator, 

104 root_parser: DispatchingParserBase[Any], 

105 plugin_metadata: DebputyPluginMetadata, 

106 keyword: str, 

107 *, 

108 inline_reference_documentation: Optional[ParserDocumentation] = None, 

109) -> None: 

110 parser = pg.generate_parser( 

111 StrDebputyParsedContent, 

112 source_content=str, 

113 inline_reference_documentation=inline_reference_documentation, 

114 ) 

115 root_parser.register_parser( 

116 keyword, 

117 parser, 

118 _parser_handler, 

119 plugin_metadata, 

120 ) 

121 

122 

123@lru_cache 

124def root_object_parser() -> DispatchingObjectParser: 

125 plugin_metadata = plugin_metadata_for_debputys_own_plugin() 

126 pg = ParserGenerator() 

127 pg.add_object_parser( 

128 "<ROOT>", 

129 unknown_keys_diagnostic_severity="warning", 

130 ) 

131 root_parser = pg.dispatchable_object_parsers["<ROOT>"] 

132 add_keyword( 

133 pg, 

134 root_parser, 

135 plugin_metadata, 

136 "Archive", 

137 inline_reference_documentation=reference_documentation( 

138 title="Archive (`Archive`)", 

139 description=textwrap.dedent( 

140 """\ 

141 The name of the large archive that the upstream work is part of, like CPAN. 

142 """ 

143 ), 

144 ), 

145 ) 

146 add_keyword( 

147 pg, 

148 root_parser, 

149 plugin_metadata, 

150 "ASCL-Id", 

151 inline_reference_documentation=reference_documentation( 

152 title="ASCL Identifier (`ASCL-Id`)", 

153 description=textwrap.dedent( 

154 """\ 

155 Identification code in the http://ascl.net 

156 """ 

157 ), 

158 ), 

159 ) 

160 add_keyword( 

161 pg, 

162 root_parser, 

163 plugin_metadata, 

164 "Bug-Database", 

165 inline_reference_documentation=reference_documentation( 

166 title="Bug database or tracker for the project (`Bug-Database`)", 

167 description=textwrap.dedent( 

168 """\ 

169 A URL to the list of known bugs for the project. 

170 """ 

171 ), 

172 ), 

173 ) 

174 add_keyword( 

175 pg, 

176 root_parser, 

177 plugin_metadata, 

178 "Bug-Submit", 

179 inline_reference_documentation=reference_documentation( 

180 title="Bug submission URL for the project (`Bug-Submit`)", 

181 description=textwrap.dedent( 

182 """\ 

183 A URL that is the place where new bug reports should be sent. 

184 """ 

185 ), 

186 ), 

187 ) 

188 add_keyword( 

189 pg, 

190 root_parser, 

191 plugin_metadata, 

192 "Cite-As", 

193 inline_reference_documentation=reference_documentation( 

194 title="Cite-As (`Cite-As`)", 

195 description=textwrap.dedent( 

196 """\ 

197 The way the authors want their software be cited in publications. 

198 

199 The value is a string which might contain a link in valid HTML syntax. 

200 """ 

201 ), 

202 ), 

203 ) 

204 add_keyword( 

205 pg, 

206 root_parser, 

207 plugin_metadata, 

208 "Changelog", 

209 inline_reference_documentation=reference_documentation( 

210 title="Changelog (`Changelog`)", 

211 description=textwrap.dedent( 

212 """\ 

213 URL to the upstream changelog. 

214 """ 

215 ), 

216 ), 

217 ) 

218 add_keyword( 

219 pg, 

220 root_parser, 

221 plugin_metadata, 

222 "CPE", 

223 inline_reference_documentation=reference_documentation( 

224 title="CPE (`CPE`)", 

225 description=textwrap.dedent( 

226 """\ 

227 One or more space separated http://cpe.mitre.org/ values useful to look up relevant CVEs 

228 in the https://nvd.nist.gov/home.cfm and other CVE sources. 

229 

230 See `CPEtagPackagesDep` for information on how this information can be used. 

231 **Example**: `cpe:/a:ethereal_group:ethereal` 

232 """ 

233 ), 

234 ), 

235 ) 

236 add_keyword( 

237 pg, 

238 root_parser, 

239 plugin_metadata, 

240 "Documentation", 

241 inline_reference_documentation=reference_documentation( 

242 title="Documentation (`Documentation`)", 

243 description=textwrap.dedent( 

244 """\ 

245 A URL to online documentation. 

246 """ 

247 ), 

248 ), 

249 ) 

250 add_keyword( 

251 pg, 

252 root_parser, 

253 plugin_metadata, 

254 "Donation", 

255 inline_reference_documentation=reference_documentation( 

256 title="Donation (`Donation`)", 

257 description=textwrap.dedent( 

258 """\ 

259 A URL to a donation form (or instructions). 

260 """ 

261 ), 

262 ), 

263 ) 

264 add_keyword( 

265 pg, 

266 root_parser, 

267 plugin_metadata, 

268 "FAQ", 

269 inline_reference_documentation=reference_documentation( 

270 title="FAQ (`FAQ`)", 

271 description=textwrap.dedent( 

272 """\ 

273 A URL to the online FAQ. 

274 """ 

275 ), 

276 ), 

277 ) 

278 add_keyword( 

279 pg, 

280 root_parser, 

281 plugin_metadata, 

282 "Funding", 

283 inline_reference_documentation=reference_documentation( 

284 title="Funding (`Funding`)", 

285 description=textwrap.dedent( 

286 """\ 

287 One or more sources of funding which have supported this project (e.g. NSF OCI-12345). 

288 """ 

289 ), 

290 ), 

291 ) 

292 add_keyword( 

293 pg, 

294 root_parser, 

295 plugin_metadata, 

296 "Gallery", 

297 inline_reference_documentation=reference_documentation( 

298 title="Gallery (`Gallery`)", 

299 description=textwrap.dedent( 

300 """\ 

301 A URL to a gallery of pictures made with the program (not screenshots). 

302 """ 

303 ), 

304 ), 

305 ) 

306 add_keyword( 

307 pg, 

308 root_parser, 

309 plugin_metadata, 

310 "Other-References", 

311 inline_reference_documentation=reference_documentation( 

312 title="Other-References (`Other-References`)", 

313 description=textwrap.dedent( 

314 """\ 

315 A URL to a upstream page containing more references. 

316 """ 

317 ), 

318 ), 

319 ) 

320 add_keyword( 

321 pg, 

322 root_parser, 

323 plugin_metadata, 

324 "Reference", 

325 inline_reference_documentation=reference_documentation( 

326 title="Reference (`Reference`)", 

327 # FIXME: Add the fields below as a nested subobject or list of such objects 

328 description=textwrap.dedent( 

329 """\ 

330 One or more bibliographic references, represented as a mapping or sequence of mappings containing 

331 the one or more of the following keys. 

332  

333 The values for the keys are always scalars, and the keys that correspond to standard BibTeX 

334 entries must provide the same content. 

335 """ 

336 ), 

337 ), 

338 ) 

339 

340 # Reference:: One or more bibliographic references, represented as a mapping or sequence of mappings containing the one or more of the following keys. The values for the keys are always scalars, and the keys that correspond to standard BibTeX entries must provide the same content. 

341 # 

342 # Author:: Author list in BibTeX friendly syntax (separating multiple authors by the keyword "and" and using as few as possible abbreviations in the names, as proposed in http://nwalsh.com/tex/texhelp/bibtx-23.html). 

343 # 

344 # Booktitle:: Title of the book the article is published in 

345 # 

346 # DOI:: This is the digital object identifier of the academic publication describing the packaged work. 

347 # 

348 # Editor:: Editor of the book the article is published in 

349 # 

350 # Eprint:: Hyperlink to the PDF file of the article. 

351 # 

352 # ISBN:: International Standard Book Number of the book if the article is part of the book or the reference is a book 

353 # 

354 # ISSN:: International Standard Serial Number of the periodical publication if the article is part of a series 

355 # 

356 # Journal:: Abbreviated journal name [To be discussed: which standard to recommend ?]. 

357 # 

358 # Number:: Issue number. 

359 # 

360 # Pages:: Article page number(s). [To be discussed] Page number separator must be a single ASCII hyphen. What do we do with condensed notations like 401-10 ? 

361 # 

362 # PMID:: ID number in the https://www.ncbi.nlm.nih.gov/pubmed/ database. 

363 # 

364 # Publisher:: Publisher of the book containing the article 

365 # 

366 # Title:: Article title. 

367 # 

368 # Type:: A http://www.bibtex.org/Format indicating what is cited. Typical values are {{{article}}}, {{{book}}}, or {{{inproceedings}}}. [To be discussed]. In case this field is not present, {{{article}}} is assumed. 

369 # 

370 # URL:: Hyperlink to the abstract of the article. This should not point to the full version because this is specified by Eprint. Please also do not drop links to pubmed here because this would be redundant to PMID. 

371 # 

372 # Volume:: Journal volume. 

373 # 

374 # Year:: Year of publication 

375 # 

376 

377 add_keyword( 

378 pg, 

379 root_parser, 

380 plugin_metadata, 

381 "Registration", 

382 inline_reference_documentation=reference_documentation( 

383 title="Registration (`Registration`)", 

384 description=textwrap.dedent( 

385 """\ 

386 A URL to a registration form (or instructions). This could be registration of bug reporting 

387 accounts, registration for counting/contacting users etc. 

388 """ 

389 ), 

390 ), 

391 ) 

392 add_keyword( 

393 pg, 

394 root_parser, 

395 plugin_metadata, 

396 "Registry", 

397 # FIXME: Add List of `Name`, `Entry` objects 

398 inline_reference_documentation=reference_documentation( 

399 title="Registry (`Registry`)", 

400 description=textwrap.dedent( 

401 """\ 

402 This field shall point to external catalogs/registries of software. 

403  

404 The field features an array of "Name (of registry) - Entry (ID of software in that catalog)" pairs. 

405 The names and entries shall only be names, not complete URIs, to avoid any bias on mirrors etc. 

406 Example: 

407 ```yaml 

408 Registry: 

409 - Name: bio.tools 

410 Entry: clustalw 

411 - Name: OMICtools 

412 Entry: OMICS_02562 

413 - Name: SciCrunch 

414 Entry: SCR_002909 

415 ``` 

416 """ 

417 ), 

418 ), 

419 ) 

420 

421 add_keyword( 

422 pg, 

423 root_parser, 

424 plugin_metadata, 

425 "Repository", 

426 inline_reference_documentation=reference_documentation( 

427 title="Repository (`Repository`)", 

428 description=textwrap.dedent( 

429 """\ 

430 URL to a repository containing the upstream sources. 

431 """ 

432 ), 

433 ), 

434 ) 

435 

436 add_keyword( 

437 pg, 

438 root_parser, 

439 plugin_metadata, 

440 "Repository-Browse", 

441 inline_reference_documentation=reference_documentation( 

442 title="Repository-Browse (`Repository-Browse`)", 

443 description=textwrap.dedent( 

444 """\ 

445 A URL to browse the repository containing the upstream sources. 

446 """ 

447 ), 

448 ), 

449 ) 

450 add_keyword( 

451 pg, 

452 root_parser, 

453 plugin_metadata, 

454 "Screenshots", 

455 inline_reference_documentation=reference_documentation( 

456 title="Screenshots (`Screenshots`)", 

457 description=textwrap.dedent( 

458 """\ 

459 One or more URLs to upstream pages containing screenshots (not <https://screenshots.debian.net>), 

460 represented by a scalar or a sequence of scalars. 

461 """ 

462 ), 

463 ), 

464 ) 

465 add_keyword( 

466 pg, 

467 root_parser, 

468 plugin_metadata, 

469 "Security-Contact", 

470 inline_reference_documentation=reference_documentation( 

471 title="Security-Contact (`Security-Contact`)", 

472 description=textwrap.dedent( 

473 """\ 

474 Which person, mailing list, forum, etc. to send security-related messages in the first place. 

475 """ 

476 ), 

477 ), 

478 ) 

479 add_keyword( 

480 pg, 

481 root_parser, 

482 plugin_metadata, 

483 "Webservice", 

484 inline_reference_documentation=reference_documentation( 

485 title="Webservice (`Webservice`)", 

486 description=textwrap.dedent( 

487 """\ 

488 URL to a web page where the packaged program can also be used. 

489 """ 

490 ), 

491 ), 

492 ) 

493 return root_parser 

494 

495 

496@lint_diagnostics(_DISPATCH_RULE) 

497async def _lint_debian_upstream_metadata( 

498 lint_state: LintState, 

499) -> None: 

500 lines = lint_state.lines 

501 

502 try: 

503 content = MANIFEST_YAML.load("".join(lines)) 

504 except MarkedYAMLError as e: 

505 if e.context_mark: 

506 line = e.context_mark.line 

507 column = e.context_mark.column 

508 else: 

509 line = e.problem_mark.line 

510 column = e.problem_mark.column 

511 error_range = error_range_at_position( 

512 lines, 

513 line, 

514 column, 

515 ) 

516 lint_state.emit_diagnostic( 

517 error_range, 

518 f"YAML parse error: {e}", 

519 "error", 

520 "debputy", 

521 ) 

522 except YAMLError as e: 

523 error_range = TERange( 

524 TEPosition(0, 0), 

525 TEPosition(0, len(lines[0])), 

526 ) 

527 lint_state.emit_diagnostic( 

528 error_range, 

529 f"Unknown YAML parse error: {e} [{e!r}]", 

530 "error", 

531 "debputy", 

532 ) 

533 else: 

534 feature_set = lint_state.plugin_feature_set 

535 pg = feature_set.manifest_parser_generator 

536 root_parser = root_object_parser() 

537 _lint_content( 

538 lint_state, 

539 pg, 

540 root_parser, 

541 content, 

542 ) 

543 

544 

545def _conflicting_key( 

546 lint_state: LintState, 

547 key_a: str, 

548 key_b: str, 

549 key_a_line: int, 

550 key_a_col: int, 

551 key_b_line: int, 

552 key_b_col: int, 

553) -> None: 

554 key_a_range = TERange( 

555 TEPosition( 

556 key_a_line, 

557 key_a_col, 

558 ), 

559 TEPosition( 

560 key_a_line, 

561 key_a_col + len(key_a), 

562 ), 

563 ) 

564 key_b_range = TERange( 

565 TEPosition( 

566 key_b_line, 

567 key_b_col, 

568 ), 

569 TEPosition( 

570 key_b_line, 

571 key_b_col + len(key_b), 

572 ), 

573 ) 

574 lint_state.emit_diagnostic( 

575 key_a_range, 

576 f'The "{key_a}" cannot be used with "{key_b}".', 

577 "error", 

578 "debputy", 

579 related_information=[ 

580 lint_state.related_diagnostic_information( 

581 key_b_range, f'The attribute "{key_b}" is used here.' 

582 ), 

583 ], 

584 ) 

585 

586 lint_state.emit_diagnostic( 

587 key_b_range, 

588 f'The "{key_b}" cannot be used with "{key_a}".', 

589 "error", 

590 "debputy", 

591 related_information=[ 

592 lint_state.related_diagnostic_information( 

593 key_a_range, 

594 f'The attribute "{key_a}" is used here.', 

595 ), 

596 ], 

597 ) 

598 

599 

600def _lint_attr_value( 

601 lint_state: LintState, 

602 attr: AttributeDescription, 

603 pg: ParserGenerator, 

604 value: Any, 

605) -> None: 

606 attr_type = attr.attribute_type 

607 if isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType): 

608 parser = pg.dispatch_parser_table_for(attr_type) 

609 _lint_content( 

610 lint_state, 

611 pg, 

612 parser, 

613 value, 

614 ) 

615 

616 

617def _lint_declarative_mapping_input_parser( 

618 lint_state: LintState, 

619 pg: ParserGenerator, 

620 parser: DeclarativeMappingInputParser, 

621 content: Any, 

622) -> None: 

623 if not isinstance(content, CommentedMap): 

624 return 

625 lc = content.lc 

626 for key, value in content.items(): 

627 attr = parser.manifest_attributes.get(key) 

628 line, col = lc.key(key) 

629 if attr is None: 

630 corrected_key = yaml_flag_unknown_key( 

631 lint_state, 

632 key, 

633 parser.manifest_attributes, 

634 line, 

635 col, 

636 ) 

637 if corrected_key: 

638 key = corrected_key 

639 attr = parser.manifest_attributes.get(corrected_key) 

640 if attr is None: 

641 continue 

642 

643 _lint_attr_value( 

644 lint_state, 

645 attr, 

646 pg, 

647 value, 

648 ) 

649 

650 for forbidden_key in attr.conflicting_attributes: 

651 if forbidden_key in content: 

652 con_line, con_col = lc.key(forbidden_key) 

653 _conflicting_key( 

654 lint_state, 

655 key, 

656 forbidden_key, 

657 line, 

658 col, 

659 con_line, 

660 con_col, 

661 ) 

662 for mx in parser.mutually_exclusive_attributes: 

663 matches = content.keys() & mx 

664 if len(matches) < 2: 

665 continue 

666 key, *others = list(matches) 

667 line, col = lc.key(key) 

668 for other in others: 

669 con_line, con_col = lc.key(other) 

670 _conflicting_key( 

671 lint_state, 

672 key, 

673 other, 

674 line, 

675 col, 

676 con_line, 

677 con_col, 

678 ) 

679 

680 

681def _lint_content( 

682 lint_state: LintState, 

683 pg: ParserGenerator, 

684 parser: DeclarativeInputParser[Any], 

685 content: Any, 

686) -> None: 

687 if isinstance(parser, DispatchingParserBase): 687 ↛ 716line 687 didn't jump to line 716 because the condition on line 687 was always true

688 if not isinstance(content, CommentedMap): 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true

689 return 

690 lc = content.lc 

691 for key, value in content.items(): 

692 is_known = parser.is_known_keyword(key) 

693 if not is_known: 693 ↛ 707line 693 didn't jump to line 707 because the condition on line 693 was always true

694 line, col = lc.key(key) 

695 corrected_key = yaml_flag_unknown_key( 

696 lint_state, 

697 key, 

698 parser.registered_keywords(), 

699 line, 

700 col, 

701 unknown_keys_diagnostic_severity=parser.unknown_keys_diagnostic_severity, 

702 ) 

703 if corrected_key is not None: 703 ↛ 704line 703 didn't jump to line 704 because the condition on line 703 was never true

704 key = corrected_key 

705 is_known = True 

706 

707 if is_known: 707 ↛ 708line 707 didn't jump to line 708 because the condition on line 707 was never true

708 subparser = parser.parser_for(key) 

709 assert subparser is not None 

710 _lint_content( 

711 lint_state, 

712 pg, 

713 subparser.parser, 

714 value, 

715 ) 

716 elif isinstance(parser, ListWrappedDeclarativeInputParser): 

717 if not isinstance(content, CommentedSeq): 

718 return 

719 subparser = parser.delegate 

720 for value in content: 

721 _lint_content(lint_state, pg, subparser, value) 

722 elif isinstance(parser, InPackageContextParser): 

723 if not isinstance(content, CommentedMap): 

724 return 

725 known_packages = lint_state.binary_packages 

726 lc = content.lc 

727 for k, v in content.items(): 

728 if "{{" not in k and known_packages is not None and k not in known_packages: 

729 line, col = lc.key(k) 

730 yaml_flag_unknown_key( 

731 lint_state, 

732 k, 

733 known_packages, 

734 line, 

735 col, 

736 message_format='Unknown package "{key}".', 

737 ) 

738 _lint_content(lint_state, pg, parser.delegate, v) 

739 elif isinstance(parser, DeclarativeMappingInputParser): 

740 _lint_declarative_mapping_input_parser( 

741 lint_state, 

742 pg, 

743 parser, 

744 content, 

745 ) 

746 

747 

748@lsp_completer(_DISPATCH_RULE) 

749def debian_upstream_metadata_completer( 

750 ls: "DebputyLanguageServer", 

751 params: types.CompletionParams, 

752) -> Optional[Union[types.CompletionList, Sequence[types.CompletionItem]]]: 

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

754 lines = doc.lines 

755 server_position = doc.position_codec.position_from_client_units( 

756 lines, params.position 

757 ) 

758 added_key = insert_complete_marker_snippet(lines, server_position) 

759 attempts = 1 if added_key else 2 

760 content = None 

761 

762 while attempts > 0: 

763 attempts -= 1 

764 try: 

765 content = MANIFEST_YAML.load("".join(lines)) 

766 break 

767 except MarkedYAMLError as e: 

768 context_line = ( 

769 e.context_mark.line if e.context_mark else e.problem_mark.line 

770 ) 

771 if ( 

772 e.problem_mark.line != server_position.line 

773 and context_line != server_position.line 

774 ): 

775 l_data = ( 

776 lines[e.problem_mark.line].rstrip() 

777 if e.problem_mark.line < len(lines) 

778 else "N/A (OOB)" 

779 ) 

780 

781 _info(f"Parse error on line: {e.problem_mark.line}: {l_data}") 

782 return None 

783 

784 if attempts > 0: 

785 # Try to make it a key and see if that fixes the problem 

786 new_line = ( 

787 lines[server_position.line].rstrip() + YAML_COMPLETION_HINT_KEY 

788 ) 

789 lines[server_position.line] = new_line 

790 except YAMLError: 

791 break 

792 if content is None: 

793 context = lines[server_position.line].replace("\n", "\\n") 

794 _info(f"Completion failed: parse error: Line in question: {context}") 

795 return None 

796 attribute_root_path = AttributePath.root_path(content) 

797 m = _trace_cursor(content, attribute_root_path, server_position) 

798 

799 if m is None: 

800 _info("No match") 

801 return None 

802 matched_key, attr_path, matched, parent = m 

803 _info(f"Matched path: {matched} (path: {attr_path.path}) [{matched_key=}]") 

804 feature_set = ls.plugin_feature_set 

805 root_parser = root_object_parser() 

806 segments = list(attr_path.path_segments()) 

807 km = resolve_keyword( 

808 root_parser, 

809 DEBPUTY_PLUGIN_METADATA, 

810 segments, 

811 0, 

812 feature_set.manifest_parser_generator, 

813 is_completion_attempt=True, 

814 ) 

815 if km is None: 

816 return None 

817 parser, _, at_depth_idx = km 

818 _info(f"Match leaf parser {at_depth_idx} -- {parser.__class__}") 

819 items = [] 

820 if at_depth_idx + 1 >= len(segments): 

821 if isinstance(parser, DispatchingParserBase): 

822 if matched_key: 

823 items = [ 

824 types.CompletionItem(f"{k}:") 

825 for k in parser.registered_keywords() 

826 if k not in parent 

827 and not isinstance( 

828 parser.parser_for(k).parser, 

829 DeclarativeValuelessKeywordInputParser, 

830 ) 

831 ] 

832 else: 

833 items = [ 

834 types.CompletionItem(k) 

835 for k in parser.registered_keywords() 

836 if k not in parent 

837 and isinstance( 

838 parser.parser_for(k).parser, 

839 DeclarativeValuelessKeywordInputParser, 

840 ) 

841 ] 

842 elif isinstance(parser, InPackageContextParser): 

843 binary_packages = ls.lint_state(doc).binary_packages 

844 if binary_packages is not None: 

845 items = [ 

846 types.CompletionItem(f"{p}:") 

847 for p in binary_packages 

848 if p not in parent 

849 ] 

850 elif isinstance(parser, DeclarativeMappingInputParser): 

851 if matched_key: 

852 _info("Match attributes") 

853 locked = set(parent) 

854 for mx in parser.mutually_exclusive_attributes: 

855 if not mx.isdisjoint(parent.keys()): 

856 locked.update(mx) 

857 for attr_name, attr in parser.manifest_attributes.items(): 

858 if not attr.conflicting_attributes.isdisjoint(parent.keys()): 

859 locked.add(attr_name) 

860 break 

861 items = [ 

862 types.CompletionItem(f"{k}:") 

863 for k in parser.manifest_attributes 

864 if k not in locked 

865 ] 

866 else: 

867 # Value 

868 key = segments[at_depth_idx] if len(segments) > at_depth_idx else None 

869 attr = parser.manifest_attributes.get(key) 

870 if attr is not None: 

871 _info(f"Expand value / key: {key} -- {attr.attribute_type}") 

872 items = completion_from_attr( 

873 attr, 

874 feature_set.manifest_parser_generator, 

875 matched, 

876 ) 

877 else: 

878 _info( 

879 f"Expand value / key: {key} -- !! {list(parser.manifest_attributes)}" 

880 ) 

881 elif isinstance(parser, DeclarativeNonMappingInputParser): 

882 attr = parser.alt_form_parser 

883 items = completion_from_attr( 

884 attr, 

885 feature_set.manifest_parser_generator, 

886 matched, 

887 ) 

888 return items 

889 

890 

891@lsp_hover(_DISPATCH_RULE) 

892def debputy_manifest_hover( 

893 ls: "DebputyLanguageServer", 

894 params: types.HoverParams, 

895) -> Optional[types.Hover]: 

896 return generic_yaml_hover(ls, params, lambda _: root_object_parser())