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

1400 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-02-28 21:56 +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 debian._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 debian._deb822_repro.tokens import ( 

72 Deb822FieldNameToken, 

73) 

74from debian._deb822_repro.types import FormatterCallback, TE 

75from debputy.lsp.vendoring.wrap_and_sort import _sort_packages_key 

76from debputy.lsprotocol.types import ( 

77 DiagnosticTag, 

78 Range, 

79 TextEdit, 

80 Position, 

81 CompletionItem, 

82 MarkupContent, 

83 CompletionItemTag, 

84 MarkupKind, 

85 CompletionItemKind, 

86 CompletionItemLabelDetails, 

87) 

88from debputy.manifest_parser.exceptions import ManifestParseException 

89from debputy.manifest_parser.util import AttributePath 

90from debputy.path_matcher import BasenameGlobMatch 

91from debputy.plugin.api import VirtualPath 

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

93from debputy.yaml import MANIFEST_YAML 

94 

95try: 

96 from debian._deb822_repro.locatable import ( 

97 Position as TEPosition, 

98 Range as TERange, 

99 START_POSITION, 

100 ) 

101except ImportError: 

102 pass 

103 

104 

105if TYPE_CHECKING: 

106 from debputy.lsp.maint_prefs import EffectiveFormattingPreference 

107 from debputy.lsp.debputy_ls import DebputyLanguageServer 

108 

109 

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

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

112 

113 

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

115 

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

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

118_RE_SYNOPSIS_IS_TEMPLATE = re.compile( 

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

120) 

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

122CURRENT_STANDARDS_VERSION = Version("4.7.2") 

123 

124 

125CustomFieldCheck = Callable[ 

126 [ 

127 "F", 

128 Deb822FileElement, 

129 Deb822KeyValuePairElement, 

130 "TERange", 

131 "TERange", 

132 Deb822ParagraphElement, 

133 "TEPosition", 

134 LintState, 

135 ], 

136 None, 

137] 

138 

139 

140@functools.lru_cache 

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

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

143 return { 

144 f.lower(): f 

145 for f in ( 

146 "Pre-Depends", 

147 "Depends", 

148 "Recommends", 

149 "Suggests", 

150 "Enhances", 

151 "Conflicts", 

152 "Breaks", 

153 "Replaces", 

154 "Provides", 

155 "Built-Using", 

156 "Static-Built-Using", 

157 ) 

158 } 

159 

160 

161@functools.lru_cache 

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

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

164 return { 

165 f.lower(): f 

166 for f in ( 

167 "Build-Depends", 

168 "Build-Depends-Arch", 

169 "Build-Depends-Indep", 

170 "Build-Conflicts", 

171 "Build-Conflicts-Arch", 

172 "Build-Conflicts-Indep", 

173 ) 

174 } 

175 

176 

177ALL_SECTIONS_WITHOUT_COMPONENT = frozenset( 

178 [ 

179 "admin", 

180 "cli-mono", 

181 "comm", 

182 "database", 

183 "debian-installer", 

184 "debug", 

185 "devel", 

186 "doc", 

187 "editors", 

188 "education", 

189 "electronics", 

190 "embedded", 

191 "fonts", 

192 "games", 

193 "gnome", 

194 "gnu-r", 

195 "gnustep", 

196 "golang", 

197 "graphics", 

198 "hamradio", 

199 "haskell", 

200 "httpd", 

201 "interpreters", 

202 "introspection", 

203 "java", 

204 "javascript", 

205 "kde", 

206 "kernel", 

207 "libdevel", 

208 "libs", 

209 "lisp", 

210 "localization", 

211 "mail", 

212 "math", 

213 "metapackages", 

214 "misc", 

215 "net", 

216 "news", 

217 "ocaml", 

218 "oldlibs", 

219 "otherosfs", 

220 "perl", 

221 "php", 

222 "python", 

223 "ruby", 

224 "rust", 

225 "science", 

226 "shells", 

227 "sound", 

228 "tasks", 

229 "tex", 

230 "text", 

231 "utils", 

232 "vcs", 

233 "video", 

234 "virtual", 

235 "web", 

236 "x11", 

237 "xfce", 

238 "zope", 

239 ] 

240) 

241 

242ALL_COMPONENTS = frozenset( 

243 [ 

244 "main", 

245 "restricted", # Ubuntu 

246 "non-free", 

247 "non-free-firmware", 

248 "contrib", 

249 ] 

250) 

251 

252 

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

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

255 

256 

257def _complete_section_sort_hint( 

258 keyword: Keyword, 

259 _lint_state: LintState, 

260 stanza_parts: Sequence[Deb822ParagraphElement], 

261 _value_being_completed: str, 

262) -> str | None: 

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

264 pkg = stanza.get("Package") 

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

266 break 

267 else: 

268 return None 

269 section = package_name_to_section(pkg) 

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

271 keyword_section = value_parts[-1] 

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

273 if section is None: 

274 if keyword_component == "": 

275 return keyword_section 

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

277 if keyword_section != section: 

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

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

280 

281 

282ALL_SECTIONS = allowed_values( 

283 Keyword( 

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

285 sort_text=_complete_section_sort_hint, 

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

287 ) 

288 for c, s in itertools.product( 

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

290 ALL_SECTIONS_WITHOUT_COMPONENT, 

291 ) 

292) 

293 

294 

295def all_architectures_and_wildcards( 

296 arch2table, *, allow_negations: bool = False 

297) -> Iterable[str | Keyword]: 

298 wildcards = set() 

299 yield Keyword( 

300 "any", 

301 is_exclusive=True, 

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

303 long_description=textwrap.dedent( 

304 """\ 

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

306 compiled for each and every architecture. 

307 

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

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

310 dpkg. 

311 """ 

312 ), 

313 ) 

314 yield Keyword( 

315 "all", 

316 is_exclusive=True, 

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

318 long_description=textwrap.dedent( 

319 """\ 

320 The package is an architecture independent package. This is 

321 typically appropriate for packages containing only scripts, 

322 data or documentation. 

323 

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

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

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

327 """ 

328 ), 

329 ) 

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

331 yield arch_name 

332 if allow_negations: 

333 yield f"!{arch_name}" 

334 cpu_wc = "any-" + quad_tuple.cpu_name 

335 os_wc = quad_tuple.os_name + "-any" 

336 if cpu_wc not in wildcards: 

337 yield cpu_wc 

338 if allow_negations: 

339 yield f"!{cpu_wc}" 

340 wildcards.add(cpu_wc) 

341 if os_wc not in wildcards: 

342 yield os_wc 

343 if allow_negations: 

344 yield f"!{os_wc}" 

345 wildcards.add(os_wc) 

346 # Add the remaining wildcards 

347 

348 

349@functools.lru_cache 

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

351 dpkg_arch_table = DpkgArchTable.load_arch_table() 

352 return frozenset( 

353 all_architectures_and_wildcards( 

354 dpkg_arch_table._arch2table, 

355 allow_negations=allow_negations, 

356 ) 

357 ) 

358 

359 

360def extract_first_value_and_position( 

361 kvpair: Deb822KeyValuePairElement, 

362 stanza_pos: "TEPosition", 

363 *, 

364 interpretation: Interpretation[ 

365 Deb822ParsedTokenList[Any, Any] 

366 ] = LIST_SPACE_SEPARATED_INTERPRETATION, 

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

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

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

370 kvpair_pos 

371 ) 

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

373 v = value_ref.value 

374 section_value_loc = value_ref.locatable 

375 value_range_te = section_value_loc.range_in_parent().relative_to( 

376 value_element_pos 

377 ) 

378 return v, value_range_te 

379 return None, None 

380 

381 

382def _sv_field_validation( 

383 known_field: "F", 

384 _deb822_file: Deb822FileElement, 

385 kvpair: Deb822KeyValuePairElement, 

386 _kvpair_range: "TERange", 

387 _field_name_range_te: "TERange", 

388 _stanza: Deb822ParagraphElement, 

389 stanza_position: "TEPosition", 

390 lint_state: LintState, 

391) -> None: 

392 sv_value, sv_value_range = extract_first_value_and_position( 

393 kvpair, 

394 stanza_position, 

395 ) 

396 m = _RE_SV.fullmatch(sv_value) 

397 if m is None: 

398 lint_state.emit_diagnostic( 

399 sv_value_range, 

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

401 "warning", 

402 known_field.unknown_value_authority, 

403 ) 

404 return 

405 

406 sv_version = Version(sv_value) 

407 if sv_version < CURRENT_STANDARDS_VERSION: 

408 lint_state.emit_diagnostic( 

409 sv_value_range, 

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

411 "informational", 

412 known_field.unknown_value_authority, 

413 ) 

414 return 

415 extra = m.group(2) 

416 if extra: 

417 extra_len = lint_state.position_codec.client_num_units(extra) 

418 lint_state.emit_diagnostic( 

419 TERange.between( 

420 TEPosition( 

421 sv_value_range.end_pos.line_position, 

422 sv_value_range.end_pos.cursor_position - extra_len, 

423 ), 

424 sv_value_range.end_pos, 

425 ), 

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

427 "informational", 

428 known_field.unknown_value_authority, 

429 quickfixes=[ 

430 propose_remove_range_quick_fix( 

431 proposed_title="Remove unnecessary version part" 

432 ) 

433 ], 

434 ) 

435 

436 

437def _dctrl_ma_field_validation( 

438 _known_field: "F", 

439 _deb822_file: Deb822FileElement, 

440 _kvpair: Deb822KeyValuePairElement, 

441 _kvpair_range: "TERange", 

442 _field_name_range: "TERange", 

443 stanza: Deb822ParagraphElement, 

444 stanza_position: "TEPosition", 

445 lint_state: LintState, 

446) -> None: 

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

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

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

450 ma_value, ma_value_range = extract_first_value_and_position( 

451 ma_kvpair, 

452 stanza_position, 

453 ) 

454 if ma_value == "same": 

455 lint_state.emit_diagnostic( 

456 ma_value_range, 

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

458 "error", 

459 "debputy", 

460 ) 

461 

462 

463def _udeb_only_field_validation( 

464 known_field: "F", 

465 _deb822_file: Deb822FileElement, 

466 _kvpair: Deb822KeyValuePairElement, 

467 _kvpair_range: "TERange", 

468 field_name_range: "TERange", 

469 stanza: Deb822ParagraphElement, 

470 _stanza_position: "TEPosition", 

471 lint_state: LintState, 

472) -> None: 

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

474 if package_type != "udeb": 

475 lint_state.emit_diagnostic( 

476 field_name_range, 

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

478 "warning", 

479 "debputy", 

480 ) 

481 

482 

483def _complete_only_in_arch_dep_pkgs( 

484 stanza_parts: Iterable[Deb822ParagraphElement], 

485) -> bool: 

486 for stanza in stanza_parts: 

487 arch = stanza.get("Architecture") 

488 if arch is None: 

489 continue 

490 archs = arch.split() 

491 return "all" not in archs 

492 return False 

493 

494 

495def _complete_only_for_udeb_pkgs( 

496 stanza_parts: Iterable[Deb822ParagraphElement], 

497) -> bool: 

498 for stanza in stanza_parts: 

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

500 pkg_type = stanza.get(option) 

501 if pkg_type is not None: 

502 return pkg_type == "udeb" 

503 return False 

504 

505 

506def _arch_not_all_only_field_validation( 

507 known_field: "F", 

508 _deb822_file: Deb822FileElement, 

509 _kvpair: Deb822KeyValuePairElement, 

510 _kvpair_range_te: "TERange", 

511 field_name_range_te: "TERange", 

512 stanza: Deb822ParagraphElement, 

513 _stanza_position: "TEPosition", 

514 lint_state: LintState, 

515) -> None: 

516 architecture = stanza.get("Architecture") 

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

518 lint_state.emit_diagnostic( 

519 field_name_range_te, 

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

521 "warning", 

522 "debputy", 

523 ) 

524 

525 

526def _binary_package_from_same_source( 

527 known_field: "F", 

528 _deb822_file: Deb822FileElement, 

529 _kvpair: Deb822KeyValuePairElement, 

530 kvpair_range: "TERange", 

531 _field_name_range: "TERange", 

532 stanza: Deb822ParagraphElement, 

533 stanza_position: "TEPosition", 

534 lint_state: LintState, 

535) -> None: 

536 doc_main_package_kvpair = stanza.get_kvpair_element( 

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

538 ) 

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

540 lint_state.emit_diagnostic( 

541 kvpair_range, 

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

543 "warning", 

544 "debputy", 

545 quickfixes=[propose_remove_range_quick_fix()], 

546 ) 

547 return 

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

549 doc_main_package, value_range = extract_first_value_and_position( 

550 doc_main_package_kvpair, 

551 stanza_position, 

552 ) 

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

554 return 

555 lint_state.emit_diagnostic( 

556 value_range, 

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

558 "error", 

559 "debputy", 

560 quickfixes=[ 

561 propose_correct_text_quick_fix(name) 

562 for name in lint_state.binary_packages 

563 ], 

564 ) 

565 

566 

567def _single_line_span_range_relative_to_pos( 

568 span: tuple[int, int], 

569 relative_to: "TEPosition", 

570) -> Range: 

571 return TERange( 

572 TEPosition( 

573 relative_to.line_position, 

574 relative_to.cursor_position + span[0], 

575 ), 

576 TEPosition( 

577 relative_to.line_position, 

578 relative_to.cursor_position + span[1], 

579 ), 

580 ) 

581 

582 

583def _check_extended_description_line( 

584 description_value_line: Deb822ValueLineElement, 

585 description_line_range_te: "TERange", 

586 package: str | None, 

587 lint_state: LintState, 

588) -> None: 

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

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

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

592 return 

593 description_line_with_leading_space = ( 

594 description_value_line.convert_to_text().rstrip() 

595 ) 

596 try: 

597 idx = description_line_with_leading_space.index( 

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

599 ) 

600 except ValueError: 

601 pass 

602 else: 

603 template_span = idx, idx + len( 

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

605 ) 

606 lint_state.emit_diagnostic( 

607 _single_line_span_range_relative_to_pos( 

608 template_span, 

609 description_line_range_te.start_pos, 

610 ), 

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

612 "error", 

613 "debputy", 

614 ) 

615 if len(description_line_with_leading_space) > 80: 

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

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

618 # 

619 # See also debputy#122 

620 span = 80, len(description_line_with_leading_space) 

621 lint_state.emit_diagnostic( 

622 _single_line_span_range_relative_to_pos( 

623 span, 

624 description_line_range_te.start_pos, 

625 ), 

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

627 "warning", 

628 "debputy", 

629 ) 

