Coverage for src/debputy/lsp/lsp_generic_yaml.py: 77%

659 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +0000

1import textwrap 

2from typing import ( 

3 Union, 

4 Any, 

5 Optional, 

6 List, 

7 Tuple, 

8 Iterable, 

9 TYPE_CHECKING, 

10 Callable, 

11 Sequence, 

12 get_origin, 

13 Literal, 

14 get_args, 

15 Generic, 

16 cast, 

17) 

18 

19from debputy.commands.debputy_cmd.output import OutputStyle 

20from debputy.linting.lint_util import LintState 

21from debputy.lsp.diagnostics import LintSeverity 

22from debputy.lsp.quickfixes import propose_correct_text_quick_fix 

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

24 Position as TEPosition, 

25 Range as TERange, 

26) 

27from debputy.manifest_parser.declarative_parser import ( 

28 DeclarativeMappingInputParser, 

29 ParserGenerator, 

30 AttributeDescription, 

31 DeclarativeNonMappingInputParser, 

32 BASIC_SIMPLE_TYPES, 

33) 

34from debputy.manifest_parser.parser_doc import ( 

35 render_rule, 

36 render_attribute_doc, 

37 doc_args_for_parser_doc, 

38) 

39from debputy.manifest_parser.tagging_types import DebputyDispatchableType 

40from debputy.manifest_parser.util import AttributePath 

41from debputy.plugin.api.feature_set import PluginProvidedFeatureSet 

42from debputy.plugin.api.impl import plugin_metadata_for_debputys_own_plugin 

43from debputy.plugin.api.impl_types import ( 

44 DebputyPluginMetadata, 

45 DeclarativeInputParser, 

46 DispatchingParserBase, 

47 InPackageContextParser, 

48 ListWrappedDeclarativeInputParser, 

49 PluginProvidedParser, 

50 DeclarativeValuelessKeywordInputParser, 

51 DispatchingTableParser, 

52) 

53from debputy.substitution import VariableContext 

54from debputy.util import _info, _warn, detect_possible_typo, T 

55from debputy.yaml import MANIFEST_YAML 

56from debputy.yaml.compat import ( 

57 MarkedYAMLError, 

58 YAMLError, 

59) 

60from debputy.yaml.compat import ( 

61 Node, 

62 CommentedMap, 

63 LineCol, 

64 CommentedSeq, 

65 CommentedBase, 

66) 

67 

68if TYPE_CHECKING: 

69 import lsprotocol.types as types 

70else: 

71 import debputy.lsprotocol.types as types 

72 

73try: 

74 from pygls.server import LanguageServer 

75 from debputy.lsp.debputy_ls import DebputyLanguageServer 

76except ImportError: 

77 pass 

78 

79 

80YAML_COMPLETION_HINT_KEY = "___COMPLETE:" 

81YAML_COMPLETION_HINT_VALUE = "___COMPLETE" 

82DEBPUTY_PLUGIN_METADATA = plugin_metadata_for_debputys_own_plugin() 

83 

84 

85class LSPYAMLHelper(Generic[T]): 

86 

87 def __init__( 

88 self, 

89 lint_state: LintState, 

90 pg: ParserGenerator, 

91 custom_data: T, 

92 ) -> None: 

93 self.lint_state = lint_state 

94 self.lines = _lines(lint_state.lines) 

95 self.pg = pg 

96 self.custom_data = custom_data 

97 

98 def _validate_subparser_is_valid_here( 

99 self, 

100 subparser: PluginProvidedParser, 

101 orig_key: str, 

102 line: int, 

103 col: int, 

104 ) -> None: 

105 # Subclasses can provide custom logic here 

106 pass 

107 

108 def _lint_dispatch_parser( 

109 self, 

110 parser: DispatchingParserBase, 

111 dispatch_key: str, 

112 key_pos: Optional[Tuple[int, int]], 

113 value: Optional[Any], 

114 value_pos: Optional[Tuple[int, int]], 

115 *, 

116 is_keyword_only: bool, 

117 ) -> None: 

118 is_known = parser.is_known_keyword(dispatch_key) 

119 orig_key = dispatch_key 

120 if not is_known and key_pos is not None: 

121 

122 if value is None: 

123 opts = { 

124 "message_format": 'Unknown or unsupported value "{key}".', 

125 } 

126 else: 

127 opts = {} 

128 corrected_key = yaml_flag_unknown_key( 

129 self.lint_state, 

130 dispatch_key, 

131 parser.registered_keywords(), 

132 key_pos, 

133 unknown_keys_diagnostic_severity=parser.unknown_keys_diagnostic_severity, 

134 **opts, 

135 ) 

136 if corrected_key is not None: 

137 dispatch_key = corrected_key 

138 is_known = True 

139 

140 if is_known: 

141 subparser = parser.parser_for(dispatch_key) 

142 assert subparser is not None 

143 if key_pos: 143 ↛ 152line 143 didn't jump to line 152 because the condition on line 143 was always true

144 line, col = key_pos 

145 self._validate_subparser_is_valid_here( 

146 subparser, 

147 orig_key, 

148 line, 

149 col, 

150 ) 

151 

152 if isinstance(subparser.parser, DeclarativeValuelessKeywordInputParser): 

153 if value is not None or not is_keyword_only: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

154 if value is not None: 

155 line_no, cursor_pos = value_pos if value_pos else key_pos 

156 value_range = self._remaining_line(line_no, cursor_pos) 

157 if _is_empty_range(value_range): 

158 # In the unlikely case that the value position is present but leads to 

159 # an empty range, report the key instead. 

160 line_no, cursor_pos = key_pos 

161 value_range = self._remaining_line(line_no, cursor_pos) 

162 msg = f"The keyword {dispatch_key} does not accept any value" 

163 else: 

164 line_no, cursor_pos = key_pos 

165 value_range = self._remaining_line(line_no, cursor_pos) 

166 msg = f"The keyword {dispatch_key} cannot be used as a mapping key" 

167 

168 assert not _is_empty_range(value_range) 

169 self.lint_state.emit_diagnostic( 

170 value_range, 

171 msg, 

172 "error", 

173 "debputy", 

174 ) 

175 return 

176 

177 self.lint_content( 

178 # Pycharm's type checking gets confused by the isinstance check above. 

179 cast("DeclarativeInputParser[Any]", subparser.parser), 

180 value, 

181 key=orig_key, 

182 content_pos=value_pos, 

183 ) 

184 

185 def lint_content( 

186 self, 

187 parser: DeclarativeInputParser[Any], 

188 content: Any, 

189 *, 

190 key: Optional[Union[str, int]] = None, 

191 content_pos: Optional[Tuple[int, int]] = None, 

192 ) -> None: 

193 if isinstance(parser, DispatchingParserBase): 

194 if isinstance(content, str): 

