Coverage for src/debputy/lsp/lsp_debian_control_reference_data.py: 83%

1397 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +0000

1import collections 

2import dataclasses 

3import functools 

4import importlib.resources 

5import itertools 

6import operator 

7import os.path 

8import re 

9import textwrap 

10from abc import ABC 

11from typing import ( 

12 FrozenSet, 

13 Optional, 

14 cast, 

15 List, 

16 Generic, 

17 TypeVar, 

18 Union, 

19 Tuple, 

20 Any, 

21 Set, 

22 TYPE_CHECKING, 

23 Dict, 

24 Self, 

25) 

26from collections.abc import Mapping, Iterable, Callable, Sequence, Iterator, Container 

27 

28from debian.debian_support import DpkgArchTable, Version 

29 

30import debputy.lsp.data.deb822_data as deb822_ref_data_dir 

31from debputy.filesystem_scan import VirtualPathBase 

32from debputy.linting.lint_util import LintState, with_range_in_continuous_parts 

33from debputy.linting.lint_util import te_range_to_lsp 

34from debputy.lsp.diagnostics import LintSeverity 

35from debputy.lsp.lsp_reference_keyword import ( 

36 Keyword, 

37 allowed_values, 

38 format_comp_item_synopsis_doc, 

39 LSP_DATA_DOMAIN, 

40 ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

41) 

42from debputy.lsp.quickfixes import ( 

43 propose_correct_text_quick_fix, 

44 propose_remove_range_quick_fix, 

45) 

46from debputy.lsp.ref_models.deb822_reference_parse_models import ( 

47 Deb822ReferenceData, 

48 DEB822_REFERENCE_DATA_PARSER, 

49 FieldValueClass, 

50 StaticValue, 

51 Deb822Field, 

52 UsageHint, 

53 Alias, 

54) 

55from debputy.lsp.text_edit import apply_text_edits 

56from debputy.lsp.text_util import ( 

57 normalize_dctrl_field_name, 

58 LintCapablePositionCodec, 

59 trim_end_of_line_whitespace, 

60) 

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

62 Deb822KeyValuePairElement, 

63 LIST_SPACE_SEPARATED_INTERPRETATION, 

64 Deb822ParagraphElement, 

65 Deb822FileElement, 

66 Interpretation, 

67 parse_deb822_file, 

68 Deb822ParsedTokenList, 

69 Deb822ValueLineElement, 

70) 

71from debputy.lsp.vendoring._deb822_repro.tokens import ( 

72 Deb822FieldNameToken, 

73) 

74from debputy.lsp.vendoring._deb822_repro.types import FormatterCallback 

75from debputy.lsp.vendoring._deb822_repro.types import TE 

76from debputy.lsp.vendoring.wrap_and_sort import _sort_packages_key 

77from debputy.lsprotocol.types import ( 

78 DiagnosticTag, 

79 Range, 

80 TextEdit, 

81 Position, 

82 CompletionItem, 

83 MarkupContent, 

84 CompletionItemTag, 

85 MarkupKind, 

86 CompletionItemKind, 

87 CompletionItemLabelDetails, 

88) 

89from debputy.manifest_parser.exceptions import ManifestParseException 

90from debputy.manifest_parser.util import AttributePath 

91from debputy.path_matcher import BasenameGlobMatch 

92from debputy.plugin.api import VirtualPath 

93from debputy.util import PKGNAME_REGEX, _info, detect_possible_typo, _error 

94from debputy.yaml import MANIFEST_YAML 

95 

96try: 

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

98 Position as TEPosition, 

99 Range as TERange, 

100 START_POSITION, 

101 ) 

102except ImportError: 

103 pass 

104 

105 

106if TYPE_CHECKING: 

107 from debputy.lsp.maint_prefs import EffectiveFormattingPreference 

108 from debputy.lsp.debputy_ls import DebputyLanguageServer 

109 

110 

111F = TypeVar("F", bound="Deb822KnownField", covariant=True) 

112S = TypeVar("S", bound="StanzaMetadata") 

113 

114 

115SUBSTVAR_RE = re.compile(r"[$][{][a-zA-Z0-9][a-zA-Z0-9-:]*[}]") 

116 

117_RE_SYNOPSIS_STARTS_WITH_ARTICLE = re.compile(r"^\s*(an?|the)(?:\s|$)", re.I) 

118_RE_SV = re.compile(r"(\d+[.]\d+[.]\d+)([.]\d+)?") 

119_RE_SYNOPSIS_IS_TEMPLATE = re.compile( 

120 r"^\s*(missing|<insert up to \d+ chars description>)$" 

121) 

122_RE_SYNOPSIS_IS_TOO_SHORT = re.compile(r"^\s*(\S+)$") 

123CURRENT_STANDARDS_VERSION = Version("4.7.2") 

124 

125 

126CustomFieldCheck = Callable[ 

127 [ 

128 "F", 

129 Deb822FileElement, 

130 Deb822KeyValuePairElement, 

131 "TERange", 

132 "TERange", 

133 Deb822ParagraphElement, 

134 "TEPosition", 

135 LintState, 

136 ], 

137 None, 

138] 

139 

140 

141@functools.lru_cache 

142def all_package_relationship_fields() -> Mapping[str, str]: 

143 # TODO: Pull from `dpkg-dev` when possible fallback only to the static list. 

144 return { 

145 f.lower(): f 

146 for f in ( 

147 "Pre-Depends", 

148 "Depends", 

149 "Recommends", 

150 "Suggests", 

151 "Enhances", 

152 "Conflicts", 

153 "Breaks", 

154 "Replaces", 

155 "Provides", 

156 "Built-Using", 

157 "Static-Built-Using", 

158 ) 

159 } 

160 

161 

162@functools.lru_cache 

163def all_source_relationship_fields() -> Mapping[str, str]: 

164 # TODO: Pull from `dpkg-dev` when possible fallback only to the static list. 

165 return { 

166 f.lower(): f 

167 for f in ( 

168 "Build-Depends", 

169 "Build-Depends-Arch", 

170 "Build-Depends-Indep", 

171 "Build-Conflicts", 

172 "Build-Conflicts-Arch", 

173 "Build-Conflicts-Indep", 

174 ) 

175 } 

176 

177 

178ALL_SECTIONS_WITHOUT_COMPONENT = frozenset( 

179 [ 

180 "admin", 

181 "cli-mono", 

182 "comm", 

183 "database", 

184 "debian-installer", 

185 "debug", 

186 "devel", 

187 "doc", 

188 "editors", 

189 "education", 

190 "electronics", 

191 "embedded", 

192 "fonts", 

193 "games", 

194 "gnome", 

195 "gnu-r", 

196 "gnustep", 

197 "golang", 

198 "graphics", 

199 "hamradio", 

200 "haskell", 

201 "httpd", 

202 "interpreters", 

203 "introspection", 

204 "java", 

205 "javascript", 

206 "kde", 

207 "kernel", 

208 "libdevel", 

209 "libs", 

210 "lisp", 

211 "localization", 

212 "mail", 

213 "math", 

214 "metapackages", 

215 "misc", 

216 "net", 

217 "news", 

218 "ocaml", 

219 "oldlibs", 

220 "otherosfs", 

221 "perl", 

222 "php", 

223 "python", 

224 "ruby", 

225 "rust", 

226 "science", 

227 "shells", 

228 "sound", 

229 "tasks", 

230 "tex", 

231 "text", 

232 "utils", 

233 "vcs", 

234 "video", 

235 "virtual", 

236 "web", 

237 "x11", 

238 "xfce", 

239 "zope", 

240 ] 

241) 

242 

243ALL_COMPONENTS = frozenset( 

244 [ 

245 "main", 

246 "restricted", # Ubuntu 

247 "non-free", 

248 "non-free-firmware", 

249 "contrib", 

250 ] 

251) 

252 

253 

254def _fields(*fields: F) -> Mapping[str, F]: 

255 return {normalize_dctrl_field_name(f.name.lower()): f for f in fields} 

256 

257 

258def _complete_section_sort_hint( 

259 keyword: Keyword, 

260 _lint_state: LintState, 

261 stanza_parts: Sequence[Deb822ParagraphElement], 

262 _value_being_completed: str, 

263) -> str | None: 

264 for stanza in stanza_parts: 264 ↛ 269line 264 didn't jump to line 269 because the loop on line 264 didn't complete

265 pkg = stanza.get("Package") 

266 if pkg is not None: 266 ↛ 264line 266 didn't jump to line 264 because the condition on line 266 was always true

267 break 

268 else: 

269 return None 

270 section = package_name_to_section(pkg) 

271 value_parts = keyword.value.rsplit("/", 1) 

272 keyword_section = value_parts[-1] 

273 keyword_component = f" ({value_parts[0]})" if len(value_parts) > 1 else "" 

274 if section is None: 

275 if keyword_component == "": 

276 return keyword_section 

277 return f"zz-{keyword_section}{keyword_component}" 

278 if keyword_section != section: 

279 return f"zz-{keyword_section}{keyword_component}" 

280 return f"aa-{keyword_section}{keyword_component}" 

281 

282 

283ALL_SECTIONS = allowed_values( 

284 *[ 

285 Keyword( 

286 s if c is None else f"{c}/{s}", 

287 sort_text=_complete_section_sort_hint, 

288 replaced_by=s if c == "main" else None, 

289 ) 

290 for c, s in itertools.product( 

291 itertools.chain(cast("Iterable[Optional[str]]", [None]), ALL_COMPONENTS), 

292 ALL_SECTIONS_WITHOUT_COMPONENT, 

293 ) 

294 ] 

295) 

296 

297 

298def all_architectures_and_wildcards( 

299 arch2table, *, allow_negations: bool = False 

300) -> Iterable[str | Keyword]: 

301 wildcards = set() 

302 yield Keyword( 

303 "any", 

304 is_exclusive=True, 

305 synopsis="Built once per machine architecture (native code, such as C/C++, interpreter to C bindings)", 

306 long_description=textwrap.dedent( 

307 """\ 

308 This is an architecture-dependent package, and needs to be 

309 compiled for each and every architecture. 

310 

311 The name `any` refers to the fact that this is an architecture 

312 *wildcard* matching *any machine architecture* supported by 

313 dpkg. 

314 """ 

315 ), 

316 ) 

317 yield Keyword( 

318 "all", 

319 is_exclusive=True, 

320 synopsis="Independent of machine architecture (scripts, data, documentation, or Java without JNI)", 

321 long_description=textwrap.dedent( 

322 """\ 

323 The package is an architecture independent package. This is 

324 typically appropriate for packages containing only scripts, 

325 data or documentation. 

326 

327 The name `all` refers to the fact that the same build of a package 

328 can be used for *all* architectures. Though note that it is still 

329 subject to the rules of the `Multi-Arch` field. 

330 """ 

331 ), 

332 ) 

333 for arch_name, quad_tuple in arch2table.items(): 

334 yield arch_name 

335 if allow_negations: 

336 yield f"!{arch_name}" 

337 cpu_wc = "any-" + quad_tuple.cpu_name 

338 os_wc = quad_tuple.os_name + "-any" 

339 if cpu_wc not in wildcards: 

340 yield cpu_wc 

341 if allow_negations: 

342 yield f"!{cpu_wc}" 

343 wildcards.add(cpu_wc) 

344 if os_wc not in wildcards: 

345 yield os_wc 

346 if allow_negations: 

347 yield f"!{os_wc}" 

348 wildcards.add(os_wc) 

349 # Add the remaining wildcards 

350 

351 

352@functools.lru_cache 

353def dpkg_arch_and_wildcards(*, allow_negations=False) -> frozenset[str | Keyword]: 

354 dpkg_arch_table = DpkgArchTable.load_arch_table() 

355 return frozenset( 

356 all_architectures_and_wildcards( 

357 dpkg_arch_table._arch2table, 

358 allow_negations=allow_negations, 

359 ) 

360 ) 

361 

362 

363def extract_first_value_and_position( 

364 kvpair: Deb822KeyValuePairElement, 

365 stanza_pos: "TEPosition", 

366 *, 

367 interpretation: Interpretation[ 

368 Deb822ParsedTokenList[Any, Any] 

369 ] = LIST_SPACE_SEPARATED_INTERPRETATION, 

370) -> tuple[str | None, TERange | None]: 

371 kvpair_pos = kvpair.position_in_parent().relative_to(stanza_pos) 

372 value_element_pos = kvpair.value_element.position_in_parent().relative_to( 

373 kvpair_pos 

374 ) 

375 for value_ref in kvpair.interpret_as(interpretation).iter_value_references(): 375 ↛ 382line 375 didn't jump to line 382 because the loop on line 375 didn't complete

376 v = value_ref.value 

377 section_value_loc = value_ref.locatable 

378 value_range_te = section_value_loc.range_in_parent().relative_to( 

379 value_element_pos 

380 ) 

381 return v, value_range_te 

382 return None, None 

383 

384 

385def _sv_field_validation( 

386 known_field: "F", 

387 _deb822_file: Deb822FileElement, 

388 kvpair: Deb822KeyValuePairElement, 

389 _kvpair_range: "TERange", 

390 _field_name_range_te: "TERange", 

391 _stanza: Deb822ParagraphElement, 

392 stanza_position: "TEPosition", 

393 lint_state: LintState, 

394) -> None: 

395 sv_value, sv_value_range = extract_first_value_and_position( 

396 kvpair, 

397 stanza_position, 

398 ) 

399 m = _RE_SV.fullmatch(sv_value) 

400 if m is None: 

401 lint_state.emit_diagnostic( 

402 sv_value_range, 

403 f'Not a valid standards version. Current version is "{CURRENT_STANDARDS_VERSION}"', 

404 "warning", 

405 known_field.unknown_value_authority, 

406 ) 

407 return 

408 

409 sv_version = Version(sv_value) 

410 if sv_version < CURRENT_STANDARDS_VERSION: 

411 lint_state.emit_diagnostic( 

412 sv_value_range, 

413 f"Latest Standards-Version is {CURRENT_STANDARDS_VERSION}", 

414 "informational", 

415 known_field.unknown_value_authority, 

416 ) 

417 return 

418 extra = m.group(2) 

419 if extra: 

420 extra_len = lint_state.position_codec.client_num_units(extra) 

421 lint_state.emit_diagnostic( 

422 TERange.between( 

423 TEPosition( 

424 sv_value_range.end_pos.line_position, 

425 sv_value_range.end_pos.cursor_position - extra_len, 

426 ), 

427 sv_value_range.end_pos, 

428 ), 

429 "Unnecessary version segment. This part of the version is only used for editorial changes", 

430 "informational", 

431 known_field.unknown_value_authority, 

432 quickfixes=[ 

433 propose_remove_range_quick_fix( 

434 proposed_title="Remove unnecessary version part" 

435 ) 

436 ], 

437 ) 

438 

439 

440def _dctrl_ma_field_validation( 

441 _known_field: "F", 

442 _deb822_file: Deb822FileElement, 

443 _kvpair: Deb822KeyValuePairElement, 

444 _kvpair_range: "TERange", 

445 _field_name_range: "TERange", 

446 stanza: Deb822ParagraphElement, 

447 stanza_position: "TEPosition", 

448 lint_state: LintState, 

449) -> None: 

450 ma_kvpair = stanza.get_kvpair_element(("Multi-Arch", 0), use_get=True) 

451 arch = stanza.get("Architecture", "any") 

452 if arch == "all" and ma_kvpair is not None: 452 ↛ exitline 452 didn't return from function '_dctrl_ma_field_validation' because the condition on line 452 was always true

453 ma_value, ma_value_range = extract_first_value_and_position( 

454 ma_kvpair, 

455 stanza_position, 

456 ) 

457 if ma_value == "same": 

458 lint_state.emit_diagnostic( 

459 ma_value_range, 

460 "Multi-Arch: same is not valid for Architecture: all packages. Maybe you want foreign?", 

461 "error", 

462 "debputy", 

463 ) 

464 

465 

466def _udeb_only_field_validation( 

467 known_field: "F", 

468 _deb822_file: Deb822FileElement, 

469 _kvpair: Deb822KeyValuePairElement, 

470 _kvpair_range: "TERange", 

471 field_name_range: "TERange", 

472 stanza: Deb822ParagraphElement, 

473 _stanza_position: "TEPosition", 

474 lint_state: LintState, 

475) -> None: 

476 package_type = stanza.get("Package-Type") 

477 if package_type != "udeb": 

478 lint_state.emit_diagnostic( 

479 field_name_range, 

480 f"The {known_field.name} field is only applicable to udeb packages (`Package-Type: udeb`)", 

481 "warning", 

482 "debputy", 

483 ) 

484 

485 

486def _complete_only_in_arch_dep_pkgs( 

487 stanza_parts: Iterable[Deb822ParagraphElement], 

488) -> bool: 

489 for stanza in stanza_parts: 

490 arch = stanza.get("Architecture") 

491 if arch is None: 

492 continue 

493 archs = arch.split() 

494 return "all" not in archs 

495 return False 

496 

497 

498def _complete_only_for_udeb_pkgs( 

499 stanza_parts: Iterable[Deb822ParagraphElement], 

500) -> bool: 

501 for stanza in stanza_parts: 

502 for option in ("Package-Type", "XC-Package-Type"): 

503 pkg_type = stanza.get(option) 

504 if pkg_type is not None: 

505 return pkg_type == "udeb" 

506 return False 

507 

508 

509def _arch_not_all_only_field_validation( 

510 known_field: "F", 

511 _deb822_file: Deb822FileElement, 

512 _kvpair: Deb822KeyValuePairElement, 

513 _kvpair_range_te: "TERange", 

514 field_name_range_te: "TERange", 

515 stanza: Deb822ParagraphElement, 

516 _stanza_position: "TEPosition", 

517 lint_state: LintState, 

518) -> None: 

519 architecture = stanza.get("Architecture") 

520 if architecture == "all": 520 ↛ exitline 520 didn't return from function '_arch_not_all_only_field_validation' because the condition on line 520 was always true

521 lint_state.emit_diagnostic( 

522 field_name_range_te, 

523 f"The {known_field.name} field is not applicable to arch:all packages (`Architecture: all`)", 

524 "warning", 

525 "debputy", 

526 ) 

527 

528 

529def _binary_package_from_same_source( 

530 known_field: "F", 

531 _deb822_file: Deb822FileElement, 

532 _kvpair: Deb822KeyValuePairElement, 

533 kvpair_range: "TERange", 

534 _field_name_range: "TERange", 

535 stanza: Deb822ParagraphElement, 

536 stanza_position: "TEPosition", 

537 lint_state: LintState, 

538) -> None: 

539 doc_main_package_kvpair = stanza.get_kvpair_element( 

540 (known_field.name, 0), use_get=True 

541 ) 

542 if len(lint_state.binary_packages) == 1: 

543 lint_state.emit_diagnostic( 

544 kvpair_range, 

545 f"The {known_field.name} field is redundant for source packages that only build one binary package", 

546 "warning", 

547 "debputy", 

548 quickfixes=[propose_remove_range_quick_fix()], 

549 ) 

550 return 

551 if doc_main_package_kvpair is not None: 551 ↛ exitline 551 didn't return from function '_binary_package_from_same_source' because the condition on line 551 was always true

552 doc_main_package, value_range = extract_first_value_and_position( 

553 doc_main_package_kvpair, 

554 stanza_position, 

555 ) 

556 if doc_main_package is None or doc_main_package in lint_state.binary_packages: 

557 return 

558 lint_state.emit_diagnostic( 

559 value_range, 

560 f"The {known_field.name} field must name a package listed in debian/control", 

561 "error", 

562 "debputy", 

563 quickfixes=[ 

564 propose_correct_text_quick_fix(name) 

565 for name in lint_state.binary_packages 

566 ], 

567 ) 

568 

569 