630 

631 

632def _check_synopsis( 

633 synopsis_value_line: Deb822ValueLineElement, 

634 synopsis_range_te: "TERange", 

635 field_name_range_te: "TERange", 

636 package: str | None, 

637 lint_state: LintState, 

638) -> None: 

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

640 assert synopsis_value_line.comment_element is None 

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

642 if not synopsis_text_with_leading_space: 

643 lint_state.emit_diagnostic( 

644 field_name_range_te, 

645 "Package synopsis is missing", 

646 "warning", 

647 "debputy", 

648 ) 

649 return 

650 synopsis_text_trimmed = synopsis_text_with_leading_space.lstrip() 

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

652 starts_with_article = _RE_SYNOPSIS_STARTS_WITH_ARTICLE.search( 

653 synopsis_text_with_leading_space 

654 ) 

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

656 if starts_with_article: 

657 lint_state.emit_diagnostic( 

658 _single_line_span_range_relative_to_pos( 

659 starts_with_article.span(1), 

660 synopsis_range_te.start_pos, 

661 ), 

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

663 "warning", 

664 "DevRef 6.2.2", 

665 ) 

666 if len(synopsis_text_trimmed) >= 80: 

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

668 span = synopsis_offset + 79, len(synopsis_text_with_leading_space) 

669 lint_state.emit_diagnostic( 

670 _single_line_span_range_relative_to_pos( 

671 span, 

672 synopsis_range_te.start_pos, 

673 ), 

674 "Package synopsis is too long.", 

675 "warning", 

676 "Policy 3.4.1", 

677 ) 

678 if template_match := _RE_SYNOPSIS_IS_TEMPLATE.match( 

679 synopsis_text_with_leading_space 

680 ): 

681 lint_state.emit_diagnostic( 

682 _single_line_span_range_relative_to_pos( 

683 template_match.span(1), 

684 synopsis_range_te.start_pos, 

685 ), 

686 "Package synopsis is a placeholder", 

687 "warning", 

688 "debputy", 

689 ) 

690 elif too_short_match := _RE_SYNOPSIS_IS_TOO_SHORT.match( 

691 synopsis_text_with_leading_space 

692 ): 

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

694 lint_state.emit_diagnostic( 

695 _single_line_span_range_relative_to_pos( 

696 too_short_match.span(1), 

697 synopsis_range_te.start_pos, 

698 ), 

699 "Package synopsis is too short", 

700 "warning", 

701 "debputy", 

702 ) 

703 

704 

705def dctrl_description_validator( 

706 _known_field: "F", 

707 _deb822_file: Deb822FileElement, 

708 kvpair: Deb822KeyValuePairElement, 

709 kvpair_range_te: "TERange", 

710 _field_name_range: "TERange", 

711 stanza: Deb822ParagraphElement, 

712 _stanza_position: "TEPosition", 

713 lint_state: LintState, 

714) -> None: 

715 value_lines = kvpair.value_element.value_lines 

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

717 return 

718 package = stanza.get("Package") 

719 synopsis_value_line = value_lines[0] 

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

721 kvpair_range_te.start_pos 

722 ) 

723 synopsis_line_range_te = synopsis_value_line.range_in_parent().relative_to( 

724 value_range_te.start_pos 

725 ) 

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

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

728 kvpair_range_te.start_pos 

729 ) 

730 _check_synopsis( 

731 synopsis_value_line, 

732 synopsis_line_range_te, 

733 field_name_range_te, 

734 package, 

735 lint_state, 

736 ) 

737 description_lines = value_lines[1:] 

738 else: 

739 description_lines = value_lines 

740 for description_line in description_lines: 

741 description_line_range_te = description_line.range_in_parent().relative_to( 

742 value_range_te.start_pos 

743 ) 

744 _check_extended_description_line( 

745 description_line, 

746 description_line_range_te, 

747 package, 

748 lint_state, 

749 ) 

750 

751 

752def _has_packaging_expected_file( 

753 name: str, 

754 msg: str, 

755 severity: LintSeverity = "error", 

756) -> CustomFieldCheck: 

757 

758 def _impl( 

759 _known_field: "F", 

760 _deb822_file: Deb822FileElement, 

761 _kvpair: Deb822KeyValuePairElement, 

762 kvpair_range_te: "TERange", 

763 _field_name_range_te: "TERange", 

764 _stanza: Deb822ParagraphElement, 

765 _stanza_position: "TEPosition", 

766 lint_state: LintState, 

767 ) -> None: 

768 debian_dir = lint_state.debian_dir 

769 if debian_dir is None: 

770 return 

771 cpy = debian_dir.lookup(name) 

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

773 lint_state.emit_diagnostic( 

774 kvpair_range_te, 

775 msg, 

776 severity, 

777 "debputy", 

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

779 ) 

780 

781 return _impl 

782 

783 

784_check_missing_debian_rules = _has_packaging_expected_file( 

785 "rules", 

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

787) 

788 

789 

790def _has_build_instructions( 

791 known_field: "F", 

792 deb822_file: Deb822FileElement, 

793 kvpair: Deb822KeyValuePairElement, 

794 kvpair_range_te: "TERange", 

795 field_name_range_te: "TERange", 

796 stanza: Deb822ParagraphElement, 

797 stanza_position: "TEPosition", 

798 lint_state: LintState, 

799) -> None: 

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

801 return 

802 

803 _check_missing_debian_rules( 

804 known_field, 

805 deb822_file, 

806 kvpair, 

807 kvpair_range_te, 

808 field_name_range_te, 

809 stanza, 

810 stanza_position, 

811 lint_state, 

812 ) 

813 

814 

815def _canonical_maintainer_name( 

816 known_field: "F", 

817 _deb822_file: Deb822FileElement, 

818 kvpair: Deb822KeyValuePairElement, 

819 kvpair_range_te: "TERange", 

820 _field_name_range_te: "TERange", 

821 _stanza: Deb822ParagraphElement, 

822 _stanza_position: "TEPosition", 

823 lint_state: LintState, 

824) -> None: 

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

826 kvpair_range_te.start_pos 

827 ) 

828 try: 

829 interpreted_value = kvpair.interpret_as( 

830 known_field.field_value_class.interpreter() 

831 ) 

832 except ValueError: 

833 return 

834 

835 for part in interpreted_value.iter_parts(): 

836 if part.is_separator or part.is_whitespace or part.is_whitespace: 

837 continue 

838 name_and_email = part.convert_to_text() 

839 try: 

840 email_start = name_and_email.rindex("<") 

841 email_end = name_and_email.rindex(">") 

842 email = name_and_email[email_start + 1 : email_end] 

843 except IndexError: 

844 continue 

845 

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

847 if pref is None or not pref.canonical_name: 

848 continue 

849 

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

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

852 continue 

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

854 lint_state.emit_diagnostic( 

855 value_range_te, 

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

857 "informational", 

858 "debputy", 

859 quickfixes=[propose_correct_text_quick_fix(expected)], 

860 ) 

861 

862 

863def _maintainer_field_validator( 

864 known_field: "F", 

865 _deb822_file: Deb822FileElement, 

866 kvpair: Deb822KeyValuePairElement, 

867 kvpair_range_te: "TERange", 

868 _field_name_range_te: "TERange", 

869 _stanza: Deb822ParagraphElement, 

870 _stanza_position: "TEPosition", 

871 lint_state: LintState, 

872) -> None: 

873 

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

875 kvpair_range_te.start_pos 

876 ) 

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

878 for part in interpreted_value.iter_parts(): 

879 if not part.is_separator: 

880 continue 

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

882 severity = known_field.unknown_value_severity 

883 assert severity is not None 

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

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

886 lint_state.emit_diagnostic( 

887 value_range_te, 

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

889 severity, 

890 known_field.unknown_value_authority, 

891 ) 

892 

893 

894def _use_https_instead_of_http( 

895 known_field: "F", 

896 _deb822_file: Deb822FileElement, 

897 kvpair: Deb822KeyValuePairElement, 

898 kvpair_range_te: "TERange", 

899 _field_name_range_te: "TERange", 

900 _stanza: Deb822ParagraphElement, 

901 _stanza_position: "TEPosition", 

902 lint_state: LintState, 

903) -> None: 

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

905 kvpair_range_te.start_pos 

906 ) 

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

908 for part in interpreted_value.iter_parts(): 

909 value = part.convert_to_text() 

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

911 continue 

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

913 problem_range_te = TERange.between( 

914 value_range_te.start_pos, 

915 TEPosition( 

916 value_range_te.start_pos.line_position, 

917 value_range_te.start_pos.cursor_position + 7, 

918 ), 

919 ) 

920 lint_state.emit_diagnostic( 

921 problem_range_te, 

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

923 "warning", 

924 "debputy", 

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

926 ) 

927 

928 

929def _each_value_match_regex_validation( 

930 regex: re.Pattern, 

931 *, 

932 diagnostic_severity: LintSeverity = "error", 

933 authority_reference: str | None = None, 

934) -> CustomFieldCheck: 

935 

936 def _validator( 

937 known_field: "F", 

938 _deb822_file: Deb822FileElement, 

939 kvpair: Deb822KeyValuePairElement, 

940 kvpair_range_te: "TERange", 

941 _field_name_range_te: "TERange", 

942 _stanza: Deb822ParagraphElement, 

943 _stanza_position: "TEPosition", 

944 lint_state: LintState, 

945 ) -> None: 

946 nonlocal authority_reference 

947 interpreter = known_field.field_value_class.interpreter() 

948 if interpreter is None: 

949 raise AssertionError( 

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

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

952 ) 

953 auth_ref = ( 

954 authority_reference 

955 if authority_reference is not None 

956 else known_field.unknown_value_authority 

957 ) 

958 

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

960 kvpair_range_te.start_pos 

961 ) 

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

963 v = value_ref.value 

964 m = regex.fullmatch(v) 

965 if m is not None: 

966 continue 

967 

968 if "${" in v: 

969 # Ignore substvars 

970 continue 

971 

972 section_value_loc = value_ref.locatable 

973 value_range_te = section_value_loc.range_in_parent().relative_to( 

974 value_element_pos 

975 ) 

976 lint_state.emit_diagnostic( 

977 value_range_te, 

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

979 diagnostic_severity, 

980 auth_ref, 

981 ) 

982 

983 return _validator 

984 

985 

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

987_DEP_RELATION_CLAUSE = re.compile( 

988 r""" 

989 ^ 

990 \s* 

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

992 \s* 

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

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

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

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

997 $ 

998""", 

999 re.VERBOSE | re.MULTILINE, 

1000) 

1001 

1002 

1003def _span_to_te_range( 

1004 text: str, 

1005 start_pos: int, 

1006 end_pos: int, 

1007) -> TERange: 

1008 prefix = text[0:start_pos] 

1009 prefix_plus_text = text[0:end_pos] 

1010 

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

1012 if start_line: 

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

1014 # +1 to skip past the newline 

1015 start_cursor_pos = start_pos - (start_newline_offset + 1) 

1016 else: 

1017 start_cursor_pos = start_pos 

1018 

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

1020 if end_line == start_line: 

1021 end_cursor_pos = start_cursor_pos + (end_pos - start_pos) 

1022 else: 

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

1024 end_cursor_pos = end_pos - (end_newline_offset + 1) 

1025 

1026 return TERange( 

1027 TEPosition( 

1028 start_line, 

1029 start_cursor_pos, 

1030 ), 

1031 TEPosition( 

1032 end_line, 

1033 end_cursor_pos, 

1034 ), 

1035 ) 

1036 

1037 