195 self._lint_dispatch_parser( 

196 parser, 

197 content, 

198 content_pos, 

199 None, 

200 None, 

201 is_keyword_only=True, 

202 ) 

203 

204 return 

205 if not isinstance(content, CommentedMap): 

206 return 

207 lc = content.lc 

208 for dispatch_key, value in content.items(): 

209 key_pos = lc.key(dispatch_key) 

210 value_pos = lc.value(dispatch_key) 

211 self._lint_dispatch_parser( 

212 parser, 

213 dispatch_key, 

214 key_pos, 

215 value, 

216 value_pos, 

217 is_keyword_only=False, 

218 ) 

219 

220 elif isinstance(parser, ListWrappedDeclarativeInputParser): 

221 if not isinstance(content, CommentedSeq): 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true

222 return 

223 subparser = parser.delegate 

224 lc = content.lc 

225 for idx, value in enumerate(content): 

226 value_pos = lc.item(idx) 

227 self.lint_content(subparser, value, content_pos=value_pos, key=idx) 

228 elif isinstance(parser, InPackageContextParser): 

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

230 return 

231 known_packages = self.lint_state.binary_packages 

232 lc = content.lc 

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

234 if k is None or ( 

235 "{{" not in k 

236 and known_packages is not None 

237 and k not in known_packages 

238 ): 

239 yaml_flag_unknown_key( 

240 self.lint_state, 

241 k, 

242 known_packages, 

243 lc.key(k), 

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

245 ) 

246 self.lint_content(parser.delegate, v, key=k, content_pos=lc.value(k)) 

247 elif isinstance(parser, DeclarativeMappingInputParser): 

248 self._lint_declarative_mapping_input_parser( 

249 parser, 

250 content, 

251 content_pos, 

252 key=key, 

253 ) 

254 elif isinstance(parser, DeclarativeNonMappingInputParser): 254 ↛ exitline 254 didn't return from function 'lint_content' because the condition on line 254 was always true

255 if content_pos is not None: 255 ↛ exitline 255 didn't return from function 'lint_content' because the condition on line 255 was always true

256 self._lint_attr_value( 

257 parser.alt_form_parser, 

258 key, 

259 content, 

260 content_pos, 

261 ) 

262 

263 def _lint_declarative_mapping_input_parser( 

264 self, 

265 parser: DeclarativeMappingInputParser, 

266 content: Any, 

267 content_pos: Tuple[int, int], 

268 *, 

269 key: Optional[Union[str, int]] = None, 

270 ) -> None: 

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

272 alt_form_parser = parser.alt_form_parser 

273 if alt_form_parser: 

274 self._lint_attr_value( 

275 alt_form_parser, 

276 key, 

277 content, 

278 content_pos, 

279 ) 

280 else: 

281 line_no, cursor_pos = content_pos 

282 value_range = self._remaining_line(line_no, cursor_pos) 

283 if _is_empty_range(value_range): 

284 # FIXME: We cannot report an empty range, but there is still a problem here. 

285 return 

286 if isinstance(key, str): 

287 msg = f'The value for "{key}" must be a mapping' 

288 else: 

289 msg = "The value must be a mapping" 

290 self.lint_state.emit_diagnostic( 

291 value_range, 

292 msg, 

293 "error", 

294 "debputy", 

295 ) 

296 return 

297 lc = content.lc 

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

299 attr = parser.manifest_attributes.get(key) 

300 key_pos = lc.key(key) 

301 value_pos = lc.value(key) 

302 if attr is None: 

303 corrected_key = yaml_flag_unknown_key( 

304 self.lint_state, 

305 key, 

306 parser.manifest_attributes, 

307 key_pos, 

308 ) 

309 if corrected_key: 

310 key = corrected_key 

311 attr = parser.manifest_attributes.get(corrected_key) 

312 if attr is None: 

313 continue 

314 

315 self._lint_attr_value( 

316 attr, 

317 key, 

318 value, 

319 value_pos, 

320 ) 

321 

322 for forbidden_key in attr.conflicting_attributes: 322 ↛ 323line 322 didn't jump to line 323 because the loop on line 322 never started

323 if forbidden_key in content: 

324 line, col = key_pos 

325 con_line, con_col = lc.key(forbidden_key) 

326 yaml_conflicting_key( 

327 self.lint_state, 

328 key, 

329 forbidden_key, 

330 line, 

331 col, 

332 con_line, 

333 con_col, 

334 ) 

335 for mx in parser.mutually_exclusive_attributes: 

336 matches = content.keys() & mx 

337 if len(matches) < 2: 337 ↛ 339line 337 didn't jump to line 339 because the condition on line 337 was always true

338 continue 

339 key, *others = list(matches) 

340 line, col = lc.key(key) 

341 for other in others: 

342 con_line, con_col = lc.key(other) 

343 yaml_conflicting_key( 

344 self.lint_state, 

345 key, 

346 other, 

347 line, 

348 col, 

349 con_line, 

350 con_col, 

351 ) 

352 

353 def _type_based_value_check( 

354 self, 

355 target_attr_type: type, 

356 value: Any, 

357 value_pos: Tuple[int, int], 

358 *, 

359 key: Optional[Union[str, int]] = None, 

360 ) -> bool: 

361 if issubclass(target_attr_type, DebputyDispatchableType): 

362 parser = self.pg.dispatch_parser_table_for(target_attr_type) 

363 self.lint_content( 

364 parser, 

365 value, 

366 key=key, 

367 content_pos=value_pos, 

368 ) 

369 return True 

370 return False 

371 

372 def _lint_attr_value( 

373 self, 

374 attr: AttributeDescription, 

375 key: Optional[Union[str, int]], 

376 value: Any, 

377 pos: Tuple[int, int], 

378 ) -> None: 

379 target_attr_type = attr.attribute_type 

380 orig = get_origin(target_attr_type) 

381 if orig == list and isinstance(value, CommentedSeq): 

382 lc = value.lc 

383 target_item_type = get_args(target_attr_type)[0] 

384 for idx, v in enumerate(value): 

385 v_pos = lc.item(idx) 

386 self._lint_value( 

387 idx, 

388 v, 

389 target_item_type, 

390 v_pos, 

391 ) 

392 

393 else: 

394 self._lint_value( 

395 key, 

396 value, 

397 target_attr_type, 

398 pos, 

399 ) 

400 

401 def _lint_value( 

402 self, 

403 key: Optional[Union[str, int]], 

404 value: Any, 

405 target_attr_type: Any, 

406 pos: Tuple[int, int], 

407 ) -> None: 

408 type_mapping = self.pg.get_mapped_type_from_target_type(target_attr_type) 

409 source_attr_type = target_attr_type 

410 if type_mapping is not None: 

411 source_attr_type = type_mapping.source_type 

412 valid_values: Optional[Sequence[Any]] = None 