570def _single_line_span_range_relative_to_pos( 

571 span: tuple[int, int], 

572 relative_to: "TEPosition", 

573) -> Range: 

574 return TERange( 

575 TEPosition( 

576 relative_to.line_position, 

577 relative_to.cursor_position + span[0], 

578 ), 

579 TEPosition( 

580 relative_to.line_position, 

581 relative_to.cursor_position + span[1], 

582 ), 

583 ) 

584 

585 

586def _check_extended_description_line( 

587 description_value_line: Deb822ValueLineElement, 

588 description_line_range_te: "TERange", 

589 package: str | None, 

590 lint_state: LintState, 

591) -> None: 

592 if description_value_line.comment_element is not None: 592 ↛ 595line 592 didn't jump to line 595 because the condition on line 592 was never true

593 # TODO: Fix this limitation (we get the content and the range wrong with comments. 

594 # They are rare inside a Description, so this is a 80:20 trade off 

595 return 

596 description_line_with_leading_space = ( 

597 description_value_line.convert_to_text().rstrip() 

598 ) 

599 try: 

600 idx = description_line_with_leading_space.index( 

601 "<insert long description, indented with spaces>" 

602 ) 

603 except ValueError: 

604 pass 

605 else: 

606 template_span = idx, idx + len( 

607 "<insert long description, indented with spaces>" 

608 ) 

609 lint_state.emit_diagnostic( 

610 _single_line_span_range_relative_to_pos( 

611 template_span, 

612 description_line_range_te.start_pos, 

613 ), 

614 "Unfilled or left-over template from dh_make", 

615 "error", 

616 "debputy", 

617 ) 

618 if len(description_line_with_leading_space) > 80: 

619 # Policy says nothing here, but lintian has 80 characters as hard limit and that 

620 # probably matches the limitation of package manager UIs (TUIs/GUIs) somewhere. 

621 # 

622 # See also debputy#122 

623 span = 80, len(description_line_with_leading_space) 

624 lint_state.emit_diagnostic( 

625 _single_line_span_range_relative_to_pos( 

626 span, 

627 description_line_range_te.start_pos, 

628 ), 

629 "Package description line is too long; please line wrap it.", 

630 "warning", 

631 "debputy", 

632 ) 

633 

634 

635def _check_synopsis( 

636 synopsis_value_line: Deb822ValueLineElement, 

637 synopsis_range_te: "TERange", 

638 field_name_range_te: "TERange", 

639 package: str | None, 

640 lint_state: LintState, 

641) -> None: 

642 # This function would compute range would be wrong if there is a comment 

643 assert synopsis_value_line.comment_element is None 

644 synopsis_text_with_leading_space = synopsis_value_line.convert_to_text().rstrip() 

645 if not synopsis_text_with_leading_space: 

646 lint_state.emit_diagnostic( 

647 field_name_range_te, 

648 "Package synopsis is missing", 

649 "warning", 

650 "debputy", 

651 ) 

652 return 

653 synopsis_text_trimmed = synopsis_text_with_leading_space.lstrip() 

654 synopsis_offset = len(synopsis_text_with_leading_space) - len(synopsis_text_trimmed) 

655 starts_with_article = _RE_SYNOPSIS_STARTS_WITH_ARTICLE.search( 

656 synopsis_text_with_leading_space 

657 ) 

658 # TODO: Handle ${...} expansion 

659 if starts_with_article: 

660 lint_state.emit_diagnostic( 

661 _single_line_span_range_relative_to_pos( 

662 starts_with_article.span(1), 

663 synopsis_range_te.start_pos, 

664 ), 

665 "Package synopsis starts with an article (a/an/the).", 

666 "warning", 

667 "DevRef 6.2.2", 

668 ) 

669 if len(synopsis_text_trimmed) >= 80: 

670 # Policy says `certainly under 80 characters.`, so exactly 80 characters is considered bad too. 

671 span = synopsis_offset + 79, len(synopsis_text_with_leading_space) 

672 lint_state.emit_diagnostic( 

673 _single_line_span_range_relative_to_pos( 

674 span, 

675 synopsis_range_te.start_pos, 

676 ), 

677 "Package synopsis is too long.", 

678 "warning", 

679 "Policy 3.4.1", 

680 ) 

681 if template_match := _RE_SYNOPSIS_IS_TEMPLATE.match( 

682 synopsis_text_with_leading_space 

683 ): 

684 lint_state.emit_diagnostic( 

685 _single_line_span_range_relative_to_pos( 

686 template_match.span(1), 

687 synopsis_range_te.start_pos, 

688 ), 

689 "Package synopsis is a placeholder", 

690 "warning", 

691 "debputy", 

692 ) 

693 elif too_short_match := _RE_SYNOPSIS_IS_TOO_SHORT.match( 

694 synopsis_text_with_leading_space 

695 ): 

696 if not SUBSTVAR_RE.match(synopsis_text_with_leading_space.strip()): 

697 lint_state.emit_diagnostic( 

698 _single_line_span_range_relative_to_pos( 

699 too_short_match.span(1), 

700 synopsis_range_te.start_pos, 

701 ), 

702 "Package synopsis is too short", 

703 "warning", 

704 "debputy", 

705 ) 

706 

707 

708def dctrl_description_validator( 

709 _known_field: "F", 

710 _deb822_file: Deb822FileElement, 

711 kvpair: Deb822KeyValuePairElement, 

712 kvpair_range_te: "TERange", 

713 _field_name_range: "TERange", 

714 stanza: Deb822ParagraphElement, 

715 _stanza_position: "TEPosition", 

716 lint_state: LintState, 

717) -> None: 

718 value_lines = kvpair.value_element.value_lines 

719 if not value_lines: 719 ↛ 720line 719 didn't jump to line 720 because the condition on line 719 was never true

720 return 

721 package = stanza.get("Package") 

722 synopsis_value_line = value_lines[0] 

723 value_range_te = kvpair.value_element.range_in_parent().relative_to( 

724 kvpair_range_te.start_pos 

725 ) 

726 synopsis_line_range_te = synopsis_value_line.range_in_parent().relative_to( 

727 value_range_te.start_pos 

728 ) 

729 if synopsis_value_line.continuation_line_token is None: 729 ↛ 742line 729 didn't jump to line 742 because the condition on line 729 was always true

730 field_name_range_te = kvpair.field_token.range_in_parent().relative_to( 

731 kvpair_range_te.start_pos 

732 ) 

733 _check_synopsis( 

734 synopsis_value_line, 

735 synopsis_line_range_te, 

736 field_name_range_te, 

737 package, 

738 lint_state, 

739 ) 

740 description_lines = value_lines[1:] 

741 else: 

742 description_lines = value_lines 

743 for description_line in description_lines: 

744 description_line_range_te = description_line.range_in_parent().relative_to( 

745 value_range_te.start_pos 

746 ) 

747 _check_extended_description_line( 

748 description_line, 

749 description_line_range_te, 

750 package, 

751 lint_state, 

752 ) 

753 

754 

755def _has_packaging_expected_file( 

756 name: str, 

757 msg: str, 

758 severity: LintSeverity = "error", 

759) -> CustomFieldCheck: 

760 

761 def _impl( 

762 _known_field: "F", 

763 _deb822_file: Deb822FileElement, 

764 _kvpair: Deb822KeyValuePairElement, 

765 kvpair_range_te: "TERange", 

766 _field_name_range_te: "TERange", 

767 _stanza: Deb822ParagraphElement, 

768 _stanza_position: "TEPosition", 

769 lint_state: LintState, 

770 ) -> None: 

771 debian_dir = lint_state.debian_dir 

772 if debian_dir is None: 

773 return 

774 cpy = debian_dir.lookup(name) 

775 if not cpy: 775 ↛ 776line 775 didn't jump to line 776 because the condition on line 775 was never true

776 lint_state.emit_diagnostic( 

777 kvpair_range_te, 

778 msg, 

779 severity, 

780 "debputy", 

781 diagnostic_applies_to_another_file=f"debian/{name}", 

782 ) 

783 

784 return _impl 

785 

786 

787_check_missing_debian_rules = _has_packaging_expected_file( 

788 "rules", 

789 'Missing debian/rules when the Build-Driver is unset or set to "debian-rules"', 

790) 

791 

792 

793def _has_build_instructions( 

794 known_field: "F", 

795 deb822_file: Deb822FileElement, 

796 kvpair: Deb822KeyValuePairElement, 

797 kvpair_range_te: "TERange", 

798 field_name_range_te: "TERange", 

799 stanza: Deb822ParagraphElement, 

800 stanza_position: "TEPosition", 

801 lint_state: LintState, 

802) -> None: 

803 if stanza.get("Build-Driver", "debian-rules").lower() != "debian-rules": 

804 return 

805 

806 _check_missing_debian_rules( 

807 known_field, 

808 deb822_file, 

809 kvpair, 

810 kvpair_range_te, 

811 field_name_range_te, 

812 stanza, 

813 stanza_position, 

814 lint_state, 

815 ) 

816 

817 

818def _canonical_maintainer_name( 

819 known_field: "F", 

820 _deb822_file: Deb822FileElement, 

821 kvpair: Deb822KeyValuePairElement, 

822 kvpair_range_te: "TERange", 

823 _field_name_range_te: "TERange", 

824 _stanza: Deb822ParagraphElement, 

825 _stanza_position: "TEPosition", 

826 lint_state: LintState, 

827) -> None: 

828 value_element_pos = kvpair.value_element.position_in_parent().relative_to( 

829 kvpair_range_te.start_pos 

830 ) 

831 try: 

832 interpreted_value = kvpair.interpret_as( 

833 known_field.field_value_class.interpreter() 

834 ) 

835 except ValueError: 

836 return 

837 

838 for part in interpreted_value.iter_parts(): 

839 if part.is_separator or part.is_whitespace or part.is_whitespace: 

840 continue 

841 name_and_email = part.convert_to_text() 

842 try: 

843 email_start = name_and_email.rindex("<") 

844 email_end = name_and_email.rindex(">") 

845 email = name_and_email[email_start + 1 : email_end] 

846 except IndexError: 

847 continue 

848 

849 pref = lint_state.maint_preference_table.maintainer_preferences.get(email) 

850 if pref is None or not pref.canonical_name: 

851 continue 

852 

853 expected = f"{pref.canonical_name} <{email}>" 

854 if expected == name_and_email: 854 ↛ 855line 854 didn't jump to line 855 because the condition on line 854 was never true

855 continue 

856 value_range_te = part.range_in_parent().relative_to(value_element_pos) 

857 lint_state.emit_diagnostic( 

858 value_range_te, 

859 "Non-canonical or incorrect spelling of maintainer name", 

860 "informational", 

861 "debputy", 

862 quickfixes=[propose_correct_text_quick_fix(expected)], 

863 ) 

864 

865 

866def _maintainer_field_validator( 

867 known_field: "F", 

868 _deb822_file: Deb822FileElement, 

869 kvpair: Deb822KeyValuePairElement, 

870 kvpair_range_te: "TERange", 

871 _field_name_range_te: "TERange", 

872 _stanza: Deb822ParagraphElement, 

873 _stanza_position: "TEPosition", 

874 lint_state: LintState, 

875) -> None: 

876 

877 value_element_pos = kvpair.value_element.position_in_parent().relative_to( 

878 kvpair_range_te.start_pos 

879 ) 

880 interpreted_value = kvpair.interpret_as(known_field.field_value_class.interpreter()) 

881 for part in interpreted_value.iter_parts(): 

882 if not part.is_separator: 

883 continue 

884 value_range_te = part.range_in_parent().relative_to(value_element_pos) 

885 severity = known_field.unknown_value_severity 

886 assert severity is not None 

887 # TODO: Check for a follow up maintainer and based on that the quick fix is either 

888 # to remove the dead separator OR move the trailing data into `Uploaders` 

889 lint_state.emit_diagnostic( 

890 value_range_te, 

891 'The "Maintainer" field has a trailing separator, but it is a single value field.', 

892 severity, 

893 known_field.unknown_value_authority, 

894 ) 

895 

896 

897def _use_https_instead_of_http( 

898 known_field: "F", 

899 _deb822_file: Deb822FileElement, 

900 kvpair: Deb822KeyValuePairElement, 

901 kvpair_range_te: "TERange", 

902 _field_name_range_te: "TERange", 

903 _stanza: Deb822ParagraphElement, 

904 _stanza_position: "TEPosition", 

905 lint_state: LintState, 

906) -> None: 

907 value_element_pos = kvpair.value_element.position_in_parent().relative_to( 

908 kvpair_range_te.start_pos 

909 ) 

910 interpreted_value = kvpair.interpret_as(known_field.field_value_class.interpreter()) 

911 for part in interpreted_value.iter_parts(): 

912 value = part.convert_to_text() 

913 if not value.startswith("http://"): 

914 continue 

915 value_range_te = part.range_in_parent().relative_to(value_element_pos) 

916 problem_range_te = TERange.between( 

917 value_range_te.start_pos, 

918 TEPosition( 

919 value_range_te.start_pos.line_position, 

920 value_range_te.start_pos.cursor_position + 7, 

921 ), 

922 ) 

923 lint_state.emit_diagnostic( 

924 problem_range_te, 

925 "The Format URL should use https:// rather than http://", 

926 "warning", 

927 "debputy", 

928 quickfixes=[propose_correct_text_quick_fix("https://")], 

929 ) 

930 

931 

932def _each_value_match_regex_validation( 

933 regex: re.Pattern, 

934 *, 

935 diagnostic_severity: LintSeverity = "error", 

936 authority_reference: str | None = None, 

937) -> CustomFieldCheck: 

938 

939 def _validator( 

940 known_field: "F", 

941 _deb822_file: Deb822FileElement, 

942 kvpair: Deb822KeyValuePairElement, 

943 kvpair_range_te: "TERange", 

944 _field_name_range_te: "TERange", 

945 _stanza: Deb822ParagraphElement, 

946 _stanza_position: "TEPosition", 

947 lint_state: LintState, 

948 ) -> None: 

949 nonlocal authority_reference 

950 interpreter = known_field.field_value_class.interpreter() 

951 if interpreter is None: 

952 raise AssertionError( 

953 f"{known_field.name} has field type {known_field.field_value_class}, which cannot be" 

954 f" regex validated since it does not have a tokenization" 

955 ) 

956 auth_ref = ( 

957 authority_reference 

958 if authority_reference is not None 

959 else known_field.unknown_value_authority 

960 ) 

961 

962 value_element_pos = kvpair.value_element.position_in_parent().relative_to( 

963 kvpair_range_te.start_pos 

964 ) 

965 for value_ref in kvpair.interpret_as(interpreter).iter_value_references(): 

966 v = value_ref.value 

967 m = regex.fullmatch(v) 

968 if m is not None: 

969 continue 

970 

971 if "${" in v: 

972 # Ignore substvars 

973 continue 

974 

975 section_value_loc = value_ref.locatable 

976 value_range_te = section_value_loc.range_in_parent().relative_to( 

977 value_element_pos 

978 ) 

979 lint_state.emit_diagnostic( 

980 value_range_te, 

981 f'The value "{v}" does not match the regex {regex.pattern}.', 

982 diagnostic_severity, 

983 auth_ref, 

984 ) 

985 

986 return _validator 

987 

988 

989_DEP_OR_RELATION = re.compile(r"[|]") 

990_DEP_RELATION_CLAUSE = re.compile( 

991 r""" 

992 ^ 

993 \s* 

994 (?P<name_arch_qual>[-+.a-zA-Z0-9${}:]{2,}) 

995 \s* 

996 (?: [(] \s* (?P<operator>>>|>=|>|=|<|<=|<<) \s* (?P<version> [0-9$][^)]*|[$][{]\S+[}]) \s* [)] \s* )? 

997 (?: \[ (?P<arch_restriction> [\s!\w\-]+) ] \s*)? 

998 (?: < (?P<build_profile_restriction> .+ ) > \s*)? 

999 ((?P<garbage>\S.*)\s*)? 

1000 $ 

1001""", 

1002 re.VERBOSE | re.MULTILINE, 

1003) 

1004 

1005 

1006def _span_to_te_range( 

1007 text: str, 

1008 start_pos: int, 

1009 end_pos: int, 

1010) -> TERange: 

1011 prefix = text[0:start_pos] 

1012 prefix_plus_text = text[0:end_pos] 

1013 

1014 start_line = prefix.count("\n") 

1015 if start_line: 

1016 start_newline_offset = prefix.rindex("\n") 

1017 # +1 to skip past the newline 

1018 start_cursor_pos = start_pos - (start_newline_offset + 1) 

1019 else: 

1020 start_cursor_pos = start_pos 

1021 

1022 end_line = prefix_plus_text.count("\n") 

1023 if end_line == start_line: 

1024 end_cursor_pos = start_cursor_pos + (end_pos - start_pos) 

1025 else: 

1026 end_newline_offset = prefix_plus_text.rindex("\n") 

1027 end_cursor_pos = end_pos - (end_newline_offset + 1) 

1028 

1029 return TERange( 

1030 TEPosition( 

1031 start_line, 

1032 start_cursor_pos, 

1033 ), 

1034 TEPosition( 

1035 end_line, 

1036 end_cursor_pos, 

1037 ), 

1038 ) 

1039 

1040 

1041def _split_w_spans( 

1042 v: str, 

1043 sep: str, 

1044 *, 

1045 offset: int = 0, 

1046) -> Sequence[tuple[str, int, int]]: 

1047 separator_size = len(sep) 

1048 parts = v.split(sep) 

1049 for part in parts: 

1050 size = len(part) 

1051 end_offset = offset + size 

1052 yield part, offset, end_offset 

1053 offset = end_offset + separator_size 

1054 

1055 

1056_COLLAPSE_WHITESPACE = re.compile(r"\s+") 

1057 

1058 

1059def _cleanup_rel(rel: str) -> str: 

1060 return _COLLAPSE_WHITESPACE.sub(" ", rel.strip()) 

1061 

1062 

1063def _text_to_te_position(text: str) -> "TEPosition": 

1064 newlines = text.count("\n") 

1065 if not newlines: 

1066 return TEPosition( 

1067 newlines, 

1068 len(text), 

1069 ) 

1070 last_newline_offset = text.rindex("\n") 

1071 line_offset = len(text) - (last_newline_offset + 1) 

1072 return TEPosition( 

1073 newlines, 

1074 line_offset, 

1075 ) 

1076 

1077 

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

1079class Relation: 

1080 name: str 

1081 arch_qual: str | None = None 

1082 version_operator: str | None = None 

1083 version: str | None = None 

1084 arch_restriction: str | None = None 

1085 build_profile_restriction: str | None = None 

1086 # These offsets are intended to show the relation itself. They are not 

1087 # the relation boundary offsets (they will omit leading whitespace as 

1088 # an example). 

1089 content_display_offset: int = -1 

1090 content_display_end_offset: int = -1 

1091 

1092 

1093def relation_key_variations( 

1094 relation: Relation, 

1095) -> tuple[str, str | None, str | None]: 

1096 operator_variants = ( 

1097 [relation.version_operator, None] 

1098 if relation.version_operator is not None 

1099 else [None] 

1100 ) 

1101 arch_qual_variants = ( 

1102 [relation.arch_qual, None] 

1103 if relation.arch_qual is not None and relation.arch_qual != "any" 

1104 else [None] 

1105 ) 

1106 for arch_qual, version_operator in itertools.product( 

1107 arch_qual_variants, 

1108 operator_variants, 

1109 ): 

1110 yield relation.name, arch_qual, version_operator 

1111 

1112 

1113def dup_check_relations( 

1114 known_field: "F", 

1115 relations: Sequence[Relation], 

1116 raw_value_masked_comments: str, 

1117 value_element_pos: "TEPosition", 

1118 lint_state: LintState, 

1119) -> None: 

1120 overlap_table = {} 

1121 for relation in relations: 

1122 version_operator = relation.version_operator 