1038def _split_w_spans( 

1039 v: str, 

1040 sep: str, 

1041 *, 

1042 offset: int = 0, 

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

1044 separator_size = len(sep) 

1045 parts = v.split(sep) 

1046 for part in parts: 

1047 size = len(part) 

1048 end_offset = offset + size 

1049 yield part, offset, end_offset 

1050 offset = end_offset + separator_size 

1051 

1052 

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

1054 

1055 

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

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

1058 

1059 

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

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

1062 if not newlines: 

1063 return TEPosition( 

1064 newlines, 

1065 len(text), 

1066 ) 

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

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

1069 return TEPosition( 

1070 newlines, 

1071 line_offset, 

1072 ) 

1073 

1074 

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

1076class Relation: 

1077 name: str 

1078 arch_qual: str | None = None 

1079 version_operator: str | None = None 

1080 version: str | None = None 

1081 arch_restriction: str | None = None 

1082 build_profile_restriction: str | None = None 

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

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

1085 # an example). 

1086 content_display_offset: int = -1 

1087 content_display_end_offset: int = -1 

1088 

1089 

1090def relation_key_variations( 

1091 relation: Relation, 

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

1093 operator_variants = ( 

1094 [relation.version_operator, None] 

1095 if relation.version_operator is not None 

1096 else [None] 

1097 ) 

1098 arch_qual_variants = ( 

1099 [relation.arch_qual, None] 

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

1101 else [None] 

1102 ) 

1103 for arch_qual, version_operator in itertools.product( 

1104 arch_qual_variants, 

1105 operator_variants, 

1106 ): 

1107 yield relation.name, arch_qual, version_operator 

1108 

1109 

1110def dup_check_relations( 

1111 known_field: "F", 

1112 relations: Sequence[Relation], 

1113 raw_value_masked_comments: str, 

1114 value_element_pos: "TEPosition", 

1115 lint_state: LintState, 

1116) -> None: 

1117 overlap_table = {} 

1118 for relation in relations: 

1119 version_operator = relation.version_operator 

1120 arch_qual = relation.arch_qual 

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

1122 continue 

1123 

1124 for relation_key in relation_key_variations(relation): 

1125 prev_relation = overlap_table.get(relation_key) 

1126 if prev_relation is None: 

1127 overlap_table[relation_key] = relation 

1128 else: 

1129 prev_version_operator = prev_relation.version_operator 

1130 

1131 if ( 

1132 prev_version_operator 

1133 and version_operator 

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

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

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

1137 ): 

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

1139 continue 

1140 

1141 prev_arch_qual = prev_relation.arch_qual 

1142 if ( 

1143 arch_qual != prev_arch_qual 

1144 and prev_arch_qual != "any" 

1145 and arch_qual != "any" 

1146 ): 

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

1148 # 

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

1150 continue 

1151 

1152 orig_relation_range = TERange( 

1153 _text_to_te_position( 

1154 raw_value_masked_comments[ 

1155 : prev_relation.content_display_offset 

1156 ] 

1157 ), 

1158 _text_to_te_position( 

1159 raw_value_masked_comments[ 

1160 : prev_relation.content_display_end_offset 

1161 ] 

1162 ), 

1163 ).relative_to(value_element_pos) 

1164 

1165 duplicate_relation_range = TERange( 

1166 _text_to_te_position( 

1167 raw_value_masked_comments[: relation.content_display_offset] 

1168 ), 

1169 _text_to_te_position( 

1170 raw_value_masked_comments[: relation.content_display_end_offset] 

1171 ), 

1172 ).relative_to(value_element_pos) 

1173 

1174 lint_state.emit_diagnostic( 

1175 duplicate_relation_range, 

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

1177 "warning", 

1178 known_field.unknown_value_authority, 

1179 related_information=[ 

1180 lint_state.related_diagnostic_information( 

1181 orig_relation_range, 

1182 "The previous definition", 

1183 ), 

1184 ], 

1185 ) 

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

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

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

1189 break 

1190 

1191 

1192def _dctrl_check_dep_version_operator( 

1193 known_field: "F", 

1194 version_operator: str, 

1195 version_operator_span: tuple[int, int], 

1196 version_operators: frozenset[str], 

1197 raw_value_masked_comments: str, 

1198 offset: int, 

1199 value_element_pos: "TEPosition", 

1200 lint_state: LintState, 

1201) -> bool: 

1202 if ( 

1203 version_operators 

1204 and version_operator is not None 

1205 and version_operator not in version_operators 

1206 ): 

1207 v_start_offset = offset + version_operator_span[0] 

1208 v_end_offset = offset + version_operator_span[1] 

1209 version_problem_range_te = TERange( 

1210 _text_to_te_position(raw_value_masked_comments[:v_start_offset]), 

1211 _text_to_te_position(raw_value_masked_comments[:v_end_offset]), 

1212 ).relative_to(value_element_pos) 

1213 

1214 sorted_version_operators = sorted(version_operators) 

1215 

1216 excluding_equal = f"{version_operator}{version_operator}" 

1217 including_equal = f"{version_operator}=" 

1218 

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

1220 excluding_equal in version_operators or including_equal in version_operators 

1221 ): 

1222 lint_state.emit_diagnostic( 

1223 version_problem_range_te, 

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

1225 "error", 

1226 "Policy 7.1", 

1227 quickfixes=[ 

1228 propose_correct_text_quick_fix(n) 

1229 for n in (excluding_equal, including_equal) 

1230 if not version_operators or n in version_operators 

1231 ], 

1232 ) 

1233 else: 

1234 lint_state.emit_diagnostic( 

1235 version_problem_range_te, 

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

1237 "error", 

1238 known_field.unknown_value_authority, 

1239 quickfixes=[ 

1240 propose_correct_text_quick_fix(n) for n in sorted_version_operators 

1241 ], 

1242 ) 

1243 return True 

1244 return False 

1245 

1246 

1247def _dctrl_validate_dep( 

1248 known_field: "DF", 

1249 _deb822_file: Deb822FileElement, 

1250 kvpair: Deb822KeyValuePairElement, 

1251 kvpair_range_te: "TERange", 

1252 _field_name_range: "TERange", 

1253 _stanza: Deb822ParagraphElement, 

1254 _stanza_position: "TEPosition", 

1255 lint_state: LintState, 

1256) -> None: 

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

1258 kvpair_range_te.start_pos 

1259 ) 

1260 raw_value_with_comments = kvpair.value_element.convert_to_text() 

1261 raw_value_masked_comments = "".join( 

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

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

1264 ) 

1265 if isinstance(known_field, DctrlRelationshipKnownField): 

1266 version_operators = known_field.allowed_version_operators 

1267 supports_or_relation = known_field.supports_or_relation 

1268 else: 

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

1270 supports_or_relation = True 

1271 

1272 relation_dup_table = collections.defaultdict(list) 

1273 

1274 for rel, rel_offset, rel_end_offset in _split_w_spans( 

1275 raw_value_masked_comments, "," 

1276 ): 

1277 sub_relations = [] 

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

1279 if or_rel.isspace(): 

1280 continue 

1281 if sub_relations and not supports_or_relation: 

1282 separator_range_te = TERange( 

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

1284 _text_to_te_position(raw_value_masked_comments[:offset]), 

1285 ).relative_to(value_element_pos) 

1286 lint_state.emit_diagnostic( 

1287 separator_range_te, 

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

1289 "error", 

1290 known_field.unknown_value_authority, 

1291 ) 

1292 m = _DEP_RELATION_CLAUSE.fullmatch(or_rel) 

1293 

1294 if m is not None: 

1295 garbage = m.group("garbage") 

1296 version_operator = m.group("operator") 

1297 version_operator_span = m.span("operator") 

1298 if _dctrl_check_dep_version_operator( 

1299 known_field, 

1300 version_operator, 

1301 version_operator_span, 

1302 version_operators, 

1303 raw_value_masked_comments, 

1304 offset, 

1305 value_element_pos, 

1306 lint_state, 

1307 ): 

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

1309 else: 

1310 name_arch_qual = m.group("name_arch_qual") 

1311 if ":" in name_arch_qual: 

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

1313 else: 

1314 name = name_arch_qual 

1315 arch_qual = None 

1316 sub_relations.append( 

1317 Relation( 

1318 name, 

1319 arch_qual=arch_qual, 

1320 version_operator=version_operator, 

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

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

1323 build_profile_restriction=m.group( 

1324 "build_profile_restriction" 

1325 ), 

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

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

1328 content_display_end_offset=rel_end_offset, 

1329 ) 

1330 ) 

1331 else: 

1332 garbage = None 

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

1334 

1335 if m is not None and not garbage: 

1336 continue 

1337 if m is not None: 

1338 garbage_span = m.span("garbage") 

1339 garbage_start, garbage_end = garbage_span 

1340 error_start_offset = offset + garbage_start 

1341 error_end_offset = offset + garbage_end 

1342 garbage_part = raw_value_masked_comments[ 

1343 error_start_offset:error_end_offset 

1344 ] 

1345 else: 

1346 garbage_part = None 

1347 error_start_offset = offset 

1348 error_end_offset = end_offset 

1349 

1350 problem_range_te = TERange( 

1351 _text_to_te_position(raw_value_masked_comments[:error_start_offset]), 

1352 _text_to_te_position(raw_value_masked_comments[:error_end_offset]), 

1353 ).relative_to(value_element_pos) 

1354 

1355 if garbage_part is not None: 

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

1357 msg = ( 

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

1359 " Is a separator missing before this part?" 

1360 ) 

1361 else: 

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

1363 lint_state.emit_diagnostic( 

1364 problem_range_te, 

1365 msg, 

1366 "error", 

1367 known_field.unknown_value_authority, 

1368 ) 

1369 else: 

1370 dep = _cleanup_rel( 

1371 raw_value_masked_comments[error_start_offset:error_end_offset] 

1372 ) 

1373 lint_state.emit_diagnostic( 

1374 problem_range_te, 

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

1376 "error", 

1377 known_field.unknown_value_authority, 

1378 ) 

1379 if ( 

1380 len(sub_relations) == 1 

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

1382 ): 

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

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

1385 

1386 for relations in relation_dup_table.values(): 

1387 if len(relations) > 1: 

1388 dup_check_relations( 

1389 known_field, 

1390 relations, 

1391 raw_value_masked_comments, 

1392 value_element_pos, 

1393 lint_state, 

1394 ) 

1395 

1396 

1397def _rrr_build_driver_mismatch( 

1398 _known_field: "F", 

1399 _deb822_file: Deb822FileElement, 

1400 _kvpair: Deb822KeyValuePairElement, 

1401 kvpair_range_te: "TERange", 

1402 _field_name_range: "TERange", 

1403 stanza: Deb822ParagraphElement, 

1404 _stanza_position: "TEPosition", 

1405 lint_state: LintState, 

1406) -> None: 

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

1408 if dr != "debian-rules": 

1409 lint_state.emit_diagnostic( 

1410 kvpair_range_te, 

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

1412 "informational", 

1413 "debputy", 

1414 quickfixes=[ 

1415 propose_remove_range_quick_fix( 

1416 proposed_title="Remove Rules-Requires-Root" 

1417 ) 

1418 ], 

1419 ) 

1420 

1421 

1422class Dep5Matcher(BasenameGlobMatch): 

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

1424 super().__init__( 

1425 basename_glob, 

1426 only_when_in_directory=None, 

1427 path_type=None, 

1428 recursive_match=False, 

1429 ) 

1430 

1431 

1432def _match_dep5_segment( 

1433 current_dir: VirtualPathBase, basename_glob: str 

1434) -> Iterable[VirtualPathBase]: 

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

1436 return Dep5Matcher(basename_glob).finditer(current_dir) 

1437 else: 

1438 res = current_dir.get(basename_glob) 

1439 if res is None: 

1440 return tuple() 

1441 return (res,) 

1442 

1443 

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

1445 

1446 

1447def _dep5_unnecessary_symbols( 

1448 value: str, 

1449 value_range: TERange, 

1450 lint_state: LintState, 

1451) -> None: 

1452 slash_check_index = 0 

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

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

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

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

1457 prefix_len = slashes_end 

1458 

1459 slash_check_index = prefix_len 

1460 prefix_range = TERange( 

1461 value_range.start_pos, 

1462 TEPosition( 

1463 value_range.start_pos.line_position, 

1464 value_range.start_pos.cursor_position + prefix_len, 

1465 ), 

1466 ) 

1467 lint_state.emit_diagnostic( 

1468 prefix_range, 

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

1470 "warning", 

1471 "debputy", 

1472 quickfixes=[ 

1473 propose_remove_range_quick_fix( 

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

1475 ) 

1476 ], 

1477 ) 

1478 

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

1480 m_start, m_end = m.span(0) 

1481 

1482 prefix_range = TERange( 

1483 TEPosition( 

1484 value_range.start_pos.line_position, 

1485 value_range.start_pos.cursor_position + m_start, 

1486 ), 

1487 TEPosition( 

1488 value_range.start_pos.line_position, 

1489 value_range.start_pos.cursor_position + m_end, 

1490 ), 

1491 ) 

1492 lint_state.emit_diagnostic( 

1493 prefix_range, 

1494 'Simplify to a single "/"', 

1495 "warning", 

1496 "debputy", 

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

1498 ) 

1499 

1500 

1501def _dep5_files_check( 

1502 known_field: "F", 

1503 _deb822_file: Deb822FileElement, 

1504 kvpair: Deb822KeyValuePairElement, 

1505 kvpair_range_te: "TERange", 

1506 _field_name_range: "TERange", 

1507 _stanza: Deb822ParagraphElement, 

1508 _stanza_position: "TEPosition", 

1509 lint_state: LintState, 

1510) -> None: 

1511 interpreter = known_field.field_value_class.interpreter() 

1512 assert interpreter is not None 

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

1514 kvpair_range_te.start_pos 

1515 ) 

1516 values_with_ranges = [] 

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

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

1519 full_value_range.start_pos 

1520 ) 

1521 value = value_ref.value 

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

1523 _dep5_unnecessary_symbols(value, value_range, lint_state) 

1524 

1525 source_root = lint_state.source_root 

1526 if source_root is None: 

1527 return 

1528 i = 0 

1529 limit = len(values_with_ranges) 

1530 while i < limit: 

1531 value, value_range = values_with_ranges[i] 

1532 i += 1 

1533 

1534 

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

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

1537_KNOWN_HTTPS_HOSTS = frozenset( 

1538 [ 

1539 "debian.org", 

1540 "bioconductor.org", 

1541 "cran.r-project.org", 

1542 "github.com", 

1543 "gitlab.com", 

1544 "metacpan.org", 

1545 "gnu.org", 

1546 ] 

1547) 

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

1549_NO_DOT_GIT_HOMEPAGE_HOSTS = frozenset( 

1550 { 

1551 "salsa.debian.org", 

1552 "github.com", 

1553 "gitlab.com", 

1554 } 

1555) 

1556 

1557 

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

1559 if host in known_hosts: 

1560 return True 

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

1562 try: 

1563 idx = host.index(".") 

1564 host = host[idx + 1 :] 

1565 except ValueError: 

1566 break 

1567 if host in known_hosts: 

1568 return True 

1569 return False 

1570 

1571 

1572def _validate_homepage_field( 

1573 _known_field: "F", 

1574 _deb822_file: Deb822FileElement, 

1575 kvpair: Deb822KeyValuePairElement, 

1576 kvpair_range_te: "TERange", 

1577 _field_name_range_te: "TERange", 

1578 _stanza: Deb822ParagraphElement, 

1579 _stanza_position: "TEPosition", 

1580 lint_state: LintState, 

1581) -> None: 

1582 value = kvpair.value_element.convert_to_text() 

1583 offset = 0 

1584 homepage = value 

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

1586 expected_value = m.group(1) 

1587 quickfixes = [] 

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

1589 homepage = expected_value.strip() 

1590 offset = m.start(1) 

1591 quickfixes.append(propose_correct_text_quick_fix(expected_value)) 

1592 lint_state.emit_diagnostic( 

1593 _single_line_span_range_relative_to_pos( 

1594 m.span(), 

1595 kvpair.value_element.position_in_parent().relative_to( 

1596 kvpair_range_te.start_pos 

1597 ), 

1598 ), 

1599 "Superfluous URL/URI wrapping", 

1600 "informational", 

1601 "Policy 5.6.23", 

1602 quickfixes=quickfixes, 

1603 ) 

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

1605 m = _URI_RE.search(homepage) 

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

1607 return 

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

1609 protocol = m.group("protocol") 

1610 host = m.group("host") 

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

1612 if _is_known_host(host, _REPLACED_HOSTS): 

1613 span = m.span("host") 

1614 lint_state.emit_diagnostic( 

1615 _single_line_span_range_relative_to_pos( 

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

1617 kvpair.value_element.position_in_parent().relative_to( 

1618 kvpair_range_te.start_pos 

1619 ), 

1620 ), 

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

1622 "warning", 

1623 "debputy", 

1624 ) 

1625 return 

1626 if ( 

1627 protocol == "ftp" 

1628 or protocol == "http" 

1629 and _is_known_host(host, _KNOWN_HTTPS_HOSTS) 

1630 ): 

1631 span = m.span("protocol") 

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

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

1634 quickfixes = [] 

1635 else: 

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

1637 quickfixes = [propose_correct_text_quick_fix("https")] 

1638 lint_state.emit_diagnostic( 

1639 _single_line_span_range_relative_to_pos( 

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

1641 kvpair.value_element.position_in_parent().relative_to( 

1642 kvpair_range_te.start_pos 

1643 ), 

1644 ), 

1645 msg, 

1646 "pedantic", 

1647 "debputy", 

1648 quickfixes=quickfixes, 

1649 ) 

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