413 orig = get_origin(source_attr_type) 

414 if orig == Literal: 

415 valid_values = get_args(target_attr_type) 

416 elif orig == bool or target_attr_type == bool: 

417 valid_values = (True, False) 

418 elif isinstance(target_attr_type, type) and self._type_based_value_check( 

419 target_attr_type, 

420 value, 

421 pos, 

422 key=key, 

423 ): 

424 return 

425 elif source_attr_type in BASIC_SIMPLE_TYPES: 

426 if isinstance(value, source_attr_type): 

427 return 

428 expected_type = BASIC_SIMPLE_TYPES[source_attr_type] 

429 line_no, cursor_pos = pos 

430 value_range = self._remaining_line(line_no, cursor_pos) 

431 if _is_empty_range(value_range): 431 ↛ 434line 431 didn't jump to line 434 because the condition on line 431 was always true

432 # FIXME: We cannot report an empty range, but there is still a problem here. 

433 return 

434 if isinstance(key, str): 

435 msg = f'Value for "{key}" does not match the base type: Expected {expected_type}' 

436 else: 

437 msg = f"Value does not match the base type: Expected {expected_type}" 

438 if issubclass(source_attr_type, str): 

439 quickfixes = [ 

440 propose_correct_text_quick_fix(_as_yaml_value(str(value))) 

441 ] 

442 else: 

443 quickfixes = None 

444 self.lint_state.emit_diagnostic( 

445 value_range, 

446 msg, 

447 "error", 

448 "debputy", 

449 quickfixes=quickfixes, 

450 ) 

451 return 

452 

453 if valid_values is None or value in valid_values: 

454 return 

455 line_no, cursor_pos = pos 

456 value_range = self._remaining_line(line_no, cursor_pos) 

457 if _is_empty_range(value_range): 457 ↛ 459line 457 didn't jump to line 459 because the condition on line 457 was never true

458 # FIXME: We cannot report an empty range, but there is still a problem here. 

459 return 

460 if isinstance(key, str): 460 ↛ 463line 460 didn't jump to line 463 because the condition on line 460 was always true

461 msg = f'Not a supported value for "{key}"' 

462 else: 

463 msg = "Not a supported value here" 

464 self.lint_state.emit_diagnostic( 

465 value_range, 

466 msg, 

467 "error", 

468 "debputy", 

469 quickfixes=[ 

470 propose_correct_text_quick_fix(_as_yaml_value(m)) for m in valid_values 

471 ], 

472 ) 

473 

474 def _remaining_line(self, line_no: int, pos_start: int) -> "TERange": 

475 raw_line = self.lines[line_no].rstrip() 

476 pos_end = len(raw_line) 

477 return TERange( 

478 TEPosition( 

479 line_no, 

480 pos_start, 

481 ), 

482 TEPosition( 

483 line_no, 

484 pos_end, 

485 ), 

486 ) 

487 

488 

489def _is_empty_range(token_range: "TERange") -> bool: 

490 return token_range.start_pos == token_range.end_pos 

491 

492 

493def _lines(lines: List[str]) -> List[str]: 

494 if not lines or lines[-1].endswith("\n"): 494 ↛ 497line 494 didn't jump to line 497 because the condition on line 494 was always true

495 lines = lines.copy() 

496 lines.append("") 

497 return lines 

498 

499 

500async def generic_yaml_lint( 

501 lint_state: LintState, 

502 root_parser: DeclarativeInputParser[Any], 

503 initialize_yaml_helper: Callable[[LintState], LSPYAMLHelper[Any]], 

504) -> None: 

505 lines = _lines(lint_state.lines) 

506 try: 

507 content = MANIFEST_YAML.load(lint_state.content) 

508 except MarkedYAMLError as e: 

509 if e.context_mark: 

510 line = e.context_mark.line 

511 column = e.context_mark.column 

512 else: 

513 line = e.problem_mark.line 

514 column = e.problem_mark.column 

515 error_range = error_range_at_position( 

516 lines, 

517 line, 

518 column, 

519 ) 

520 lint_state.emit_diagnostic( 

521 error_range, 

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

523 "error", 

524 "debputy", 

525 ) 

526 except YAMLError as e: 

527 error_range = TERange( 

528 TEPosition(0, 0), 

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

530 ) 

531 lint_state.emit_diagnostic( 

532 error_range, 

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

534 "error", 

535 "debputy", 

536 ) 

537 else: 

538 yaml_linter = initialize_yaml_helper(lint_state) 

539 yaml_linter.lint_content( 

540 root_parser, 

541 content, 

542 ) 

543 

544 

545def _as_yaml_value(v: Any) -> str: 

546 if isinstance(v, bool): 

547 return str(v).lower() 

548 if isinstance(v, str): 548 ↛ 550line 548 didn't jump to line 550 because the condition on line 548 was always true

549 return maybe_quote_yaml_value(str(v)) 

550 return str(v) 

551 

552 

553def resolve_hover_text_for_value( 

554 feature_set: PluginProvidedFeatureSet, 

555 parser: DeclarativeMappingInputParser, 

556 plugin_metadata: DebputyPluginMetadata, 

557 output_style: OutputStyle, 

558 show_integration_mode: bool, 

559 segment: Union[str, int], 

560 matched: Any, 

561) -> Optional[str]: 

562 

563 hover_doc_text: Optional[str] = None 

564 attr = parser.manifest_attributes.get(segment) 

565 attr_type = attr.attribute_type if attr is not None else None 

566 if attr_type is None: 566 ↛ 567line 566 didn't jump to line 567 because the condition on line 566 was never true

567 _info(f"Matched value for {segment} -- No attr or type") 

568 return None 

569 if isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType): 569 ↛ 589line 569 didn't jump to line 589 because the condition on line 569 was always true

570 parser_generator = feature_set.manifest_parser_generator 

571 parser = parser_generator.dispatch_parser_table_for(attr_type) 

572 if parser is None or not isinstance(matched, str): 572 ↛ 573line 572 didn't jump to line 573 because the condition on line 572 was never true

573 _info( 

574 f"Unknown parser for {segment} or matched is not a str -- {attr_type} {type(matched)=}" 

575 ) 

576 return None 

577 subparser = parser.parser_for(matched) 

578 if subparser is None: 578 ↛ 579line 578 didn't jump to line 579 because the condition on line 578 was never true

579 _info(f"Unknown parser for {matched} (subparser)") 

580 return None 

581 hover_doc_text = render_rule( 

582 matched, 

583 subparser.parser, 

584 plugin_metadata, 

585 output_style, 

586 show_integration_mode=show_integration_mode, 

587 ) 

588 else: 

589 _info(f"Unknown value: {matched} -- {segment}") 

590 return hover_doc_text 

591 

592 

