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

234 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +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 

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) 

497def _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 types.DiagnosticRelatedInformation( 

581 location=types.Location( 

582 lint_state.doc_uri, 

583 key_b_range, 

584 ), 

585 message=f'The attribute "{key_b}" is used here.', 

586 ) 

587 ], 

588 ) 

589 

590 lint_state.emit_diagnostic( 

591 key_b_range, 

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

593 "error", 

594 "debputy", 

595 related_information=[ 

596 types.DiagnosticRelatedInformation( 

597 location=types.Location( 

598 lint_state.doc_uri, 

599 key_a_range, 

600 ), 

601 message=f'The attribute "{key_a}" is used here.', 

602 ) 

603 ], 

604 ) 

605 

606 

607def _lint_attr_value( 

608 lint_state: LintState, 

609 attr: AttributeDescription, 

610 pg: ParserGenerator, 

611 value: Any, 

612) -> None: 

613 attr_type = attr.attribute_type 

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

615 parser = pg.dispatch_parser_table_for(attr_type) 

616 _lint_content( 

617 lint_state, 

618 pg, 

619 parser, 

620 value, 

621 ) 

622 

623 

624def _lint_declarative_mapping_input_parser( 

625 lint_state: LintState, 

626 pg: ParserGenerator, 

627 parser: DeclarativeMappingInputParser, 

628 content: Any, 

629) -> None: 

630 if not isinstance(content, CommentedMap): 

631 return 

632 lc = content.lc 

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

634 attr = parser.manifest_attributes.get(key) 

635 line, col = lc.key(key) 

636 if attr is None: 

637 corrected_key = yaml_flag_unknown_key( 

638 lint_state, 

639 key, 

640 parser.manifest_attributes, 

641 line, 

642 col, 

643 ) 

644 if corrected_key: 

645 key = corrected_key 

646 attr = parser.manifest_attributes.get(corrected_key) 

647 if attr is None: 

648 continue 

649 

650 _lint_attr_value( 

651 lint_state, 

652 attr, 

653 pg, 

654 value, 

655 ) 

656 

657 for forbidden_key in attr.conflicting_attributes: 

658 if forbidden_key in content: 

659 con_line, con_col = lc.key(forbidden_key) 

660 _conflicting_key( 

661 lint_state, 

662 key, 

663 forbidden_key, 

664 line, 

665 col, 

666 con_line, 

667 con_col, 

668 ) 

669 for mx in parser.mutually_exclusive_attributes: 

670 matches = content.keys() & mx 

671 if len(matches) < 2: 

672 continue 

673 key, *others = list(matches) 

674 line, col = lc.key(key) 

675 for other in others: 

676 con_line, con_col = lc.key(other) 

677 _conflicting_key( 

678 lint_state, 

679 key, 

680 other, 

681 line, 

682 col, 

683 con_line, 

684 con_col, 

685 ) 

686 

687 

688def _lint_content( 

689 lint_state: LintState, 

690 pg: ParserGenerator, 

691 parser: DeclarativeInputParser[Any], 

692 content: Any, 

693) -> None: 

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

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

696 return 

697 lc = content.lc 

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

699 is_known = parser.is_known_keyword(key) 

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

701 line, col = lc.key(key) 

702 corrected_key = yaml_flag_unknown_key( 

703 lint_state, 

704 key, 

705 parser.registered_keywords(), 

706 line, 

707 col, 

708 unknown_keys_diagnostic_severity=parser.unknown_keys_diagnostic_severity, 

709 ) 

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

711 key = corrected_key 

712 is_known = True 

713 

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

715 subparser = parser.parser_for(key) 

716 assert subparser is not None 

717 _lint_content( 

718 lint_state, 

719 pg, 

720 subparser.parser, 

721 value, 

722 ) 

723 elif isinstance(parser, ListWrappedDeclarativeInputParser): 

724 if not isinstance(content, CommentedSeq): 

725 return 

726 subparser = parser.delegate 

727 for value in content: 

728 _lint_content(lint_state, pg, subparser, value) 

729 elif isinstance(parser, InPackageContextParser): 

730 if not isinstance(content, CommentedMap): 

731 return 

732 print(lint_state) 

733 known_packages = lint_state.binary_packages 

734 lc = content.lc 

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

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

737 line, col = lc.key(k) 