1651 span = m.span("path") 

1652 msg = "Unnecessary suffix" 

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

1654 lint_state.emit_diagnostic( 

1655 _single_line_span_range_relative_to_pos( 

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

1657 kvpair.value_element.position_in_parent().relative_to( 

1658 kvpair_range_te.start_pos 

1659 ), 

1660 ), 

1661 msg, 

1662 "pedantic", 

1663 "debputy", 

1664 quickfixes=quickfixes, 

1665 ) 

1666 

1667 

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

1669 def _validator( 

1670 known_field: "F", 

1671 deb822_file: Deb822FileElement, 

1672 kvpair: Deb822KeyValuePairElement, 

1673 kvpair_range_te: "TERange", 

1674 field_name_range_te: "TERange", 

1675 stanza: Deb822ParagraphElement, 

1676 stanza_position: "TEPosition", 

1677 lint_state: LintState, 

1678 ) -> None: 

1679 for check in checks: 

1680 check( 

1681 known_field, 

1682 deb822_file, 

1683 kvpair, 

1684 kvpair_range_te, 

1685 field_name_range_te, 

1686 stanza, 

1687 stanza_position, 

1688 lint_state, 

1689 ) 

1690 

1691 return _validator 

1692 

1693 

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

1695class PackageNameSectionRule: 

1696 section: str 

1697 check: Callable[[str], bool] 

1698 

1699 

1700def _package_name_section_rule( 

1701 section: str, 

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

1703 *, 

1704 confirm_re: re.Pattern | None = None, 

1705) -> PackageNameSectionRule: 

1706 if confirm_re is not None: 

1707 assert callable(check) 

1708 

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

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

1711 

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

1713 

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

1715 return check.search(v) is not None 

1716 

1717 else: 

1718 _impl = check 

1719 

1720 return PackageNameSectionRule(section, _impl) 

1721 

1722 

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

1724_PKGNAME_VS_SECTION_RULES = [ 

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

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

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

1728 _package_name_section_rule( 

1729 "httpd", 

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

1731 ), 

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

1733 _package_name_section_rule( 

1734 "gnustep", 

1735 lambda n: n.endswith( 

1736 ( 

1737 ".framework", 

1738 ".framework-common", 

1739 ".tool", 

1740 ".tool-common", 

1741 ".app", 

1742 ".app-common", 

1743 ) 

1744 ), 

1745 ), 

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

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

1748 _package_name_section_rule( 

1749 "zope", 

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

1751 ), 

1752 _package_name_section_rule( 

1753 "python", 

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

1755 ), 

1756 _package_name_section_rule( 

1757 "gnu-r", 

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

1759 ), 

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

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

1762 _package_name_section_rule( 

1763 "lisp", 

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

1765 ), 

1766 _package_name_section_rule( 

1767 "lisp", 

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

1769 ), 

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

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

1772 _package_name_section_rule( 

1773 "perl", 

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

1775 ), 

1776 _package_name_section_rule( 

1777 "cli-mono", 

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

1779 ), 

1780 _package_name_section_rule( 

1781 "java", 

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

1783 ), 

1784 _package_name_section_rule( 

1785 "php", 

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

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

1788 ), 

1789 _package_name_section_rule( 

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

1791 ), 

1792 _package_name_section_rule( 

1793 "haskell", 

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

1795 ), 

1796 _package_name_section_rule( 

1797 "ruby", 

1798 lambda n: "-ruby" in n, 

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

1800 ), 

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

1802 _package_name_section_rule( 

1803 "rust", 

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

1805 ), 

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

1807 _package_name_section_rule( 

1808 "ocaml", 

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

1810 ), 

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

1812 _package_name_section_rule( 

1813 "interpreters", 

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

1815 ), 

1816 _package_name_section_rule( 

1817 "introspection", 

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

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

1820 ), 

1821 _package_name_section_rule( 

1822 "fonts", 

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

1824 ), 

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

1826 _package_name_section_rule( 

1827 "localization", 

1828 lambda n: n.startswith( 

1829 ( 

1830 "aspell-", 

1831 "hunspell-", 

1832 "myspell-", 

1833 "mythes-", 

1834 "dict-freedict-", 

1835 "gcompris-sound-", 

1836 ) 

1837 ), 

1838 ), 

1839 _package_name_section_rule( 

1840 "localization", 

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

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

1843 ), 

1844 _package_name_section_rule( 

1845 "localization", 

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

1847 ), 

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

1849 _package_name_section_rule( 

1850 "libdevel", 

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

1852 ), 

1853 _package_name_section_rule( 

1854 "libs", 

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

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

1857 ), 

1858] 

1859 

1860 

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

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

1863@functools.lru_cache(64) 

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

1865 for rule in _PKGNAME_VS_SECTION_RULES: 

1866 if rule.check(name): 

1867 return rule.section 

1868 return None 

1869 

1870 

1871def _unknown_value_check( 

1872 field_name: str, 

1873 value: str, 

1874 known_values: Mapping[str, Keyword], 

1875 unknown_value_severity: LintSeverity | None, 

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

1877 known_value = known_values.get(value) 

1878 message = None 

1879 severity = unknown_value_severity 

1880 fix_data = None 

1881 if known_value is None: 

1882 candidates = detect_possible_typo( 

1883 value, 

1884 known_values, 

1885 ) 

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

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

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

1889 else: 

1890 hint_text = "" 

1891 fix_data = None 

1892 severity = unknown_value_severity 

1893 fix_text = hint_text 

1894 if candidates: 

1895 match = candidates[0] 

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

1897 known_value = known_values[match] 

1898 fix_text = ( 

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

1900 ) 

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

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

1903 return None, None, None, None 

1904 if severity is None: 

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

1906 # It always has leading whitespace 

1907 message = fix_text.strip() 

1908 else: 

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

1910 return known_value, message, severity, fix_data 

1911 

1912 

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

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

1915 

1916 

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

1918 return path 

1919 

1920 

1921def _should_ignore_dir( 

1922 path: VirtualPath, 

1923 *, 

1924 supports_dir_match: bool = False, 

1925 match_non_persistent_paths: bool = False, 

1926) -> bool: 

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

1928 return True 

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

1930 if ( 

1931 not match_non_persistent_paths 

1932 and cachedir_tag is not None 

1933 and cachedir_tag.is_file 

1934 ): 

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

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

1937 start = fd.read(43) 

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

1939 return True 

1940 return False 

1941 

1942 

1943@dataclasses.dataclass(slots=True) 

1944class Deb822KnownField: 

1945 name: str 

1946 field_value_class: FieldValueClass 

1947 warn_if_default: bool = True 

1948 unknown_value_authority: str = "debputy" 

1949 missing_field_authority: str = "debputy" 

1950 replaced_by: str | None = None 

1951 deprecated_with_no_replacement: bool = False 

1952 missing_field_severity: LintSeverity | None = None 

1953 default_value: str | None = None 

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

1955 unknown_value_severity: LintSeverity | None = "error" 

1956 translation_context: str = "" 

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

1958 synopsis: str | None = None 

1959 usage_hint: UsageHint | None = None 

1960 long_description: str | None = None 

1961 spellcheck_value: bool = False 

1962 inheritable_from_other_stanza: bool = False 

1963 show_as_inherited: bool = True 

1964 custom_field_check: CustomFieldCheck | None = None 

1965 can_complete_field_in_stanza: None | ( 

1966 Callable[[Iterable[Deb822ParagraphElement]], bool] 

1967 ) = None 

1968 is_substvars_disabled_even_if_allowed_by_stanza: bool = False 

1969 is_alias_of: str | None = None 

1970 is_completion_suggestion: bool = True 

1971 

1972 def synopsis_translated( 

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

1974 ) -> str | None: 

1975 if self.synopsis is None: 

1976 return None 

1977 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1978 self.translation_context, 

1979 self.synopsis, 

1980 ) 

1981 

1982 def long_description_translated( 

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

1984 ) -> str | None: 

1985 if self.long_description_translated is None: 1985 ↛ 1986line 1985 didn't jump to line 1986 because the condition on line 1985 was never true

1986 return None 

1987 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1988 self.translation_context, 

1989 self.long_description, 

1990 ) 

1991 

1992 def _can_complete_field_in_stanza( 

1993 self, 

1994 stanza_parts: Sequence[Deb822ParagraphElement], 

1995 ) -> bool: 

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

1997 return False 

1998 return ( 

1999 self.can_complete_field_in_stanza is None 

2000 or self.can_complete_field_in_stanza(stanza_parts) 

2001 ) 

2002 

2003 def complete_field( 

2004 self, 

2005 lint_state: LintState, 

2006 stanza_parts: Sequence[Deb822ParagraphElement], 

2007 markdown_kind: MarkupKind, 

2008 ) -> CompletionItem | None: 

2009 if not self._can_complete_field_in_stanza(stanza_parts): 

2010 return None 

2011 name = self.name 

2012 complete_as = name + ": " 

2013 options = self.value_options_for_completer( 

2014 lint_state, 

2015 stanza_parts, 

2016 "", 

2017 markdown_kind, 

2018 is_completion_for_field=True, 

2019 ) 

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

2021 value = options[0].insert_text 

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

2023 complete_as += value 

2024 tags = [] 

2025 is_deprecated = False 

2026 if self.replaced_by or self.deprecated_with_no_replacement: 

2027 is_deprecated = True 

2028 tags.append(CompletionItemTag.Deprecated) 

2029 

2030 doc = self.long_description 

2031 if doc: 

2032 doc = MarkupContent( 

2033 value=doc, 

2034 kind=markdown_kind, 

2035 ) 

2036 else: 

2037 doc = None 

2038 

2039 return CompletionItem( 

2040 name, 

2041 insert_text=complete_as, 

2042 deprecated=is_deprecated, 

2043 tags=tags, 

2044 detail=format_comp_item_synopsis_doc( 

2045 self.usage_hint, 

2046 self.synopsis_translated(lint_state), 

2047 is_deprecated, 

2048 ), 

2049 documentation=doc, 

2050 ) 

2051 

2052 def _complete_files( 

2053 self, 

2054 base_dir: VirtualPathBase | None, 

2055 value_being_completed: str, 

2056 *, 

2057 is_dep5_file_list: bool = False, 

2058 supports_dir_match: bool = False, 

2059 supports_spaces_in_filename: bool = False, 

2060 match_non_persistent_paths: bool = False, 

2061 ) -> Sequence[CompletionItem] | None: 

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

2063 if base_dir is None or not base_dir.is_dir: 

2064 return None 

2065 

2066 if is_dep5_file_list: 

2067 supports_spaces_in_filename = True 

2068 supports_dir_match = False 

2069 match_non_persistent_paths = False 

2070 

2071 if value_being_completed == "": 

2072 current_dir = base_dir 

2073 unmatched_parts: Sequence[str] = () 

2074 else: 

2075 current_dir, unmatched_parts = base_dir.attempt_lookup( 

2076 value_being_completed 

2077 ) 

2078 

2079 if len(unmatched_parts) > 1: 

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

2081 return None 

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

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

2084 return None 

2085 items = [] 

2086 

2087 path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path 

2088 

2089 for child in current_dir.iterdir(): 

2090 if child.is_symlink and is_dep5_file_list: 

2091 continue 

2092 if not supports_spaces_in_filename and ( 

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

2094 ): 

2095 continue 

2096 sort_text = ( 

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

2098 ) 

2099 if child.is_dir: 

2100 if _should_ignore_dir( 

2101 child, 

2102 supports_dir_match=supports_dir_match, 

2103 match_non_persistent_paths=match_non_persistent_paths, 

2104 ): 

2105 continue 

2106 items.append( 

2107 CompletionItem( 

2108 f"{child.path}/", 

2109 label_details=CompletionItemLabelDetails( 

2110 description=child.path, 

2111 ), 

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

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

2114 sort_text=sort_text, 

2115 kind=CompletionItemKind.Folder, 

2116 ) 

2117 ) 

2118 else: 

2119 items.append( 

2120 CompletionItem( 

2121 child.path, 

2122 label_details=CompletionItemLabelDetails( 

2123 description=child.path, 

2124 ), 

2125 insert_text=path_escaper(child.path), 

2126 filter_text=child.path, 

2127 sort_text=sort_text, 

2128 kind=CompletionItemKind.File, 

2129 ) 

2130 ) 

2131 return items 

2132 

2133 def value_options_for_completer( 

2134 self, 

2135 lint_state: LintState, 

2136 stanza_parts: Sequence[Deb822ParagraphElement], 

2137 value_being_completed: str, 

2138 markdown_kind: MarkupKind, 

2139 *, 

2140 is_completion_for_field: bool = False, 

2141 ) -> Sequence[CompletionItem] | None: 

2142 known_values = self.known_values 

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

2144 if is_completion_for_field: 

2145 return None 

2146 return self._complete_files( 

2147 lint_state.source_root, 

2148 value_being_completed, 

2149 is_dep5_file_list=True, 

2150 ) 

2151 

2152 if known_values is None: 

2153 return None 

2154 if is_completion_for_field and ( 

2155 len(known_values) == 1 

2156 or ( 

2157 len(known_values) == 2 

2158 and self.warn_if_default 

2159 and self.default_value is not None 

2160 ) 

2161 ): 

2162 value = next( 

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

2164 None, 

2165 ) 

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

2167 return None 

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

2169 return [ 

2170 keyword.as_completion_item( 

2171 lint_state, 

2172 stanza_parts, 

2173 value_being_completed, 

2174 markdown_kind, 

2175 ) 

2176 for keyword in known_values.values() 

2177 if keyword.is_keyword_valid_completion_in_stanza(stanza_parts) 

2178 and keyword.is_completion_suggestion 

2179 ] 

2180 

2181 def field_omitted_diagnostics( 

2182 self, 

2183 deb822_file: Deb822FileElement, 

2184 representation_field_range: "TERange", 

2185 stanza: Deb822ParagraphElement, 

2186 stanza_position: "TEPosition", 

2187 header_stanza: Deb822FileElement | None, 

2188 lint_state: LintState, 

2189 ) -> None: 

2190 missing_field_severity = self.missing_field_severity 

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

2192 return 

2193 

2194 if ( 

2195 self.inheritable_from_other_stanza 

2196 and header_stanza is not None 

2197 and self.name in header_stanza 

2198 ): 

2199 return 

2200 

2201 lint_state.emit_diagnostic( 

2202 representation_field_range, 

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

2204 missing_field_severity, 

2205 self.missing_field_authority, 

2206 ) 

2207 