593def resolve_hover_text( 

594 feature_set: PluginProvidedFeatureSet, 

595 parser: Optional[Union[DeclarativeInputParser[Any], DispatchingParserBase]], 

596 plugin_metadata: DebputyPluginMetadata, 

597 output_style: OutputStyle, 

598 show_integration_mode: bool, 

599 segments: List[Union[str, int]], 

600 at_depth_idx: int, 

601 matched: Any, 

602 matched_key: bool, 

603) -> Optional[str]: 

604 hover_doc_text: Optional[str] = None 

605 if at_depth_idx == len(segments): 

606 segment = segments[at_depth_idx - 1] 

607 _info(f"Matched {segment} at ==, {matched_key=} ") 

608 hover_doc_text = render_rule( 

609 segment, 

610 parser, 

611 plugin_metadata, 

612 output_style, 

613 is_root_rule=False, 

614 show_integration_mode=show_integration_mode, 

615 ) 

616 elif at_depth_idx + 1 == len(segments) and isinstance( 616 ↛ 641line 616 didn't jump to line 641 because the condition on line 616 was always true

617 parser, DeclarativeMappingInputParser 

618 ): 

619 segment = segments[at_depth_idx] 

620 _info(f"Matched {segment} at -1, {matched_key=} ") 

621 if isinstance(segment, str): 621 ↛ 643line 621 didn't jump to line 643 because the condition on line 621 was always true

622 if not matched_key: 

623 hover_doc_text = resolve_hover_text_for_value( 

624 feature_set, 

625 parser, 

626 plugin_metadata, 

627 output_style, 

628 show_integration_mode, 

629 segment, 

630 matched, 

631 ) 

632 if matched_key or hover_doc_text is None: 

633 rule_name = _guess_rule_name(segments, at_depth_idx) 

634 hover_doc_text = _render_param_doc( 

635 rule_name, 

636 parser, 

637 plugin_metadata, 

638 segment, 

639 ) 

640 else: 

641 _info(f"No doc: {at_depth_idx=} {len(segments)=}") 

642 

643 return hover_doc_text 

644 

645 

646def as_hover_doc( 

647 ls: "DebputyLanguageServer", 

648 hover_doc_text: Optional[str], 

649) -> Optional[types.Hover]: 

650 if hover_doc_text is None: 650 ↛ 651line 650 didn't jump to line 651 because the condition on line 650 was never true

651 return None 

652 return types.Hover( 

653 contents=types.MarkupContent( 

654 kind=ls.hover_markup_format( 

655 types.MarkupKind.Markdown, 

656 types.MarkupKind.PlainText, 

657 ), 

658 value=hover_doc_text, 

659 ), 

660 ) 

661 

662 

663def _render_param_doc( 

664 rule_name: str, 

665 declarative_parser: DeclarativeMappingInputParser, 

666 plugin_metadata: DebputyPluginMetadata, 

667 attribute: str, 

668) -> Optional[str]: 

669 attr = declarative_parser.source_attributes.get(attribute) 

670 if attr is None: 670 ↛ 671line 670 didn't jump to line 671 because the condition on line 670 was never true

671 return None 

672 

673 doc_args, parser_doc = doc_args_for_parser_doc( 

674 rule_name, 

675 declarative_parser, 

676 plugin_metadata, 

677 ) 

678 rendered_docs = render_attribute_doc( 

679 declarative_parser, 

680 declarative_parser.source_attributes, 

681 declarative_parser.input_time_required_parameters, 

682 declarative_parser.at_least_one_of, 

683 parser_doc, 

684 doc_args, 

685 is_interactive=True, 

686 rule_name=rule_name, 

687 ) 

688 

689 for attributes, rendered_doc in rendered_docs: 689 ↛ 698line 689 didn't jump to line 698 because the loop on line 689 didn't complete

690 if attribute in attributes: 

691 full_doc = [ 

692 f"# Attribute `{attribute}`", 

693 "", 

694 ] 

695 full_doc.extend(rendered_doc) 

696 

697 return "\n".join(full_doc) 

698 return None 

699 

700 

701def _guess_rule_name(segments: List[Union[str, int]], idx: int) -> str: 

702 orig_idx = idx 

703 idx -= 1 

704 while idx >= 0: 704 ↛ 709line 704 didn't jump to line 709 because the condition on line 704 was always true

705 segment = segments[idx] 

706 if isinstance(segment, str): 

707 return segment 

708 idx -= 1 

709 _warn(f"Unable to derive rule name from {segments} [{orig_idx}]") 

710 return "<Bug: unknown rule name>" 

711 

712 

713def is_at(position: types.Position, lc_pos: Tuple[int, int]) -> bool: 

714 return position.line == lc_pos[0] and position.character == lc_pos[1] 

715 

716 

717def is_before(position: types.Position, lc_pos: Tuple[int, int]) -> bool: 

718 line, column = lc_pos 

719 if position.line < line: 

720 return True 

721 if position.line == line and position.character < column: 

722 return True 

723 return False 

724 

725 

726def is_after(position: types.Position, lc_pos: Tuple[int, int]) -> bool: 

727 line, column = lc_pos 

728 if position.line > line: 

729 return True 

730 if position.line == line and position.character > column: 

731 return True 

732 return False 

733 

734 

735def error_range_at_position( 

736 lines: List[str], 

737 line_no: int, 

738 char_offset: int, 

739) -> TERange: 

740 line = lines[line_no] 

741 line_len = len(line) 

742 start_idx = char_offset 

743 end_idx = start_idx 

744 

745 if line[start_idx].isspace(): 

746 

747 def _check(x: str) -> bool: 

748 return not x.isspace() 

749 

750 else: 

751 

752 def _check(x: str) -> bool: 

753 return x.isspace() 

754 

755 for i in range(end_idx, line_len): 

756 end_idx = i 

757 if _check(line[i]): 

758 break 

759 

760 for i in range(start_idx, -1, -1): 

761 if i > 0 and _check(line[i]): 

762 break 

763 start_idx = i 

764 

765 return TERange( 

766 TEPosition(line_no, start_idx), 

767 TEPosition(line_no, end_idx), 

768 ) 

769 

770 

771def _escape(v: str) -> str: 

772 return '"' + v.replace("\n", "\\n") + '"' 

773 

774 

775def insert_complete_marker_snippet( 

776 lines: List[str], 

777 server_position: types.Position, 

778) -> bool: 

779 _info(f"Complete at {server_position}") 

780 line_no = server_position.line 

781 line = lines[line_no] if line_no < len(lines) else "" 

782 

783 lhs_ws = line[: server_position.character] 

784 lhs = lhs_ws.strip() 

785 open_quote = "" 

786 rhs = line[server_position.character + 1 :] 

787 for q in ('"', "'"): 

788 if rhs.endswith(q): 788 ↛ 789line 788 didn't jump to line 789 because the condition on line 788 was never true