1123 arch_qual = relation.arch_qual 

1124 if relation.arch_restriction or relation.build_profile_restriction: 1124 ↛ 1125line 1124 didn't jump to line 1125 because the condition on line 1124 was never true

1125 continue 

1126 

1127 for relation_key in relation_key_variations(relation): 

1128 prev_relation = overlap_table.get(relation_key) 

1129 if prev_relation is None: 

1130 overlap_table[relation_key] = relation 

1131 else: 

1132 prev_version_operator = prev_relation.version_operator 

1133 

1134 if ( 

1135 prev_version_operator 

1136 and version_operator 

1137 and prev_version_operator[0] != version_operator[0] 

1138 and version_operator[0] in ("<", ">") 

1139 and prev_version_operator[0] in ("<", ">") 

1140 ): 

1141 # foo (>= 1), foo (<< 2) and similar should not trigger a warning. 

1142 continue 

1143 

1144 prev_arch_qual = prev_relation.arch_qual 

1145 if ( 

1146 arch_qual != prev_arch_qual 

1147 and prev_arch_qual != "any" 

1148 and arch_qual != "any" 

1149 ): 

1150 # foo:amd64 != foo:native and that might matter - especially for "libfoo-dev, libfoo-dev:native" 

1151 # 

1152 # This check is probably a too forgiving in some corner cases. 

1153 continue 

1154 

1155 orig_relation_range = TERange( 

1156 _text_to_te_position( 

1157 raw_value_masked_comments[ 

1158 : prev_relation.content_display_offset 

1159 ] 

1160 ), 

1161 _text_to_te_position( 

1162 raw_value_masked_comments[ 

1163 : prev_relation.content_display_end_offset 

1164 ] 

1165 ), 

1166 ).relative_to(value_element_pos) 

1167 

1168 duplicate_relation_range = TERange( 

1169 _text_to_te_position( 

1170 raw_value_masked_comments[: relation.content_display_offset] 

1171 ), 

1172 _text_to_te_position( 

1173 raw_value_masked_comments[: relation.content_display_end_offset] 

1174 ), 

1175 ).relative_to(value_element_pos) 

1176 

1177 lint_state.emit_diagnostic( 

1178 duplicate_relation_range, 

1179 "Duplicate relationship. Merge with the previous relationship", 

1180 "warning", 

1181 known_field.unknown_value_authority, 

1182 related_information=[ 

1183 lint_state.related_diagnostic_information( 

1184 orig_relation_range, 

1185 "The previous definition", 

1186 ), 

1187 ], 

1188 ) 

1189 # We only emit for the first duplicate "key" for each relation. Odds are remaining 

1190 # keys point to the same match. Even if they do not, it does not really matter as 

1191 # we already pointed out an issue for the user to follow up on. 

1192 break 

1193 

1194 

1195def _dctrl_check_dep_version_operator( 

1196 known_field: "F", 

1197 version_operator: str, 

1198 version_operator_span: tuple[int, int], 

1199 version_operators: frozenset[str], 

1200 raw_value_masked_comments: str, 

1201 offset: int, 

1202 value_element_pos: "TEPosition", 

1203 lint_state: LintState, 

1204) -> bool: 

1205 if ( 

1206 version_operators 

1207 and version_operator is not None 

1208 and version_operator not in version_operators 

1209 ): 

1210 v_start_offset = offset + version_operator_span[0] 

1211 v_end_offset = offset + version_operator_span[1] 

1212 version_problem_range_te = TERange( 

1213 _text_to_te_position(raw_value_masked_comments[:v_start_offset]), 

1214 _text_to_te_position(raw_value_masked_comments[:v_end_offset]), 

1215 ).relative_to(value_element_pos) 

1216 

1217 sorted_version_operators = sorted(version_operators) 

1218 

1219 excluding_equal = f"{version_operator}{version_operator}" 

1220 including_equal = f"{version_operator}=" 

1221 

1222 if version_operator in (">", "<") and ( 

1223 excluding_equal in version_operators or including_equal in version_operators 

1224 ): 

1225 lint_state.emit_diagnostic( 

1226 version_problem_range_te, 

1227 f'Obsolete version operator "{version_operator}" that is no longer supported.', 

1228 "error", 

1229 "Policy 7.1", 

1230 quickfixes=[ 

1231 propose_correct_text_quick_fix(n) 

1232 for n in (excluding_equal, including_equal) 

1233 if not version_operators or n in version_operators 

1234 ], 

1235 ) 

1236 else: 

1237 lint_state.emit_diagnostic( 

1238 version_problem_range_te, 

1239 f'The version operator "{version_operator}" is not allowed in {known_field.name}', 

1240 "error", 

1241 known_field.unknown_value_authority, 

1242 quickfixes=[ 

1243 propose_correct_text_quick_fix(n) for n in sorted_version_operators 

1244 ], 

1245 ) 

1246 return True 

1247 return False 

1248 

1249 

1250def _dctrl_validate_dep( 

1251 known_field: "DF", 

1252 _deb822_file: Deb822FileElement, 

1253 kvpair: Deb822KeyValuePairElement, 

1254 kvpair_range_te: "TERange", 

1255 _field_name_range: "TERange", 

1256 _stanza: Deb822ParagraphElement, 

1257 _stanza_position: "TEPosition", 

1258 lint_state: LintState, 

1259) -> None: 

1260 value_element_pos = kvpair.value_element.position_in_parent().relative_to( 

1261 kvpair_range_te.start_pos 

1262 ) 

1263 raw_value_with_comments = kvpair.value_element.convert_to_text() 

1264 raw_value_masked_comments = "".join( 

1265 (line if not line.startswith("#") else (" " * (len(line) - 1)) + "\n") 

1266 for line in raw_value_with_comments.splitlines(keepends=True) 

1267 ) 

1268 if isinstance(known_field, DctrlRelationshipKnownField): 

1269 version_operators = known_field.allowed_version_operators 

1270 supports_or_relation = known_field.supports_or_relation 

1271 else: 

1272 version_operators = frozenset({">>", ">=", "=", "<=", "<<"}) 

1273 supports_or_relation = True 

1274 

1275 relation_dup_table = collections.defaultdict(list) 

1276 

1277 for rel, rel_offset, rel_end_offset in _split_w_spans( 

1278 raw_value_masked_comments, "," 

1279 ): 

1280 sub_relations = [] 

1281 for or_rel, offset, end_offset in _split_w_spans(rel, "|", offset=rel_offset): 

1282 if or_rel.isspace(): 

1283 continue 

1284 if sub_relations and not supports_or_relation: 

1285 separator_range_te = TERange( 

1286 _text_to_te_position(raw_value_masked_comments[: offset - 1]), 

1287 _text_to_te_position(raw_value_masked_comments[:offset]), 

1288 ).relative_to(value_element_pos) 

1289 lint_state.emit_diagnostic( 

1290 separator_range_te, 

1291 f'The field {known_field.name} does not support "|" (OR) in relations.', 

1292 "error", 

1293 known_field.unknown_value_authority, 

1294 ) 

1295 m = _DEP_RELATION_CLAUSE.fullmatch(or_rel) 

1296 

1297 if m is not None: 

1298 garbage = m.group("garbage") 

1299 version_operator = m.group("operator") 

1300 version_operator_span = m.span("operator") 

1301 if _dctrl_check_dep_version_operator( 

1302 known_field, 

1303 version_operator, 

1304 version_operator_span, 

1305 version_operators, 

1306 raw_value_masked_comments, 

1307 offset, 

1308 value_element_pos, 

1309 lint_state, 

1310 ): 

1311 sub_relations.append(Relation("<BROKEN>")) 

1312 else: 

1313 name_arch_qual = m.group("name_arch_qual") 

1314 if ":" in name_arch_qual: 

1315 name, arch_qual = name_arch_qual.split(":", 1) 

1316 else: 

1317 name = name_arch_qual 

1318 arch_qual = None 

1319 sub_relations.append( 

1320 Relation( 

1321 name, 

1322 arch_qual=arch_qual, 

1323 version_operator=version_operator, 

1324 version=m.group("version"), 

1325 arch_restriction=m.group("build_profile_restriction"), 

1326 build_profile_restriction=m.group( 

1327 "build_profile_restriction" 

1328 ), 

1329 content_display_offset=offset + m.start("name_arch_qual"), 

1330 # TODO: This should be trimmed in the end. 

1331 content_display_end_offset=rel_end_offset, 

1332 ) 

1333 ) 

1334 else: 

1335 garbage = None 

1336 sub_relations.append(Relation("<BROKEN>")) 

1337 

1338 if m is not None and not garbage: 

1339 continue 

1340 if m is not None: 

1341 garbage_span = m.span("garbage") 

1342 garbage_start, garbage_end = garbage_span 

1343 error_start_offset = offset + garbage_start 

1344 error_end_offset = offset + garbage_end 

1345 garbage_part = raw_value_masked_comments[ 

1346 error_start_offset:error_end_offset 

1347 ] 

1348 else: 

1349 garbage_part = None 

1350 error_start_offset = offset 

1351 error_end_offset = end_offset 

1352 

1353 problem_range_te = TERange( 

1354 _text_to_te_position(raw_value_masked_comments[:error_start_offset]), 

1355 _text_to_te_position(raw_value_masked_comments[:error_end_offset]), 

1356 ).relative_to(value_element_pos) 

1357 

1358 if garbage_part is not None: 

1359 if _DEP_RELATION_CLAUSE.fullmatch(garbage_part) is not None: 

1360 msg = ( 

1361 "Trailing data after a relationship that might be a second relationship." 

1362 " Is a separator missing before this part?" 

1363 ) 

1364 else: 

1365 msg = "Parse error of the relationship. Either a syntax error or a missing separator somewhere." 

1366 lint_state.emit_diagnostic( 

1367 problem_range_te, 

1368 msg, 

1369 "error", 

1370 known_field.unknown_value_authority, 

1371 ) 

1372 else: 

1373 dep = _cleanup_rel( 

1374 raw_value_masked_comments[error_start_offset:error_end_offset] 

1375 ) 

1376 lint_state.emit_diagnostic( 

1377 problem_range_te, 

1378 f'Could not parse "{dep}" as a dependency relation.', 

1379 "error", 

1380 known_field.unknown_value_authority, 

1381 ) 

1382 if ( 

1383 len(sub_relations) == 1 

1384 and (relation := sub_relations[0]).name != "<BROKEN>" 

1385 ): 

1386 # We ignore OR-relations in the dup-check for now. We also skip relations with problems. 

1387 relation_dup_table[relation.name].append(relation) 

1388 

1389 for relations in relation_dup_table.values(): 

1390 if len(relations) > 1: 

1391 dup_check_relations( 

1392 known_field, 

1393 relations, 

1394 raw_value_masked_comments, 

1395 value_element_pos, 

1396 lint_state, 

1397 ) 

1398 

1399 

1400def _rrr_build_driver_mismatch( 

1401 _known_field: "F", 

1402 _deb822_file: Deb822FileElement, 

1403 _kvpair: Deb822KeyValuePairElement, 

1404 kvpair_range_te: "TERange", 

1405 _field_name_range: "TERange", 

1406 stanza: Deb822ParagraphElement, 

1407 _stanza_position: "TEPosition", 

1408 lint_state: LintState, 

1409) -> None: 

1410 dr = stanza.get("Build-Driver", "debian-rules") 

1411 if dr != "debian-rules": 

1412 lint_state.emit_diagnostic( 

1413 kvpair_range_te, 

1414 f'The Rules-Requires-Root field is irrelevant for the Build-Driver "{dr}".', 

1415 "informational", 

1416 "debputy", 

1417 quickfixes=[ 

1418 propose_remove_range_quick_fix( 

1419 proposed_title="Remove Rules-Requires-Root" 

1420 ) 

1421 ], 

1422 ) 

1423 

1424 

1425class Dep5Matcher(BasenameGlobMatch): 

1426 def __init__(self, basename_glob: str) -> None: 

1427 super().__init__( 

1428 basename_glob, 

1429 only_when_in_directory=None, 

1430 path_type=None, 

1431 recursive_match=False, 

1432 ) 

1433 

1434 

1435def _match_dep5_segment( 

1436 current_dir: VirtualPathBase, basename_glob: str 

1437) -> Iterable[VirtualPathBase]: 

1438 if "*" in basename_glob or "?" in basename_glob: 

1439 return Dep5Matcher(basename_glob).finditer(current_dir) 

1440 else: 

1441 res = current_dir.get(basename_glob) 

1442 if res is None: 

1443 return tuple() 

1444 return (res,) 

1445 

1446 

1447_RE_SLASHES = re.compile(r"//+") 

1448 

1449 

1450def _dep5_unnecessary_symbols( 

1451 value: str, 

1452 value_range: TERange, 

1453 lint_state: LintState, 

1454) -> None: 

1455 slash_check_index = 0 

1456 if value.startswith(("./", "/")): 

1457 prefix_len = 1 if value[0] == "/" else 2 

1458 if value[prefix_len - 1 : prefix_len + 2].startswith("//"): 1458 ↛ 1462line 1458 didn't jump to line 1462 because the condition on line 1458 was always true

1459 _, slashes_end = _RE_SLASHES.search(value).span() 

1460 prefix_len = slashes_end 

1461 

1462 slash_check_index = prefix_len 

1463 prefix_range = TERange( 

1464 value_range.start_pos, 

1465 TEPosition( 

1466 value_range.start_pos.line_position, 

1467 value_range.start_pos.cursor_position + prefix_len, 

1468 ), 

1469 ) 

1470 lint_state.emit_diagnostic( 

1471 prefix_range, 

1472 f'Unnecessary prefix "{value[0:prefix_len]}"', 

1473 "warning", 

1474 "debputy", 

1475 quickfixes=[ 

1476 propose_remove_range_quick_fix( 

1477 proposed_title=f'Delete "{value[0:prefix_len]}"' 

1478 ) 

1479 ], 

1480 ) 

1481 

1482 for m in _RE_SLASHES.finditer(value, slash_check_index): 

1483 m_start, m_end = m.span(0) 

1484 

1485 prefix_range = TERange( 

1486 TEPosition( 

1487 value_range.start_pos.line_position, 

1488 value_range.start_pos.cursor_position + m_start, 

1489 ), 

1490 TEPosition( 

1491 value_range.start_pos.line_position, 

1492 value_range.start_pos.cursor_position + m_end, 

1493 ), 

1494 ) 

1495 lint_state.emit_diagnostic( 

1496 prefix_range, 

1497 'Simplify to a single "/"', 

1498 "warning", 

1499 "debputy", 

1500 quickfixes=[propose_correct_text_quick_fix("/")], 

1501 ) 

1502 

1503 

1504def _dep5_files_check( 

1505 known_field: "F", 

1506 _deb822_file: Deb822FileElement, 

1507 kvpair: Deb822KeyValuePairElement, 

1508 kvpair_range_te: "TERange", 

1509 _field_name_range: "TERange", 

1510 _stanza: Deb822ParagraphElement, 

1511 _stanza_position: "TEPosition", 

1512 lint_state: LintState, 

1513) -> None: 

1514 interpreter = known_field.field_value_class.interpreter() 

1515 assert interpreter is not None 

1516 full_value_range = kvpair.value_element.range_in_parent().relative_to( 

1517 kvpair_range_te.start_pos 

1518 ) 

1519 values_with_ranges = [] 

1520 for value_ref in kvpair.interpret_as(interpreter).iter_value_references(): 

1521 value_range = value_ref.locatable.range_in_parent().relative_to( 

1522 full_value_range.start_pos 

1523 ) 

1524 value = value_ref.value 

1525 values_with_ranges.append((value_ref.value, value_range)) 

1526 _dep5_unnecessary_symbols(value, value_range, lint_state) 

1527 

1528 source_root = lint_state.source_root 

1529 if source_root is None: 

1530 return 

1531 i = 0 

1532 limit = len(values_with_ranges) 

1533 while i < limit: 

1534 value, value_range = values_with_ranges[i] 

1535 i += 1 

1536 

1537 

1538_HOMEPAGE_CLUTTER_RE = re.compile(r"<(?:UR[LI]:)?(.*)>") 

1539_URI_RE = re.compile(r"(?P<protocol>[a-z0-9]+)://(?P<host>[^\s/+]+)(?P<path>/[^\s?]*)?") 

1540_KNOWN_HTTPS_HOSTS = frozenset( 

1541 [ 

1542 "debian.org", 

1543 "bioconductor.org", 

1544 "cran.r-project.org", 

1545 "github.com", 

1546 "gitlab.com", 

1547 "metacpan.org", 

1548 "gnu.org", 

1549 ] 

1550) 

1551_REPLACED_HOSTS = frozenset({"alioth.debian.org"}) 

1552_NO_DOT_GIT_HOMEPAGE_HOSTS = frozenset( 

1553 { 

1554 "salsa.debian.org", 

1555 "github.com", 

1556 "gitlab.com", 

1557 } 

1558) 

1559 

1560 

1561def _is_known_host(host: str, known_hosts: Container[str]) -> bool: 

1562 if host in known_hosts: 

1563 return True 

1564 while host: 1564 ↛ 1572line 1564 didn't jump to line 1572 because the condition on line 1564 was always true

1565 try: 

1566 idx = host.index(".") 

1567 host = host[idx + 1 :] 

1568 except ValueError: 

1569 break 

1570 if host in known_hosts: 

1571 return True 

1572 return False 

1573 

1574 

1575def _validate_homepage_field( 

1576 _known_field: "F", 

1577 _deb822_file: Deb822FileElement, 

1578 kvpair: Deb822KeyValuePairElement, 

1579 kvpair_range_te: "TERange", 

1580 _field_name_range_te: "TERange", 

1581 _stanza: Deb822ParagraphElement, 

1582 _stanza_position: "TEPosition", 

1583 lint_state: LintState, 

1584) -> None: 

1585 value = kvpair.value_element.convert_to_text() 

1586 offset = 0 

1587 homepage = value 

1588 if "<" in value and (m := _HOMEPAGE_CLUTTER_RE.search(value)): 

1589 expected_value = m.group(1) 

1590 quickfixes = [] 

1591 if expected_value: 1591 ↛ 1595line 1591 didn't jump to line 1595 because the condition on line 1591 was always true

1592 homepage = expected_value.strip() 

1593 offset = m.start(1) 

1594 quickfixes.append(propose_correct_text_quick_fix(expected_value)) 

1595 lint_state.emit_diagnostic( 

1596 _single_line_span_range_relative_to_pos( 

1597 m.span(), 

1598 kvpair.value_element.position_in_parent().relative_to( 

1599 kvpair_range_te.start_pos 

1600 ), 

1601 ), 

1602 "Superfluous URL/URI wrapping", 

1603 "informational", 

1604 "Policy 5.6.23", 

1605 quickfixes=quickfixes, 

1606 ) 

1607 # Note falling through here can cause "two rounds" for debputy lint --auto-fix 

1608 m = _URI_RE.search(homepage) 

1609 if not m: 1609 ↛ 1610line 1609 didn't jump to line 1610 because the condition on line 1609 was never true

1610 return 

1611 # TODO relative to lintian: `bad-homepage` and most of the `fields/bad-homepages` hints. 

1612 protocol = m.group("protocol") 

1613 host = m.group("host") 

1614 path = m.group("path") or "" 

1615 if _is_known_host(host, _REPLACED_HOSTS): 

1616 span = m.span("host") 

1617 lint_state.emit_diagnostic( 

1618 _single_line_span_range_relative_to_pos( 

1619 (span[0] + offset, span[1] + offset), 

1620 kvpair.value_element.position_in_parent().relative_to( 

1621 kvpair_range_te.start_pos 

1622 ), 

1623 ), 

1624 f'The server "{host}" is no longer in use.', 

1625 "warning", 

1626 "debputy", 

1627 ) 

1628 return 