2208 async def field_diagnostics( 

2209 self, 

2210 deb822_file: Deb822FileElement, 

2211 kvpair: Deb822KeyValuePairElement, 

2212 stanza: Deb822ParagraphElement, 

2213 stanza_position: "TEPosition", 

2214 kvpair_range_te: "TERange", 

2215 lint_state: LintState, 

2216 *, 

2217 field_name_typo_reported: bool = False, 

2218 ) -> None: 

2219 field_name_token = kvpair.field_token 

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

2221 kvpair_range_te.start_pos 

2222 ) 

2223 field_name = field_name_token.text 

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

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

2226 # in one but not the other. 

2227 field_value = stanza[field_name] 

2228 self._diagnostics_for_field_name( 

2229 kvpair_range_te, 

2230 field_name_token, 

2231 field_name_range_te, 

2232 field_name_typo_reported, 

2233 lint_state, 

2234 ) 

2235 if self.custom_field_check is not None: 

2236 self.custom_field_check( 

2237 self, 

2238 deb822_file, 

2239 kvpair, 

2240 kvpair_range_te, 

2241 field_name_range_te, 

2242 stanza, 

2243 stanza_position, 

2244 lint_state, 

2245 ) 

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

2247 if self.spellcheck_value: 

2248 words = kvpair.interpret_as(LIST_SPACE_SEPARATED_INTERPRETATION) 

2249 spell_checker = lint_state.spellchecker() 

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

2251 kvpair_range_te.start_pos 

2252 ) 

2253 async for word_ref in lint_state.slow_iter( 

2254 words.iter_value_references(), yield_every=25 

2255 ): 

2256 token = word_ref.value 

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

2258 corrections = spell_checker.provide_corrections_for(word) 

2259 if not corrections: 

2260 continue 

2261 word_loc = word_ref.locatable 

2262 word_pos_te = word_loc.position_in_parent().relative_to( 

2263 value_position 

2264 ) 

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

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

2267 word_size = TERange( 

2268 START_POSITION, 

2269 TEPosition(0, endpos - pos), 

2270 ) 

2271 lint_state.emit_diagnostic( 

2272 TERange.from_position_and_size(word_pos_te, word_size), 

2273 f'Spelling "{word}"', 

2274 "spelling", 

2275 "debputy", 

2276 quickfixes=[ 

2277 propose_correct_text_quick_fix(c) for c in corrections 

2278 ], 

2279 enable_non_interactive_auto_fix=False, 

2280 ) 

2281 else: 

2282 self._known_value_diagnostics( 

2283 kvpair, 

2284 kvpair_range_te.start_pos, 

2285 lint_state, 

2286 ) 

2287 

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

2289 lint_state.emit_diagnostic( 

2290 kvpair_range_te, 

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

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

2293 "warning", 

2294 "debputy", 

2295 ) 

2296 

2297 def _diagnostics_for_field_name( 

2298 self, 

2299 kvpair_range: "TERange", 

2300 token: Deb822FieldNameToken, 

2301 token_range: "TERange", 

2302 typo_detected: bool, 

2303 lint_state: LintState, 

2304 ) -> None: 

2305 field_name = token.text 

2306 # Defeat the case-insensitivity from python-debian 

2307 field_name_cased = str(field_name) 

2308 if self.deprecated_with_no_replacement: 

2309 lint_state.emit_diagnostic( 

2310 kvpair_range, 

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

2312 "warning", 

2313 "debputy", 

2314 quickfixes=[propose_remove_range_quick_fix()], 

2315 tags=[DiagnosticTag.Deprecated], 

2316 ) 

2317 elif self.replaced_by is not None: 

2318 lint_state.emit_diagnostic( 

2319 token_range, 

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

2321 "warning", 

2322 "debputy", 

2323 tags=[DiagnosticTag.Deprecated], 

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

2325 ) 

2326 

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

2328 lint_state.emit_diagnostic( 

2329 token_range, 

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

2331 "pedantic", 

2332 self.unknown_value_authority, 

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

2334 ) 

2335 

2336 def _dep5_file_list_diagnostics( 

2337 self, 

2338 kvpair: Deb822KeyValuePairElement, 

2339 kvpair_position: "TEPosition", 

2340 lint_state: LintState, 

2341 ) -> None: 

2342 source_root = lint_state.source_root 

2343 if ( 

2344 self.field_value_class != FieldValueClass.DEP5_FILE_LIST 

2345 or source_root is None 

2346 ): 

2347 return 

2348 interpreter = self.field_value_class.interpreter() 

2349 values = kvpair.interpret_as(interpreter) 

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

2351 kvpair_position 

2352 ) 

2353 

2354 assert interpreter is not None 

2355 

2356 for token in values.iter_parts(): 

2357 if token.is_whitespace: 

2358 continue 

2359 text = token.convert_to_text() 

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

2361 # TODO: We should validate these as well 

2362 continue 

2363 matched_path, missing_part = source_root.attempt_lookup(text) 

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

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

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

2367 # do not have the infrastructure for). 

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

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

2370 lint_state.emit_diagnostic( 

2371 path_range_te, 

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

2373 "warning", 

2374 self.unknown_value_authority, 

2375 quickfixes=[ 

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

2377 ], 

2378 ) 

2379 

2380 def _known_value_diagnostics( 

2381 self, 

2382 kvpair: Deb822KeyValuePairElement, 

2383 kvpair_position: "TEPosition", 

2384 lint_state: LintState, 

2385 ) -> None: 

2386 unknown_value_severity = self.unknown_value_severity 

2387 interpreter = self.field_value_class.interpreter() 

2388 if interpreter is None: 

2389 return 

2390 try: 

2391 values = kvpair.interpret_as(interpreter) 

2392 except ValueError: 

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

2394 kvpair_position 

2395 ) 

2396 lint_state.emit_diagnostic( 

2397 value_range, 

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

2399 "pedantic", 

2400 "debputy", 

2401 ) 

2402 return 

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

2404 kvpair_position 

2405 ) 

2406 

2407 last_token_non_ws_sep_token: TE | None = None 

2408 for token in values.iter_parts(): 

2409 if token.is_whitespace: 

2410 continue 

2411 if not token.is_separator: 

2412 last_token_non_ws_sep_token = None 

2413 continue 

2414 if last_token_non_ws_sep_token is not None: 

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

2416 lint_state.emit_diagnostic( 

2417 sep_range_te, 

2418 "Duplicate separator", 

2419 "error", 

2420 self.unknown_value_authority, 

2421 ) 

2422 last_token_non_ws_sep_token = token 

2423 

2424 allowed_values = self.known_values 

2425 if not allowed_values: 

2426 return 

2427 

2428 first_value = None 

2429 first_exclusive_value_ref = None 

2430 first_exclusive_value = None 

2431 has_emitted_for_exclusive = False 

2432 

2433 for value_ref in values.iter_value_references(): 

2434 value = value_ref.value 

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

2436 first_value is not None 

2437 and self.field_value_class == FieldValueClass.SINGLE_VALUE 

2438 ): 

2439 value_loc = value_ref.locatable 

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

2441 lint_state.emit_diagnostic( 

2442 range_position_te, 

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

2444 "error", 

2445 self.unknown_value_authority, 

2446 ) 

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

2448 continue 

2449 

2450 if first_exclusive_value_ref is not None and not has_emitted_for_exclusive: 

2451 assert first_exclusive_value is not None 

2452 value_loc = first_exclusive_value_ref.locatable 

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

2454 lint_state.emit_diagnostic( 

2455 value_range_te, 

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

2457 "error", 

2458 self.unknown_value_authority, 

2459 ) 

2460 

2461 known_value, unknown_value_message, unknown_severity, typo_fix_data = ( 

2462 _unknown_value_check( 

2463 self.name, 

2464 value, 

2465 self.known_values, 

2466 unknown_value_severity, 

2467 ) 

2468 ) 

2469 value_loc = value_ref.locatable 

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

2471 

2472 if known_value and known_value.is_exclusive: 

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

2474 first_exclusive_value_ref = value_ref 

2475 if first_value is not None: 

2476 has_emitted_for_exclusive = True 

2477 lint_state.emit_diagnostic( 

2478 value_range, 

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

2480 "error", 

2481 self.unknown_value_authority, 

2482 ) 

2483 

2484 if first_value is None: 

2485 first_value = value 

2486 

2487 if unknown_value_message is not None: 

2488 assert unknown_severity is not None 

2489 lint_state.emit_diagnostic( 

2490 value_range, 

2491 unknown_value_message, 

2492 unknown_severity, 

2493 self.unknown_value_authority, 

2494 quickfixes=typo_fix_data, 

2495 ) 

2496 

2497 if known_value is not None and known_value.is_deprecated: 

2498 replacement = known_value.replaced_by 

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

2500 obsolete_value_message = ( 

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

2502 ) 

2503 obsolete_fix_data = [propose_correct_text_quick_fix(replacement)] 

2504 else: 

2505 obsolete_value_message = ( 

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

2507 ) 

2508 obsolete_fix_data = None 

2509 lint_state.emit_diagnostic( 

2510 value_range, 

2511 obsolete_value_message, 

2512 "warning", 

2513 "debputy", 

2514 quickfixes=obsolete_fix_data, 

2515 ) 

2516 

2517 def _reformat_field_name( 

2518 self, 

2519 effective_preference: "EffectiveFormattingPreference", 

2520 stanza_range: TERange, 

2521 kvpair: Deb822KeyValuePairElement, 

2522 position_codec: LintCapablePositionCodec, 

2523 lines: list[str], 

2524 ) -> Iterable[TextEdit]: 

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

2526 return 

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

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

2529 return 

2530 

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

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

2533 ) 

2534 

2535 edit_range = position_codec.range_to_client_units( 

2536 lines, 

2537 Range( 

2538 Position( 

2539 field_name_range_te.start_pos.line_position, 

2540 field_name_range_te.start_pos.cursor_position, 

2541 ), 

2542 Position( 

2543 field_name_range_te.start_pos.line_position, 

2544 field_name_range_te.end_pos.cursor_position, 

2545 ), 

2546 ), 

2547 ) 

2548 yield TextEdit( 

2549 edit_range, 

2550 self.name, 

2551 ) 

2552 

2553 def reformat_field( 

2554 self, 

2555 effective_preference: "EffectiveFormattingPreference", 

2556 stanza_range: TERange, 

2557 kvpair: Deb822KeyValuePairElement, 

2558 formatter: FormatterCallback, 

2559 position_codec: LintCapablePositionCodec, 

2560 lines: list[str], 

2561 ) -> Iterable[TextEdit]: 

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

2563 yield from self._reformat_field_name( 

2564 effective_preference, 

2565 stanza_range, 

2566 kvpair, 

2567 position_codec, 

2568 lines, 

2569 ) 

2570 return trim_end_of_line_whitespace( 

2571 position_codec, 

2572 lines, 

2573 line_range=range( 

2574 kvpair_range.start_pos.line_position, 

2575 kvpair_range.end_pos.line_position, 

2576 ), 

2577 ) 

2578 

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

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

2581 

2582 

2583@dataclasses.dataclass(slots=True) 

2584class DctrlLikeKnownField(Deb822KnownField): 

2585 

2586 def reformat_field( 

2587 self, 

2588 effective_preference: "EffectiveFormattingPreference", 

2589 stanza_range: TERange, 

2590 kvpair: Deb822KeyValuePairElement, 

2591 formatter: FormatterCallback, 

2592 position_codec: LintCapablePositionCodec, 

2593 lines: list[str], 

2594 ) -> Iterable[TextEdit]: 

2595 interpretation = self.field_value_class.interpreter() 

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

2597 not effective_preference.deb822_normalize_field_content 

2598 or interpretation is None 

2599 ): 

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

2601 effective_preference, 

2602 stanza_range, 

2603 kvpair, 

2604 formatter, 

2605 position_codec, 

2606 lines, 

2607 ) 

2608 return 

2609 if not self.reformattable_field: 

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

2611 effective_preference, 

2612 stanza_range, 

2613 kvpair, 

2614 formatter, 

2615 position_codec, 

2616 lines, 

2617 ) 

2618 return 

2619 

2620 # Preserve the name fixes from the super call. 

2621 yield from self._reformat_field_name( 

2622 effective_preference, 

2623 stanza_range, 

2624 kvpair, 

2625 position_codec, 

2626 lines, 

2627 ) 

2628 

2629 seen: set[str] = set() 

2630 old_kvpair_range = kvpair.range_in_parent() 

2631 sort = self.is_sortable_field 

2632 

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

2634 field_content = kvpair.interpret_as(interpretation) 

2635 old_value = field_content.convert_to_text(with_field_name=False) 

2636 for package_ref in field_content.iter_value_references(): 

2637 value = package_ref.value 

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

2639 stanza_range.start_pos 

2640 ) 

2641 sublines = lines[ 

2642 value_range.start_pos.line_position : value_range.end_pos.line_position 

2643 ] 

2644 

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

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

2647 return 

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

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

2650 else: 

2651 new_value = value 

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

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

2654 package_ref.value = new_value 

2655 seen.add(new_value) 

2656 else: 

2657 package_ref.remove() 

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

2659 field_content.sort(key=_sort_packages_key) 

2660 field_content.value_formatter(formatter) 

2661 field_content.reformat_when_finished() 

2662 

2663 new_value = field_content.convert_to_text(with_field_name=False) 

2664 if new_value != old_value: 

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

2666 old_kvpair_range.start_pos 

2667 ) 

2668 range_server_units = te_range_to_lsp( 

2669 value_range.relative_to(stanza_range.start_pos) 

2670 ) 

2671 yield TextEdit( 

2672 position_codec.range_to_client_units(lines, range_server_units), 

2673 new_value, 

2674 ) 

2675 

2676 @property 

2677 def reformattable_field(self) -> bool: 

2678 return self.is_relationship_field or self.is_sortable_field 

2679 

2680 @property 

2681 def is_relationship_field(self) -> bool: 

2682 return False 

2683 

2684 @property 

2685 def is_sortable_field(self) -> bool: 

2686 return self.is_relationship_field 

2687 

2688 

2689@dataclasses.dataclass(slots=True) 

2690class DTestsCtrlKnownField(DctrlLikeKnownField): 

2691 @property 

2692 def is_relationship_field(self) -> bool: 

2693 return self.name == "Depends" 

2694 

2695 @property 

2696 def is_sortable_field(self) -> bool: 

2697 return self.is_relationship_field or self.name in ( 

2698 "Features", 

2699 "Restrictions", 

2700 "Tests", 

2701 ) 

2702 

2703 

2704@dataclasses.dataclass(slots=True) 