789 break 

790 qc = lhs.count(q) & 1 

791 if qc: 

792 open_quote = q 

793 break 

794 

795 if lhs.endswith(":"): 

796 _info("Insertion of value (key seen)") 

797 new_line = ( 

798 line[: server_position.character] 

799 + YAML_COMPLETION_HINT_VALUE 

800 + f"{open_quote}\n" 

801 ) 

802 elif lhs.startswith("-"): 

803 _info("Insertion of key or value (list item)") 

804 # Respect the provided indentation 

805 snippet = ( 

806 YAML_COMPLETION_HINT_KEY if ":" not in lhs else YAML_COMPLETION_HINT_VALUE 

807 ) 

808 new_line = line[: server_position.character] + snippet + f"{open_quote}\n" 

809 elif not lhs or (lhs_ws and not lhs_ws[0].isspace()): 

810 _info(f"Insertion of key or value: {_escape(line[server_position.character:])}") 

811 # Respect the provided indentation 

812 snippet = ( 

813 YAML_COMPLETION_HINT_KEY if ":" not in lhs else YAML_COMPLETION_HINT_VALUE 

814 ) 

815 new_line = line[: server_position.character] + snippet + f"{open_quote}\n" 

816 elif lhs.isalpha() and ":" not in lhs: 

817 _info(f"Expanding value to a key: {_escape(line[server_position.character:])}") 

818 # Respect the provided indentation 

819 new_line = ( 

820 line[: server_position.character] 

821 + YAML_COMPLETION_HINT_KEY 

822 + f"{open_quote}\n" 

823 ) 

824 elif open_quote: 

825 _info( 

826 f"Expanding value inside a string: {_escape(line[server_position.character:])}" 

827 ) 

828 new_line = ( 

829 line[: server_position.character] 

830 + YAML_COMPLETION_HINT_VALUE 

831 + f"{open_quote}\n" 

832 ) 

833 else: 

834 c = ( 

835 line[server_position.character] 

836 if server_position.character < len(line) 

837 else "(OOB)" 

838 ) 

839 _info(f"Not touching line: {_escape(line)} -- {_escape(c)}") 

840 return False 

841 _info(f'Evaluating complete on synthetic line: "{new_line}"') 

842 if line_no < len(lines): 842 ↛ 844line 842 didn't jump to line 844 because the condition on line 842 was always true

843 lines[line_no] = new_line 

844 elif line_no == len(lines): 

845 lines.append(new_line) 

846 else: 

847 return False 

848 return True 

849 

850 

851def _keywords_with_parser( 

852 parser: Union[DeclarativeMappingInputParser, DispatchingParserBase], 

853) -> Tuple[str, PluginProvidedParser]: 

854 for keyword in parser.registered_keywords(): 

855 pp_subparser = parser.parser_for(keyword) 

856 yield keyword, pp_subparser 

857 

858 

859def yaml_key_range( 

860 key: Optional[str], 

861 line: int, 

862 col: int, 

863) -> "TERange": 

864 key_len = len(key) if key else 1 

865 return TERange.between( 

866 TEPosition(line, col), 

867 TEPosition(line, col + key_len), 

868 ) 

869 

870 

871def yaml_flag_unknown_key( 

872 lint_state: LintState, 

873 key: Optional[str], 

874 expected_keys: Iterable[str], 

875 key_pos: Tuple[int, int], 

876 *, 

877 message_format: str = 'Unknown or unsupported key "{key}".', 

878 unknown_keys_diagnostic_severity: Optional[LintSeverity] = "error", 

879) -> Optional[str]: 

880 line, col = key_pos 

881 key_range = yaml_key_range(key, line, col) 

882 

883 candidates = detect_possible_typo(key, expected_keys) if key is not None else () 

884 extra = "" 

885 corrected_key = None 

886 if candidates: 

887 extra = f' It looks like a typo of "{candidates[0]}".' 

888 # TODO: We should be able to tell that `install-doc` and `install-docs` are the same. 

889 # That would enable this to work in more cases. 

890 corrected_key = candidates[0] if len(candidates) == 1 else None 

891 if unknown_keys_diagnostic_severity is None: 891 ↛ 892line 891 didn't jump to line 892 because the condition on line 891 was never true

892 message_format = f"Possible typo of {candidates[0]}." 

893 extra = "" 

894 elif unknown_keys_diagnostic_severity is None: 894 ↛ 895line 894 didn't jump to line 895 because the condition on line 894 was never true

895 return None 

896 

897 if key is None: 

898 message_format = "Missing key" 

899 if unknown_keys_diagnostic_severity is not None: 899 ↛ 907line 899 didn't jump to line 907 because the condition on line 899 was always true

900 lint_state.emit_diagnostic( 

901 key_range, 

902 message_format.format(key=key) + extra, 

903 unknown_keys_diagnostic_severity, 

904 "debputy", 

905 quickfixes=[propose_correct_text_quick_fix(n) for n in candidates], 

906 ) 

907 return corrected_key 

908 

909 

910def yaml_conflicting_key( 

911 lint_state: LintState, 

912 key_a: str, 

913 key_b: str, 

914 key_a_line: int, 

915 key_a_col: int, 

916 key_b_line: int, 

917 key_b_col: int, 

918) -> None: 

919 key_a_range = TERange( 

920 TEPosition( 

921 key_a_line, 

922 key_a_col, 

923 ), 

924 TEPosition( 

925 key_a_line, 

926 key_a_col + len(key_a), 

927 ), 

928 ) 

929 key_b_range = TERange( 

930 TEPosition( 

931 key_b_line, 

932 key_b_col, 

933 ), 

934 TEPosition( 

935 key_b_line, 

936 key_b_col + len(key_b), 

937 ), 

938 ) 

939 lint_state.emit_diagnostic( 

940 key_a_range, 

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

942 "error", 

943 "debputy", 

944 related_information=[ 

945 lint_state.related_diagnostic_information( 

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

947 ), 

948 ], 

949 ) 

950 

951 lint_state.emit_diagnostic( 

952 key_b_range, 

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

954 "error", 

955 "debputy", 

956 related_information=[ 

957 lint_state.related_diagnostic_information( 

958 key_a_range, 

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

960 ), 

961 ], 

962 ) 

963 

964 

965def resolve_keyword( 

966 current_parser: Union[DeclarativeInputParser[Any], DispatchingParserBase], 

967 current_plugin: DebputyPluginMetadata, 

968 segments: List[Union[str, int]], 

969 segment_idx: int, 

970 parser_generator: ParserGenerator, 

971 *, 

972 is_completion_attempt: bool = False, 

973) -> Optional[ 

974 Tuple[ 

975 Union[DeclarativeInputParser[Any], DispatchingParserBase], 

976 DebputyPluginMetadata, 

977 int, 

978 ] 

979]: 