1629 if ( 

1630 protocol == "ftp" 

1631 or protocol == "http" 

1632 and _is_known_host(host, _KNOWN_HTTPS_HOSTS) 

1633 ): 

1634 span = m.span("protocol") 

1635 if protocol == "ftp" and not _is_known_host(host, _KNOWN_HTTPS_HOSTS): 1635 ↛ 1636line 1635 didn't jump to line 1636 because the condition on line 1635 was never true

1636 msg = "Insecure protocol for website (check if a https:// variant is available)" 

1637 quickfixes = [] 

1638 else: 

1639 msg = "Replace with https://. The host is known to support https" 

1640 quickfixes = [propose_correct_text_quick_fix("https")] 

1641 lint_state.emit_diagnostic( 

1642 _single_line_span_range_relative_to_pos( 

1643 (span[0] + offset, span[1] + offset), 

1644 kvpair.value_element.position_in_parent().relative_to( 

1645 kvpair_range_te.start_pos 

1646 ), 

1647 ), 

1648 msg, 

1649 "pedantic", 

1650 "debputy", 

1651 quickfixes=quickfixes, 

1652 ) 

1653 if path.endswith(".git") and _is_known_host(host, _NO_DOT_GIT_HOMEPAGE_HOSTS): 

1654 span = m.span("path") 

1655 msg = "Unnecessary suffix" 

1656 quickfixes = [propose_correct_text_quick_fix(path[:-4])] 

1657 lint_state.emit_diagnostic( 

1658 _single_line_span_range_relative_to_pos( 

1659 (span[1] - 4 + offset, span[1] + offset), 

1660 kvpair.value_element.position_in_parent().relative_to( 

1661 kvpair_range_te.start_pos 

1662 ), 

1663 ), 

1664 msg, 

1665 "pedantic", 

1666 "debputy", 

1667 quickfixes=quickfixes, 

1668 ) 

1669 

1670 

1671def _combined_custom_field_check(*checks: CustomFieldCheck) -> CustomFieldCheck: 

1672 def _validator( 

1673 known_field: "F", 

1674 deb822_file: Deb822FileElement, 

1675 kvpair: Deb822KeyValuePairElement, 

1676 kvpair_range_te: "TERange", 

1677 field_name_range_te: "TERange", 

1678 stanza: Deb822ParagraphElement, 

1679 stanza_position: "TEPosition", 

1680 lint_state: LintState, 

1681 ) -> None: 

1682 for check in checks: 

1683 check( 

1684 known_field, 

1685 deb822_file, 

1686 kvpair, 

1687 kvpair_range_te, 

1688 field_name_range_te, 

1689 stanza, 

1690 stanza_position, 

1691 lint_state, 

1692 ) 

1693 

1694 return _validator 

1695 

1696 

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

1698class PackageNameSectionRule: 

1699 section: str 

1700 check: Callable[[str], bool] 

1701 

1702 

1703def _package_name_section_rule( 

1704 section: str, 

1705 check: Callable[[str], bool] | re.Pattern, 

1706 *, 

1707 confirm_re: re.Pattern | None = None, 

1708) -> PackageNameSectionRule: 

1709 if confirm_re is not None: 

1710 assert callable(check) 

1711 

1712 def _impl(v: str) -> bool: 

1713 return check(v) and confirm_re.search(v) 

1714 

1715 elif isinstance(check, re.Pattern): 1715 ↛ 1717line 1715 didn't jump to line 1717 because the condition on line 1715 was never true

1716 

1717 def _impl(v: str) -> bool: 

1718 return check.search(v) is not None 

1719 

1720 else: 

1721 _impl = check 

1722 

1723 return PackageNameSectionRule(section, _impl) 

1724 

1725 

1726# rules: order is important (first match wins in case of a conflict) 

1727_PKGNAME_VS_SECTION_RULES = [ 

1728 _package_name_section_rule("debian-installer", lambda n: n.endswith("-udeb")), 

1729 _package_name_section_rule("doc", lambda n: n.endswith(("-doc", "-docs"))), 

1730 _package_name_section_rule("debug", lambda n: n.endswith(("-dbg", "-dbgsym"))), 

1731 _package_name_section_rule( 

1732 "httpd", 

1733 lambda n: n.startswith(("lighttpd-mod", "libapache2-mod-", "libnginx-mod-")), 

1734 ), 

1735 _package_name_section_rule("gnustep", lambda n: n.startswith("gnustep-")), 

1736 _package_name_section_rule( 

1737 "gnustep", 

1738 lambda n: n.endswith( 

1739 ( 

1740 ".framework", 

1741 ".framework-common", 

1742 ".tool", 

1743 ".tool-common", 

1744 ".app", 

1745 ".app-common", 

1746 ) 

1747 ), 

1748 ), 

1749 _package_name_section_rule("embedded", lambda n: n.startswith("moblin-")), 

1750 _package_name_section_rule("javascript", lambda n: n.startswith("node-")), 

1751 _package_name_section_rule( 

1752 "zope", 

1753 lambda n: n.startswith(("python-zope", "python3-zope", "zope")), 

1754 ), 

1755 _package_name_section_rule( 

1756 "python", 

1757 lambda n: n.startswith(("python-", "python3-")), 

1758 ), 

1759 _package_name_section_rule( 

1760 "gnu-r", 

1761 lambda n: n.startswith(("r-cran-", "r-bioc-", "r-other-")), 

1762 ), 

1763 _package_name_section_rule("editors", lambda n: n.startswith("elpa-")), 

1764 _package_name_section_rule("lisp", lambda n: n.startswith("cl-")), 

1765 _package_name_section_rule( 

1766 "lisp", 

1767 lambda n: "-elisp-" in n or n.endswith("-elisp"), 

1768 ), 

1769 _package_name_section_rule( 

1770 "lisp", 

1771 lambda n: n.startswith("lib") and n.endswith("-guile"), 

1772 ), 

1773 _package_name_section_rule("lisp", lambda n: n.startswith("guile-")), 

1774 _package_name_section_rule("golang", lambda n: n.startswith("golang-")), 

1775 _package_name_section_rule( 

1776 "perl", 

1777 lambda n: n.startswith("lib") and n.endswith("-perl"), 

1778 ), 

1779 _package_name_section_rule( 

1780 "cli-mono", 

1781 lambda n: n.startswith("lib") and n.endswith(("-cil", "-cil-dev")), 

1782 ), 

1783 _package_name_section_rule( 

1784 "java", 

1785 lambda n: n.startswith("lib") and n.endswith(("-java", "-gcj", "-jni")), 

1786 ), 

1787 _package_name_section_rule( 

1788 "php", 

1789 lambda n: n.startswith(("libphp", "php")), 

1790 confirm_re=re.compile(r"^(?:lib)?php(?:\d(?:\.\d)?)?-"), 

1791 ), 

1792 _package_name_section_rule( 

1793 "php", lambda n: n.startswith("lib-") and n.endswith("-php") 

1794 ), 

1795 _package_name_section_rule( 

1796 "haskell", 

1797 lambda n: n.startswith(("haskell-", "libhugs-", "libghc-", "libghc6-")), 

1798 ), 

1799 _package_name_section_rule( 

1800 "ruby", 

1801 lambda n: "-ruby" in n, 

1802 confirm_re=re.compile(r"^lib.*-ruby(?:1\.\d)?$"), 

1803 ), 

1804 _package_name_section_rule("ruby", lambda n: n.startswith("ruby-")), 

1805 _package_name_section_rule( 

1806 "rust", 

1807 lambda n: n.startswith("librust-") and n.endswith("-dev"), 

1808 ), 

1809 _package_name_section_rule("rust", lambda n: n.startswith("rust-")), 

1810 _package_name_section_rule( 

1811 "ocaml", 

1812 lambda n: n.startswith("lib-") and n.endswith(("-ocaml-dev", "-camlp4-dev")), 

1813 ), 

1814 _package_name_section_rule("javascript", lambda n: n.startswith("libjs-")), 

1815 _package_name_section_rule( 

1816 "interpreters", 

1817 lambda n: n.startswith("lib-") and n.endswith(("-tcl", "-lua", "-gst")), 

1818 ), 

1819 _package_name_section_rule( 

1820 "introspection", 

1821 lambda n: n.startswith("gir-"), 

1822 confirm_re=re.compile(r"^gir\d+\.\d+-.*-\d+\.\d+$"), 

1823 ), 

1824 _package_name_section_rule( 

1825 "fonts", 

1826 lambda n: n.startswith(("xfonts-", "fonts-", "ttf-")), 

1827 ), 

1828 _package_name_section_rule("admin", lambda n: n.startswith(("libnss-", "libpam-"))), 

1829 _package_name_section_rule( 

1830 "localization", 

1831 lambda n: n.startswith( 

1832 ( 

1833 "aspell-", 

1834 "hunspell-", 

1835 "myspell-", 

1836 "mythes-", 

1837 "dict-freedict-", 

1838 "gcompris-sound-", 

1839 ) 

1840 ), 

1841 ), 

1842 _package_name_section_rule( 

1843 "localization", 

1844 lambda n: n.startswith("hyphen-"), 

1845 confirm_re=re.compile(r"^hyphen-[a-z]{2}(?:-[a-z]{2})?$"), 

1846 ), 

1847 _package_name_section_rule( 

1848 "localization", 

1849 lambda n: "-l10n-" in n or n.endswith("-l10n"), 

1850 ), 

1851 _package_name_section_rule("kernel", lambda n: n.endswith(("-dkms", "-firmware"))), 

1852 _package_name_section_rule( 

1853 "libdevel", 

1854 lambda n: n.startswith("lib") and n.endswith(("-dev", "-headers")), 

1855 ), 

1856 _package_name_section_rule( 

1857 "libs", 

1858 lambda n: n.startswith("lib"), 

1859 confirm_re=re.compile(r"^lib.*\d[ad]?$"), 

1860 ), 

1861] 

1862 

1863 

1864# Fiddling with the package name can cause a lot of changes (diagnostic scans), so we have an upper bound 

1865# on the cache. The number is currently just taken out of a hat. 

1866@functools.lru_cache(64) 

1867def package_name_to_section(name: str) -> str | None: 

1868 for rule in _PKGNAME_VS_SECTION_RULES: 

1869 if rule.check(name): 

1870 return rule.section 

1871 return None 

1872 

1873 

1874def _unknown_value_check( 

1875 field_name: str, 

1876 value: str, 

1877 known_values: Mapping[str, Keyword], 

1878 unknown_value_severity: LintSeverity | None, 

1879) -> tuple[Keyword | None, str | None, LintSeverity | None, Any | None]: 

1880 known_value = known_values.get(value) 

1881 message = None 

1882 severity = unknown_value_severity 

1883 fix_data = None 

1884 if known_value is None: 

1885 candidates = detect_possible_typo( 

1886 value, 

1887 known_values, 

1888 ) 

1889 if len(known_values) < 5: 1889 ↛ 1890line 1889 didn't jump to line 1890 because the condition on line 1889 was never true

1890 values = ", ".join(sorted(known_values)) 

1891 hint_text = f" Known values for this field: {values}" 

1892 else: 

1893 hint_text = "" 

1894 fix_data = None 

1895 severity = unknown_value_severity 

1896 fix_text = hint_text 

1897 if candidates: 

1898 match = candidates[0] 

1899 if len(candidates) == 1: 1899 ↛ 1901line 1899 didn't jump to line 1901 because the condition on line 1899 was always true

1900 known_value = known_values[match] 

1901 fix_text = ( 

1902 f' It is possible that the value is a typo of "{match}".{fix_text}' 

1903 ) 

1904 fix_data = [propose_correct_text_quick_fix(m) for m in candidates] 

1905 elif severity is None: 1905 ↛ 1906line 1905 didn't jump to line 1906 because the condition on line 1905 was never true

1906 return None, None, None, None 

1907 if severity is None: 

1908 severity = cast("LintSeverity", "warning") 

1909 # It always has leading whitespace 

1910 message = fix_text.strip() 

1911 else: 

1912 message = f'The value "{value}" is not supported in {field_name}.{fix_text}' 

1913 return known_value, message, severity, fix_data 

1914 

1915 

1916def _dep5_escape_path(path: str) -> str: 

1917 return path.replace(" ", "?") 

1918 

1919 

1920def _noop_escape_path(path: str) -> str: 

1921 return path 

1922 

1923 

1924def _should_ignore_dir( 

1925 path: VirtualPath, 

1926 *, 

1927 supports_dir_match: bool = False, 

1928 match_non_persistent_paths: bool = False, 

1929) -> bool: 

1930 if not supports_dir_match and not any(path.iterdir): 

1931 return True 

1932 cachedir_tag = path.get("CACHEDIR.TAG") 

1933 if ( 

1934 not match_non_persistent_paths 

1935 and cachedir_tag is not None 

1936 and cachedir_tag.is_file 

1937 ): 

1938 # https://bford.info/cachedir/ 

1939 with cachedir_tag.open(byte_io=True, buffering=64) as fd: 

1940 start = fd.read(43) 

1941 if start == b"Signature: 8a477f597d28d172789f06886806bc55": 

1942 return True 

1943 return False 

1944 

1945 

1946@dataclasses.dataclass(slots=True) 

1947class Deb822KnownField: 

1948 name: str 

1949 field_value_class: FieldValueClass 

1950 warn_if_default: bool = True 

1951 unknown_value_authority: str = "debputy" 

1952 missing_field_authority: str = "debputy" 

1953 replaced_by: str | None = None 

1954 deprecated_with_no_replacement: bool = False 

1955 missing_field_severity: LintSeverity | None = None 

1956 default_value: str | None = None 

1957 known_values: Mapping[str, Keyword] | None = None 

1958 unknown_value_severity: LintSeverity | None = "error" 

1959 translation_context: str = "" 

1960 # One-line description for space-constrained docs (such as completion docs) 

1961 synopsis: str | None = None 

1962 usage_hint: UsageHint | None = None 

1963 long_description: str | None = None 

1964 spellcheck_value: bool = False 

1965 inheritable_from_other_stanza: bool = False 

1966 show_as_inherited: bool = True 

1967 custom_field_check: CustomFieldCheck | None = None 

1968 can_complete_field_in_stanza: None | ( 

1969 Callable[[Iterable[Deb822ParagraphElement]], bool] 

1970 ) = None 

1971 is_substvars_disabled_even_if_allowed_by_stanza: bool = False 

1972 is_alias_of: str | None = None 

1973 is_completion_suggestion: bool = True 

1974 

1975 def synopsis_translated( 

1976 self, translation_provider: Union["DebputyLanguageServer", "LintState"] 

1977 ) -> str: 

1978 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1979 self.translation_context, 

1980 self.synopsis, 

1981 ) 

1982 

1983 def long_description_translated( 

1984 self, translation_provider: Union["DebputyLanguageServer", "LintState"] 

1985 ) -> str: 

1986 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1987 self.translation_context, 

1988 self.long_description, 

1989 ) 

1990 

1991 def _can_complete_field_in_stanza( 

1992 self, 

1993 stanza_parts: Sequence[Deb822ParagraphElement], 

1994 ) -> bool: 

1995 if not self.is_completion_suggestion: 1995 ↛ 1996line 1995 didn't jump to line 1996 because the condition on line 1995 was never true

1996 return False 

1997 return ( 

1998 self.can_complete_field_in_stanza is None 

1999 or self.can_complete_field_in_stanza(stanza_parts) 

2000 ) 

2001 

2002 def complete_field( 

2003 self, 

2004 lint_state: LintState, 

2005 stanza_parts: Sequence[Deb822ParagraphElement], 

2006 markdown_kind: MarkupKind, 

2007 ) -> CompletionItem | None: 

2008 if not self._can_complete_field_in_stanza(stanza_parts): 

2009 return None 

2010 name = self.name 

2011 complete_as = name + ": " 

2012 options = self.value_options_for_completer( 

2013 lint_state, 

2014 stanza_parts, 

2015 "", 

2016 markdown_kind, 

2017 is_completion_for_field=True, 

2018 ) 

2019 if options is not None and len(options) == 1: 

2020 value = options[0].insert_text 

2021 if value is not None: 2021 ↛ 2023line 2021 didn't jump to line 2023 because the condition on line 2021 was always true

2022 complete_as += value 

2023 tags = [] 

2024 is_deprecated = False 

2025 if self.replaced_by or self.deprecated_with_no_replacement: 

2026 is_deprecated = True 

2027 tags.append(CompletionItemTag.Deprecated) 

2028 

2029 doc = self.long_description 

2030 if doc: 

2031 doc = MarkupContent( 

2032 value=doc, 

2033 kind=markdown_kind, 

2034 ) 

2035 else: 

2036 doc = None 

2037 

2038 return CompletionItem( 

2039 name, 

2040 insert_text=complete_as, 

2041 deprecated=is_deprecated, 

2042 tags=tags, 

2043 detail=format_comp_item_synopsis_doc( 

2044 self.usage_hint, 

2045 self.synopsis_translated(lint_state), 

2046 is_deprecated, 

2047 ), 

2048 documentation=doc, 

2049 ) 

2050 

2051 def _complete_files( 

2052 self, 

2053 base_dir: VirtualPathBase | None, 

2054 value_being_completed: str, 

2055 *, 

2056 is_dep5_file_list: bool = False, 

2057 supports_dir_match: bool = False, 

2058 supports_spaces_in_filename: bool = False, 

2059 match_non_persistent_paths: bool = False, 

2060 ) -> Sequence[CompletionItem] | None: 

2061 _info(f"_complete_files: {base_dir.fs_path} - {value_being_completed!r}") 

2062 if base_dir is None or not base_dir.is_dir: 

2063 return None 

2064 

2065 if is_dep5_file_list: 

2066 supports_spaces_in_filename = True 

2067 supports_dir_match = False 

2068 match_non_persistent_paths = False 

2069 

2070 if value_being_completed == "": 

2071 current_dir = base_dir 

2072 unmatched_parts: Sequence[str] = () 

2073 else: 

2074 current_dir, unmatched_parts = base_dir.attempt_lookup( 

2075 value_being_completed 

2076 ) 

2077 

2078 if len(unmatched_parts) > 1: 

2079 # Unknown directory part / glob, and we currently do not deal with that. 

2080 return None 

2081 if len(unmatched_parts) == 1 and unmatched_parts[0] == "*": 

2082 # Avoid convincing the client to remove the star (seen with emacs) 

2083 return None 

2084 items = [] 

2085 

2086 path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path 

2087 

2088 for child in current_dir.iterdir: 

2089 if child.is_symlink and is_dep5_file_list: 

2090 continue 

2091 if not supports_spaces_in_filename and ( 

2092 " " in child.name or "\t" in child.name 

2093 ): 

2094 continue 

2095 sort_text = ( 

2096 f"z-{child.name}" if child.name.startswith(".") else f"a-{child.name}" 

2097 ) 

2098 if child.is_dir: 

2099 if _should_ignore_dir( 

2100 child, 

2101 supports_dir_match=supports_dir_match, 

2102 match_non_persistent_paths=match_non_persistent_paths, 

2103 ): 

2104 continue 

2105 items.append( 

2106 CompletionItem( 

2107 f"{child.path}/", 

2108 label_details=CompletionItemLabelDetails( 

2109 description=child.path, 

2110 ), 

2111 insert_text=path_escaper(f"{child.path}/"), 

2112 filter_text=f"{child.path}/", 

2113 sort_text=sort_text, 

2114 kind=CompletionItemKind.Folder, 

2115 ) 

2116 ) 

2117 else: 

2118 items.append( 

2119 CompletionItem( 

2120 child.path, 

2121 label_details=CompletionItemLabelDetails( 

2122 description=child.path, 

2123 ), 

2124 insert_text=path_escaper(child.path), 

2125 filter_text=child.path, 

2126 sort_text=sort_text, 

2127 kind=CompletionItemKind.File, 

2128 ) 

2129 ) 