2705class DctrlKnownField(DctrlLikeKnownField): 

2706 

2707 def field_omitted_diagnostics( 

2708 self, 

2709 deb822_file: Deb822FileElement, 

2710 representation_field_range: "TERange", 

2711 stanza: Deb822ParagraphElement, 

2712 stanza_position: "TEPosition", 

2713 header_stanza: Deb822FileElement | None, 

2714 lint_state: LintState, 

2715 ) -> None: 

2716 missing_field_severity = self.missing_field_severity 

2717 if missing_field_severity is None: 

2718 return 

2719 

2720 if ( 

2721 self.inheritable_from_other_stanza 

2722 and header_stanza is not None 

2723 and self.name in header_stanza 

2724 ): 

2725 return 

2726 

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

2728 stanzas = list(deb822_file)[1:] 

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

2730 return 

2731 

2732 lint_state.emit_diagnostic( 

2733 representation_field_range, 

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

2735 missing_field_severity, 

2736 self.missing_field_authority, 

2737 ) 

2738 

2739 def reformat_field( 

2740 self, 

2741 effective_preference: "EffectiveFormattingPreference", 

2742 stanza_range: TERange, 

2743 kvpair: Deb822KeyValuePairElement, 

2744 formatter: FormatterCallback, 

2745 position_codec: LintCapablePositionCodec, 

2746 lines: list[str], 

2747 ) -> Iterable[TextEdit]: 

2748 if ( 

2749 self.name == "Architecture" 

2750 and effective_preference.deb822_normalize_field_content 

2751 ): 

2752 interpretation = self.field_value_class.interpreter() 

2753 assert interpretation is not None 

2754 interpreted = kvpair.interpret_as(interpretation) 

2755 archs = list(interpreted) 

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

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

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

2759 reformat_edits = list( 

2760 self._reformat_field_name( 

2761 effective_preference, 

2762 stanza_range, 

2763 kvpair, 

2764 position_codec, 

2765 lines, 

2766 ) 

2767 ) 

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

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

2770 kvpair.range_in_parent().start_pos 

2771 ) 

2772 kvpair_range = te_range_to_lsp( 

2773 value_range.relative_to(stanza_range.start_pos) 

2774 ) 

2775 reformat_edits.append( 

2776 TextEdit( 

2777 position_codec.range_to_client_units(lines, kvpair_range), 

2778 new_value, 

2779 ) 

2780 ) 

2781 return reformat_edits 

2782 

2783 return super(DctrlKnownField, self).reformat_field( 

2784 effective_preference, 

2785 stanza_range, 

2786 kvpair, 

2787 formatter, 

2788 position_codec, 

2789 lines, 

2790 ) 

2791 

2792 @property 

2793 def is_relationship_field(self) -> bool: 

2794 name_lc = self.name.lower() 

2795 return ( 

2796 name_lc in all_package_relationship_fields() 

2797 or name_lc in all_source_relationship_fields() 

2798 ) 

2799 

2800 @property 

2801 def reformattable_field(self) -> bool: 

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

2803 

2804 

2805@dataclasses.dataclass(slots=True) 

2806class DctrlRelationshipKnownField(DctrlKnownField): 

2807 allowed_version_operators: frozenset[str] = frozenset() 

2808 supports_or_relation: bool = True 

2809 

2810 @property 

2811 def is_relationship_field(self) -> bool: 

2812 return True 

2813 

2814 

2815SOURCE_FIELDS = _fields( 

2816 DctrlKnownField( 

2817 "Source", 

2818 FieldValueClass.SINGLE_VALUE, 

2819 custom_field_check=_combined_custom_field_check( 

2820 _each_value_match_regex_validation(PKGNAME_REGEX), 

2821 _has_packaging_expected_file( 

2822 "copyright", 

2823 "No copyright file (package license)", 

2824 severity="warning", 

2825 ), 

2826 _has_packaging_expected_file( 

2827 "changelog", 

2828 "No Debian changelog file", 

2829 severity="error", 

2830 ), 

2831 _has_build_instructions, 

2832 ), 

2833 ), 

2834 DctrlKnownField( 

2835 "Standards-Version", 

2836 FieldValueClass.SINGLE_VALUE, 

2837 custom_field_check=_sv_field_validation, 

2838 ), 

2839 DctrlKnownField( 

2840 "Section", 

2841 FieldValueClass.SINGLE_VALUE, 

2842 known_values=ALL_SECTIONS, 

2843 ), 

2844 DctrlKnownField( 

2845 "Priority", 

2846 FieldValueClass.SINGLE_VALUE, 

2847 ), 

2848 DctrlKnownField( 

2849 "Maintainer", 

2850 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2851 custom_field_check=_combined_custom_field_check( 

2852 _maintainer_field_validator, 

2853 _canonical_maintainer_name, 

2854 ), 

2855 ), 

2856 DctrlKnownField( 

2857 "Uploaders", 

2858 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2859 custom_field_check=_canonical_maintainer_name, 

2860 ), 

2861 DctrlRelationshipKnownField( 

2862 "Build-Depends", 

2863 FieldValueClass.COMMA_SEPARATED_LIST, 

2864 custom_field_check=_dctrl_validate_dep, 

2865 ), 

2866 DctrlRelationshipKnownField( 

2867 "Build-Depends-Arch", 

2868 FieldValueClass.COMMA_SEPARATED_LIST, 

2869 custom_field_check=_dctrl_validate_dep, 

2870 ), 

2871 DctrlRelationshipKnownField( 

2872 "Build-Depends-Indep", 

2873 FieldValueClass.COMMA_SEPARATED_LIST, 

2874 custom_field_check=_dctrl_validate_dep, 

2875 ), 

2876 DctrlRelationshipKnownField( 

2877 "Build-Conflicts", 

2878 FieldValueClass.COMMA_SEPARATED_LIST, 

2879 supports_or_relation=False, 

2880 custom_field_check=_dctrl_validate_dep, 

2881 ), 

2882 DctrlRelationshipKnownField( 

2883 "Build-Conflicts-Arch", 

2884 FieldValueClass.COMMA_SEPARATED_LIST, 

2885 supports_or_relation=False, 

2886 custom_field_check=_dctrl_validate_dep, 

2887 ), 

2888 DctrlRelationshipKnownField( 

2889 "Build-Conflicts-Indep", 

2890 FieldValueClass.COMMA_SEPARATED_LIST, 

2891 supports_or_relation=False, 

2892 custom_field_check=_dctrl_validate_dep, 

2893 ), 

2894 DctrlKnownField( 

2895 "Rules-Requires-Root", 

2896 FieldValueClass.SPACE_SEPARATED_LIST, 

2897 custom_field_check=_rrr_build_driver_mismatch, 

2898 ), 

2899 DctrlKnownField( 

2900 "X-Style", 

2901 FieldValueClass.SINGLE_VALUE, 

2902 known_values=ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

2903 ), 

2904 DctrlKnownField( 

2905 "Homepage", 

2906 FieldValueClass.SINGLE_VALUE, 

2907 custom_field_check=_validate_homepage_field, 

2908 ), 

2909) 

2910 

2911 

2912BINARY_FIELDS = _fields( 

2913 DctrlKnownField( 

2914 "Package", 

2915 FieldValueClass.SINGLE_VALUE, 

2916 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

2917 ), 

2918 DctrlKnownField( 

2919 "Architecture", 

2920 FieldValueClass.SPACE_SEPARATED_LIST, 

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

2922 known_values=allowed_values(dpkg_arch_and_wildcards()), 

2923 ), 

2924 DctrlKnownField( 

2925 "Pre-Depends", 

2926 FieldValueClass.COMMA_SEPARATED_LIST, 

2927 custom_field_check=_dctrl_validate_dep, 

2928 ), 

2929 DctrlKnownField( 

2930 "Depends", 

2931 FieldValueClass.COMMA_SEPARATED_LIST, 

2932 custom_field_check=_dctrl_validate_dep, 

2933 ), 

2934 DctrlKnownField( 

2935 "Recommends", 

2936 FieldValueClass.COMMA_SEPARATED_LIST, 

2937 custom_field_check=_dctrl_validate_dep, 

2938 ), 

2939 DctrlKnownField( 

2940 "Suggests", 

2941 FieldValueClass.COMMA_SEPARATED_LIST, 

2942 custom_field_check=_dctrl_validate_dep, 

2943 ), 

2944 DctrlKnownField( 

2945 "Enhances", 

2946 FieldValueClass.COMMA_SEPARATED_LIST, 

2947 custom_field_check=_dctrl_validate_dep, 

2948 ), 

2949 DctrlRelationshipKnownField( 

2950 "Provides", 

2951 FieldValueClass.COMMA_SEPARATED_LIST, 

2952 custom_field_check=_dctrl_validate_dep, 

2953 supports_or_relation=False, 

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

2955 ), 

2956 DctrlRelationshipKnownField( 

2957 "Conflicts", 

2958 FieldValueClass.COMMA_SEPARATED_LIST, 

2959 custom_field_check=_dctrl_validate_dep, 

2960 supports_or_relation=False, 

2961 ), 

2962 DctrlRelationshipKnownField( 

2963 "Breaks", 

2964 FieldValueClass.COMMA_SEPARATED_LIST, 

2965 custom_field_check=_dctrl_validate_dep, 

2966 supports_or_relation=False, 

2967 ), 

2968 DctrlRelationshipKnownField( 

2969 "Replaces", 

2970 FieldValueClass.COMMA_SEPARATED_LIST, 

2971 custom_field_check=_dctrl_validate_dep, 

2972 ), 

2973 DctrlKnownField( 

2974 "Build-Profiles", 

2975 FieldValueClass.BUILD_PROFILES_LIST, 

2976 ), 

2977 DctrlKnownField( 

2978 "Section", 

2979 FieldValueClass.SINGLE_VALUE, 

2980 known_values=ALL_SECTIONS, 

2981 ), 

2982 DctrlRelationshipKnownField( 

2983 "Built-Using", 

2984 FieldValueClass.COMMA_SEPARATED_LIST, 

2985 custom_field_check=_arch_not_all_only_field_validation, 

2986 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

2987 supports_or_relation=False, 

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

2989 ), 

2990 DctrlRelationshipKnownField( 

2991 "Static-Built-Using", 

2992 FieldValueClass.COMMA_SEPARATED_LIST, 

2993 custom_field_check=_arch_not_all_only_field_validation, 

2994 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

2995 supports_or_relation=False, 

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

2997 ), 

2998 DctrlKnownField( 

2999 "Multi-Arch", 

3000 FieldValueClass.SINGLE_VALUE, 

3001 custom_field_check=_dctrl_ma_field_validation, 

3002 known_values=allowed_values( 

3003 ( 

3004 Keyword( 

3005 "same", 

3006 can_complete_keyword_in_stanza=_complete_only_in_arch_dep_pkgs, 

3007 ), 

3008 ), 

3009 ), 

3010 ), 

3011 DctrlKnownField( 

3012 "XB-Installer-Menu-Item", 

3013 FieldValueClass.SINGLE_VALUE, 

3014 can_complete_field_in_stanza=_complete_only_for_udeb_pkgs, 

3015 custom_field_check=_combined_custom_field_check( 

3016 _udeb_only_field_validation, 

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

3018 ), 

3019 ), 

3020 DctrlKnownField( 

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

3022 FieldValueClass.SINGLE_VALUE, 

3023 custom_field_check=_arch_not_all_only_field_validation, 

3024 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3025 ), 

3026 DctrlKnownField( 

3027 "X-Doc-Main-Package", 

3028 FieldValueClass.SINGLE_VALUE, 

3029 custom_field_check=_binary_package_from_same_source, 

3030 ), 

3031 DctrlKnownField( 

3032 "X-Time64-Compat", 

3033 FieldValueClass.SINGLE_VALUE, 

3034 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3035 custom_field_check=_combined_custom_field_check( 

3036 _each_value_match_regex_validation(PKGNAME_REGEX), 

3037 _arch_not_all_only_field_validation, 

3038 ), 

3039 ), 

3040 DctrlKnownField( 

3041 "Description", 

3042 FieldValueClass.FREE_TEXT_FIELD, 

3043 custom_field_check=dctrl_description_validator, 

3044 ), 

3045 DctrlKnownField( 

3046 "XB-Cnf-Visible-Pkgname", 

3047 FieldValueClass.SINGLE_VALUE, 

3048 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

3049 ), 

3050 DctrlKnownField( 

3051 "Homepage", 

3052 FieldValueClass.SINGLE_VALUE, 

3053 show_as_inherited=False, 

3054 custom_field_check=_validate_homepage_field, 

3055 ), 

3056) 

3057_DEP5_HEADER_FIELDS = _fields( 

3058 Deb822KnownField( 

3059 "Format", 

3060 FieldValueClass.SINGLE_VALUE, 

3061 custom_field_check=_use_https_instead_of_http, 

3062 ), 

3063) 

3064_DEP5_FILES_FIELDS = _fields( 

3065 Deb822KnownField( 

3066 "Files", 

3067 FieldValueClass.DEP5_FILE_LIST, 

3068 custom_field_check=_dep5_files_check, 

3069 ), 

3070) 

3071_DEP5_LICENSE_FIELDS = _fields( 

3072 Deb822KnownField( 

3073 "License", 

3074 FieldValueClass.FREE_TEXT_FIELD, 

3075 ), 

3076) 

3077 

3078_DTESTSCTRL_FIELDS = _fields( 

3079 DTestsCtrlKnownField( 

3080 "Architecture", 

3081 FieldValueClass.SPACE_SEPARATED_LIST, 

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

3083 known_values=allowed_values(dpkg_arch_and_wildcards(allow_negations=True)), 

3084 ), 

3085) 

3086_DWATCH_HEADER_FIELDS = _fields() 

3087_DWATCH_TEMPLATE_FIELDS = _fields() 

3088_DWATCH_SOURCE_FIELDS = _fields() 

3089 

3090 

3091@dataclasses.dataclass(slots=True) 

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

3093 stanza_type_name: str 

3094 stanza_fields: Mapping[str, F] 

3095 is_substvars_allowed_in_stanza: bool 

3096 

3097 async def stanza_diagnostics( 

3098 self, 

3099 deb822_file: Deb822FileElement, 

3100 stanza: Deb822ParagraphElement, 

3101 stanza_position_in_file: "TEPosition", 

3102 lint_state: LintState, 

3103 *, 

3104 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3105 confusable_with_stanza_name: str | None = None, 

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

3107 ) -> None: 

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

3109 confusable_with_stanza_metadata is None 

3110 ): 