980 if segment_idx >= len(segments): 

981 return current_parser, current_plugin, segment_idx 

982 current_segment = segments[segment_idx] 

983 if isinstance(current_parser, ListWrappedDeclarativeInputParser): 

984 if isinstance(current_segment, int): 984 ↛ 991line 984 didn't jump to line 991 because the condition on line 984 was always true

985 current_parser = current_parser.delegate 

986 segment_idx += 1 

987 if segment_idx >= len(segments): 987 ↛ 988line 987 didn't jump to line 988 because the condition on line 987 was never true

988 return current_parser, current_plugin, segment_idx 

989 current_segment = segments[segment_idx] 

990 

991 if not isinstance(current_segment, str): 991 ↛ 992line 991 didn't jump to line 992 because the condition on line 991 was never true

992 return None 

993 

994 if is_completion_attempt and current_segment.endswith( 

995 (YAML_COMPLETION_HINT_KEY, YAML_COMPLETION_HINT_VALUE) 

996 ): 

997 return current_parser, current_plugin, segment_idx 

998 

999 if isinstance(current_parser, InPackageContextParser): 

1000 return resolve_keyword( 

1001 current_parser.delegate, 

1002 current_plugin, 

1003 segments, 

1004 segment_idx + 1, 

1005 parser_generator, 

1006 is_completion_attempt=is_completion_attempt, 

1007 ) 

1008 elif isinstance(current_parser, DispatchingParserBase): 

1009 if not current_parser.is_known_keyword(current_segment): 1009 ↛ 1010line 1009 didn't jump to line 1010 because the condition on line 1009 was never true

1010 if is_completion_attempt: 

1011 return current_parser, current_plugin, segment_idx 

1012 return None 

1013 subparser = current_parser.parser_for(current_segment) 

1014 segment_idx += 1 

1015 if segment_idx < len(segments): 

1016 return resolve_keyword( 

1017 subparser.parser, 

1018 subparser.plugin_metadata, 

1019 segments, 

1020 segment_idx, 

1021 parser_generator, 

1022 is_completion_attempt=is_completion_attempt, 

1023 ) 

1024 return subparser.parser, subparser.plugin_metadata, segment_idx 

1025 elif isinstance(current_parser, DeclarativeMappingInputParser): 1025 ↛ 1047line 1025 didn't jump to line 1047 because the condition on line 1025 was always true

1026 attr = current_parser.manifest_attributes.get(current_segment) 

1027 attr_type = attr.attribute_type if attr is not None else None 

1028 if ( 

1029 attr_type is not None 

1030 and isinstance(attr_type, type) 

1031 and issubclass(attr_type, DebputyDispatchableType) 

1032 ): 

1033 subparser = parser_generator.dispatch_parser_table_for(attr_type) 

1034 if subparser is not None and ( 

1035 is_completion_attempt or segment_idx + 1 < len(segments) 

1036 ): 

1037 return resolve_keyword( 

1038 subparser, 

1039 current_plugin, 

1040 segments, 

1041 segment_idx + 1, 

1042 parser_generator, 

1043 is_completion_attempt=is_completion_attempt, 

1044 ) 

1045 return current_parser, current_plugin, segment_idx 

1046 else: 

1047 _info(f"Unknown parser: {current_parser.__class__}") 

1048 return None 

1049 

1050 

1051def _trace_cursor( 

1052 content: Any, 

1053 attribute_path: AttributePath, 

1054 server_position: types.Position, 

1055) -> Optional[Tuple[bool, AttributePath, Any, Any]]: 

1056 matched_key: Optional[Union[str, int]] = None 

1057 matched: Optional[Node] = None 

1058 matched_was_key: bool = False 

1059 

1060 if isinstance(content, CommentedMap): 

1061 dict_lc: LineCol = content.lc 

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

1063 k_lc = dict_lc.key(k) 

1064 if is_before(server_position, k_lc): 1064 ↛ 1065line 1064 didn't jump to line 1065 because the condition on line 1064 was never true

1065 break 

1066 v_lc = dict_lc.value(k) 

1067 if is_before(server_position, v_lc): 

1068 # TODO: Handle ":" and "whitespace" 

1069 matched = k 

1070 matched_key = k 

1071 matched_was_key = True 

1072 break 

1073 matched = v 

1074 matched_key = k 

1075 elif isinstance(content, CommentedSeq): 1075 ↛ 1084line 1075 didn't jump to line 1084 because the condition on line 1075 was always true

1076 list_lc: LineCol = content.lc 

1077 for idx, value in enumerate(content): 

1078 i_lc = list_lc.item(idx) 

1079 if is_before(server_position, i_lc): 1079 ↛ 1080line 1079 didn't jump to line 1080 because the condition on line 1079 was never true

1080 break 

1081 matched_key = idx 

1082 matched = value 

1083 

1084 if matched is not None: 1084 ↛ 1090line 1084 didn't jump to line 1090 because the condition on line 1084 was always true

1085 assert matched_key is not None 

1086 sub_path = attribute_path[matched_key] 

1087 if not matched_was_key and isinstance(matched, CommentedBase): 

1088 return _trace_cursor(matched, sub_path, server_position) 

1089 return matched_was_key, sub_path, matched, content 

1090 return None 

1091 

1092 

1093def maybe_quote_yaml_value(v: str) -> str: 

1094 if v and v[0].isdigit(): 

1095 try: 

1096 float(v) 

1097 return f"'{v}'" 

1098 except ValueError: 

1099 pass 

1100 return v 

1101 

1102 

1103def _complete_value(v: Any) -> str: 

1104 if isinstance(v, str): 1104 ↛ 1106line 1104 didn't jump to line 1106 because the condition on line 1104 was always true

1105 return maybe_quote_yaml_value(v) 

1106 return str(v) 

1107 

1108 