2130 return items 

2131 

2132 def value_options_for_completer( 

2133 self, 

2134 lint_state: LintState, 

2135 stanza_parts: Sequence[Deb822ParagraphElement], 

2136 value_being_completed: str, 

2137 markdown_kind: MarkupKind, 

2138 *, 

2139 is_completion_for_field: bool = False, 

2140 ) -> Sequence[CompletionItem] | None: 

2141 known_values = self.known_values 

2142 if self.field_value_class == FieldValueClass.DEP5_FILE_LIST: 2142 ↛ 2143line 2142 didn't jump to line 2143 because the condition on line 2142 was never true

2143 if is_completion_for_field: 

2144 return None 

2145 return self._complete_files( 

2146 lint_state.source_root, 

2147 value_being_completed, 

2148 is_dep5_file_list=True, 

2149 ) 

2150 

2151 if known_values is None: 

2152 return None 

2153 if is_completion_for_field and ( 

2154 len(known_values) == 1 

2155 or ( 

2156 len(known_values) == 2 

2157 and self.warn_if_default 

2158 and self.default_value is not None 

2159 ) 

2160 ): 

2161 value = next( 

2162 iter(v for v in self.known_values if v != self.default_value), 

2163 None, 

2164 ) 

2165 if value is None: 2165 ↛ 2166line 2165 didn't jump to line 2166 because the condition on line 2165 was never true

2166 return None 

2167 return [CompletionItem(value, insert_text=value)] 

2168 return [ 

2169 keyword.as_completion_item( 

2170 lint_state, 

2171 stanza_parts, 

2172 value_being_completed, 

2173 markdown_kind, 

2174 ) 

2175 for keyword in known_values.values() 

2176 if keyword.is_keyword_valid_completion_in_stanza(stanza_parts) 

2177 and keyword.is_completion_suggestion 

2178 ] 

2179 

2180 def field_omitted_diagnostics( 

2181 self, 

2182 deb822_file: Deb822FileElement, 

2183 representation_field_range: "TERange", 

2184 stanza: Deb822ParagraphElement, 

2185 stanza_position: "TEPosition", 

2186 header_stanza: Deb822FileElement | None, 

2187 lint_state: LintState, 

2188 ) -> None: 

2189 missing_field_severity = self.missing_field_severity 

2190 if missing_field_severity is None: 2190 ↛ 2193line 2190 didn't jump to line 2193 because the condition on line 2190 was always true

2191 return 

2192 

2193 if ( 

2194 self.inheritable_from_other_stanza 

2195 and header_stanza is not None 

2196 and self.name in header_stanza 

2197 ): 

2198 return 

2199 

2200 lint_state.emit_diagnostic( 

2201 representation_field_range, 

2202 f"Stanza is missing field {self.name}", 

2203 missing_field_severity, 

2204 self.missing_field_authority, 

2205 ) 

2206 

2207 async def field_diagnostics( 

2208 self, 

2209 deb822_file: Deb822FileElement, 

2210 kvpair: Deb822KeyValuePairElement, 

2211 stanza: Deb822ParagraphElement, 

2212 stanza_position: "TEPosition", 

2213 kvpair_range_te: "TERange", 

2214 lint_state: LintState, 

2215 *, 

2216 field_name_typo_reported: bool = False, 

2217 ) -> None: 

2218 field_name_token = kvpair.field_token 

2219 field_name_range_te = kvpair.field_token.range_in_parent().relative_to( 

2220 kvpair_range_te.start_pos 

2221 ) 

2222 field_name = field_name_token.text 

2223 # The `self.name` attribute is the canonical name whereas `field_name` is the name used. 

2224 # This distinction is important for `d/control` where `X[CBS]-` prefixes might be used 

2225 # in one but not the other. 

2226 field_value = stanza[field_name] 

2227 self._diagnostics_for_field_name( 

2228 kvpair_range_te, 

2229 field_name_token, 

2230 field_name_range_te, 

2231 field_name_typo_reported, 

2232 lint_state, 

2233 ) 

2234 if self.custom_field_check is not None: 

2235 self.custom_field_check( 

2236 self, 

2237 deb822_file, 

2238 kvpair, 

2239 kvpair_range_te, 

2240 field_name_range_te, 

2241 stanza, 

2242 stanza_position, 

2243 lint_state, 

2244 ) 

2245 self._dep5_file_list_diagnostics(kvpair, kvpair_range_te.start_pos, lint_state) 

2246 if self.spellcheck_value: 

2247 words = kvpair.interpret_as(LIST_SPACE_SEPARATED_INTERPRETATION) 

2248 spell_checker = lint_state.spellchecker() 

2249 value_position = kvpair.value_element.position_in_parent().relative_to( 

2250 kvpair_range_te.start_pos 

2251 ) 

2252 async for word_ref in lint_state.slow_iter( 

2253 words.iter_value_references(), yield_every=25 

2254 ): 

2255 token = word_ref.value 

2256 for word, pos, endpos in spell_checker.iter_words(token): 

2257 corrections = spell_checker.provide_corrections_for(word) 

2258 if not corrections: 

2259 continue 

2260 word_loc = word_ref.locatable 

2261 word_pos_te = word_loc.position_in_parent().relative_to( 

2262 value_position 

2263 ) 

2264 if pos: 2264 ↛ 2265line 2264 didn't jump to line 2265 because the condition on line 2264 was never true

2265 word_pos_te = TEPosition(0, pos).relative_to(word_pos_te) 

2266 word_size = TERange( 

2267 START_POSITION, 

2268 TEPosition(0, endpos - pos), 

2269 ) 

2270 lint_state.emit_diagnostic( 

2271 TERange.from_position_and_size(word_pos_te, word_size), 

2272 f'Spelling "{word}"', 

2273 "spelling", 

2274 "debputy", 

2275 quickfixes=[ 

2276 propose_correct_text_quick_fix(c) for c in corrections 

2277 ], 

2278 enable_non_interactive_auto_fix=False, 

2279 ) 

2280 else: 

2281 self._known_value_diagnostics( 

2282 kvpair, 

2283 kvpair_range_te.start_pos, 

2284 lint_state, 

2285 ) 

2286 

2287 if self.warn_if_default and field_value == self.default_value: 2287 ↛ 2288line 2287 didn't jump to line 2288 because the condition on line 2287 was never true

2288 lint_state.emit_diagnostic( 

2289 kvpair_range_te, 

2290 f'The field "{field_name}" is redundant as it is set to the default value and the field' 

2291 " should only be used in exceptional cases.", 

2292 "warning", 

2293 "debputy", 

2294 ) 

2295 

2296 def _diagnostics_for_field_name( 

2297 self, 

2298 kvpair_range: "TERange", 

2299 token: Deb822FieldNameToken, 

2300 token_range: "TERange", 

2301 typo_detected: bool, 

2302 lint_state: LintState, 

2303 ) -> None: 

2304 field_name = token.text 

2305 # Defeat the case-insensitivity from python-debian 

2306 field_name_cased = str(field_name) 

2307 if self.deprecated_with_no_replacement: 

2308 lint_state.emit_diagnostic( 

2309 kvpair_range, 

2310 f'"{field_name_cased}" is deprecated and no longer used', 

2311 "warning", 

2312 "debputy", 

2313 quickfixes=[propose_remove_range_quick_fix()], 

2314 tags=[DiagnosticTag.Deprecated], 

2315 ) 

2316 elif self.replaced_by is not None: 

2317 lint_state.emit_diagnostic( 

2318 token_range, 

2319 f'"{field_name_cased}" has been replaced by "{self.replaced_by}"', 

2320 "warning", 

2321 "debputy", 

2322 tags=[DiagnosticTag.Deprecated], 

2323 quickfixes=[propose_correct_text_quick_fix(self.replaced_by)], 

2324 ) 

2325 

2326 if not typo_detected and field_name_cased != self.name: 

2327 lint_state.emit_diagnostic( 

2328 token_range, 

2329 f'Non-canonical spelling of "{self.name}"', 

2330 "pedantic", 

2331 self.unknown_value_authority, 

2332 quickfixes=[propose_correct_text_quick_fix(self.name)], 

2333 ) 

2334 

2335 def _dep5_file_list_diagnostics( 

2336 self, 

2337 kvpair: Deb822KeyValuePairElement, 

2338 kvpair_position: "TEPosition", 

2339 lint_state: LintState, 

2340 ) -> None: 

2341 source_root = lint_state.source_root 

2342 if ( 

2343 self.field_value_class != FieldValueClass.DEP5_FILE_LIST 

2344 or source_root is None 

2345 ): 

2346 return 

2347 interpreter = self.field_value_class.interpreter() 

2348 values = kvpair.interpret_as(interpreter) 

2349 value_off = kvpair.value_element.position_in_parent().relative_to( 

2350 kvpair_position 

2351 ) 

2352 

2353 assert interpreter is not None 

2354 

2355 for token in values.iter_parts(): 

2356 if token.is_whitespace: 

2357 continue 

2358 text = token.convert_to_text() 

2359 if "?" in text or "*" in text: 2359 ↛ 2361line 2359 didn't jump to line 2361 because the condition on line 2359 was never true

2360 # TODO: We should validate these as well 

2361 continue 

2362 matched_path, missing_part = source_root.attempt_lookup(text) 

2363 # It is common practice to delete "dirty" files during clean. This causes files listed 

2364 # in `debian/copyright` to go missing and as a consequence, we do not validate whether 

2365 # they are present (that would require us to check the `.orig.tar`, which we could but 

2366 # do not have the infrastructure for). 

2367 if not missing_part and matched_path.is_dir: 2367 ↛ 2355line 2367 didn't jump to line 2355 because the condition on line 2367 was always true

2368 path_range_te = token.range_in_parent().relative_to(value_off) 

2369 lint_state.emit_diagnostic( 

2370 path_range_te, 

2371 "Directories cannot be a match. Use `dir/*` to match everything in it", 

2372 "warning", 

2373 self.unknown_value_authority, 

2374 quickfixes=[ 

2375 propose_correct_text_quick_fix(f"{matched_path.path}/*") 

2376 ], 

2377 ) 

2378 

2379 def _known_value_diagnostics( 

2380 self, 

2381 kvpair: Deb822KeyValuePairElement, 

2382 kvpair_position: "TEPosition", 

2383 lint_state: LintState, 

2384 ) -> None: 

2385 unknown_value_severity = self.unknown_value_severity 

2386 interpreter = self.field_value_class.interpreter() 

2387 if interpreter is None: 

2388 return 

2389 try: 

2390 values = kvpair.interpret_as(interpreter) 

2391 except ValueError: 

2392 value_range = kvpair.value_element.range_in_parent().relative_to( 

2393 kvpair_position 

2394 ) 

2395 lint_state.emit_diagnostic( 

2396 value_range, 

2397 "Error while parsing field (diagnostics related to this field may be incomplete)", 

2398 "pedantic", 

2399 "debputy", 

2400 ) 

2401 return 

2402 value_off = kvpair.value_element.position_in_parent().relative_to( 

2403 kvpair_position 

2404 ) 

2405 

2406 last_token_non_ws_sep_token: TE | None = None 

2407 for token in values.iter_parts(): 

2408 if token.is_whitespace: 

2409 continue 

2410 if not token.is_separator: 

2411 last_token_non_ws_sep_token = None 

2412 continue 

2413 if last_token_non_ws_sep_token is not None: 

2414 sep_range_te = token.range_in_parent().relative_to(value_off) 

2415 lint_state.emit_diagnostic( 

2416 sep_range_te, 

2417 "Duplicate separator", 

2418 "error", 

2419 self.unknown_value_authority, 

2420 ) 

2421 last_token_non_ws_sep_token = token 

2422 

2423 allowed_values = self.known_values 

2424 if not allowed_values: 

2425 return 

2426 

2427 first_value = None 

2428 first_exclusive_value_ref = None 

2429 first_exclusive_value = None 

2430 has_emitted_for_exclusive = False 

2431 

2432 for value_ref in values.iter_value_references(): 

2433 value = value_ref.value 

2434 if ( 2434 ↛ 2438line 2434 didn't jump to line 2438 because the condition on line 2434 was never true

2435 first_value is not None 

2436 and self.field_value_class == FieldValueClass.SINGLE_VALUE 

2437 ): 

2438 value_loc = value_ref.locatable 

2439 range_position_te = value_loc.range_in_parent().relative_to(value_off) 

2440 lint_state.emit_diagnostic( 

2441 range_position_te, 

2442 f"The field {self.name} can only have exactly one value.", 

2443 "error", 

2444 self.unknown_value_authority, 

2445 ) 

2446 # TODO: Add quickfix if the value is also invalid 

2447 continue 

2448 

2449 if first_exclusive_value_ref is not None and not has_emitted_for_exclusive: 

2450 assert first_exclusive_value is not None 

2451 value_loc = first_exclusive_value_ref.locatable 

2452 value_range_te = value_loc.range_in_parent().relative_to(value_off) 

2453 lint_state.emit_diagnostic( 

2454 value_range_te, 

2455 f'The value "{first_exclusive_value}" cannot be used with other values.', 

2456 "error", 

2457 self.unknown_value_authority, 

2458 ) 

2459 

2460 known_value, unknown_value_message, unknown_severity, typo_fix_data = ( 

2461 _unknown_value_check( 

2462 self.name, 

2463 value, 

2464 self.known_values, 

2465 unknown_value_severity, 

2466 ) 

2467 ) 

2468 value_loc = value_ref.locatable 

2469 value_range = value_loc.range_in_parent().relative_to(value_off) 

2470 

2471 if known_value and known_value.is_exclusive: 

2472 first_exclusive_value = known_value.value # In case of typos. 

2473 first_exclusive_value_ref = value_ref 

2474 if first_value is not None: 

2475 has_emitted_for_exclusive = True 

2476 lint_state.emit_diagnostic( 

2477 value_range, 

2478 f'The value "{known_value.value}" cannot be used with other values.', 

2479 "error", 

2480 self.unknown_value_authority, 

2481 ) 

2482 

2483 if first_value is None: 

2484 first_value = value 

2485 

2486 if unknown_value_message is not None: 

2487 assert unknown_severity is not None 

2488 lint_state.emit_diagnostic( 

2489 value_range, 

2490 unknown_value_message, 

2491 unknown_severity, 

2492 self.unknown_value_authority, 

2493 quickfixes=typo_fix_data, 

2494 ) 

2495 

2496 if known_value is not None and known_value.is_deprecated: 

2497 replacement = known_value.replaced_by 

2498 if replacement is not None: 2498 ↛ 2504line 2498 didn't jump to line 2504 because the condition on line 2498 was always true

2499 obsolete_value_message = ( 

2500 f'The value "{value}" has been replaced by "{replacement}"' 

2501 ) 

2502 obsolete_fix_data = [propose_correct_text_quick_fix(replacement)] 

2503 else: 

2504 obsolete_value_message = ( 

2505 f'The value "{value}" is obsolete without a single replacement' 

2506 ) 

2507 obsolete_fix_data = None 

2508 lint_state.emit_diagnostic( 

2509 value_range, 

2510 obsolete_value_message, 

2511 "warning", 

2512 "debputy", 

2513 quickfixes=obsolete_fix_data, 

2514 ) 

2515 

2516 def _reformat_field_name( 

2517 self, 

2518 effective_preference: "EffectiveFormattingPreference", 

2519 stanza_range: TERange, 

2520 kvpair: Deb822KeyValuePairElement, 

2521 position_codec: LintCapablePositionCodec, 

2522 lines: list[str], 

2523 ) -> Iterable[TextEdit]: 

2524 if not effective_preference.deb822_auto_canonical_size_field_names: 2524 ↛ 2525line 2524 didn't jump to line 2525 because the condition on line 2524 was never true

2525 return 

2526 # The `str(kvpair.field_name)` is to avoid the "magic" from `python3-debian`'s Deb822 keys. 

2527 if str(kvpair.field_name) == self.name: 

2528 return 

2529 

2530 field_name_range_te = kvpair.field_token.range_in_parent().relative_to( 

2531 kvpair.range_in_parent().relative_to(stanza_range.start_pos).start_pos 

2532 ) 

2533 

2534 edit_range = position_codec.range_to_client_units( 

2535 lines, 

2536 Range( 

2537 Position( 

2538 field_name_range_te.start_pos.line_position, 

2539 field_name_range_te.start_pos.cursor_position, 

2540 ), 

2541 Position( 

2542 field_name_range_te.start_pos.line_position, 

2543 field_name_range_te.end_pos.cursor_position, 

2544 ), 

2545 ), 

2546 ) 

2547 yield TextEdit( 

2548 edit_range, 

2549 self.name, 

2550 ) 

2551 

2552 def reformat_field( 

2553 self, 

2554 effective_preference: "EffectiveFormattingPreference", 

2555 stanza_range: TERange, 

2556 kvpair: Deb822KeyValuePairElement, 

2557 formatter: FormatterCallback, 

2558 position_codec: LintCapablePositionCodec, 

2559 lines: list[str], 

2560 ) -> Iterable[TextEdit]: 

2561 kvpair_range = kvpair.range_in_parent().relative_to(stanza_range.start_pos) 

2562 yield from self._reformat_field_name( 

2563 effective_preference, 

2564 stanza_range, 

2565 kvpair, 

2566 position_codec, 

2567 lines, 

2568 ) 

2569 return trim_end_of_line_whitespace( 

2570 position_codec, 

2571 lines, 

2572 line_range=range( 

2573 kvpair_range.start_pos.line_position, 

2574 kvpair_range.end_pos.line_position, 

2575 ), 

2576 ) 

2577 

2578 def replace(self, **changes: Any) -> "Self": 

2579 return dataclasses.replace(self, **changes) 

2580 

2581 

2582@dataclasses.dataclass(slots=True) 

2583class DctrlLikeKnownField(Deb822KnownField): 

2584 

2585 def reformat_field( 

2586 self, 

2587 effective_preference: "EffectiveFormattingPreference", 

2588 stanza_range: TERange, 

2589 kvpair: Deb822KeyValuePairElement, 

2590 formatter: FormatterCallback, 

2591 position_codec: LintCapablePositionCodec, 

2592 lines: list[str], 

2593 ) -> Iterable[TextEdit]: 

2594 interpretation = self.field_value_class.interpreter() 

2595 if ( 2595 ↛ 2599line 2595 didn't jump to line 2599 because the condition on line 2595 was never true

2596 not effective_preference.deb822_normalize_field_content 

2597 or interpretation is None 

2598 ): 

2599 yield from super(DctrlLikeKnownField, self).reformat_field( 

2600 effective_preference, 

2601 stanza_range, 

2602 kvpair, 

2603 formatter, 

2604 position_codec, 

2605 lines, 

2606 ) 

2607 return 

2608 if not self.reformattable_field: 

2609 yield from super(DctrlLikeKnownField, self).reformat_field( 

2610 effective_preference, 

2611 stanza_range, 

2612 kvpair, 

2613 formatter, 

2614 position_codec, 

2615 lines, 

2616 ) 

2617 return 

2618 

2619 # Preserve the name fixes from the super call. 

2620 yield from self._reformat_field_name( 

2621 effective_preference, 

2622 stanza_range, 

2623 kvpair, 

2624 position_codec, 

2625 lines, 

2626 ) 

2627 

2628 seen: set[str] = set() 

2629 old_kvpair_range = kvpair.range_in_parent() 

2630 sort = self.is_sortable_field 

2631 

2632 # Avoid the context manager as we do not want to perform the change (it would contaminate future ranges) 

2633 field_content = kvpair.interpret_as(interpretation) 

2634 old_value = field_content.convert_to_text(with_field_name=False) 

2635 for package_ref in field_content.iter_value_references(): 

2636 value = package_ref.value 

2637 value_range = package_ref.locatable.range_in_parent().relative_to( 

2638 stanza_range.start_pos 

2639 ) 