738 yaml_flag_unknown_key( 

739 lint_state, 

740 k, 

741 known_packages, 

742 line, 

743 col, 

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

745 ) 

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

747 elif isinstance(parser, DeclarativeMappingInputParser): 

748 _lint_declarative_mapping_input_parser( 

749 lint_state, 

750 pg, 

751 parser, 

752 content, 

753 ) 

754 

755 

756@lsp_completer(_DISPATCH_RULE) 

757def debian_upstream_metadata_completer( 

758 ls: "DebputyLanguageServer", 

759 params: types.CompletionParams, 

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

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

762 lines = doc.lines 

763 server_position = doc.position_codec.position_from_client_units( 

764 lines, params.position 

765 ) 

766 added_key = insert_complete_marker_snippet(lines, server_position) 

767 attempts = 1 if added_key else 2 

768 content = None 

769 

770 while attempts > 0: 

771 attempts -= 1 

772 try: 

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

774 break 

775 except MarkedYAMLError as e: 

776 context_line = ( 

777 e.context_mark.line if e.context_mark else e.problem_mark.line 

778 ) 

779 if ( 

780 e.problem_mark.line != server_position.line 

781 and context_line != server_position.line 

782 ): 

783 l_data = ( 

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

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

786 else "N/A (OOB)" 

787 ) 

788 

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

790 return None 

791 

792 if attempts > 0: 

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

794 new_line = ( 

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

796 ) 

797 lines[server_position.line] = new_line 

798 except YAMLError: 

799 break 

800 if content is None: 

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

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

803 return None 

804 attribute_root_path = AttributePath.root_path(content) 

805 m = _trace_cursor(content, attribute_root_path, server_position) 

806 

807 if m is None: 

808 _info("No match") 

809 return None 

810 matched_key, attr_path, matched, parent = m 

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

812 feature_set = ls.plugin_feature_set 

813 root_parser = root_object_parser() 

814 segments = list(attr_path.path_segments()) 

815 km = resolve_keyword( 

816 root_parser, 

817 DEBPUTY_PLUGIN_METADATA, 

818 segments, 

819 0, 

820 feature_set.manifest_parser_generator, 

821 is_completion_attempt=True, 

822 ) 

823 if km is None: 

824 return None 

825 parser, _, at_depth_idx = km 

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

827 items = [] 

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

829 if isinstance(parser, DispatchingParserBase): 

830 if matched_key: 

831 items = [ 

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

833 for k in parser.registered_keywords() 

834 if k not in parent 

835 and not isinstance( 

836 parser.parser_for(k).parser, 

837 DeclarativeValuelessKeywordInputParser, 

838 ) 

839 ] 

840 else: 

841 items = [ 

842 types.CompletionItem(k) 

843 for k in parser.registered_keywords() 

844 if k not in parent 

845 and isinstance( 

846 parser.parser_for(k).parser, 

847 DeclarativeValuelessKeywordInputParser, 

848 ) 

849 ] 

850 elif isinstance(parser, InPackageContextParser): 

851 binary_packages = ls.lint_state(doc).binary_packages 

852 if binary_packages is not None: 

853 items = [ 

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

855 for p in binary_packages 

856 if p not in parent 

857 ] 

858 elif isinstance(parser, DeclarativeMappingInputParser): 

859 if matched_key: 

860 _info("Match attributes") 

861 locked = set(parent) 

862 for mx in parser.mutually_exclusive_attributes: 

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

864 locked.update(mx) 

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

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

867 locked.add(attr_name) 

868 break 

869 items = [ 

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

871 for k in parser.manifest_attributes 

872 if k not in locked 

873 ] 

874 else: 

875 # Value 

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

877 attr = parser.manifest_attributes.get(key) 

878 if attr is not None: 

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

880 items = completion_from_attr( 

881 attr, 

882 feature_set.manifest_parser_generator, 

883 matched, 

884 ) 

885 else: 

886 _info( 

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

888 ) 

889 elif isinstance(parser, DeclarativeNonMappingInputParser): 

890 attr = parser.alt_form_parser 

891 items = completion_from_attr( 

892 attr, 

893 feature_set.manifest_parser_generator, 

894 matched, 

895 ) 

896 return items 

897 

898 

899@lsp_hover(_DISPATCH_RULE) 

900def debputy_manifest_hover( 

901 ls: "DebputyLanguageServer", 

902 params: types.HoverParams, 

903) -> Optional[types.Hover]: 

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