1109def completion_from_attr( 

1110 attr: AttributeDescription, 

1111 pg: ParserGenerator, 

1112 matched: Any, 

1113 *, 

1114 matched_key: bool = False, 

1115 has_colon: bool = False, 

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

1117 type_mapping = pg.get_mapped_type_from_target_type(attr.attribute_type) 

1118 if type_mapping is not None: 1118 ↛ 1119line 1118 didn't jump to line 1119 because the condition on line 1118 was never true

1119 attr_type = type_mapping.source_type 

1120 else: 

1121 attr_type = attr.attribute_type 

1122 

1123 orig = get_origin(attr_type) 

1124 valid_values: Sequence[Any] = tuple() 

1125 

1126 if orig == Literal: 

1127 valid_values = get_args(attr_type) 

1128 elif orig == bool or attr.attribute_type == bool: 

1129 valid_values = ("true", "false") 

1130 elif isinstance(attr_type, type) and issubclass(attr_type, DebputyDispatchableType): 1130 ↛ 1143line 1130 didn't jump to line 1143 because the condition on line 1130 was always true

1131 parser: DispatchingTableParser[Any] = pg.dispatch_parser_table_for(attr_type) 

1132 if parser is None: 1132 ↛ 1133line 1132 didn't jump to line 1133 because the condition on line 1132 was never true

1133 return None 

1134 valid_values = [ 

1135 k if has_colon or not matched_key else f"{k}:" 

1136 for k in parser.registered_keywords() 

1137 if isinstance( 

1138 parser.parser_for(k).parser, DeclarativeValuelessKeywordInputParser 

1139 ) 

1140 ^ matched_key 

1141 ] 

1142 

1143 if matched in valid_values: 1143 ↛ 1144line 1143 didn't jump to line 1144 because the condition on line 1143 was never true

1144 _info(f"Already filled: {matched} is one of {valid_values}") 

1145 return None 

1146 if valid_values: 1146 ↛ 1148line 1146 didn't jump to line 1148 because the condition on line 1146 was always true

1147 return [types.CompletionItem(_complete_value(x)) for x in valid_values] 

1148 return None 

1149 

1150 

1151def completion_item( 

1152 quoted_keyword: str, 

1153 pp_subparser: PluginProvidedParser, 

1154) -> types.CompletionItem: 

1155 inline_reference_documentation = pp_subparser.parser.inline_reference_documentation 

1156 synopsis = ( 

1157 inline_reference_documentation.synopsis 

1158 if inline_reference_documentation 

1159 else None 

1160 ) 

1161 return types.CompletionItem( 

1162 quoted_keyword, 

1163 detail=synopsis, 

1164 ) 

1165 

1166 

1167def _is_inside_manifest_variable_substitution( 

1168 lines: List[str], 

1169 server_position: types.Position, 

1170) -> bool: 

1171 

1172 current_line = lines[server_position.line] 

1173 try: 

1174 open_idx = current_line[0 : server_position.character].rindex("{{") 

1175 return "}}" not in current_line[open_idx : server_position.character] 

1176 except ValueError: 

1177 return False 

1178 

1179 

1180def _manifest_substitution_variable_at_position( 

1181 lines: List[str], 

1182 server_position: types.Position, 

1183) -> Optional[str]: 

1184 current_line = lines[server_position.line] 

1185 try: 

1186 open_idx = current_line[0 : server_position.character].rindex("{{") + 2 

1187 if "}}" in current_line[open_idx : server_position.character]: 1187 ↛ 1188line 1187 didn't jump to line 1188 because the condition on line 1187 was never true

1188 return None 

1189 variable_len = current_line[open_idx:].index("}}") 

1190 close_idx = open_idx + variable_len 

1191 except ValueError as e: 

1192 return None 

1193 return current_line[open_idx:close_idx] 

1194 

1195 

1196def _insert_complete_marker_and_parse_yaml( 

1197 lines: List[str], 

1198 server_position: types.Position, 

1199) -> Optional[Any]: 

1200 added_key = insert_complete_marker_snippet(lines, server_position) 

1201 attempts = 1 if added_key else 2 

1202 content = None 

1203 while attempts > 0: 1203 ↛ 1235line 1203 didn't jump to line 1235 because the condition on line 1203 was always true

1204 attempts -= 1 

1205 try: 

1206 # Since we mutated the lines to insert a token, `doc.source` cannot 

1207 # be used here. 

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

1209 break 

1210 except MarkedYAMLError as e: 

1211 context_line = ( 

1212 e.context_mark.line if e.context_mark else e.problem_mark.line 

1213 ) 

1214 if ( 

1215 e.problem_mark.line != server_position.line 

1216 and context_line != server_position.line 

1217 ): 

1218 l_data = ( 

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

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

1221 else "N/A (OOB)" 

1222 ) 

1223 

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

1225 return None 

1226 

1227 if attempts > 0: 

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

1229 new_line = ( 

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

1231 ) 

1232 lines[server_position.line] = new_line 

1233 except YAMLError: 

1234 break 

1235 return content 

1236 

1237 

1238def generic_yaml_completer( 

1239 ls: "DebputyLanguageServer", 

1240 params: types.CompletionParams, 

1241 root_parser: DeclarativeInputParser[Any], 

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

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

1244 lines = _lines(doc.lines) 

1245 server_position = doc.position_codec.position_from_client_units( 

1246 lines, params.position 

1247 ) 

1248 orig_line = lines[server_position.line].rstrip() 

1249 has_colon = ":" in orig_line 

1250 

1251 content = _insert_complete_marker_and_parse_yaml(lines, server_position) 

1252 if content is None: 1252 ↛ 1253line 1252 didn't jump to line 1253 because the condition on line 1252 was never true

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

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

1255 return None 

1256 attribute_root_path = AttributePath.root_path(content) 

1257 m = _trace_cursor(content, attribute_root_path, server_position) 

1258 

1259 if m is None: 1259 ↛ 1260line 1259 didn't jump to line 1260 because the condition on line 1259 was never true

1260 _info("No match") 

1261 return None 

1262 matched_key, attr_path, matched, parent = m 

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

1264 feature_set = ls.plugin_feature_set 

1265 segments = list(attr_path.path_segments()) 

1266 km = resolve_keyword( 

1267 root_parser, 

1268 DEBPUTY_PLUGIN_METADATA, 

1269 segments, 

1270 0, 

1271 feature_set.manifest_parser_generator, 

1272 is_completion_attempt=True, 

1273 ) 

1274 if km is None: 1274 ↛ 1275line 1274 didn't jump to line 1275 because the condition on line 1274 was never true

1275 return None 

1276 parser, _, at_depth_idx = km 

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

1278 items = [] 

1279 if at_depth_idx + 1 < len(segments): 1279 ↛ 1280line 1279 didn't jump to line 1280 because the condition on line 1279 was never true

1280 return items 

1281 

1282 if _is_inside_manifest_variable_substitution(lines, server_position): 

1283 return [ 

1284 types.CompletionItem( 

1285 pv.variable_name, 

1286 detail=pv.variable_reference_documentation, 

1287 sort_text=( 

1288 f"zz-{pv.variable_name}" 

1289 if pv.is_for_special_case 

1290 else pv.variable_name 

1291 ), 

1292 ) 

1293 for pv in ls.plugin_feature_set.manifest_variables.values() 

1294 if not pv.is_internal 

1295 ] 

1296 

1297 if isinstance(parser, DispatchingParserBase): 

1298 if matched_key: 

1299 items = [ 

1300 completion_item( 

1301 ( 

1302 maybe_quote_yaml_value(k) 

1303 if has_colon 

1304 else f"{maybe_quote_yaml_value(k)}:" 

1305 ), 

1306 pp_subparser, 

1307 ) 

1308 for k, pp_subparser in _keywords_with_parser(parser) 

1309 if k not in parent 

1310 and not isinstance( 

1311 pp_subparser.parser, 

1312 DeclarativeValuelessKeywordInputParser, 

1313 ) 

1314 ] 

1315 else: 

1316 items = [ 

1317 completion_item(maybe_quote_yaml_value(k), pp_subparser) 

1318 for k, pp_subparser in _keywords_with_parser(parser) 

1319 if k not in parent 

1320 and isinstance( 

1321 pp_subparser.parser, 

1322 DeclarativeValuelessKeywordInputParser, 

1323 ) 

1324 ] 

1325 elif isinstance(parser, InPackageContextParser): 1325 ↛ 1326line 1325 didn't jump to line 1326 because the condition on line 1325 was never true

1326 binary_packages = ls.lint_state(doc).binary_packages 

1327 if binary_packages is not None: 

1328 items = [ 

1329 types.CompletionItem( 

1330 maybe_quote_yaml_value(p) 

1331 if has_colon 

1332 else f"{maybe_quote_yaml_value(p)}:" 

1333 ) 

1334 for p in binary_packages 

1335 if p not in parent 

1336 ] 

1337 elif isinstance(parser, DeclarativeMappingInputParser): 

1338 if matched_key: 

1339 _info("Match attributes") 

1340 locked = set(parent) 

1341 for mx in parser.mutually_exclusive_attributes: 

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

1343 locked.update(mx) 

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

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

1346 locked.add(attr_name) 

1347 break 

1348 items = [ 

1349 types.CompletionItem( 

1350 maybe_quote_yaml_value(k) 

1351 if has_colon 

1352 else f"{maybe_quote_yaml_value(k)}:" 

1353 ) 

1354 for k in parser.manifest_attributes 

1355 if k not in locked 

1356 ] 

1357 else: 

1358 # Value 

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

1360 value_attr = ( 

1361 parser.manifest_attributes.get(key) if isinstance(key, str) else None 

1362 ) 

1363 if value_attr is not None: 1363 ↛ 1373line 1363 didn't jump to line 1373 because the condition on line 1363 was always true

1364 _info(f"Expand value / key: {key} -- {value_attr.attribute_type}") 

1365 return completion_from_attr( 

1366 value_attr, 

1367 feature_set.manifest_parser_generator, 

1368 matched, 

1369 matched_key=False, 

1370 has_colon=has_colon, 

1371 ) 

1372 else: 

1373 _info( 

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

1375 ) 

1376 elif isinstance(parser, DeclarativeNonMappingInputParser): 1376 ↛ 1385line 1376 didn't jump to line 1385 because the condition on line 1376 was always true

1377 alt_attr = parser.alt_form_parser 

1378 return completion_from_attr( 

1379 alt_attr, 

1380 feature_set.manifest_parser_generator, 

1381 matched, 

1382 matched_key=matched_key, 

1383 has_colon=has_colon, 

1384 ) 

1385 return items 

1386 

1387 

1388def generic_yaml_hover( 

1389 ls: "DebputyLanguageServer", 

1390 params: types.HoverParams, 

1391 root_parser_initializer: Callable[ 

1392 [ParserGenerator], Union[DeclarativeInputParser[Any], DispatchingParserBase] 

1393 ], 

1394 *, 

1395 show_integration_mode: bool = False, 

1396) -> Optional[types.Hover]: 

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

1398 lines = doc.lines 

1399 position_codec = doc.position_codec 

1400 server_position = position_codec.position_from_client_units(lines, params.position) 

1401 

1402 try: 

1403 content = MANIFEST_YAML.load(doc.source) 

1404 except YAMLError: 

1405 return None 

1406 attribute_root_path = AttributePath.root_path(content) 

1407 m = _trace_cursor(content, attribute_root_path, server_position) 

1408 if m is None: 1408 ↛ 1409line 1408 didn't jump to line 1409 because the condition on line 1408 was never true

1409 _info("No match") 

1410 return None 

1411 matched_key, attr_path, matched, _ = m 

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

1413 

1414 feature_set = ls.plugin_feature_set 

1415 parser_generator = feature_set.manifest_parser_generator 

1416 root_parser = root_parser_initializer(parser_generator) 

1417 segments = list(attr_path.path_segments()) 

1418 km = resolve_keyword( 

1419 root_parser, 

1420 DEBPUTY_PLUGIN_METADATA, 

1421 segments, 

1422 0, 

1423 parser_generator, 

1424 ) 

1425 if km is None: 1425 ↛ 1426line 1425 didn't jump to line 1426 because the condition on line 1425 was never true

1426 _info("No keyword match") 

1427 return None 

1428 parser, plugin_metadata, at_depth_idx = km 

1429 

1430 manifest_variable_at_pos = _manifest_substitution_variable_at_position( 

1431 lines, server_position 

1432 ) 

1433 

1434 if manifest_variable_at_pos: 

1435 variable = ls.plugin_feature_set.manifest_variables.get( 

1436 manifest_variable_at_pos 

1437 ) 

1438 if variable is not None: 1438 ↛ 1467line 1438 didn't jump to line 1467 because the condition on line 1438 was always true

1439 var_doc = ( 

1440 variable.variable_reference_documentation 

1441 or "No documentation available" 

1442 ) 

1443 

1444 if variable.is_context_specific_variable: 1444 ↛ 1445line 1444 didn't jump to line 1445 because the condition on line 1444 was never true

1445 value = "\nThe value depends on the context" 

1446 else: 

1447 debian_dir = ls.lint_state(doc).debian_dir 

1448 value = "" 

1449 if debian_dir: 1449 ↛ 1450line 1449 didn't jump to line 1450 because the condition on line 1449 was never true

1450 variable_context = VariableContext(debian_dir) 

1451 try: 

1452 resolved = variable.resolve(variable_context) 

1453 value = f"\nResolves to: `{resolved}`" 

1454 except RuntimeError: 

1455 pass 

1456 

1457 hover_doc_text = textwrap.dedent( 

1458 """\ 

1459 # `{NAME}` 

1460 

1461 {DOC} 

1462 {VALUE} 

1463 """ 

1464 ).format(NAME=variable.variable_name, DOC=var_doc, VALUE=value) 

1465 return as_hover_doc(ls, hover_doc_text) 

1466 

1467 _info( 

1468 f"Match leaf parser {at_depth_idx}/{len(segments)} -- {parser.__class__} -- {manifest_variable_at_pos}" 

1469 ) 

1470 hover_doc_text = resolve_hover_text( 

1471 feature_set, 

1472 parser, 

1473 plugin_metadata, 

1474 ls.hover_output_style, 

1475 show_integration_mode, 

1476 segments, 

1477 at_depth_idx, 

1478 matched, 

1479 matched_key, 

1480 ) 

1481 return as_hover_doc(ls, hover_doc_text)