2640 sublines = lines[ 

2641 value_range.start_pos.line_position : value_range.end_pos.line_position 

2642 ] 

2643 

2644 # debputy#112: Avoid truncating "inline comments" 

2645 if any(line.startswith("#") for line in sublines): 2645 ↛ 2646line 2645 didn't jump to line 2646 because the condition on line 2645 was never true

2646 return 

2647 if self.is_relationship_field: 2647 ↛ 2650line 2647 didn't jump to line 2650 because the condition on line 2647 was always true

2648 new_value = " | ".join(x.strip() for x in value.split("|")) 

2649 else: 

2650 new_value = value 

2651 if not sort or new_value not in seen: 2651 ↛ 2656line 2651 didn't jump to line 2656 because the condition on line 2651 was always true

2652 if new_value != value: 2652 ↛ 2653line 2652 didn't jump to line 2653 because the condition on line 2652 was never true

2653 package_ref.value = new_value 

2654 seen.add(new_value) 

2655 else: 

2656 package_ref.remove() 

2657 if sort: 2657 ↛ 2659line 2657 didn't jump to line 2659 because the condition on line 2657 was always true

2658 field_content.sort(key=_sort_packages_key) 

2659 field_content.value_formatter(formatter) 

2660 field_content.reformat_when_finished() 

2661 

2662 new_value = field_content.convert_to_text(with_field_name=False) 

2663 if new_value != old_value: 

2664 value_range = kvpair.value_element.range_in_parent().relative_to( 

2665 old_kvpair_range.start_pos 

2666 ) 

2667 range_server_units = te_range_to_lsp( 

2668 value_range.relative_to(stanza_range.start_pos) 

2669 ) 

2670 yield TextEdit( 

2671 position_codec.range_to_client_units(lines, range_server_units), 

2672 new_value, 

2673 ) 

2674 

2675 @property 

2676 def reformattable_field(self) -> bool: 

2677 return self.is_relationship_field or self.is_sortable_field 

2678 

2679 @property 

2680 def is_relationship_field(self) -> bool: 

2681 return False 

2682 

2683 @property 

2684 def is_sortable_field(self) -> bool: 

2685 return self.is_relationship_field 

2686 

2687 

2688@dataclasses.dataclass(slots=True) 

2689class DTestsCtrlKnownField(DctrlLikeKnownField): 

2690 @property 

2691 def is_relationship_field(self) -> bool: 

2692 return self.name == "Depends" 

2693 

2694 @property 

2695 def is_sortable_field(self) -> bool: 

2696 return self.is_relationship_field or self.name in ( 

2697 "Features", 

2698 "Restrictions", 

2699 "Tests", 

2700 ) 

2701 

2702 

2703@dataclasses.dataclass(slots=True) 

2704class DctrlKnownField(DctrlLikeKnownField): 

2705 

2706 def field_omitted_diagnostics( 

2707 self, 

2708 deb822_file: Deb822FileElement, 

2709 representation_field_range: "TERange", 

2710 stanza: Deb822ParagraphElement, 

2711 stanza_position: "TEPosition", 

2712 header_stanza: Deb822FileElement | None, 

2713 lint_state: LintState, 

2714 ) -> None: 

2715 missing_field_severity = self.missing_field_severity 

2716 if missing_field_severity is None: 

2717 return 

2718 

2719 if ( 

2720 self.inheritable_from_other_stanza 

2721 and header_stanza is not None 

2722 and self.name in header_stanza 

2723 ): 

2724 return 

2725 

2726 if self.name == "Standards-Version": 

2727 stanzas = list(deb822_file)[1:] 

2728 if all(s.get("Package-Type") == "udeb" for s in stanzas): 

2729 return 

2730 

2731 lint_state.emit_diagnostic( 

2732 representation_field_range, 

2733 f"Stanza is missing field {self.name}", 

2734 missing_field_severity, 

2735 self.missing_field_authority, 

2736 ) 

2737 

2738 def reformat_field( 

2739 self, 

2740 effective_preference: "EffectiveFormattingPreference", 

2741 stanza_range: TERange, 

2742 kvpair: Deb822KeyValuePairElement, 

2743 formatter: FormatterCallback, 

2744 position_codec: LintCapablePositionCodec, 

2745 lines: list[str], 

2746 ) -> Iterable[TextEdit]: 

2747 if ( 

2748 self.name == "Architecture" 

2749 and effective_preference.deb822_normalize_field_content 

2750 ): 

2751 interpretation = self.field_value_class.interpreter() 

2752 assert interpretation is not None 

2753 interpreted = kvpair.interpret_as(interpretation) 

2754 archs = list(interpreted) 

2755 # Sort, with wildcard entries (such as linux-any) first: 

2756 archs = sorted(archs, key=lambda x: ("any" not in x, x)) 

2757 new_value = f" {' '.join(archs)}\n" 

2758 reformat_edits = list( 

2759 self._reformat_field_name( 

2760 effective_preference, 

2761 stanza_range, 

2762 kvpair, 

2763 position_codec, 

2764 lines, 

2765 ) 

2766 ) 

2767 if new_value != interpreted.convert_to_text(with_field_name=False): 

2768 value_range = kvpair.value_element.range_in_parent().relative_to( 

2769 kvpair.range_in_parent().start_pos 

2770 ) 

2771 kvpair_range = te_range_to_lsp( 

2772 value_range.relative_to(stanza_range.start_pos) 

2773 ) 

2774 reformat_edits.append( 

2775 TextEdit( 

2776 position_codec.range_to_client_units(lines, kvpair_range), 

2777 new_value, 

2778 ) 

2779 ) 

2780 return reformat_edits 

2781 

2782 return super(DctrlKnownField, self).reformat_field( 

2783 effective_preference, 

2784 stanza_range, 

2785 kvpair, 

2786 formatter, 

2787 position_codec, 

2788 lines, 

2789 ) 

2790 

2791 @property 

2792 def is_relationship_field(self) -> bool: 

2793 name_lc = self.name.lower() 

2794 return ( 

2795 name_lc in all_package_relationship_fields() 

2796 or name_lc in all_source_relationship_fields() 

2797 ) 

2798 

2799 @property 

2800 def reformattable_field(self) -> bool: 

2801 return self.is_relationship_field or self.name == "Uploaders" 

2802 

2803 

2804@dataclasses.dataclass(slots=True) 

2805class DctrlRelationshipKnownField(DctrlKnownField): 

2806 allowed_version_operators: frozenset[str] = frozenset() 

2807 supports_or_relation: bool = True 

2808 

2809 @property 

2810 def is_relationship_field(self) -> bool: 

2811 return True 

2812 

2813 

2814SOURCE_FIELDS = _fields( 

2815 DctrlKnownField( 

2816 "Source", 

2817 FieldValueClass.SINGLE_VALUE, 

2818 custom_field_check=_combined_custom_field_check( 

2819 _each_value_match_regex_validation(PKGNAME_REGEX), 

2820 _has_packaging_expected_file( 

2821 "copyright", 

2822 "No copyright file (package license)", 

2823 severity="warning", 

2824 ), 

2825 _has_packaging_expected_file( 

2826 "changelog", 

2827 "No Debian changelog file", 

2828 severity="error", 

2829 ), 

2830 _has_build_instructions, 

2831 ), 

2832 ), 

2833 DctrlKnownField( 

2834 "Standards-Version", 

2835 FieldValueClass.SINGLE_VALUE, 

2836 custom_field_check=_sv_field_validation, 

2837 ), 

2838 DctrlKnownField( 

2839 "Section", 

2840 FieldValueClass.SINGLE_VALUE, 

2841 known_values=ALL_SECTIONS, 

2842 ), 

2843 DctrlKnownField( 

2844 "Priority", 

2845 FieldValueClass.SINGLE_VALUE, 

2846 ), 

2847 DctrlKnownField( 

2848 "Maintainer", 

2849 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2850 custom_field_check=_combined_custom_field_check( 

2851 _maintainer_field_validator, 

2852 _canonical_maintainer_name, 

2853 ), 

2854 ), 

2855 DctrlKnownField( 

2856 "Uploaders", 

2857 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2858 custom_field_check=_canonical_maintainer_name, 

2859 ), 

2860 DctrlRelationshipKnownField( 

2861 "Build-Depends", 

2862 FieldValueClass.COMMA_SEPARATED_LIST, 

2863 custom_field_check=_dctrl_validate_dep, 

2864 ), 

2865 DctrlRelationshipKnownField( 

2866 "Build-Depends-Arch", 

2867 FieldValueClass.COMMA_SEPARATED_LIST, 

2868 custom_field_check=_dctrl_validate_dep, 

2869 ), 

2870 DctrlRelationshipKnownField( 

2871 "Build-Depends-Indep", 

2872 FieldValueClass.COMMA_SEPARATED_LIST, 

2873 custom_field_check=_dctrl_validate_dep, 

2874 ), 

2875 DctrlRelationshipKnownField( 

2876 "Build-Conflicts", 

2877 FieldValueClass.COMMA_SEPARATED_LIST, 

2878 supports_or_relation=False, 

2879 custom_field_check=_dctrl_validate_dep, 

2880 ), 

2881 DctrlRelationshipKnownField( 

2882 "Build-Conflicts-Arch", 

2883 FieldValueClass.COMMA_SEPARATED_LIST, 

2884 supports_or_relation=False, 

2885 custom_field_check=_dctrl_validate_dep, 

2886 ), 

2887 DctrlRelationshipKnownField( 

2888 "Build-Conflicts-Indep", 

2889 FieldValueClass.COMMA_SEPARATED_LIST, 

2890 supports_or_relation=False, 

2891 custom_field_check=_dctrl_validate_dep, 

2892 ), 

2893 DctrlKnownField( 

2894 "Rules-Requires-Root", 

2895 FieldValueClass.SPACE_SEPARATED_LIST, 

2896 custom_field_check=_rrr_build_driver_mismatch, 

2897 ), 

2898 DctrlKnownField( 

2899 "X-Style", 

2900 FieldValueClass.SINGLE_VALUE, 

2901 known_values=ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

2902 ), 

2903 DctrlKnownField( 

2904 "Homepage", 

2905 FieldValueClass.SINGLE_VALUE, 

2906 custom_field_check=_validate_homepage_field, 

2907 ), 

2908) 

2909 

2910 

2911BINARY_FIELDS = _fields( 

2912 DctrlKnownField( 

2913 "Package", 

2914 FieldValueClass.SINGLE_VALUE, 

2915 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

2916 ), 

2917 DctrlKnownField( 

2918 "Architecture", 

2919 FieldValueClass.SPACE_SEPARATED_LIST, 

2920 # FIXME: Specialize validation for architecture ("!foo" is not a "typo" and should have a better warning) 

2921 known_values=allowed_values(*dpkg_arch_and_wildcards()), 

2922 ), 

2923 DctrlKnownField( 

2924 "Pre-Depends", 

2925 FieldValueClass.COMMA_SEPARATED_LIST, 

2926 custom_field_check=_dctrl_validate_dep, 

2927 ), 

2928 DctrlKnownField( 

2929 "Depends", 

2930 FieldValueClass.COMMA_SEPARATED_LIST, 

2931 custom_field_check=_dctrl_validate_dep, 

2932 ), 

2933 DctrlKnownField( 

2934 "Recommends", 

2935 FieldValueClass.COMMA_SEPARATED_LIST, 

2936 custom_field_check=_dctrl_validate_dep, 

2937 ), 

2938 DctrlKnownField( 

2939 "Suggests", 

2940 FieldValueClass.COMMA_SEPARATED_LIST, 

2941 custom_field_check=_dctrl_validate_dep, 

2942 ), 

2943 DctrlKnownField( 

2944 "Enhances", 

2945 FieldValueClass.COMMA_SEPARATED_LIST, 

2946 custom_field_check=_dctrl_validate_dep, 

2947 ), 

2948 DctrlRelationshipKnownField( 

2949 "Provides", 

2950 FieldValueClass.COMMA_SEPARATED_LIST, 

2951 custom_field_check=_dctrl_validate_dep, 

2952 supports_or_relation=False, 

2953 allowed_version_operators=frozenset(["="]), 

2954 ), 

2955 DctrlRelationshipKnownField( 

2956 "Conflicts", 

2957 FieldValueClass.COMMA_SEPARATED_LIST, 

2958 custom_field_check=_dctrl_validate_dep, 

2959 supports_or_relation=False, 

2960 ), 

2961 DctrlRelationshipKnownField( 

2962 "Breaks", 

2963 FieldValueClass.COMMA_SEPARATED_LIST, 

2964 custom_field_check=_dctrl_validate_dep, 

2965 supports_or_relation=False, 

2966 ), 

2967 DctrlRelationshipKnownField( 

2968 "Replaces", 

2969 FieldValueClass.COMMA_SEPARATED_LIST, 

2970 custom_field_check=_dctrl_validate_dep, 

2971 ), 

2972 DctrlKnownField( 

2973 "Build-Profiles", 

2974 FieldValueClass.BUILD_PROFILES_LIST, 

2975 ), 

2976 DctrlKnownField( 

2977 "Section", 

2978 FieldValueClass.SINGLE_VALUE, 

2979 known_values=ALL_SECTIONS, 

2980 ), 

2981 DctrlRelationshipKnownField( 

2982 "Built-Using", 

2983 FieldValueClass.COMMA_SEPARATED_LIST, 

2984 custom_field_check=_arch_not_all_only_field_validation, 

2985 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

2986 supports_or_relation=False, 

2987 allowed_version_operators=frozenset(["="]), 

2988 ), 

2989 DctrlRelationshipKnownField( 

2990 "Static-Built-Using", 

2991 FieldValueClass.COMMA_SEPARATED_LIST, 

2992 custom_field_check=_arch_not_all_only_field_validation, 

2993 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

2994 supports_or_relation=False, 

2995 allowed_version_operators=frozenset(["="]), 

2996 ), 

2997 DctrlKnownField( 

2998 "Multi-Arch", 

2999 FieldValueClass.SINGLE_VALUE, 

3000 custom_field_check=_dctrl_ma_field_validation, 

3001 known_values=allowed_values( 

3002 Keyword( 

3003 "same", 

3004 can_complete_keyword_in_stanza=_complete_only_in_arch_dep_pkgs, 

3005 ), 

3006 ), 

3007 ), 

3008 DctrlKnownField( 

3009 "XB-Installer-Menu-Item", 

3010 FieldValueClass.SINGLE_VALUE, 

3011 can_complete_field_in_stanza=_complete_only_for_udeb_pkgs, 

3012 custom_field_check=_combined_custom_field_check( 

3013 _udeb_only_field_validation, 

3014 _each_value_match_regex_validation(re.compile(r"^[1-9]\d{3,4}$")), 

3015 ), 

3016 ), 

3017 DctrlKnownField( 

3018 "X-DH-Build-For-Type", 

3019 FieldValueClass.SINGLE_VALUE, 

3020 custom_field_check=_arch_not_all_only_field_validation, 

3021 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3022 ), 

3023 DctrlKnownField( 

3024 "X-Doc-Main-Package", 

3025 FieldValueClass.SINGLE_VALUE, 

3026 custom_field_check=_binary_package_from_same_source, 

3027 ), 

3028 DctrlKnownField( 

3029 "X-Time64-Compat", 

3030 FieldValueClass.SINGLE_VALUE, 

3031 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3032 custom_field_check=_combined_custom_field_check( 

3033 _each_value_match_regex_validation(PKGNAME_REGEX), 

3034 _arch_not_all_only_field_validation, 

3035 ), 

3036 ), 

3037 DctrlKnownField( 

3038 "Description", 

3039 FieldValueClass.FREE_TEXT_FIELD, 

3040 custom_field_check=dctrl_description_validator, 

3041 ), 

3042 DctrlKnownField( 

3043 "XB-Cnf-Visible-Pkgname", 

3044 FieldValueClass.SINGLE_VALUE, 

3045 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

3046 ), 

3047 DctrlKnownField( 

3048 "Homepage", 

3049 FieldValueClass.SINGLE_VALUE, 

3050 show_as_inherited=False, 

3051 custom_field_check=_validate_homepage_field, 

3052 ), 

3053) 

3054_DEP5_HEADER_FIELDS = _fields( 

3055 Deb822KnownField( 

3056 "Format", 

3057 FieldValueClass.SINGLE_VALUE, 

3058 custom_field_check=_use_https_instead_of_http, 

3059 ), 

3060) 

3061_DEP5_FILES_FIELDS = _fields( 

3062 Deb822KnownField( 

3063 "Files", 

3064 FieldValueClass.DEP5_FILE_LIST, 

3065 custom_field_check=_dep5_files_check, 

3066 ), 

3067) 

3068_DEP5_LICENSE_FIELDS = _fields( 

3069 Deb822KnownField( 

3070 "License", 

3071 FieldValueClass.FREE_TEXT_FIELD, 

3072 ), 

3073) 

3074 

3075_DTESTSCTRL_FIELDS = _fields( 

3076 DTestsCtrlKnownField( 

3077 "Architecture", 

3078 FieldValueClass.SPACE_SEPARATED_LIST, 

3079 # FIXME: Specialize validation for architecture ("!fou" to "foo" would be bad) 

3080 known_values=allowed_values(*dpkg_arch_and_wildcards(allow_negations=True)), 

3081 ), 

3082) 

3083_DWATCH_HEADER_FIELDS = _fields() 

3084_DWATCH_TEMPLATE_FIELDS = _fields() 

3085_DWATCH_SOURCE_FIELDS = _fields() 

3086 

3087 

3088@dataclasses.dataclass(slots=True) 

3089class StanzaMetadata(Mapping[str, F], Generic[F], ABC): 

3090 stanza_type_name: str 

3091 stanza_fields: Mapping[str, F] 

3092 is_substvars_allowed_in_stanza: bool 

3093 

3094 async def stanza_diagnostics( 

3095 self, 

3096 deb822_file: Deb822FileElement, 

3097 stanza: Deb822ParagraphElement, 

3098 stanza_position_in_file: "TEPosition", 

3099 lint_state: LintState, 

3100 *, 

3101 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3102 confusable_with_stanza_name: str | None = None, 

3103 confusable_with_stanza_metadata: Optional["StanzaMetadata[F]"] = None, 

3104 ) -> None: 

3105 if (confusable_with_stanza_name is None) ^ ( 3105 ↛ 3108line 3105 didn't jump to line 3108 because the condition on line 3105 was never true

3106 confusable_with_stanza_metadata is None 

3107 ): 

3108 raise ValueError( 

3109 "confusable_with_stanza_name and confusable_with_stanza_metadata must be used together" 

3110 ) 

3111 _, representation_field_range = self.stanza_representation( 

3112 stanza, 

3113 stanza_position_in_file, 

3114 ) 

3115 known_fields = self.stanza_fields 

3116 self.omitted_field_diagnostics( 

3117 lint_state, 

3118 deb822_file, 

3119 stanza, 

3120 stanza_position_in_file, 

3121 inherit_from_stanza=inherit_from_stanza, 

3122 representation_field_range=representation_field_range, 

3123 ) 

3124 seen_fields: dict[str, tuple[str, str, "TERange", list[Range], set[str]]] = {} 

3125 

3126 async for kvpair_range, kvpair in lint_state.slow_iter( 

3127 with_range_in_continuous_parts( 

3128 stanza.iter_parts(), 

3129 start_relative_to=stanza_position_in_file, 

3130 ), 

3131 yield_every=1, 

3132 ): 

3133 if not isinstance(kvpair, Deb822KeyValuePairElement): 3133 ↛ 3134line 3133 didn't jump to line 3134 because the condition on line 3133 was never true

3134 continue 

3135 field_name_token = kvpair.field_token 

3136 field_name = field_name_token.text 

3137 field_name_lc = field_name.lower() 

3138 # Defeat any tricks from `python-debian` from here on out 