3111 raise ValueError( 

3112 "confusable_with_stanza_name and confusable_with_stanza_metadata must be used together" 

3113 ) 

3114 _, representation_field_range = self.stanza_representation( 

3115 stanza, 

3116 stanza_position_in_file, 

3117 ) 

3118 known_fields = self.stanza_fields 

3119 self.omitted_field_diagnostics( 

3120 lint_state, 

3121 deb822_file, 

3122 stanza, 

3123 stanza_position_in_file, 

3124 inherit_from_stanza=inherit_from_stanza, 

3125 representation_field_range=representation_field_range, 

3126 ) 

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

3128 

3129 async for kvpair_range, kvpair in lint_state.slow_iter( 

3130 with_range_in_continuous_parts( 

3131 stanza.iter_parts(), 

3132 start_relative_to=stanza_position_in_file, 

3133 ), 

3134 yield_every=1, 

3135 ): 

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

3137 continue 

3138 field_name_token = kvpair.field_token 

3139 field_name = field_name_token.text 

3140 field_name_lc = field_name.lower() 

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

3142 field_name = str(field_name) 

3143 normalized_field_name_lc = self.normalize_field_name(field_name_lc) 

3144 known_field = known_fields.get(normalized_field_name_lc) 

3145 field_value = stanza[field_name] 

3146 kvpair_range_te = kvpair.range_in_parent().relative_to( 

3147 stanza_position_in_file 

3148 ) 

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

3150 kvpair_range_te.start_pos 

3151 ) 

3152 field_position_te = field_range.start_pos 

3153 field_name_typo_detected = False 

3154 dup_field_key = ( 

3155 known_field.name 

3156 if known_field is not None 

3157 else normalized_field_name_lc 

3158 ) 

3159 existing_field_range = seen_fields.get(dup_field_key) 

3160 if existing_field_range is not None: 

3161 existing_field_range[3].append(field_range) 

3162 existing_field_range[4].add(field_name) 

3163 else: 

3164 normalized_field_name = self.normalize_field_name(field_name) 

3165 seen_fields[dup_field_key] = ( 

3166 known_field.name if known_field else field_name, 

3167 normalized_field_name, 

3168 field_range, 

3169 [], 

3170 {field_name}, 

3171 ) 

3172 

3173 if known_field is None: 

3174 candidates = detect_possible_typo( 

3175 normalized_field_name_lc, known_fields 

3176 ) 

3177 if candidates: 

3178 known_field = known_fields[candidates[0]] 

3179 field_range = TERange.from_position_and_size( 

3180 field_position_te, kvpair.field_token.size() 

3181 ) 

3182 field_name_typo_detected = True 

3183 lint_state.emit_diagnostic( 

3184 field_range, 

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

3186 "warning", 

3187 "debputy", 

3188 quickfixes=[ 

3189 propose_correct_text_quick_fix(known_fields[m].name) 

3190 for m in candidates 

3191 ], 

3192 ) 

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

3194 lint_state.emit_diagnostic( 

3195 field_range, 

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

3197 "error", 

3198 "Policy 5.1", 

3199 ) 

3200 continue 

3201 if known_field is None: 

3202 known_else_where = confusable_with_stanza_metadata.stanza_fields.get( 

3203 normalized_field_name_lc 

3204 ) 

3205 if known_else_where is not None: 

3206 lint_state.emit_diagnostic( 

3207 field_range, 

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

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

3210 "error", 

3211 known_else_where.missing_field_authority, 

3212 ) 

3213 continue 

3214 await known_field.field_diagnostics( 

3215 deb822_file, 

3216 kvpair, 

3217 stanza, 

3218 stanza_position_in_file, 

3219 kvpair_range_te, 

3220 lint_state, 

3221 field_name_typo_reported=field_name_typo_detected, 

3222 ) 

3223 

3224 inherit_value = ( 

3225 inherit_from_stanza.get(field_name) if inherit_from_stanza else None 

3226 ) 

3227 

3228 if ( 

3229 known_field.inheritable_from_other_stanza 

3230 and inherit_value is not None 

3231 and field_value == inherit_value 

3232 ): 

3233 quick_fix = propose_remove_range_quick_fix( 

3234 proposed_title="Remove redundant definition" 

3235 ) 

3236 lint_state.emit_diagnostic( 

3237 kvpair_range_te, 

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

3239 "informational", 

3240 "debputy", 

3241 quickfixes=[quick_fix], 

3242 ) 

3243 for ( 

3244 field_name, 

3245 normalized_field_name, 

3246 field_range, 

3247 duplicates, 

3248 used_fields, 

3249 ) in seen_fields.values(): 

3250 if not duplicates: 

3251 continue 

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

3253 via_aliases_msg = " (via aliases)" 

3254 else: 

3255 via_aliases_msg = "" 

3256 for dup_range in duplicates: 

3257 lint_state.emit_diagnostic( 

3258 dup_range, 

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

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

3261 "error", 

3262 "Policy 5.1", 

3263 related_information=[ 

3264 lint_state.related_diagnostic_information( 

3265 field_range, 

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

3267 ), 

3268 ], 

3269 ) 

3270 

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

3272 key_lc = key.lower() 

3273 key_norm = normalize_dctrl_field_name(key_lc) 

3274 return self.stanza_fields[key_norm] 

3275 

3276 def __len__(self) -> int: 

3277 return len(self.stanza_fields) 

3278 

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

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

3281 

3282 def omitted_field_diagnostics( 

3283 self, 

3284 lint_state: LintState, 

3285 deb822_file: Deb822FileElement, 

3286 stanza: Deb822ParagraphElement, 

3287 stanza_position: "TEPosition", 

3288 *, 

3289 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3290 representation_field_range: Range | None = None, 

3291 ) -> None: 

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

3293 _, representation_field_range = self.stanza_representation( 

3294 stanza, 

3295 stanza_position, 

3296 ) 

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

3298 if known_field.name in stanza: 

3299 continue 

3300 

3301 known_field.field_omitted_diagnostics( 

3302 deb822_file, 

3303 representation_field_range, 

3304 stanza, 

3305 stanza_position, 

3306 inherit_from_stanza, 

3307 lint_state, 

3308 ) 

3309 

3310 def _paragraph_representation_field( 

3311 self, 

3312 paragraph: Deb822ParagraphElement, 

3313 ) -> Deb822KeyValuePairElement: 

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

3315 

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

3317 return field_name 

3318 

3319 def stanza_representation( 

3320 self, 

3321 stanza: Deb822ParagraphElement, 

3322 stanza_position: TEPosition, 

3323 ) -> tuple[Deb822KeyValuePairElement, TERange]: 

3324 representation_field = self._paragraph_representation_field(stanza) 

3325 representation_field_range = representation_field.range_in_parent().relative_to( 

3326 stanza_position 

3327 ) 

3328 return representation_field, representation_field_range 

3329 

3330 def reformat_stanza( 

3331 self, 

3332 effective_preference: "EffectiveFormattingPreference", 

3333 stanza: Deb822ParagraphElement, 

3334 stanza_range: TERange, 

3335 formatter: FormatterCallback, 

3336 position_codec: LintCapablePositionCodec, 

3337 lines: list[str], 

3338 ) -> Iterable[TextEdit]: 

3339 for field_name in stanza: 

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

3341 if known_field is None: 

3342 continue 

3343 kvpair = stanza.get_kvpair_element(field_name) 

3344 yield from known_field.reformat_field( 

3345 effective_preference, 

3346 stanza_range, 

3347 kvpair, 

3348 formatter, 

3349 position_codec, 

3350 lines, 

3351 ) 

3352 

3353 

3354@dataclasses.dataclass(slots=True) 

3355class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3356 pass 

3357 

3358 

3359@dataclasses.dataclass(slots=True) 

3360class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]): 

3361 

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

3363 return normalize_dctrl_field_name(field_name) 

3364 

3365 

3366@dataclasses.dataclass(slots=True) 

3367class DTestsCtrlStanzaMetadata(StanzaMetadata[DTestsCtrlKnownField]): 

3368 

3369 def omitted_field_diagnostics( 

3370 self, 

3371 lint_state: LintState, 

3372 deb822_file: Deb822FileElement, 

3373 stanza: Deb822ParagraphElement, 

3374 stanza_position: "TEPosition", 

3375 *, 

3376 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3377 representation_field_range: Range | None = None, 

3378 ) -> None: 

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

3380 _, representation_field_range = self.stanza_representation( 

3381 stanza, 

3382 stanza_position, 

3383 ) 

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

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

3386 lint_state.emit_diagnostic( 

3387 representation_field_range, 

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

3389 "error", 

3390 # TODO: Better authority_reference 

3391 auth_ref, 

3392 ) 

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

3394 lint_state.emit_diagnostic( 

3395 representation_field_range, 

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

3397 "error", 

3398 # TODO: Better authority_reference 

3399 auth_ref, 

3400 ) 

3401 

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

3403 # always do the super call. 

3404 super(DTestsCtrlStanzaMetadata, self).omitted_field_diagnostics( 

3405 lint_state, 

3406 deb822_file, 

3407 stanza, 

3408 stanza_position, 

3409 representation_field_range=representation_field_range, 

3410 inherit_from_stanza=inherit_from_stanza, 

3411 ) 

3412 

3413 

3414@dataclasses.dataclass(slots=True) 

3415class DebianWatchStanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3416 

3417 def omitted_field_diagnostics( 

3418 self, 

3419 lint_state: LintState, 

3420 deb822_file: Deb822FileElement, 

3421 stanza: Deb822ParagraphElement, 

3422 stanza_position: "TEPosition", 

3423 *, 

3424 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3425 representation_field_range: Range | None = None, 

3426 ) -> None: 

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

3428 _, representation_field_range = self.stanza_representation( 

3429 stanza, 

3430 stanza_position, 

3431 ) 

3432 

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

3434 self.stanza_type_name != "Header" 

3435 and "Source" not in stanza 

3436 and "Template" not in stanza 

3437 ): 

3438 lint_state.emit_diagnostic( 

3439 representation_field_range, 

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

3441 "error", 

3442 # TODO: Better authority_reference 

3443 "debputy", 

3444 ) 

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

3446 # call until this error is resolved. 

3447 return 

3448 

3449 super(DebianWatchStanzaMetadata, self).omitted_field_diagnostics( 

3450 lint_state, 

3451 deb822_file, 

3452 stanza, 

3453 stanza_position, 

3454 representation_field_range=representation_field_range, 

3455 inherit_from_stanza=inherit_from_stanza, 

3456 ) 

3457 

3458 

3459def lsp_reference_data_dir() -> str: 

3460 return os.path.join( 

3461 os.path.dirname(__file__), 

3462 "data", 

3463 ) 

3464 

3465 

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

3467 

3468 def __init__(self) -> None: 

3469 self._is_initialized = False 

3470 self._data: Deb822ReferenceData | None = None 

3471 

3472 @property 

3473 def reference_data_basename(self) -> str: 

3474 raise NotImplementedError 

3475 

3476 def _new_field( 

3477 self, 

3478 name: str, 

3479 field_value_type: FieldValueClass, 

3480 ) -> F: 

3481 raise NotImplementedError 

3482 

3483 def _reference_data(self) -> Deb822ReferenceData: 

3484 ref = self._data 

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

3486 return ref 

3487 

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

3489 self.reference_data_basename 

3490 ) 

3491 

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

3493 raw = MANIFEST_YAML.load(fd) 

3494 

3495 attr_path = AttributePath.root_path(p) 

3496 try: 

3497 ref = DEB822_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

3498 except ManifestParseException as e: 

3499 raise ValueError( 

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

3501 ) from e 

3502 self._data = ref 

3503 return ref 

3504 

3505 @property 

3506 def is_initialized(self) -> bool: 

3507 return self._is_initialized 

3508 

3509 def ensure_initialized(self) -> None: 

3510 if self.is_initialized: 

3511 return 

3512 # Enables us to use __getitem__ 

3513 self._is_initialized = True 

3514 ref_data = self._reference_data() 

3515 ref_defs = ref_data.get("definitions") 

3516 variables = {} 

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

3518 for ref_variable in ref_variables: 

3519 name = ref_variable["name"] 

3520 fallback = ref_variable["fallback"] 

3521 variables[name] = fallback 

3522 

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

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

3525 return None 

3526 try: 

3527 return template.format(**variables) 

3528 except ValueError as e: 

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

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

3531 

3532 for ref_stanza_type in ref_data["stanza_types"]: 

3533 stanza_name = ref_stanza_type["stanza_name"] 

3534 stanza = self[stanza_name] 

3535 stanza_fields = dict(stanza.stanza_fields) 

3536 stanza.stanza_fields = stanza_fields 

3537 for ref_field in ref_stanza_type["fields"]: 

3538 _resolve_field( 

3539 ref_field, 

3540 stanza_fields, 

3541 self._new_field, 

3542 _resolve_doc, 

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

3544 ) 

3545 

3546 def file_metadata_applies_to_file( 

3547 self, 

3548 deb822_file: Deb822FileElement | None, 

3549 ) -> bool: 

3550 return deb822_file is not None 

3551 

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

3553 return self.guess_stanza_classification_by_idx(stanza_idx) 

3554 

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

3556 raise NotImplementedError 

3557 

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

3559 raise NotImplementedError 

3560 

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

3562 raise NotImplementedError 

3563 

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

3565 try: 

3566 return self[item] 

3567 except KeyError: 

3568 return None 

3569 

3570 def reformat( 

3571 self, 

3572 effective_preference: "EffectiveFormattingPreference", 

3573 deb822_file: Deb822FileElement, 

3574 formatter: FormatterCallback, 

3575 _content: str, 

3576 position_codec: LintCapablePositionCodec, 

3577 lines: list[str], 

3578 ) -> Iterable[TextEdit]: 

3579 stanza_idx = -1 

3580 for token_or_element in deb822_file.iter_parts(): 

3581 if isinstance(token_or_element, Deb822ParagraphElement): 

3582 stanza_range = token_or_element.range_in_parent() 

3583 stanza_idx += 1 

3584 stanza_metadata = self.classify_stanza(token_or_element, stanza_idx) 

3585 yield from stanza_metadata.reformat_stanza( 

3586 effective_preference, 

3587 token_or_element, 

3588 stanza_range, 

3589 formatter, 

3590 position_codec, 

3591 lines, 

3592 ) 

3593 else: 

3594 token_range = token_or_element.range_in_parent() 

3595 yield from trim_end_of_line_whitespace( 

3596 position_codec, 

3597 lines, 

3598 line_range=range( 

3599 token_range.start_pos.line_position, 

3600 token_range.end_pos.line_position, 

3601 ), 

3602 ) 

3603 