3139 field_name = str(field_name) 

3140 normalized_field_name_lc = self.normalize_field_name(field_name_lc) 

3141 known_field = known_fields.get(normalized_field_name_lc) 

3142 field_value = stanza[field_name] 

3143 kvpair_range_te = kvpair.range_in_parent().relative_to( 

3144 stanza_position_in_file 

3145 ) 

3146 field_range = kvpair.field_token.range_in_parent().relative_to( 

3147 kvpair_range_te.start_pos 

3148 ) 

3149 field_position_te = field_range.start_pos 

3150 field_name_typo_detected = False 

3151 dup_field_key = ( 

3152 known_field.name 

3153 if known_field is not None 

3154 else normalized_field_name_lc 

3155 ) 

3156 existing_field_range = seen_fields.get(dup_field_key) 

3157 if existing_field_range is not None: 

3158 existing_field_range[3].append(field_range) 

3159 existing_field_range[4].add(field_name) 

3160 else: 

3161 normalized_field_name = self.normalize_field_name(field_name) 

3162 seen_fields[dup_field_key] = ( 

3163 known_field.name if known_field else field_name, 

3164 normalized_field_name, 

3165 field_range, 

3166 [], 

3167 {field_name}, 

3168 ) 

3169 

3170 if known_field is None: 

3171 candidates = detect_possible_typo( 

3172 normalized_field_name_lc, known_fields 

3173 ) 

3174 if candidates: 

3175 known_field = known_fields[candidates[0]] 

3176 field_range = TERange.from_position_and_size( 

3177 field_position_te, kvpair.field_token.size() 

3178 ) 

3179 field_name_typo_detected = True 

3180 lint_state.emit_diagnostic( 

3181 field_range, 

3182 f'The "{field_name}" looks like a typo of "{known_field.name}".', 

3183 "warning", 

3184 "debputy", 

3185 quickfixes=[ 

3186 propose_correct_text_quick_fix(known_fields[m].name) 

3187 for m in candidates 

3188 ], 

3189 ) 

3190 if field_value.strip() == "": 3190 ↛ 3191line 3190 didn't jump to line 3191 because the condition on line 3190 was never true

3191 lint_state.emit_diagnostic( 

3192 field_range, 

3193 f"The {field_name} has no value. Either provide a value or remove it.", 

3194 "error", 

3195 "Policy 5.1", 

3196 ) 

3197 continue 

3198 if known_field is None: 

3199 known_else_where = confusable_with_stanza_metadata.stanza_fields.get( 

3200 normalized_field_name_lc 

3201 ) 

3202 if known_else_where is not None: 

3203 lint_state.emit_diagnostic( 

3204 field_range, 

3205 f"The {kvpair.field_name} is defined for use in the" 

3206 f' "{confusable_with_stanza_name}" stanza. Please move it to the right place or remove it', 

3207 "error", 

3208 known_else_where.missing_field_authority, 

3209 ) 

3210 continue 

3211 await known_field.field_diagnostics( 

3212 deb822_file, 

3213 kvpair, 

3214 stanza, 

3215 stanza_position_in_file, 

3216 kvpair_range_te, 

3217 lint_state, 

3218 field_name_typo_reported=field_name_typo_detected, 

3219 ) 

3220 

3221 inherit_value = ( 

3222 inherit_from_stanza.get(field_name) if inherit_from_stanza else None 

3223 ) 

3224 

3225 if ( 

3226 known_field.inheritable_from_other_stanza 

3227 and inherit_value is not None 

3228 and field_value == inherit_value 

3229 ): 

3230 quick_fix = propose_remove_range_quick_fix( 

3231 proposed_title="Remove redundant definition" 

3232 ) 

3233 lint_state.emit_diagnostic( 

3234 kvpair_range_te, 

3235 f"The field {field_name} duplicates the value from the Source stanza.", 

3236 "informational", 

3237 "debputy", 

3238 quickfixes=[quick_fix], 

3239 ) 

3240 for ( 

3241 field_name, 

3242 normalized_field_name, 

3243 field_range, 

3244 duplicates, 

3245 used_fields, 

3246 ) in seen_fields.values(): 

3247 if not duplicates: 

3248 continue 

3249 if len(used_fields) != 1 or field_name not in used_fields: 

3250 via_aliases_msg = " (via aliases)" 

3251 else: 

3252 via_aliases_msg = "" 

3253 for dup_range in duplicates: 

3254 lint_state.emit_diagnostic( 

3255 dup_range, 

3256 f'The field "{field_name}"{via_aliases_msg} was used multiple times in this stanza.' 

3257 " Please ensure the field is only used once per stanza.", 

3258 "error", 

3259 "Policy 5.1", 

3260 related_information=[ 

3261 lint_state.related_diagnostic_information( 

3262 field_range, 

3263 message=f"First definition of {field_name}", 

3264 ), 

3265 ], 

3266 ) 

3267 

3268 def __getitem__(self, key: str) -> F: 

3269 key_lc = key.lower() 

3270 key_norm = normalize_dctrl_field_name(key_lc) 

3271 return self.stanza_fields[key_norm] 

3272 

3273 def __len__(self) -> int: 

3274 return len(self.stanza_fields) 

3275 

3276 def __iter__(self) -> Iterator[str]: 

3277 return iter(self.stanza_fields.keys()) 

3278 

3279 def omitted_field_diagnostics( 

3280 self, 

3281 lint_state: LintState, 

3282 deb822_file: Deb822FileElement, 

3283 stanza: Deb822ParagraphElement, 

3284 stanza_position: "TEPosition", 

3285 *, 

3286 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3287 representation_field_range: Range | None = None, 

3288 ) -> None: 

3289 if representation_field_range is None: 3289 ↛ 3290line 3289 didn't jump to line 3290 because the condition on line 3289 was never true

3290 _, representation_field_range = self.stanza_representation( 

3291 stanza, 

3292 stanza_position, 

3293 ) 

3294 for known_field in self.stanza_fields.values(): 

3295 if known_field.name in stanza: 

3296 continue 

3297 

3298 known_field.field_omitted_diagnostics( 

3299 deb822_file, 

3300 representation_field_range, 

3301 stanza, 

3302 stanza_position, 

3303 inherit_from_stanza, 

3304 lint_state, 

3305 ) 

3306 

3307 def _paragraph_representation_field( 

3308 self, 

3309 paragraph: Deb822ParagraphElement, 

3310 ) -> Deb822KeyValuePairElement: 

3311 return next(iter(paragraph.iter_parts_of_type(Deb822KeyValuePairElement))) 

3312 

3313 def normalize_field_name(self, field_name: str) -> str: 

3314 return field_name 

3315 

3316 def stanza_representation( 

3317 self, 

3318 stanza: Deb822ParagraphElement, 

3319 stanza_position: TEPosition, 

3320 ) -> tuple[Deb822KeyValuePairElement, TERange]: 

3321 representation_field = self._paragraph_representation_field(stanza) 

3322 representation_field_range = representation_field.range_in_parent().relative_to( 

3323 stanza_position 

3324 ) 

3325 return representation_field, representation_field_range 

3326 

3327 def reformat_stanza( 

3328 self, 

3329 effective_preference: "EffectiveFormattingPreference", 

3330 stanza: Deb822ParagraphElement, 

3331 stanza_range: TERange, 

3332 formatter: FormatterCallback, 

3333 position_codec: LintCapablePositionCodec, 

3334 lines: list[str], 

3335 ) -> Iterable[TextEdit]: 

3336 for field_name in stanza: 

3337 known_field = self.stanza_fields.get(field_name.lower()) 

3338 if known_field is None: 

3339 continue 

3340 kvpair = stanza.get_kvpair_element(field_name) 

3341 yield from known_field.reformat_field( 

3342 effective_preference, 

3343 stanza_range, 

3344 kvpair, 

3345 formatter, 

3346 position_codec, 

3347 lines, 

3348 ) 

3349 

3350 

3351@dataclasses.dataclass(slots=True) 

3352class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3353 pass 

3354 

3355 

3356@dataclasses.dataclass(slots=True) 

3357class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]): 

3358 

3359 def normalize_field_name(self, field_name: str) -> str: 

3360 return normalize_dctrl_field_name(field_name) 

3361 

3362 

3363@dataclasses.dataclass(slots=True) 

3364class DTestsCtrlStanzaMetadata(StanzaMetadata[DTestsCtrlKnownField]): 

3365 

3366 def omitted_field_diagnostics( 

3367 self, 

3368 lint_state: LintState, 

3369 deb822_file: Deb822FileElement, 

3370 stanza: Deb822ParagraphElement, 

3371 stanza_position: "TEPosition", 

3372 *, 

3373 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3374 representation_field_range: Range | None = None, 

3375 ) -> None: 

3376 if representation_field_range is None: 3376 ↛ 3377line 3376 didn't jump to line 3377 because the condition on line 3376 was never true

3377 _, representation_field_range = self.stanza_representation( 

3378 stanza, 

3379 stanza_position, 

3380 ) 

3381 auth_ref = self.stanza_fields["tests"].missing_field_authority 

3382 if "Tests" not in stanza and "Test-Command" not in stanza: 

3383 lint_state.emit_diagnostic( 

3384 representation_field_range, 

3385 'Stanza must have either a "Tests" or a "Test-Command" field', 

3386 "error", 

3387 # TODO: Better authority_reference 

3388 auth_ref, 

3389 ) 

3390 if "Tests" in stanza and "Test-Command" in stanza: 

3391 lint_state.emit_diagnostic( 

3392 representation_field_range, 

3393 'Stanza cannot have both a "Tests" and a "Test-Command" field', 

3394 "error", 

3395 # TODO: Better authority_reference 

3396 auth_ref, 

3397 ) 

3398 

3399 # Note that since we do not use the field names for stanza classification, we 

3400 # always do the super call. 

3401 super(DTestsCtrlStanzaMetadata, self).omitted_field_diagnostics( 

3402 lint_state, 

3403 deb822_file, 

3404 stanza, 

3405 stanza_position, 

3406 representation_field_range=representation_field_range, 

3407 inherit_from_stanza=inherit_from_stanza, 

3408 ) 

3409 

3410 

3411@dataclasses.dataclass(slots=True) 

3412class DebianWatchStanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3413 

3414 def omitted_field_diagnostics( 

3415 self, 

3416 lint_state: LintState, 

3417 deb822_file: Deb822FileElement, 

3418 stanza: Deb822ParagraphElement, 

3419 stanza_position: "TEPosition", 

3420 *, 

3421 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3422 representation_field_range: Range | None = None, 

3423 ) -> None: 

3424 if representation_field_range is None: 3424 ↛ 3425line 3424 didn't jump to line 3425 because the condition on line 3424 was never true

3425 _, representation_field_range = self.stanza_representation( 

3426 stanza, 

3427 stanza_position, 

3428 ) 

3429 

3430 if ( 3430 ↛ 3435line 3430 didn't jump to line 3435 because the condition on line 3430 was never true

3431 self.stanza_type_name != "Header" 

3432 and "Source" not in stanza 

3433 and "Template" not in stanza 

3434 ): 

3435 lint_state.emit_diagnostic( 

3436 representation_field_range, 

3437 'Stanza must have either a "Source" or a "Template" field', 

3438 "error", 

3439 # TODO: Better authority_reference 

3440 "debputy", 

3441 ) 

3442 # The required fields depends on which stanza it is. Therefore, we omit the super 

3443 # call until this error is resolved. 

3444 return 

3445 

3446 super(DebianWatchStanzaMetadata, self).omitted_field_diagnostics( 

3447 lint_state, 

3448 deb822_file, 

3449 stanza, 

3450 stanza_position, 

3451 representation_field_range=representation_field_range, 

3452 inherit_from_stanza=inherit_from_stanza, 

3453 ) 

3454 

3455 

3456def lsp_reference_data_dir() -> str: 

3457 return os.path.join( 

3458 os.path.dirname(__file__), 

3459 "data", 

3460 ) 

3461 

3462 

3463class Deb822FileMetadata(Generic[S, F]): 

3464 

3465 def __init__(self) -> None: 

3466 self._is_initialized = False 

3467 self._data: Deb822ReferenceData | None = None 

3468 

3469 @property 

3470 def reference_data_basename(self) -> str: 

3471 raise NotImplementedError 

3472 

3473 def _new_field( 

3474 self, 

3475 name: str, 

3476 field_value_type: FieldValueClass, 

3477 ) -> F: 

3478 raise NotImplementedError 

3479 

3480 def _reference_data(self) -> Deb822ReferenceData: 

3481 ref = self._data 

3482 if ref is not None: 3482 ↛ 3483line 3482 didn't jump to line 3483 because the condition on line 3482 was never true

3483 return ref 

3484 

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

3486 self.reference_data_basename 

3487 ) 

3488 

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

3490 raw = MANIFEST_YAML.load(fd) 

3491 

3492 attr_path = AttributePath.root_path(p) 

3493 try: 

3494 ref = DEB822_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

3495 except ManifestParseException as e: 

3496 raise ValueError( 

3497 f"Internal error: Could not parse reference data [{self.reference_data_basename}]: {e.message}" 

3498 ) from e 

3499 self._data = ref 

3500 return ref 

3501 

3502 @property 

3503 def is_initialized(self) -> bool: 

3504 return self._is_initialized 

3505 

3506 def ensure_initialized(self) -> None: 

3507 if self.is_initialized: 

3508 return 

3509 # Enables us to use __getitem__ 

3510 self._is_initialized = True 

3511 ref_data = self._reference_data() 

3512 ref_defs = ref_data.get("definitions") 

3513 variables = {} 

3514 ref_variables = ref_defs.get("variables", []) if ref_defs else [] 

3515 for ref_variable in ref_variables: 

3516 name = ref_variable["name"] 

3517 fallback = ref_variable["fallback"] 

3518 variables[name] = fallback 

3519 

3520 def _resolve_doc(template: str | None) -> str | None: 

3521 if template is None: 3521 ↛ 3522line 3521 didn't jump to line 3522 because the condition on line 3521 was never true

3522 return None 

3523 try: 

3524 return template.format(**variables) 

3525 except ValueError as e: 

3526 template_escaped = template.replace("\n", "\\r") 

3527 _error(f"Bad template: {template_escaped}: {e}") 

3528 

3529 for ref_stanza_type in ref_data["stanza_types"]: 

3530 stanza_name = ref_stanza_type["stanza_name"] 

3531 stanza = self[stanza_name] 

3532 stanza_fields = dict(stanza.stanza_fields) 

3533 stanza.stanza_fields = stanza_fields 

3534 for ref_field in ref_stanza_type["fields"]: 

3535 _resolve_field( 

3536 ref_field, 

3537 stanza_fields, 

3538 self._new_field, 

3539 _resolve_doc, 

3540 f"Stanza:{stanza.stanza_type_name}|Field:{ref_field['canonical_name']}", 

3541 ) 

3542 

3543 def file_metadata_applies_to_file( 

3544 self, 

3545 deb822_file: Deb822FileElement | None, 

3546 ) -> bool: 

3547 return deb822_file is not None 

3548 

3549 def classify_stanza(self, stanza: Deb822ParagraphElement, stanza_idx: int) -> S: 

3550 return self.guess_stanza_classification_by_idx(stanza_idx) 

3551 

3552 def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S: 

3553 raise NotImplementedError 

3554 

3555 def stanza_types(self) -> Iterable[S]: 

3556 raise NotImplementedError 

3557 

3558 def __getitem__(self, item: str) -> S: 

3559 raise NotImplementedError 

3560 

3561 def get(self, item: str) -> S | None: 

3562 try: 

3563 return self[item] 

3564 except KeyError: 

3565 return None 

3566 

3567 def reformat( 

3568 self, 

3569 effective_preference: "EffectiveFormattingPreference", 

3570 deb822_file: Deb822FileElement, 

3571 formatter: FormatterCallback, 

3572 _content: str, 

3573 position_codec: LintCapablePositionCodec, 

3574 lines: list[str], 

3575 ) -> Iterable[TextEdit]: 

3576 stanza_idx = -1 

3577 for token_or_element in deb822_file.iter_parts(): 

3578 if isinstance(token_or_element, Deb822ParagraphElement): 

3579 stanza_range = token_or_element.range_in_parent() 

3580 stanza_idx += 1 

3581 stanza_metadata = self.classify_stanza(token_or_element, stanza_idx) 

3582 yield from stanza_metadata.reformat_stanza( 

3583 effective_preference, 

3584 token_or_element, 

3585 stanza_range, 

3586 formatter, 

3587 position_codec, 

3588 lines, 

3589 ) 

3590 else: 

3591 token_range = token_or_element.range_in_parent() 

3592 yield from trim_end_of_line_whitespace( 

3593 position_codec, 

3594 lines, 

3595 line_range=range( 

3596 token_range.start_pos.line_position, 

3597 token_range.end_pos.line_position, 

3598 ), 

3599 ) 

3600 

3601 

3602_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata( 

3603 "Source", 

3604 SOURCE_FIELDS, 

3605 is_substvars_allowed_in_stanza=False, 

3606) 

3607_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata( 

3608 "Package", 

3609 BINARY_FIELDS, 

3610 is_substvars_allowed_in_stanza=True, 

3611) 

3612 

3613_DEP5_HEADER_STANZA = Dep5StanzaMetadata( 

3614 "Header", 

3615 _DEP5_HEADER_FIELDS, 

3616 is_substvars_allowed_in_stanza=False, 

3617) 

3618_DEP5_FILES_STANZA = Dep5StanzaMetadata( 

3619 "Files", 

3620 _DEP5_FILES_FIELDS, 

3621 is_substvars_allowed_in_stanza=False, 

3622) 

3623_DEP5_LICENSE_STANZA = Dep5StanzaMetadata( 

3624 "License", 

3625 _DEP5_LICENSE_FIELDS, 

3626 is_substvars_allowed_in_stanza=False, 

3627) 

3628 

3629_DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata( 

3630 "Tests", 

3631 _DTESTSCTRL_FIELDS, 

3632 is_substvars_allowed_in_stanza=False, 

3633) 

3634 

3635_WATCH_HEADER_HEADER_STANZA = DebianWatchStanzaMetadata( 

3636 "Header", 

3637 _DWATCH_HEADER_FIELDS, 

3638 is_substvars_allowed_in_stanza=False, 

3639) 

3640_WATCH_SOURCE_STANZA = DebianWatchStanzaMetadata( 

3641 "Source", 

3642 _DWATCH_SOURCE_FIELDS, 

3643 is_substvars_allowed_in_stanza=False, 

3644) 

3645 

3646 

3647class Dep5FileMetadata(Deb822FileMetadata[Dep5StanzaMetadata, Deb822KnownField]): 

3648 

3649 @property 

3650 def reference_data_basename(self) -> str: 

3651 return "debian_copyright_reference_data.yaml" 

3652 

3653 def _new_field( 

3654 self, 

3655 name: str, 

3656 field_value_type: FieldValueClass, 

3657 ) -> F: 

3658 return Deb822KnownField(name, field_value_type) 

3659 

3660 def file_metadata_applies_to_file( 

3661 self, 

3662 deb822_file: Deb822FileElement | None, 

3663 ) -> bool: 

3664 if not super().file_metadata_applies_to_file(deb822_file): 3664 ↛ 3665line 3664 didn't jump to line 3665 because the condition on line 3664 was never true

3665 return False 

3666 first_stanza = next(iter(deb822_file), None) 

3667 if first_stanza is None or "Format" not in first_stanza: 

3668 # No parseable stanzas or the first one did not have a Format, which is necessary. 

3669 return False 

3670 

3671 for part in deb822_file.iter_parts(): 3671 ↛ 3677line 3671 didn't jump to line 3677 because the loop on line 3671 didn't complete

3672 if part.is_error: 

3673 # Error first, then it might just be a "Format:" in the middle of a free-text file. 

3674 return False 

3675 if part is first_stanza: 3675 ↛ 3671line 3675 didn't jump to line 3671 because the condition on line 3675 was always true

3676 break 

3677 return True 

3678 

3679 def classify_stanza( 

3680 self, 

3681 stanza: Deb822ParagraphElement, 

3682 stanza_idx: int, 

3683 ) -> Dep5StanzaMetadata: 

3684 self.ensure_initialized() 

3685 if stanza_idx == 0: 3685 ↛ 3686line 3685 didn't jump to line 3686 because the condition on line 3685 was never true

3686 return _DEP5_HEADER_STANZA 

3687 if stanza_idx > 0: 3687 ↛ 3691line 3687 didn't jump to line 3691 because the condition on line 3687 was always true

3688 if "Files" in stanza: 

3689 return _DEP5_FILES_STANZA 

3690 return _DEP5_LICENSE_STANZA 

3691 raise ValueError("The stanza_idx must be 0 or greater") 

3692 

3693 def guess_stanza_classification_by_idx(self, stanza_idx: int) -> Dep5StanzaMetadata: 

3694 self.ensure_initialized() 

3695 if stanza_idx == 0: 

3696 return _DEP5_HEADER_STANZA 

3697 if stanza_idx > 0: 

3698 return _DEP5_FILES_STANZA 

3699 raise ValueError("The stanza_idx must be 0 or greater") 

3700 

3701 def stanza_types(self) -> Iterable[Dep5StanzaMetadata]: 

3702 self.ensure_initialized() 

3703 # Order assumption made in the LSP code. 

3704 yield _DEP5_HEADER_STANZA 

3705 yield _DEP5_FILES_STANZA 

3706 yield _DEP5_LICENSE_STANZA 

3707 

3708 def __getitem__(self, item: str) -> Dep5StanzaMetadata: 

3709 self.ensure_initialized() 

3710 if item == "Header": 

3711 return _DEP5_HEADER_STANZA 

3712 if item == "Files": 

3713 return _DEP5_FILES_STANZA 

3714 if item == "License": 3714 ↛ 3716line 3714 didn't jump to line 3716 because the condition on line 3714 was always true

3715 return _DEP5_LICENSE_STANZA 

3716 raise KeyError(item) 

3717 

3718 

3719class DebianWatch5FileMetadata( 

3720 Deb822FileMetadata[DebianWatchStanzaMetadata, Deb822KnownField] 

3721): 

3722 

3723 @property 

3724 def reference_data_basename(self) -> str: 

3725 return "debian_watch_reference_data.yaml" 

3726 

3727 def _new_field( 

3728 self, 

3729 name: str, 

3730 field_value_type: FieldValueClass, 

3731 ) -> F: 

3732 return Deb822KnownField(name, field_value_type) 

3733 

3734 def file_metadata_applies_to_file( 

3735 self, deb822_file: Deb822FileElement | None 

3736 ) -> bool: 

3737 if not super().file_metadata_applies_to_file(deb822_file): 3737 ↛ 3738line 3737 didn't jump to line 3738 because the condition on line 3737 was never true

3738 return False 

3739 first_stanza = next(iter(deb822_file), None) 

3740 

3741 if first_stanza is None or "Version" not in first_stanza: 3741 ↛ 3743line 3741 didn't jump to line 3743 because the condition on line 3741 was never true

3742 # No parseable stanzas or the first one did not have a Version field, which is necessary. 

3743 return False 

3744 

3745 try: 

3746 if int(first_stanza.get("Version")) < 5: 3746 ↛ 3747line 3746 didn't jump to line 3747 because the condition on line 3746 was never true

3747 return False 

3748 except (ValueError, IndexError, TypeError): 

3749 return False 

3750 

3751 for part in deb822_file.iter_parts(): 3751 ↛ 3757line 3751 didn't jump to line 3757 because the loop on line 3751 didn't complete

3752 if part.is_error: 3752 ↛ 3754line 3752 didn't jump to line 3754 because the condition on line 3752 was never true

3753 # Error first, then it might just be a "Version:" in the middle of a free-text file. 

3754 return False 

3755 if part is first_stanza: 3755 ↛ 3751line 3755 didn't jump to line 3751 because the condition on line 3755 was always true

3756 break 

3757 return True 

3758 

3759 def classify_stanza( 

3760 self, 

3761 stanza: Deb822ParagraphElement, 

3762 stanza_idx: int, 

3763 ) -> DebianWatchStanzaMetadata: 

3764 self.ensure_initialized() 

3765 if stanza_idx == 0: 

3766 return _WATCH_HEADER_HEADER_STANZA 

3767 if stanza_idx > 0: 3767 ↛ 3769line 3767 didn't jump to line 3769 because the condition on line 3767 was always true

3768 return _WATCH_SOURCE_STANZA 

3769 raise ValueError("The stanza_idx must be 0 or greater") 

3770 

3771 def guess_stanza_classification_by_idx( 

3772 self, stanza_idx: int 

3773 ) -> DebianWatchStanzaMetadata: 

3774 self.ensure_initialized() 

3775 if stanza_idx == 0: 3775 ↛ 3776line 3775 didn't jump to line 3776 because the condition on line 3775 was never true

3776 return _WATCH_HEADER_HEADER_STANZA 

3777 if stanza_idx > 0: 3777 ↛ 3779line 3777 didn't jump to line 3779 because the condition on line 3777 was always true

3778 return _WATCH_SOURCE_STANZA 

3779 raise ValueError("The stanza_idx must be 0 or greater") 

3780 

3781 def stanza_types(self) -> Iterable[DebianWatchStanzaMetadata]: 

3782 self.ensure_initialized() 

3783 # Order assumption made in the LSP code. 

3784 yield _WATCH_HEADER_HEADER_STANZA 

3785 yield _WATCH_SOURCE_STANZA 

3786 

3787 def __getitem__(self, item: str) -> DebianWatchStanzaMetadata: 

3788 self.ensure_initialized() 

3789 if item == "Header": 

3790 return _WATCH_HEADER_HEADER_STANZA 

3791 if item == "Source": 3791 ↛ 3793line 3791 didn't jump to line 3793 because the condition on line 3791 was always true

3792 return _WATCH_SOURCE_STANZA 

3793 raise KeyError(item) 

3794 

3795 

3796def _resolve_keyword( 

3797 ref_value: StaticValue, 

3798 known_values: dict[str, Keyword], 

3799 resolve_template: Callable[[str | None], str | None], 

3800 translation_context: str, 

3801) -> None: 

3802 value_key = ref_value["value"] 

3803 changes = { 

3804 "translation_context": translation_context, 

3805 } 

3806 try: 

3807 known_value = known_values[value_key] 

3808 except KeyError: 

3809 known_value = Keyword(value_key) 

3810 known_values[value_key] = known_value 

3811 else: 

3812 if known_value.is_alias_of: 3812 ↛ 3813line 3812 didn't jump to line 3813 because the condition on line 3812 was never true

3813 raise ValueError( 

3814 f"The value {known_value.value} has an alias {known_value.is_alias_of} that conflicts with" 

3815 f' {value_key} or the data file used an alias in its `canonical-name` rather than the "true" name' 

3816 ) 

3817 value_doc = ref_value.get("documentation") 

3818 if value_doc is not None: 

3819 changes["synopsis"] = value_doc.get("synopsis") 

3820 changes["long_description"] = resolve_template( 

3821 value_doc.get("long_description") 

3822 ) 

3823 if is_exclusive := ref_value.get("is_exclusive"): 

3824 changes["is_exclusive"] = is_exclusive 

3825 if (sort_key := ref_value.get("sort_key")) is not None: 

3826 changes["sort_text"] = sort_key 

3827 if (usage_hint := ref_value.get("usage_hint")) is not None: 

3828 changes["usage_hint"] = usage_hint 

3829 if changes: 3829 ↛ 3833line 3829 didn't jump to line 3833 because the condition on line 3829 was always true

3830 known_value = known_value.replace(**changes) 

3831 known_values[value_key] = known_value 

3832 

3833 _expand_aliases( 

3834 known_value, 

3835 known_values, 

3836 operator.attrgetter("value"), 

3837 ref_value.get("aliases"), 

3838 "The value `{ALIAS}` is an alias of `{NAME}`.", 

3839 ) 

3840 

3841 

3842def _resolve_field( 

3843 ref_field: Deb822Field, 

3844 stanza_fields: dict[str, F], 

3845 field_constructor: Callable[[str, FieldValueClass], F], 

3846 resolve_template: Callable[[str | None], str | None], 

3847 translation_context: str, 

3848) -> None: 

3849 field_name = ref_field["canonical_name"] 

3850 field_value_type = FieldValueClass.from_key(ref_field["field_value_type"]) 

3851 doc = ref_field.get("documentation") 

3852 ref_values = ref_field.get("values", []) 

3853 norm_field_name = normalize_dctrl_field_name(field_name.lower()) 

3854 

3855 try: 

3856 field = stanza_fields[norm_field_name] 

3857 except KeyError: 

3858 field = field_constructor( 

3859 field_name, 

3860 field_value_type, 

3861 ) 

3862 stanza_fields[norm_field_name] = field 

3863 else: 

3864 if field.name != field_name: 3864 ↛ 3865line 3864 didn't jump to line 3865 because the condition on line 3864 was never true

3865 _error( 

3866 f'Error in reference data: Code uses "{field.name}" as canonical name and the data file' 

3867 f" uses {field_name}. Please ensure the data is correctly aligned." 

3868 ) 

3869 if field.field_value_class != field_value_type: 3869 ↛ 3870line 3869 didn't jump to line 3870 because the condition on line 3869 was never true

3870 _error( 

3871 f'Error in reference data for field "{field.name}": Code has' 

3872 f" {field.field_value_class.key} and the data file uses {field_value_type.key}" 

3873 f" for field-value-type. Please ensure the data is correctly aligned." 

3874 ) 

3875 if field.is_alias_of: 3875 ↛ 3876line 3875 didn't jump to line 3876 because the condition on line 3875 was never true

3876 raise ValueError( 

3877 f"The field {field.name} has an alias {field.is_alias_of} that conflicts with" 

3878 f' {field_name} or the data file used an alias in its `canonical-name` rather than the "true" name' 

3879 ) 

3880 

3881 if doc is not None: 

3882 field.synopsis = doc.get("synopsis") 

3883 field.long_description = resolve_template(doc.get("long_description")) 

3884 

3885 field.default_value = ref_field.get("default_value") 

3886 field.warn_if_default = ref_field.get("warn_if_default", True) 

3887 field.spellcheck_value = ref_field.get("spellcheck_value", False) 

3888 field.deprecated_with_no_replacement = ref_field.get( 

3889 "is_obsolete_without_replacement", False 

3890 ) 

3891 field.replaced_by = ref_field.get("replaced_by") 

3892 field.translation_context = translation_context 

3893 field.usage_hint = ref_field.get("usage_hint") 

3894 field.missing_field_severity = ref_field.get("missing_field_severity", None) 

3895 unknown_value_severity = ref_field.get("unknown_value_severity", "error") 

3896 field.unknown_value_severity = ( 

3897 None if unknown_value_severity == "none" else unknown_value_severity 

3898 ) 

3899 field.unknown_value_authority = ref_field.get("unknown_value_authority", "debputy") 

3900 field.missing_field_authority = ref_field.get("missing_field_authority", "debputy") 

3901 field.is_substvars_disabled_even_if_allowed_by_stanza = not ref_field.get( 

3902 "supports_substvars", 

3903 True, 

3904 ) 

3905 field.inheritable_from_other_stanza = ref_field.get( 

3906 "inheritable_from_other_stanza", 

3907 False, 

3908 ) 

3909 

3910 known_values = field.known_values 

3911 if known_values is None: 

3912 known_values = {} 

3913 else: 

3914 known_values = dict(known_values) 

3915 

3916 for ref_value in ref_values: 

3917 _resolve_keyword(ref_value, known_values, resolve_template, translation_context) 

3918 

3919 if known_values: 

3920 field.known_values = known_values 

3921 

3922 _expand_aliases( 

3923 field, 

3924 stanza_fields, 

3925 operator.attrgetter("name"), 

3926 ref_field.get("aliases"), 

3927 "The field `{ALIAS}` is an alias of `{NAME}`.", 

3928 ) 

3929 

3930 

3931A = TypeVar("A", Keyword, Deb822KnownField) 

3932 

3933 

3934def _expand_aliases( 

3935 item: A, 

3936 item_container: dict[str, A], 

3937 canonical_name_resolver: Callable[[A], str], 

3938 aliases_ref: list[Alias] | None, 

3939 doc_template: str, 

3940) -> None: 

3941 if aliases_ref is None: 

3942 return 

3943 name = canonical_name_resolver(item) 

3944 assert name is not None, "canonical_name_resolver is not allowed to return None" 

3945 for alias_ref in aliases_ref: 

3946 alias_name = alias_ref["alias"] 

3947 alias_doc = item.long_description 

3948 is_completion_suggestion = alias_ref.get("is_completion_suggestion", False) 

3949 doc_suffix = doc_template.format(NAME=name, ALIAS=alias_name) 

3950 if alias_doc: 

3951 alias_doc += f"\n\n{doc_suffix}" 

3952 else: 

3953 alias_doc = doc_suffix 

3954 alias_field = item.replace( 

3955 long_description=alias_doc, 

3956 is_alias_of=name, 

3957 is_completion_suggestion=is_completion_suggestion, 

3958 ) 

3959 alias_key = alias_name.lower() 

3960 if alias_name in item_container: 3960 ↛ 3961line 3960 didn't jump to line 3961 because the condition on line 3960 was never true

3961 existing_name = canonical_name_resolver(item_container[alias_key]) 

3962 assert ( 

3963 existing_name is not None 

3964 ), "canonical_name_resolver is not allowed to return None" 

3965 raise ValueError( 

3966 f"The value {name} has an alias {alias_name} that conflicts with {existing_name}" 

3967 ) 

3968 item_container[alias_key] = alias_field 

3969 

3970 

3971class DctrlFileMetadata(Deb822FileMetadata[DctrlStanzaMetadata, DctrlKnownField]): 

3972 

3973 @property 

3974 def reference_data_basename(self) -> str: 

3975 return "debian_control_reference_data.yaml" 

3976 

3977 def _new_field( 

3978 self, 

3979 name: str, 

3980 field_value_type: FieldValueClass, 

3981 ) -> F: 

3982 return DctrlKnownField(name, field_value_type) 

3983 

3984 def guess_stanza_classification_by_idx( 

3985 self, 

3986 stanza_idx: int, 

3987 ) -> DctrlStanzaMetadata: 

3988 self.ensure_initialized() 

3989 if stanza_idx == 0: 

3990 return _DCTRL_SOURCE_STANZA 

3991 if stanza_idx > 0: 3991 ↛ 3993line 3991 didn't jump to line 3993 because the condition on line 3991 was always true

3992 return _DCTRL_PACKAGE_STANZA 

3993 raise ValueError("The stanza_idx must be 0 or greater") 

3994 

3995 def stanza_types(self) -> Iterable[DctrlStanzaMetadata]: 

3996 self.ensure_initialized() 

3997 # Order assumption made in the LSP code. 

3998 yield _DCTRL_SOURCE_STANZA 

3999 yield _DCTRL_PACKAGE_STANZA 

4000 

4001 def __getitem__(self, item: str) -> DctrlStanzaMetadata: 

4002 self.ensure_initialized() 

4003 if item == "Source": 

4004 return _DCTRL_SOURCE_STANZA 

4005 if item == "Package": 4005 ↛ 4007line 4005 didn't jump to line 4007 because the condition on line 4005 was always true

4006 return _DCTRL_PACKAGE_STANZA 

4007 raise KeyError(item) 

4008 

4009 def reformat( 

4010 self, 

4011 effective_preference: "EffectiveFormattingPreference", 

4012 deb822_file: Deb822FileElement, 

4013 formatter: FormatterCallback, 

4014 content: str, 

4015 position_codec: LintCapablePositionCodec, 

4016 lines: list[str], 

4017 ) -> Iterable[TextEdit]: 

4018 edits = list( 

4019 super().reformat( 

4020 effective_preference, 

4021 deb822_file, 

4022 formatter, 

4023 content, 

4024 position_codec, 

4025 lines, 

4026 ) 

4027 ) 

4028 

4029 if ( 4029 ↛ 4034line 4029 didn't jump to line 4034 because the condition on line 4029 was always true

4030 not effective_preference.deb822_normalize_stanza_order 

4031 or deb822_file.find_first_error_element() is not None 

4032 ): 

4033 return edits 

4034 names = [] 

4035 for idx, stanza in enumerate(deb822_file): 

4036 if idx < 2: 

4037 continue 

4038 name = stanza.get("Package") 

4039 if name is None: 

4040 return edits 

4041 names.append(name) 

4042 

4043 reordered = sorted(names) 

4044 if names == reordered: 

4045 return edits 

4046 

4047 if edits: 

4048 content = apply_text_edits(content, lines, edits) 

4049 lines = content.splitlines(keepends=True) 

4050 deb822_file = parse_deb822_file( 

4051 lines, 

4052 accept_files_with_duplicated_fields=True, 

4053 accept_files_with_error_tokens=True, 

4054 ) 

4055 

4056 stanzas = list(deb822_file) 

4057 reordered_stanza = stanzas[:2] + sorted( 

4058 stanzas[2:], key=operator.itemgetter("Package") 

4059 ) 

4060 bits = [] 

4061 stanza_idx = 0 

4062 for token_or_element in deb822_file.iter_parts(): 

4063 if isinstance(token_or_element, Deb822ParagraphElement): 

4064 bits.append(reordered_stanza[stanza_idx].dump()) 

4065 stanza_idx += 1 

4066 else: 

4067 bits.append(token_or_element.convert_to_text()) 

4068 

4069 new_content = "".join(bits) 

4070 

4071 return [ 

4072 TextEdit( 

4073 Range( 

4074 Position(0, 0), 

4075 Position(len(lines) + 1, 0), 

4076 ), 

4077 new_content, 

4078 ) 

4079 ] 

4080 

4081 

4082class DTestsCtrlFileMetadata( 

4083 Deb822FileMetadata[DTestsCtrlStanzaMetadata, DTestsCtrlKnownField] 

4084): 

4085 

4086 @property 

4087 def reference_data_basename(self) -> str: 

4088 return "debian_tests_control_reference_data.yaml" 

4089 

4090 def _new_field( 

4091 self, 

4092 name: str, 

4093 field_value_type: FieldValueClass, 

4094 ) -> F: 

4095 return DTestsCtrlKnownField(name, field_value_type) 

4096 

4097 def guess_stanza_classification_by_idx(self, stanza_idx: int) -> S: 

4098 if stanza_idx >= 0: 4098 ↛ 4101line 4098 didn't jump to line 4101 because the condition on line 4098 was always true

4099 self.ensure_initialized() 

4100 return _DTESTSCTRL_STANZA 

4101 raise ValueError("The stanza_idx must be 0 or greater") 

4102 

4103 def stanza_types(self) -> Iterable[S]: 

4104 self.ensure_initialized() 

4105 yield _DTESTSCTRL_STANZA 

4106 

4107 def __getitem__(self, item: str) -> S: 

4108 self.ensure_initialized() 

4109 if item == "Tests": 4109 ↛ 4111line 4109 didn't jump to line 4111 because the condition on line 4109 was always true

4110 return _DTESTSCTRL_STANZA 

4111 raise KeyError(item) 

4112 

4113 

4114TRANSLATABLE_DEB822_FILE_METADATA: Sequence[ 

4115 Callable[[], Deb822FileMetadata[Any, Any]] 

4116] = [ 

4117 DctrlFileMetadata, 

4118 Dep5FileMetadata, 

4119 DTestsCtrlFileMetadata, 

4120]