3604 

3605_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata( 

3606 "Source", 

3607 SOURCE_FIELDS, 

3608 is_substvars_allowed_in_stanza=False, 

3609) 

3610_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata( 

3611 "Package", 

3612 BINARY_FIELDS, 

3613 is_substvars_allowed_in_stanza=True, 

3614) 

3615 

3616_DEP5_HEADER_STANZA = Dep5StanzaMetadata( 

3617 "Header", 

3618 _DEP5_HEADER_FIELDS, 

3619 is_substvars_allowed_in_stanza=False, 

3620) 

3621_DEP5_FILES_STANZA = Dep5StanzaMetadata( 

3622 "Files", 

3623 _DEP5_FILES_FIELDS, 

3624 is_substvars_allowed_in_stanza=False, 

3625) 

3626_DEP5_LICENSE_STANZA = Dep5StanzaMetadata( 

3627 "License", 

3628 _DEP5_LICENSE_FIELDS, 

3629 is_substvars_allowed_in_stanza=False, 

3630) 

3631 

3632_DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata( 

3633 "Tests", 

3634 _DTESTSCTRL_FIELDS, 

3635 is_substvars_allowed_in_stanza=False, 

3636) 

3637 

3638_WATCH_HEADER_HEADER_STANZA = DebianWatchStanzaMetadata( 

3639 "Header", 

3640 _DWATCH_HEADER_FIELDS, 

3641 is_substvars_allowed_in_stanza=False, 

3642) 

3643_WATCH_SOURCE_STANZA = DebianWatchStanzaMetadata( 

3644 "Source", 

3645 _DWATCH_SOURCE_FIELDS, 

3646 is_substvars_allowed_in_stanza=False, 

3647) 

3648 

3649 

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

3651 

3652 @property 

3653 def reference_data_basename(self) -> str: 

3654 return "debian_copyright_reference_data.yaml" 

3655 

3656 def _new_field( 

3657 self, 

3658 name: str, 

3659 field_value_type: FieldValueClass, 

3660 ) -> F: 

3661 return Deb822KnownField(name, field_value_type) 

3662 

3663 def file_metadata_applies_to_file( 

3664 self, 

3665 deb822_file: Deb822FileElement | None, 

3666 ) -> bool: 

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

3668 return False 

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

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

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

3672 return False 

3673 

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

3675 if part.is_error: 

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

3677 return False 

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

3679 break 

3680 return True 

3681 

3682 def classify_stanza( 

3683 self, 

3684 stanza: Deb822ParagraphElement, 

3685 stanza_idx: int, 

3686 ) -> Dep5StanzaMetadata: 

3687 self.ensure_initialized() 

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

3689 return _DEP5_HEADER_STANZA 

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

3691 if "Files" in stanza: 

3692 return _DEP5_FILES_STANZA 

3693 return _DEP5_LICENSE_STANZA 

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

3695 

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

3697 self.ensure_initialized() 

3698 if stanza_idx == 0: 

3699 return _DEP5_HEADER_STANZA 

3700 if stanza_idx > 0: 

3701 return _DEP5_FILES_STANZA 

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

3703 

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

3705 self.ensure_initialized() 

3706 # Order assumption made in the LSP code. 

3707 yield _DEP5_HEADER_STANZA 

3708 yield _DEP5_FILES_STANZA 

3709 yield _DEP5_LICENSE_STANZA 

3710 

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

3712 self.ensure_initialized() 

3713 if item == "Header": 

3714 return _DEP5_HEADER_STANZA 

3715 if item == "Files": 

3716 return _DEP5_FILES_STANZA 

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

3718 return _DEP5_LICENSE_STANZA 

3719 raise KeyError(item) 

3720 

3721 

3722class DebianWatch5FileMetadata( 

3723 Deb822FileMetadata[DebianWatchStanzaMetadata, Deb822KnownField] 

3724): 

3725 

3726 @property 

3727 def reference_data_basename(self) -> str: 

3728 return "debian_watch_reference_data.yaml" 

3729 

3730 def _new_field( 

3731 self, 

3732 name: str, 

3733 field_value_type: FieldValueClass, 

3734 ) -> F: 

3735 return Deb822KnownField(name, field_value_type) 

3736 

3737 def file_metadata_applies_to_file( 

3738 self, deb822_file: Deb822FileElement | None 

3739 ) -> bool: 

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

3741 return False 

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

3743 

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

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

3746 return False 

3747 

3748 try: 

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

3750 return False 

3751 except (ValueError, IndexError, TypeError): 

3752 return False 

3753 

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

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

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

3757 return False 

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

3759 break 

3760 return True 

3761 

3762 def classify_stanza( 

3763 self, 

3764 stanza: Deb822ParagraphElement, 

3765 stanza_idx: int, 

3766 ) -> DebianWatchStanzaMetadata: 

3767 self.ensure_initialized() 

3768 if stanza_idx == 0: 

3769 return _WATCH_HEADER_HEADER_STANZA 

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

3771 return _WATCH_SOURCE_STANZA 

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

3773 

3774 def guess_stanza_classification_by_idx( 

3775 self, stanza_idx: int 

3776 ) -> DebianWatchStanzaMetadata: 

3777 self.ensure_initialized() 

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

3779 return _WATCH_HEADER_HEADER_STANZA 

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

3781 return _WATCH_SOURCE_STANZA 

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

3783 

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

3785 self.ensure_initialized() 

3786 # Order assumption made in the LSP code. 

3787 yield _WATCH_HEADER_HEADER_STANZA 

3788 yield _WATCH_SOURCE_STANZA 

3789 

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

3791 self.ensure_initialized() 

3792 if item == "Header": 

3793 return _WATCH_HEADER_HEADER_STANZA 

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

3795 return _WATCH_SOURCE_STANZA 

3796 raise KeyError(item) 

3797 

3798 

3799def _resolve_keyword( 

3800 ref_value: StaticValue, 

3801 known_values: dict[str, Keyword], 

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

3803 translation_context: str, 

3804) -> None: 

3805 value_key = ref_value["value"] 

3806 changes = { 

3807 "translation_context": translation_context, 

3808 } 

3809 try: 

3810 known_value = known_values[value_key] 

3811 except KeyError: 

3812 known_value = Keyword(value_key) 

3813 known_values[value_key] = known_value 

3814 else: 

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

3816 raise ValueError( 

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

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

3819 ) 

3820 value_doc = ref_value.get("documentation") 

3821 if value_doc is not None: 

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

3823 changes["long_description"] = resolve_template( 

3824 value_doc.get("long_description") 

3825 ) 

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

3827 changes["is_exclusive"] = is_exclusive 

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

3829 changes["sort_text"] = sort_key 

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

3831 changes["usage_hint"] = usage_hint 

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

3833 known_value = known_value.replace(**changes) 

3834 known_values[value_key] = known_value 

3835 

3836 _expand_aliases( 

3837 known_value, 

3838 known_values, 

3839 operator.attrgetter("value"), 

3840 ref_value.get("aliases"), 

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

3842 ) 

3843 

3844 

3845def _resolve_field( 

3846 ref_field: Deb822Field, 

3847 stanza_fields: dict[str, F], 

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

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

3850 translation_context: str, 

3851) -> None: 

3852 field_name = ref_field["canonical_name"] 

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

3854 doc = ref_field.get("documentation") 

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

3856 norm_field_name = normalize_dctrl_field_name(field_name.lower()) 

3857 

3858 try: 

3859 field = stanza_fields[norm_field_name] 

3860 except KeyError: 

3861 field = field_constructor( 

3862 field_name, 

3863 field_value_type, 

3864 ) 

3865 stanza_fields[norm_field_name] = field 

3866 else: 

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

3868 _error( 

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

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

3871 ) 

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

3873 _error( 

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

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

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

3877 ) 

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

3879 raise ValueError( 

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

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

3882 ) 

3883 

3884 if doc is not None: 

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

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

3887 

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

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

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

3891 field.deprecated_with_no_replacement = ref_field.get( 

3892 "is_obsolete_without_replacement", False 

3893 ) 

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

3895 field.translation_context = translation_context 

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

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

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

3899 field.unknown_value_severity = ( 

3900 None if unknown_value_severity == "none" else unknown_value_severity 

3901 ) 

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

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

3904 field.is_substvars_disabled_even_if_allowed_by_stanza = not ref_field.get( 

3905 "supports_substvars", 

3906 True, 

3907 ) 

3908 field.inheritable_from_other_stanza = ref_field.get( 

3909 "inheritable_from_other_stanza", 

3910 False, 

3911 ) 

3912 

3913 known_values = field.known_values 

3914 if known_values is None: 

3915 known_values = {} 

3916 else: 

3917 known_values = dict(known_values) 

3918 

3919 for ref_value in ref_values: 

3920 _resolve_keyword(ref_value, known_values, resolve_template, translation_context) 

3921 

3922 if known_values: 

3923 field.known_values = known_values 

3924 

3925 _expand_aliases( 

3926 field, 

3927 stanza_fields, 

3928 operator.attrgetter("name"), 

3929 ref_field.get("aliases"), 

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

3931 ) 

3932 

3933 

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

3935 

3936 

3937def _expand_aliases( 

3938 item: A, 

3939 item_container: dict[str, A], 

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

3941 aliases_ref: list[Alias] | None, 

3942 doc_template: str, 

3943) -> None: 

3944 if aliases_ref is None: 

3945 return 

3946 name = canonical_name_resolver(item) 

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

3948 for alias_ref in aliases_ref: 

3949 alias_name = alias_ref["alias"] 

3950 alias_doc = item.long_description 

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

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

3953 if alias_doc: 

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

3955 else: 

3956 alias_doc = doc_suffix 

3957 alias_field = item.replace( 

3958 long_description=alias_doc, 

3959 is_alias_of=name, 

3960 is_completion_suggestion=is_completion_suggestion, 

3961 ) 

3962 alias_key = alias_name.lower() 

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

3964 existing_name = canonical_name_resolver(item_container[alias_key]) 

3965 assert ( 

3966 existing_name is not None 

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

3968 raise ValueError( 

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

3970 ) 

3971 item_container[alias_key] = alias_field 

3972 

3973 

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

3975 

3976 @property 

3977 def reference_data_basename(self) -> str: 

3978 return "debian_control_reference_data.yaml" 

3979 

3980 def _new_field( 

3981 self, 

3982 name: str, 

3983 field_value_type: FieldValueClass, 

3984 ) -> F: 

3985 return DctrlKnownField(name, field_value_type) 

3986 

3987 def guess_stanza_classification_by_idx( 

3988 self, 

3989 stanza_idx: int, 

3990 ) -> DctrlStanzaMetadata: 

3991 self.ensure_initialized() 

3992 if stanza_idx == 0: 

3993 return _DCTRL_SOURCE_STANZA 

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

3995 return _DCTRL_PACKAGE_STANZA 

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

3997 

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

3999 self.ensure_initialized() 

4000 # Order assumption made in the LSP code. 

4001 yield _DCTRL_SOURCE_STANZA 

4002 yield _DCTRL_PACKAGE_STANZA 

4003 

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

4005 self.ensure_initialized() 

4006 if item == "Source": 

4007 return _DCTRL_SOURCE_STANZA 

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

4009 return _DCTRL_PACKAGE_STANZA 

4010 raise KeyError(item) 

4011 

4012 def reformat( 

4013 self, 

4014 effective_preference: "EffectiveFormattingPreference", 

4015 deb822_file: Deb822FileElement, 

4016 formatter: FormatterCallback, 

4017 content: str, 

4018 position_codec: LintCapablePositionCodec, 

4019 lines: list[str], 

4020 ) -> Iterable[TextEdit]: 

4021 edits = list( 

4022 super().reformat( 

4023 effective_preference, 

4024 deb822_file, 

4025 formatter, 

4026 content, 

4027 position_codec, 

4028 lines, 

4029 ) 

4030 ) 

4031 

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

4033 not effective_preference.deb822_normalize_stanza_order 

4034 or deb822_file.find_first_error_element() is not None 

4035 ): 

4036 return edits 

4037 names = [] 

4038 for idx, stanza in enumerate(deb822_file): 

4039 if idx < 2: 

4040 continue 

4041 name = stanza.get("Package") 

4042 if name is None: 

4043 return edits 

4044 names.append(name) 

4045 

4046 reordered = sorted(names) 

4047 if names == reordered: 

4048 return edits 

4049 

4050 if edits: 

4051 content = apply_text_edits(content, lines, edits) 

4052 lines = content.splitlines(keepends=True) 

4053 deb822_file = parse_deb822_file( 

4054 lines, 

4055 accept_files_with_duplicated_fields=True, 

4056 accept_files_with_error_tokens=True, 

4057 ) 

4058 

4059 stanzas = list(deb822_file) 

4060 reordered_stanza = stanzas[:2] + sorted( 

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

4062 ) 

4063 bits = [] 

4064 stanza_idx = 0 

4065 for token_or_element in deb822_file.iter_parts(): 

4066 if isinstance(token_or_element, Deb822ParagraphElement): 

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

4068 stanza_idx += 1 

4069 else: 

4070 bits.append(token_or_element.convert_to_text()) 

4071 

4072 new_content = "".join(bits) 

4073 

4074 return [ 

4075 TextEdit( 

4076 Range( 

4077 Position(0, 0), 

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

4079 ), 

4080 new_content, 

4081 ) 

4082 ] 

4083 

4084 

4085class DTestsCtrlFileMetadata( 

4086 Deb822FileMetadata[DTestsCtrlStanzaMetadata, DTestsCtrlKnownField] 

4087): 

4088 

4089 @property 

4090 def reference_data_basename(self) -> str: 

4091 return "debian_tests_control_reference_data.yaml" 

4092 

4093 def _new_field( 

4094 self, 

4095 name: str, 

4096 field_value_type: FieldValueClass, 

4097 ) -> F: 

4098 return DTestsCtrlKnownField(name, field_value_type) 

4099 

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

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

4102 self.ensure_initialized() 

4103 return _DTESTSCTRL_STANZA 

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

4105 

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

4107 self.ensure_initialized() 

4108 yield _DTESTSCTRL_STANZA 

4109 

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

4111 self.ensure_initialized() 

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

4113 return _DTESTSCTRL_STANZA 

4114 raise KeyError(item) 

4115 

4116 

4117TRANSLATABLE_DEB822_FILE_METADATA: Sequence[ 

4118 Callable[[], Deb822FileMetadata[Any, Any]] 

4119] = [ 

4120 DctrlFileMetadata, 

4121 Dep5FileMetadata, 

4122 DTestsCtrlFileMetadata, 

4123]