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

1399 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-10-04 09:01 +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.4") 

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 This is an architecture-dependent package, and needs to be 

305 compiled for each and every architecture. 

306 

307 The name `any` refers to that this is an architecture 

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

309 dpkg. 

310 """), 

311 ) 

312 yield Keyword( 

313 "all", 

314 is_exclusive=True, 

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

316 long_description=textwrap.dedent("""\ 

317 The package is an architecture independent package. This is 

318 typically appropriate for packages containing only scripts, 

319 data or documentation. 

320 

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

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

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

324 """), 

325 ) 

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

327 yield arch_name 

328 if allow_negations: 

329 yield f"!{arch_name}" 

330 cpu_wc = "any-" + quad_tuple.cpu_name 

331 os_wc = quad_tuple.os_name + "-any" 

332 if cpu_wc not in wildcards: 

333 yield cpu_wc 

334 if allow_negations: 

335 yield f"!{cpu_wc}" 

336 wildcards.add(cpu_wc) 

337 if os_wc not in wildcards: 

338 yield os_wc 

339 if allow_negations: 

340 yield f"!{os_wc}" 

341 wildcards.add(os_wc) 

342 # Add the remaining wildcards 

343 

344 

345@functools.lru_cache 

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

347 dpkg_arch_table = DpkgArchTable.load_arch_table() 

348 return frozenset( 

349 all_architectures_and_wildcards( 

350 dpkg_arch_table._arch2table, 

351 allow_negations=allow_negations, 

352 ) 

353 ) 

354 

355 

356def extract_first_value_and_position( 

357 kvpair: Deb822KeyValuePairElement, 

358 stanza_pos: "TEPosition", 

359 *, 

360 interpretation: Interpretation[ 

361 Deb822ParsedTokenList[Any, Any] 

362 ] = LIST_SPACE_SEPARATED_INTERPRETATION, 

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

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

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

366 kvpair_pos 

367 ) 

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

369 v = value_ref.value 

370 section_value_loc = value_ref.locatable 

371 value_range_te = section_value_loc.range_in_parent().relative_to( 

372 value_element_pos 

373 ) 

374 return v, value_range_te 

375 return None, None 

376 

377 

378def _sv_field_validation( 

379 known_field: "F", 

380 _deb822_file: Deb822FileElement, 

381 kvpair: Deb822KeyValuePairElement, 

382 _kvpair_range: "TERange", 

383 _field_name_range_te: "TERange", 

384 _stanza: Deb822ParagraphElement, 

385 stanza_position: "TEPosition", 

386 lint_state: LintState, 

387) -> None: 

388 sv_value, sv_value_range = extract_first_value_and_position( 

389 kvpair, 

390 stanza_position, 

391 ) 

392 m = _RE_SV.fullmatch(sv_value) 

393 if m is None: 

394 lint_state.emit_diagnostic( 

395 sv_value_range, 

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

397 "warning", 

398 known_field.unknown_value_authority, 

399 ) 

400 return 

401 

402 sv_version = Version(sv_value) 

403 if sv_version < CURRENT_STANDARDS_VERSION: 

404 lint_state.emit_diagnostic( 

405 sv_value_range, 

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

407 "informational", 

408 known_field.unknown_value_authority, 

409 ) 

410 return 

411 extra = m.group(2) 

412 if extra: 

413 extra_len = lint_state.position_codec.client_num_units(extra) 

414 lint_state.emit_diagnostic( 

415 TERange.between( 

416 TEPosition( 

417 sv_value_range.end_pos.line_position, 

418 sv_value_range.end_pos.cursor_position - extra_len, 

419 ), 

420 sv_value_range.end_pos, 

421 ), 

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

423 "informational", 

424 known_field.unknown_value_authority, 

425 quickfixes=[ 

426 propose_remove_range_quick_fix( 

427 proposed_title="Remove unnecessary version part" 

428 ) 

429 ], 

430 ) 

431 

432 

433def _dctrl_ma_field_validation( 

434 _known_field: "F", 

435 _deb822_file: Deb822FileElement, 

436 _kvpair: Deb822KeyValuePairElement, 

437 _kvpair_range: "TERange", 

438 _field_name_range: "TERange", 

439 stanza: Deb822ParagraphElement, 

440 stanza_position: "TEPosition", 

441 lint_state: LintState, 

442) -> None: 

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

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

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

446 ma_value, ma_value_range = extract_first_value_and_position( 

447 ma_kvpair, 

448 stanza_position, 

449 ) 

450 if ma_value == "same": 

451 lint_state.emit_diagnostic( 

452 ma_value_range, 

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

454 "error", 

455 "debputy", 

456 ) 

457 

458 

459def _udeb_only_field_validation( 

460 known_field: "F", 

461 _deb822_file: Deb822FileElement, 

462 _kvpair: Deb822KeyValuePairElement, 

463 _kvpair_range: "TERange", 

464 field_name_range: "TERange", 

465 stanza: Deb822ParagraphElement, 

466 _stanza_position: "TEPosition", 

467 lint_state: LintState, 

468) -> None: 

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

470 if package_type != "udeb": 

471 lint_state.emit_diagnostic( 

472 field_name_range, 

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

474 "warning", 

475 "debputy", 

476 ) 

477 

478 

479def _not_applicable_to_udeb_field_validation( 

480 known_field: "F", 

481 _deb822_file: Deb822FileElement, 

482 _kvpair: Deb822KeyValuePairElement, 

483 kvpair_range: "TERange", 

484 _field_name_range: "TERange", 

485 stanza: Deb822ParagraphElement, 

486 _stanza_position: "TEPosition", 

487 lint_state: LintState, 

488) -> None: 

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

490 if package_type == "udeb": 

491 lint_state.emit_diagnostic( 

492 kvpair_range, 

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

494 "warning", 

495 "debputy", 

496 quickfixes=[propose_remove_range_quick_fix()], 

497 ) 

498 

499 

500def _complete_only_in_arch_dep_pkgs( 

501 stanza_parts: Iterable[Deb822ParagraphElement], 

502) -> bool: 

503 for stanza in stanza_parts: 

504 arch = stanza.get("Architecture") 

505 if arch is None: 

506 continue 

507 archs = arch.split() 

508 return "all" not in archs 

509 return False 

510 

511 

512def _complete_only_for_udeb_pkgs( 

513 stanza_parts: Iterable[Deb822ParagraphElement], 

514) -> bool: 

515 for stanza in stanza_parts: 

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

517 pkg_type = stanza.get(option) 

518 if pkg_type is not None: 

519 return pkg_type == "udeb" 

520 return False 

521 

522 

523def _arch_not_all_only_field_validation( 

524 known_field: "F", 

525 _deb822_file: Deb822FileElement, 

526 _kvpair: Deb822KeyValuePairElement, 

527 _kvpair_range_te: "TERange", 

528 field_name_range_te: "TERange", 

529 stanza: Deb822ParagraphElement, 

530 _stanza_position: "TEPosition", 

531 lint_state: LintState, 

532) -> None: 

533 architecture = stanza.get("Architecture") 

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

535 lint_state.emit_diagnostic( 

536 field_name_range_te, 

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

538 "warning", 

539 "debputy", 

540 ) 

541 

542 

543def _binary_package_from_same_source( 

544 known_field: "F", 

545 _deb822_file: Deb822FileElement, 

546 _kvpair: Deb822KeyValuePairElement, 

547 kvpair_range: "TERange", 

548 _field_name_range: "TERange", 

549 stanza: Deb822ParagraphElement, 

550 stanza_position: "TEPosition", 

551 lint_state: LintState, 

552) -> None: 

553 doc_main_package_kvpair = stanza.get_kvpair_element( 

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

555 ) 

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

557 lint_state.emit_diagnostic( 

558 kvpair_range, 

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

560 "warning", 

561 "debputy", 

562 quickfixes=[propose_remove_range_quick_fix()], 

563 ) 

564 return 

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

566 doc_main_package, value_range = extract_first_value_and_position( 

567 doc_main_package_kvpair, 

568 stanza_position, 

569 ) 

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

571 return 

572 lint_state.emit_diagnostic( 

573 value_range, 

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

575 "error", 

576 "debputy", 

577 quickfixes=[ 

578 propose_correct_text_quick_fix(name) 

579 for name in lint_state.binary_packages 

580 ], 

581 ) 

582 

583 

584def _single_line_span_range_relative_to_pos( 

585 span: tuple[int, int], 

586 relative_to: "TEPosition", 

587) -> Range: 

588 return TERange( 

589 TEPosition( 

590 relative_to.line_position, 

591 relative_to.cursor_position + span[0], 

592 ), 

593 TEPosition( 

594 relative_to.line_position, 

595 relative_to.cursor_position + span[1], 

596 ), 

597 ) 

598 

599 

600def _check_extended_description_line( 

601 description_value_line: Deb822ValueLineElement, 

602 description_line_range_te: "TERange", 

603 package: str | None, 

604 lint_state: LintState, 

605) -> None: 

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

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

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

609 return 

610 description_line_with_leading_space = ( 

611 description_value_line.convert_to_text().rstrip() 

612 ) 

613 try: 

614 idx = description_line_with_leading_space.index( 

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

616 ) 

617 except ValueError: 

618 pass 

619 else: 

620 template_span = idx, idx + len( 

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

622 ) 

623 lint_state.emit_diagnostic( 

624 _single_line_span_range_relative_to_pos( 

625 template_span, 

626 description_line_range_te.start_pos, 

627 ), 

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

629 "error", 

630 "debputy", 

631 ) 

632 if len(description_line_with_leading_space) > 80: 

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

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

635 # 

636 # See also debputy#122 

637 span = 80, len(description_line_with_leading_space) 

638 lint_state.emit_diagnostic( 

639 _single_line_span_range_relative_to_pos( 

640 span, 

641 description_line_range_te.start_pos, 

642 ), 

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

644 "warning", 

645 "debputy", 

646 ) 

647 

648 

649def _check_synopsis( 

650 synopsis_value_line: Deb822ValueLineElement, 

651 synopsis_range_te: "TERange", 

652 field_name_range_te: "TERange", 

653 package: str | None, 

654 lint_state: LintState, 

655) -> None: 

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

657 assert synopsis_value_line.comment_element is None 

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

659 if not synopsis_text_with_leading_space: 

660 lint_state.emit_diagnostic( 

661 field_name_range_te, 

662 "Package synopsis is missing", 

663 "warning", 

664 "debputy", 

665 ) 

666 return 

667 synopsis_text_trimmed = synopsis_text_with_leading_space.lstrip() 

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

669 starts_with_article = _RE_SYNOPSIS_STARTS_WITH_ARTICLE.search( 

670 synopsis_text_with_leading_space 

671 ) 

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

673 if starts_with_article: 

674 lint_state.emit_diagnostic( 

675 _single_line_span_range_relative_to_pos( 

676 starts_with_article.span(1), 

677 synopsis_range_te.start_pos, 

678 ), 

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

680 "warning", 

681 "DevRef 6.2.2", 

682 ) 

683 if len(synopsis_text_trimmed) >= 80: 

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

685 span = synopsis_offset + 79, len(synopsis_text_with_leading_space) 

686 lint_state.emit_diagnostic( 

687 _single_line_span_range_relative_to_pos( 

688 span, 

689 synopsis_range_te.start_pos, 

690 ), 

691 "Package synopsis is too long.", 

692 "warning", 

693 "Policy 3.4.1", 

694 ) 

695 if template_match := _RE_SYNOPSIS_IS_TEMPLATE.match( 

696 synopsis_text_with_leading_space 

697 ): 

698 lint_state.emit_diagnostic( 

699 _single_line_span_range_relative_to_pos( 

700 template_match.span(1), 

701 synopsis_range_te.start_pos, 

702 ), 

703 "Package synopsis is a placeholder", 

704 "warning", 

705 "debputy", 

706 ) 

707 elif too_short_match := _RE_SYNOPSIS_IS_TOO_SHORT.match( 

708 synopsis_text_with_leading_space 

709 ): 

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

711 lint_state.emit_diagnostic( 

712 _single_line_span_range_relative_to_pos( 

713 too_short_match.span(1), 

714 synopsis_range_te.start_pos, 

715 ), 

716 "Package synopsis is too short", 

717 "warning", 

718 "debputy", 

719 ) 

720 

721 

722def dctrl_description_validator( 

723 _known_field: "F", 

724 _deb822_file: Deb822FileElement, 

725 kvpair: Deb822KeyValuePairElement, 

726 kvpair_range_te: "TERange", 

727 _field_name_range: "TERange", 

728 stanza: Deb822ParagraphElement, 

729 _stanza_position: "TEPosition", 

730 lint_state: LintState, 

731) -> None: 

732 value_lines = kvpair.value_element.value_lines 

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

734 return 

735 package = stanza.get("Package") 

736 synopsis_value_line = value_lines[0] 

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

738 kvpair_range_te.start_pos 

739 ) 

740 synopsis_line_range_te = synopsis_value_line.range_in_parent().relative_to( 

741 value_range_te.start_pos 

742 ) 

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

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

745 kvpair_range_te.start_pos 

746 ) 

747 _check_synopsis( 

748 synopsis_value_line, 

749 synopsis_line_range_te, 

750 field_name_range_te, 

751 package, 

752 lint_state, 

753 ) 

754 description_lines = value_lines[1:] 

755 else: 

756 description_lines = value_lines 

757 for description_line in description_lines: 

758 description_line_range_te = description_line.range_in_parent().relative_to( 

759 value_range_te.start_pos 

760 ) 

761 _check_extended_description_line( 

762 description_line, 

763 description_line_range_te, 

764 package, 

765 lint_state, 

766 ) 

767 

768 

769def _has_packaging_expected_file( 

770 name: str, 

771 msg: str, 

772 severity: LintSeverity = "error", 

773) -> CustomFieldCheck: 

774 

775 def _impl( 

776 _known_field: "F", 

777 _deb822_file: Deb822FileElement, 

778 _kvpair: Deb822KeyValuePairElement, 

779 kvpair_range_te: "TERange", 

780 _field_name_range_te: "TERange", 

781 _stanza: Deb822ParagraphElement, 

782 _stanza_position: "TEPosition", 

783 lint_state: LintState, 

784 ) -> None: 

785 debian_dir = lint_state.debian_dir 

786 if debian_dir is None: 

787 return 

788 cpy = debian_dir.lookup(name) 

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

790 lint_state.emit_diagnostic( 

791 kvpair_range_te, 

792 msg, 

793 severity, 

794 "debputy", 

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

796 ) 

797 

798 return _impl 

799 

800 

801_check_missing_debian_rules = _has_packaging_expected_file( 

802 "rules", 

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

804) 

805 

806 

807def _has_build_instructions( 

808 known_field: "F", 

809 deb822_file: Deb822FileElement, 

810 kvpair: Deb822KeyValuePairElement, 

811 kvpair_range_te: "TERange", 

812 field_name_range_te: "TERange", 

813 stanza: Deb822ParagraphElement, 

814 stanza_position: "TEPosition", 

815 lint_state: LintState, 

816) -> None: 

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

818 return 

819 

820 _check_missing_debian_rules( 

821 known_field, 

822 deb822_file, 

823 kvpair, 

824 kvpair_range_te, 

825 field_name_range_te, 

826 stanza, 

827 stanza_position, 

828 lint_state, 

829 ) 

830 

831 

832def _canonical_maintainer_name( 

833 known_field: "F", 

834 _deb822_file: Deb822FileElement, 

835 kvpair: Deb822KeyValuePairElement, 

836 kvpair_range_te: "TERange", 

837 _field_name_range_te: "TERange", 

838 _stanza: Deb822ParagraphElement, 

839 _stanza_position: "TEPosition", 

840 lint_state: LintState, 

841) -> None: 

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

843 kvpair_range_te.start_pos 

844 ) 

845 try: 

846 interpreted_value = kvpair.interpret_as( 

847 known_field.field_value_class.interpreter() 

848 ) 

849 except ValueError: 

850 return 

851 

852 for part in interpreted_value.iter_parts(): 

853 if part.is_separator or part.is_whitespace or part.is_whitespace: 

854 continue 

855 name_and_email = part.convert_to_text() 

856 try: 

857 email_start = name_and_email.rindex("<") 

858 email_end = name_and_email.rindex(">") 

859 email = name_and_email[email_start + 1 : email_end] 

860 except IndexError: 

861 continue 

862 

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

864 if pref is None or not pref.canonical_name: 

865 continue 

866 

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

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

869 continue 

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

871 lint_state.emit_diagnostic( 

872 value_range_te, 

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

874 "informational", 

875 "debputy", 

876 quickfixes=[propose_correct_text_quick_fix(expected)], 

877 ) 

878 

879 

880def _maintainer_field_validator( 

881 known_field: "F", 

882 _deb822_file: Deb822FileElement, 

883 kvpair: Deb822KeyValuePairElement, 

884 kvpair_range_te: "TERange", 

885 _field_name_range_te: "TERange", 

886 _stanza: Deb822ParagraphElement, 

887 _stanza_position: "TEPosition", 

888 lint_state: LintState, 

889) -> None: 

890 

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

892 kvpair_range_te.start_pos 

893 ) 

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

895 for part in interpreted_value.iter_parts(): 

896 if not part.is_separator: 

897 continue 

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

899 severity = known_field.unknown_value_severity 

900 assert severity is not None 

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

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

903 lint_state.emit_diagnostic( 

904 value_range_te, 

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

906 severity, 

907 known_field.unknown_value_authority, 

908 ) 

909 

910 

911def _use_https_instead_of_http( 

912 known_field: "F", 

913 _deb822_file: Deb822FileElement, 

914 kvpair: Deb822KeyValuePairElement, 

915 kvpair_range_te: "TERange", 

916 _field_name_range_te: "TERange", 

917 _stanza: Deb822ParagraphElement, 

918 _stanza_position: "TEPosition", 

919 lint_state: LintState, 

920) -> None: 

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

922 kvpair_range_te.start_pos 

923 ) 

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

925 for part in interpreted_value.iter_parts(): 

926 value = part.convert_to_text() 

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

928 continue 

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

930 problem_range_te = TERange.between( 

931 value_range_te.start_pos, 

932 TEPosition( 

933 value_range_te.start_pos.line_position, 

934 value_range_te.start_pos.cursor_position + 7, 

935 ), 

936 ) 

937 lint_state.emit_diagnostic( 

938 problem_range_te, 

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

940 "warning", 

941 "debputy", 

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

943 ) 

944 

945 

946def _each_value_match_regex_validation( 

947 regex: re.Pattern, 

948 *, 

949 diagnostic_severity: LintSeverity = "error", 

950 authority_reference: str | None = None, 

951) -> CustomFieldCheck: 

952 

953 def _validator( 

954 known_field: "F", 

955 _deb822_file: Deb822FileElement, 

956 kvpair: Deb822KeyValuePairElement, 

957 kvpair_range_te: "TERange", 

958 _field_name_range_te: "TERange", 

959 _stanza: Deb822ParagraphElement, 

960 _stanza_position: "TEPosition", 

961 lint_state: LintState, 

962 ) -> None: 

963 nonlocal authority_reference 

964 interpreter = known_field.field_value_class.interpreter() 

965 if interpreter is None: 

966 raise AssertionError( 

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

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

969 ) 

970 auth_ref = ( 

971 authority_reference 

972 if authority_reference is not None 

973 else known_field.unknown_value_authority 

974 ) 

975 

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

977 kvpair_range_te.start_pos 

978 ) 

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

980 v = value_ref.value 

981 m = regex.fullmatch(v) 

982 if m is not None: 

983 continue 

984 

985 if "${" in v: 

986 # Ignore substvars 

987 continue 

988 

989 section_value_loc = value_ref.locatable 

990 value_range_te = section_value_loc.range_in_parent().relative_to( 

991 value_element_pos 

992 ) 

993 lint_state.emit_diagnostic( 

994 value_range_te, 

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

996 diagnostic_severity, 

997 auth_ref, 

998 ) 

999 

1000 return _validator 

1001 

1002 

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

1004_DEP_RELATION_CLAUSE = re.compile( 

1005 r""" 

1006 ^ 

1007 \s* 

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

1009 \s* 

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

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

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

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

1014 $ 

1015""", 

1016 re.VERBOSE | re.MULTILINE, 

1017) 

1018 

1019 

1020def _span_to_te_range( 

1021 text: str, 

1022 start_pos: int, 

1023 end_pos: int, 

1024) -> TERange: 

1025 prefix = text[0:start_pos] 

1026 prefix_plus_text = text[0:end_pos] 

1027 

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

1029 if start_line: 

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

1031 # +1 to skip past the newline 

1032 start_cursor_pos = start_pos - (start_newline_offset + 1) 

1033 else: 

1034 start_cursor_pos = start_pos 

1035 

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

1037 if end_line == start_line: 

1038 end_cursor_pos = start_cursor_pos + (end_pos - start_pos) 

1039 else: 

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

1041 end_cursor_pos = end_pos - (end_newline_offset + 1) 

1042 

1043 return TERange( 

1044 TEPosition( 

1045 start_line, 

1046 start_cursor_pos, 

1047 ), 

1048 TEPosition( 

1049 end_line, 

1050 end_cursor_pos, 

1051 ), 

1052 ) 

1053 

1054 

1055def _split_w_spans( 

1056 v: str, 

1057 sep: str, 

1058 *, 

1059 offset: int = 0, 

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

1061 separator_size = len(sep) 

1062 parts = v.split(sep) 

1063 for part in parts: 

1064 size = len(part) 

1065 end_offset = offset + size 

1066 yield part, offset, end_offset 

1067 offset = end_offset + separator_size 

1068 

1069 

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

1071 

1072 

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

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

1075 

1076 

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

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

1079 if not newlines: 

1080 return TEPosition( 

1081 newlines, 

1082 len(text), 

1083 ) 

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

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

1086 return TEPosition( 

1087 newlines, 

1088 line_offset, 

1089 ) 

1090 

1091 

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

1093class Relation: 

1094 name: str 

1095 arch_qual: str | None = None 

1096 version_operator: str | None = None 

1097 version: str | None = None 

1098 arch_restriction: str | None = None 

1099 build_profile_restriction: str | None = None 

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

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

1102 # an example). 

1103 content_display_offset: int = -1 

1104 content_display_end_offset: int = -1 

1105 

1106 

1107def relation_key_variations( 

1108 relation: Relation, 

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

1110 operator_variants = ( 

1111 [relation.version_operator, None] 

1112 if relation.version_operator is not None 

1113 else [None] 

1114 ) 

1115 arch_qual_variants = ( 

1116 [relation.arch_qual, None] 

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

1118 else [None] 

1119 ) 

1120 for arch_qual, version_operator in itertools.product( 

1121 arch_qual_variants, 

1122 operator_variants, 

1123 ): 

1124 yield relation.name, arch_qual, version_operator 

1125 

1126 

1127def dup_check_relations( 

1128 known_field: "F", 

1129 relations: Sequence[Relation], 

1130 raw_value_masked_comments: str, 

1131 value_element_pos: "TEPosition", 

1132 lint_state: LintState, 

1133) -> None: 

1134 overlap_table = {} 

1135 for relation in relations: 

1136 version_operator = relation.version_operator 

1137 arch_qual = relation.arch_qual 

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

1139 continue 

1140 

1141 for relation_key in relation_key_variations(relation): 

1142 prev_relation = overlap_table.get(relation_key) 

1143 if prev_relation is None: 

1144 overlap_table[relation_key] = relation 

1145 else: 

1146 prev_version_operator = prev_relation.version_operator 

1147 

1148 if ( 

1149 prev_version_operator 

1150 and version_operator 

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

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

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

1154 ): 

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

1156 continue 

1157 

1158 prev_arch_qual = prev_relation.arch_qual 

1159 if ( 

1160 arch_qual != prev_arch_qual 

1161 and prev_arch_qual != "any" 

1162 and arch_qual != "any" 

1163 ): 

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

1165 # 

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

1167 continue 

1168 

1169 if ( 

1170 known_field.name == "Provides" 

1171 and version_operator == "=" 

1172 and prev_version_operator == version_operator 

1173 and relation.version != prev_relation.version 

1174 ): 

1175 # Provides: foo (= 1), foo (= 2) is legal and not mergeable 

1176 continue 

1177 

1178 orig_relation_range = TERange( 

1179 _text_to_te_position( 

1180 raw_value_masked_comments[ 

1181 : prev_relation.content_display_offset 

1182 ] 

1183 ), 

1184 _text_to_te_position( 

1185 raw_value_masked_comments[ 

1186 : prev_relation.content_display_end_offset 

1187 ] 

1188 ), 

1189 ).relative_to(value_element_pos) 

1190 

1191 duplicate_relation_range = TERange( 

1192 _text_to_te_position( 

1193 raw_value_masked_comments[: relation.content_display_offset] 

1194 ), 

1195 _text_to_te_position( 

1196 raw_value_masked_comments[: relation.content_display_end_offset] 

1197 ), 

1198 ).relative_to(value_element_pos) 

1199 

1200 lint_state.emit_diagnostic( 

1201 duplicate_relation_range, 

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

1203 "warning", 

1204 known_field.unknown_value_authority, 

1205 related_information=[ 

1206 lint_state.related_diagnostic_information( 

1207 orig_relation_range, 

1208 "The previous definition", 

1209 ), 

1210 ], 

1211 ) 

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

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

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

1215 break 

1216 

1217 

1218def _dctrl_check_dep_version_operator( 

1219 known_field: "F", 

1220 version_operator: str, 

1221 version_operator_span: tuple[int, int], 

1222 version_operators: frozenset[str], 

1223 raw_value_masked_comments: str, 

1224 offset: int, 

1225 value_element_pos: "TEPosition", 

1226 lint_state: LintState, 

1227) -> bool: 

1228 if ( 

1229 version_operators 

1230 and version_operator is not None 

1231 and version_operator not in version_operators 

1232 ): 

1233 v_start_offset = offset + version_operator_span[0] 

1234 v_end_offset = offset + version_operator_span[1] 

1235 version_problem_range_te = TERange( 

1236 _text_to_te_position(raw_value_masked_comments[:v_start_offset]), 

1237 _text_to_te_position(raw_value_masked_comments[:v_end_offset]), 

1238 ).relative_to(value_element_pos) 

1239 

1240 sorted_version_operators = sorted(version_operators) 

1241 

1242 excluding_equal = f"{version_operator}{version_operator}" 

1243 including_equal = f"{version_operator}=" 

1244 

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

1246 excluding_equal in version_operators or including_equal in version_operators 

1247 ): 

1248 lint_state.emit_diagnostic( 

1249 version_problem_range_te, 

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

1251 "error", 

1252 "Policy 7.1", 

1253 quickfixes=[ 

1254 propose_correct_text_quick_fix(n) 

1255 for n in (excluding_equal, including_equal) 

1256 if not version_operators or n in version_operators 

1257 ], 

1258 ) 

1259 else: 

1260 lint_state.emit_diagnostic( 

1261 version_problem_range_te, 

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

1263 "error", 

1264 known_field.unknown_value_authority, 

1265 quickfixes=[ 

1266 propose_correct_text_quick_fix(n) for n in sorted_version_operators 

1267 ], 

1268 ) 

1269 return True 

1270 return False 

1271 

1272 

1273def _dctrl_validate_dep( 

1274 known_field: "DF", 

1275 _deb822_file: Deb822FileElement, 

1276 kvpair: Deb822KeyValuePairElement, 

1277 kvpair_range_te: "TERange", 

1278 _field_name_range: "TERange", 

1279 _stanza: Deb822ParagraphElement, 

1280 _stanza_position: "TEPosition", 

1281 lint_state: LintState, 

1282) -> None: 

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

1284 kvpair_range_te.start_pos 

1285 ) 

1286 raw_value_with_comments = kvpair.value_element.convert_to_text() 

1287 raw_value_masked_comments = "".join( 

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

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

1290 ) 

1291 if isinstance(known_field, DctrlRelationshipKnownField): 

1292 version_operators = known_field.allowed_version_operators 

1293 supports_or_relation = known_field.supports_or_relation 

1294 else: 

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

1296 supports_or_relation = True 

1297 

1298 relation_dup_table = collections.defaultdict(list) 

1299 

1300 for rel, rel_offset, rel_end_offset in _split_w_spans( 

1301 raw_value_masked_comments, "," 

1302 ): 

1303 sub_relations = [] 

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

1305 if or_rel.isspace(): 

1306 continue 

1307 if sub_relations and not supports_or_relation: 

1308 separator_range_te = TERange( 

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

1310 _text_to_te_position(raw_value_masked_comments[:offset]), 

1311 ).relative_to(value_element_pos) 

1312 lint_state.emit_diagnostic( 

1313 separator_range_te, 

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

1315 "error", 

1316 known_field.unknown_value_authority, 

1317 ) 

1318 m = _DEP_RELATION_CLAUSE.fullmatch(or_rel) 

1319 

1320 if m is not None: 

1321 garbage = m.group("garbage") 

1322 version_operator = m.group("operator") 

1323 version_operator_span = m.span("operator") 

1324 if _dctrl_check_dep_version_operator( 

1325 known_field, 

1326 version_operator, 

1327 version_operator_span, 

1328 version_operators, 

1329 raw_value_masked_comments, 

1330 offset, 

1331 value_element_pos, 

1332 lint_state, 

1333 ): 

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

1335 else: 

1336 name_arch_qual = m.group("name_arch_qual") 

1337 if ":" in name_arch_qual: 

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

1339 else: 

1340 name = name_arch_qual 

1341 arch_qual = None 

1342 sub_relations.append( 

1343 Relation( 

1344 name, 

1345 arch_qual=arch_qual, 

1346 version_operator=version_operator, 

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

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

1349 build_profile_restriction=m.group( 

1350 "build_profile_restriction" 

1351 ), 

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

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

1354 content_display_end_offset=rel_end_offset, 

1355 ) 

1356 ) 

1357 else: 

1358 garbage = None 

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

1360 

1361 if m is not None and not garbage: 

1362 continue 

1363 if m is not None: 

1364 garbage_span = m.span("garbage") 

1365 garbage_start, garbage_end = garbage_span 

1366 error_start_offset = offset + garbage_start 

1367 error_end_offset = offset + garbage_end 

1368 garbage_part = raw_value_masked_comments[ 

1369 error_start_offset:error_end_offset 

1370 ] 

1371 else: 

1372 garbage_part = None 

1373 error_start_offset = offset 

1374 error_end_offset = end_offset 

1375 

1376 problem_range_te = TERange( 

1377 _text_to_te_position(raw_value_masked_comments[:error_start_offset]), 

1378 _text_to_te_position(raw_value_masked_comments[:error_end_offset]), 

1379 ).relative_to(value_element_pos) 

1380 

1381 if garbage_part is not None: 

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

1383 msg = ( 

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

1385 " Is a separator missing before this part?" 

1386 ) 

1387 else: 

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

1389 lint_state.emit_diagnostic( 

1390 problem_range_te, 

1391 msg, 

1392 "error", 

1393 known_field.unknown_value_authority, 

1394 ) 

1395 else: 

1396 dep = _cleanup_rel( 

1397 raw_value_masked_comments[error_start_offset:error_end_offset] 

1398 ) 

1399 lint_state.emit_diagnostic( 

1400 problem_range_te, 

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

1402 "error", 

1403 known_field.unknown_value_authority, 

1404 ) 

1405 if ( 

1406 len(sub_relations) == 1 

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

1408 ): 

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

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

1411 

1412 for relations in relation_dup_table.values(): 

1413 if len(relations) > 1: 

1414 dup_check_relations( 

1415 known_field, 

1416 relations, 

1417 raw_value_masked_comments, 

1418 value_element_pos, 

1419 lint_state, 

1420 ) 

1421 

1422 

1423def _rrr_build_driver_mismatch( 

1424 _known_field: "F", 

1425 _deb822_file: Deb822FileElement, 

1426 _kvpair: Deb822KeyValuePairElement, 

1427 kvpair_range_te: "TERange", 

1428 _field_name_range: "TERange", 

1429 stanza: Deb822ParagraphElement, 

1430 _stanza_position: "TEPosition", 

1431 lint_state: LintState, 

1432) -> None: 

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

1434 if dr != "debian-rules": 

1435 lint_state.emit_diagnostic( 

1436 kvpair_range_te, 

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

1438 "informational", 

1439 "debputy", 

1440 quickfixes=[ 

1441 propose_remove_range_quick_fix( 

1442 proposed_title="Remove Rules-Requires-Root" 

1443 ) 

1444 ], 

1445 ) 

1446 

1447 

1448class Dep5Matcher(BasenameGlobMatch): 

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

1450 super().__init__( 

1451 basename_glob, 

1452 only_when_in_directory=None, 

1453 path_type=None, 

1454 recursive_match=False, 

1455 ) 

1456 

1457 

1458def _match_dep5_segment( 

1459 current_dir: VirtualPathBase, basename_glob: str 

1460) -> Iterable[VirtualPathBase]: 

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

1462 return Dep5Matcher(basename_glob).finditer(current_dir) 

1463 else: 

1464 res = current_dir.get(basename_glob) 

1465 if res is None: 

1466 return tuple() 

1467 return (res,) 

1468 

1469 

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

1471 

1472 

1473def _dep5_unnecessary_symbols( 

1474 value: str, 

1475 value_range: TERange, 

1476 lint_state: LintState, 

1477) -> None: 

1478 slash_check_index = 0 

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

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

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

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

1483 prefix_len = slashes_end 

1484 

1485 slash_check_index = prefix_len 

1486 prefix_range = TERange( 

1487 value_range.start_pos, 

1488 TEPosition( 

1489 value_range.start_pos.line_position, 

1490 value_range.start_pos.cursor_position + prefix_len, 

1491 ), 

1492 ) 

1493 lint_state.emit_diagnostic( 

1494 prefix_range, 

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

1496 "warning", 

1497 "debputy", 

1498 quickfixes=[ 

1499 propose_remove_range_quick_fix( 

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

1501 ) 

1502 ], 

1503 ) 

1504 

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

1506 m_start, m_end = m.span(0) 

1507 

1508 prefix_range = TERange( 

1509 TEPosition( 

1510 value_range.start_pos.line_position, 

1511 value_range.start_pos.cursor_position + m_start, 

1512 ), 

1513 TEPosition( 

1514 value_range.start_pos.line_position, 

1515 value_range.start_pos.cursor_position + m_end, 

1516 ), 

1517 ) 

1518 lint_state.emit_diagnostic( 

1519 prefix_range, 

1520 'Simplify to a single "/"', 

1521 "warning", 

1522 "debputy", 

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

1524 ) 

1525 

1526 

1527def _dep5_files_check( 

1528 known_field: "F", 

1529 _deb822_file: Deb822FileElement, 

1530 kvpair: Deb822KeyValuePairElement, 

1531 kvpair_range_te: "TERange", 

1532 _field_name_range: "TERange", 

1533 _stanza: Deb822ParagraphElement, 

1534 _stanza_position: "TEPosition", 

1535 lint_state: LintState, 

1536) -> None: 

1537 interpreter = known_field.field_value_class.interpreter() 

1538 assert interpreter is not None 

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

1540 kvpair_range_te.start_pos 

1541 ) 

1542 values_with_ranges = [] 

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

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

1545 full_value_range.start_pos 

1546 ) 

1547 value = value_ref.value 

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

1549 _dep5_unnecessary_symbols(value, value_range, lint_state) 

1550 

1551 source_root = lint_state.source_root 

1552 if source_root is None: 

1553 return 

1554 i = 0 

1555 limit = len(values_with_ranges) 

1556 while i < limit: 

1557 value, value_range = values_with_ranges[i] 

1558 i += 1 

1559 

1560 

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

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

1563_KNOWN_HTTPS_HOSTS = frozenset( 

1564 [ 

1565 "debian.org", 

1566 "bioconductor.org", 

1567 "cran.r-project.org", 

1568 "github.com", 

1569 "gitlab.com", 

1570 "metacpan.org", 

1571 "gnu.org", 

1572 ] 

1573) 

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

1575_NO_DOT_GIT_HOMEPAGE_HOSTS = frozenset( 

1576 { 

1577 "salsa.debian.org", 

1578 "github.com", 

1579 "gitlab.com", 

1580 } 

1581) 

1582 

1583 

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

1585 if host in known_hosts: 

1586 return True 

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

1588 try: 

1589 idx = host.index(".") 

1590 host = host[idx + 1 :] 

1591 except ValueError: 

1592 break 

1593 if host in known_hosts: 

1594 return True 

1595 return False 

1596 

1597 

1598def _validate_homepage_field( 

1599 _known_field: "F", 

1600 _deb822_file: Deb822FileElement, 

1601 kvpair: Deb822KeyValuePairElement, 

1602 kvpair_range_te: "TERange", 

1603 _field_name_range_te: "TERange", 

1604 _stanza: Deb822ParagraphElement, 

1605 _stanza_position: "TEPosition", 

1606 lint_state: LintState, 

1607) -> None: 

1608 value = kvpair.value_element.convert_to_text() 

1609 offset = 0 

1610 homepage = value 

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

1612 expected_value = m.group(1) 

1613 quickfixes = [] 

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

1615 homepage = expected_value.strip() 

1616 offset = m.start(1) 

1617 quickfixes.append(propose_correct_text_quick_fix(expected_value)) 

1618 lint_state.emit_diagnostic( 

1619 _single_line_span_range_relative_to_pos( 

1620 m.span(), 

1621 kvpair.value_element.position_in_parent().relative_to( 

1622 kvpair_range_te.start_pos 

1623 ), 

1624 ), 

1625 "Superfluous URL/URI wrapping", 

1626 "informational", 

1627 "Policy 5.6.23", 

1628 quickfixes=quickfixes, 

1629 ) 

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

1631 m = _URI_RE.search(homepage) 

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

1633 return 

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

1635 protocol = m.group("protocol") 

1636 host = m.group("host") 

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

1638 if _is_known_host(host, _REPLACED_HOSTS): 

1639 span = m.span("host") 

1640 lint_state.emit_diagnostic( 

1641 _single_line_span_range_relative_to_pos( 

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

1643 kvpair.value_element.position_in_parent().relative_to( 

1644 kvpair_range_te.start_pos 

1645 ), 

1646 ), 

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

1648 "warning", 

1649 "debputy", 

1650 ) 

1651 return 

1652 if ( 

1653 protocol == "ftp" 

1654 or protocol == "http" 

1655 and _is_known_host(host, _KNOWN_HTTPS_HOSTS) 

1656 ): 

1657 span = m.span("protocol") 

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

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

1660 quickfixes = [] 

1661 else: 

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

1663 quickfixes = [propose_correct_text_quick_fix("https")] 

1664 lint_state.emit_diagnostic( 

1665 _single_line_span_range_relative_to_pos( 

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

1667 kvpair.value_element.position_in_parent().relative_to( 

1668 kvpair_range_te.start_pos 

1669 ), 

1670 ), 

1671 msg, 

1672 "pedantic", 

1673 "debputy", 

1674 quickfixes=quickfixes, 

1675 ) 

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

1677 span = m.span("path") 

1678 msg = "Unnecessary suffix" 

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

1680 lint_state.emit_diagnostic( 

1681 _single_line_span_range_relative_to_pos( 

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

1683 kvpair.value_element.position_in_parent().relative_to( 

1684 kvpair_range_te.start_pos 

1685 ), 

1686 ), 

1687 msg, 

1688 "pedantic", 

1689 "debputy", 

1690 quickfixes=quickfixes, 

1691 ) 

1692 

1693 

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

1695 def _validator( 

1696 known_field: "F", 

1697 deb822_file: Deb822FileElement, 

1698 kvpair: Deb822KeyValuePairElement, 

1699 kvpair_range_te: "TERange", 

1700 field_name_range_te: "TERange", 

1701 stanza: Deb822ParagraphElement, 

1702 stanza_position: "TEPosition", 

1703 lint_state: LintState, 

1704 ) -> None: 

1705 for check in checks: 

1706 check( 

1707 known_field, 

1708 deb822_file, 

1709 kvpair, 

1710 kvpair_range_te, 

1711 field_name_range_te, 

1712 stanza, 

1713 stanza_position, 

1714 lint_state, 

1715 ) 

1716 

1717 return _validator 

1718 

1719 

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

1721class PackageNameSectionRule: 

1722 section: str 

1723 check: Callable[[str], bool] 

1724 

1725 

1726def _package_name_section_rule( 

1727 section: str, 

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

1729 *, 

1730 confirm_re: re.Pattern | None = None, 

1731) -> PackageNameSectionRule: 

1732 if confirm_re is not None: 

1733 assert callable(check) 

1734 

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

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

1737 

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

1739 

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

1741 return check.search(v) is not None 

1742 

1743 else: 

1744 _impl = check 

1745 

1746 return PackageNameSectionRule(section, _impl) 

1747 

1748 

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

1750_PKGNAME_VS_SECTION_RULES = [ 

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

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

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

1754 _package_name_section_rule( 

1755 "httpd", 

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

1757 ), 

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

1759 _package_name_section_rule( 

1760 "gnustep", 

1761 lambda n: n.endswith( 

1762 ( 

1763 ".framework", 

1764 ".framework-common", 

1765 ".tool", 

1766 ".tool-common", 

1767 ".app", 

1768 ".app-common", 

1769 ) 

1770 ), 

1771 ), 

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

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

1774 _package_name_section_rule( 

1775 "zope", 

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

1777 ), 

1778 _package_name_section_rule( 

1779 "python", 

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

1781 ), 

1782 _package_name_section_rule( 

1783 "gnu-r", 

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

1785 ), 

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

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

1788 _package_name_section_rule( 

1789 "lisp", 

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

1791 ), 

1792 _package_name_section_rule( 

1793 "lisp", 

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

1795 ), 

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

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

1798 _package_name_section_rule( 

1799 "perl", 

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

1801 ), 

1802 _package_name_section_rule( 

1803 "cli-mono", 

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

1805 ), 

1806 _package_name_section_rule( 

1807 "java", 

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

1809 ), 

1810 _package_name_section_rule( 

1811 "php", 

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

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

1814 ), 

1815 _package_name_section_rule( 

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

1817 ), 

1818 _package_name_section_rule( 

1819 "haskell", 

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

1821 ), 

1822 _package_name_section_rule( 

1823 "ruby", 

1824 lambda n: "-ruby" in n, 

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

1826 ), 

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

1828 _package_name_section_rule( 

1829 "rust", 

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

1831 ), 

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

1833 _package_name_section_rule( 

1834 "ocaml", 

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

1836 ), 

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

1838 _package_name_section_rule( 

1839 "interpreters", 

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

1841 ), 

1842 _package_name_section_rule( 

1843 "introspection", 

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

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

1846 ), 

1847 _package_name_section_rule( 

1848 "fonts", 

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

1850 ), 

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

1852 _package_name_section_rule( 

1853 "localization", 

1854 lambda n: n.startswith( 

1855 ( 

1856 "aspell-", 

1857 "hunspell-", 

1858 "myspell-", 

1859 "mythes-", 

1860 "dict-freedict-", 

1861 "gcompris-sound-", 

1862 ) 

1863 ), 

1864 ), 

1865 _package_name_section_rule( 

1866 "localization", 

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

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

1869 ), 

1870 _package_name_section_rule( 

1871 "localization", 

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

1873 ), 

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

1875 _package_name_section_rule( 

1876 "libdevel", 

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

1878 ), 

1879 _package_name_section_rule( 

1880 "libs", 

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

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

1883 ), 

1884] 

1885 

1886 

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

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

1889@functools.lru_cache(64) 

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

1891 for rule in _PKGNAME_VS_SECTION_RULES: 

1892 if rule.check(name): 

1893 return rule.section 

1894 return None 

1895 

1896 

1897def _unknown_value_check( 

1898 field_name: str, 

1899 value: str, 

1900 known_values: Mapping[str, Keyword], 

1901 unknown_value_severity: LintSeverity | None, 

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

1903 known_value = known_values.get(value) 

1904 message = None 

1905 severity = unknown_value_severity 

1906 fix_data = None 

1907 if known_value is None: 

1908 candidates = detect_possible_typo( 

1909 value, 

1910 known_values, 

1911 ) 

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

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

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

1915 else: 

1916 hint_text = "" 

1917 fix_data = None 

1918 severity = unknown_value_severity 

1919 fix_text = hint_text 

1920 if candidates: 

1921 match = candidates[0] 

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

1923 known_value = known_values[match] 

1924 fix_text = ( 

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

1926 ) 

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

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

1929 return None, None, None, None 

1930 if severity is None: 

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

1932 # It always has leading whitespace 

1933 message = fix_text.strip() 

1934 else: 

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

1936 return known_value, message, severity, fix_data 

1937 

1938 

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

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

1941 

1942 

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

1944 return path 

1945 

1946 

1947def _should_ignore_dir( 

1948 path: VirtualPath, 

1949 *, 

1950 supports_dir_match: bool = False, 

1951 match_non_persistent_paths: bool = False, 

1952) -> bool: 

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

1954 return True 

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

1956 if ( 

1957 not match_non_persistent_paths 

1958 and cachedir_tag is not None 

1959 and cachedir_tag.is_file 

1960 ): 

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

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

1963 start = fd.read(43) 

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

1965 return True 

1966 return False 

1967 

1968 

1969@dataclasses.dataclass(slots=True) 

1970class Deb822KnownField: 

1971 name: str 

1972 field_value_class: FieldValueClass 

1973 warn_if_default: bool = True 

1974 unknown_value_authority: str = "debputy" 

1975 missing_field_authority: str = "debputy" 

1976 replaced_by: str | None = None 

1977 deprecated_with_no_replacement: bool = False 

1978 missing_field_severity: LintSeverity | None = None 

1979 default_value: str | None = None 

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

1981 unknown_value_severity: LintSeverity | None = "error" 

1982 translation_context: str = "" 

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

1984 synopsis: str | None = None 

1985 usage_hint: UsageHint | None = None 

1986 long_description: str | None = None 

1987 spellcheck_value: bool = False 

1988 inheritable_from_other_stanza: bool = False 

1989 show_as_inherited: bool = True 

1990 custom_field_check: CustomFieldCheck | None = None 

1991 can_complete_field_in_stanza: None | ( 

1992 Callable[[Iterable[Deb822ParagraphElement]], bool] 

1993 ) = None 

1994 is_substvars_disabled_even_if_allowed_by_stanza: bool = False 

1995 is_alias_of: str | None = None 

1996 is_completion_suggestion: bool = True 

1997 

1998 def synopsis_translated( 

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

2000 ) -> str | None: 

2001 if self.synopsis is None: 

2002 return None 

2003 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

2004 self.translation_context, 

2005 self.synopsis, 

2006 ) 

2007 

2008 def long_description_translated( 

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

2010 ) -> str | None: 

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

2012 return None 

2013 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

2014 self.translation_context, 

2015 self.long_description, 

2016 ) 

2017 

2018 def _can_complete_field_in_stanza( 

2019 self, 

2020 stanza_parts: Sequence[Deb822ParagraphElement], 

2021 ) -> bool: 

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

2023 return False 

2024 return ( 

2025 self.can_complete_field_in_stanza is None 

2026 or self.can_complete_field_in_stanza(stanza_parts) 

2027 ) 

2028 

2029 def complete_field( 

2030 self, 

2031 lint_state: LintState, 

2032 stanza_parts: Sequence[Deb822ParagraphElement], 

2033 markdown_kind: MarkupKind, 

2034 ) -> CompletionItem | None: 

2035 if not self._can_complete_field_in_stanza(stanza_parts): 

2036 return None 

2037 name = self.name 

2038 complete_as = name + ": " 

2039 options = self.value_options_for_completer( 

2040 lint_state, 

2041 stanza_parts, 

2042 "", 

2043 markdown_kind, 

2044 is_completion_for_field=True, 

2045 ) 

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

2047 value = options[0].insert_text 

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

2049 complete_as += value 

2050 tags = [] 

2051 is_deprecated = False 

2052 if self.replaced_by or self.deprecated_with_no_replacement: 

2053 is_deprecated = True 

2054 tags.append(CompletionItemTag.Deprecated) 

2055 

2056 doc = self.long_description 

2057 if doc: 

2058 doc = MarkupContent( 

2059 value=doc, 

2060 kind=markdown_kind, 

2061 ) 

2062 else: 

2063 doc = None 

2064 

2065 return CompletionItem( 

2066 name, 

2067 insert_text=complete_as, 

2068 deprecated=is_deprecated, 

2069 tags=tags, 

2070 detail=format_comp_item_synopsis_doc( 

2071 self.usage_hint, 

2072 self.synopsis_translated(lint_state), 

2073 is_deprecated, 

2074 ), 

2075 documentation=doc, 

2076 ) 

2077 

2078 def _complete_files( 

2079 self, 

2080 base_dir: VirtualPathBase | None, 

2081 value_being_completed: str, 

2082 *, 

2083 is_dep5_file_list: bool = False, 

2084 supports_dir_match: bool = False, 

2085 supports_spaces_in_filename: bool = False, 

2086 match_non_persistent_paths: bool = False, 

2087 ) -> Sequence[CompletionItem] | None: 

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

2089 if base_dir is None or not base_dir.is_dir: 

2090 return None 

2091 

2092 if is_dep5_file_list: 

2093 supports_spaces_in_filename = True 

2094 supports_dir_match = False 

2095 match_non_persistent_paths = False 

2096 

2097 if value_being_completed == "": 

2098 current_dir = base_dir 

2099 unmatched_parts: Sequence[str] = () 

2100 else: 

2101 current_dir, unmatched_parts = base_dir.attempt_lookup( 

2102 value_being_completed 

2103 ) 

2104 

2105 if len(unmatched_parts) > 1: 

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

2107 return None 

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

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

2110 return None 

2111 items = [] 

2112 

2113 path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path 

2114 

2115 for child in current_dir.iterdir(): 

2116 if child.is_symlink and is_dep5_file_list: 

2117 continue 

2118 if not supports_spaces_in_filename and ( 

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

2120 ): 

2121 continue 

2122 sort_text = ( 

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

2124 ) 

2125 if child.is_dir: 

2126 if _should_ignore_dir( 

2127 child, 

2128 supports_dir_match=supports_dir_match, 

2129 match_non_persistent_paths=match_non_persistent_paths, 

2130 ): 

2131 continue 

2132 items.append( 

2133 CompletionItem( 

2134 f"{child.path}/", 

2135 label_details=CompletionItemLabelDetails( 

2136 description=child.path, 

2137 ), 

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

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

2140 sort_text=sort_text, 

2141 kind=CompletionItemKind.Folder, 

2142 ) 

2143 ) 

2144 else: 

2145 items.append( 

2146 CompletionItem( 

2147 child.path, 

2148 label_details=CompletionItemLabelDetails( 

2149 description=child.path, 

2150 ), 

2151 insert_text=path_escaper(child.path), 

2152 filter_text=child.path, 

2153 sort_text=sort_text, 

2154 kind=CompletionItemKind.File, 

2155 ) 

2156 ) 

2157 return items 

2158 

2159 def value_options_for_completer( 

2160 self, 

2161 lint_state: LintState, 

2162 stanza_parts: Sequence[Deb822ParagraphElement], 

2163 value_being_completed: str, 

2164 markdown_kind: MarkupKind, 

2165 *, 

2166 is_completion_for_field: bool = False, 

2167 ) -> Sequence[CompletionItem] | None: 

2168 known_values = self.known_values 

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

2170 if is_completion_for_field: 

2171 return None 

2172 return self._complete_files( 

2173 lint_state.source_root, 

2174 value_being_completed, 

2175 is_dep5_file_list=True, 

2176 ) 

2177 

2178 if known_values is None: 

2179 return None 

2180 if is_completion_for_field and ( 

2181 len(known_values) == 1 

2182 or ( 

2183 len(known_values) == 2 

2184 and self.warn_if_default 

2185 and self.default_value is not None 

2186 ) 

2187 ): 

2188 value = next( 

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

2190 None, 

2191 ) 

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

2193 return None 

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

2195 return [ 

2196 keyword.as_completion_item( 

2197 lint_state, 

2198 stanza_parts, 

2199 value_being_completed, 

2200 markdown_kind, 

2201 ) 

2202 for keyword in known_values.values() 

2203 if keyword.is_keyword_valid_completion_in_stanza(stanza_parts) 

2204 and keyword.is_completion_suggestion 

2205 ] 

2206 

2207 def field_omitted_diagnostics( 

2208 self, 

2209 deb822_file: Deb822FileElement, 

2210 representation_field_range: "TERange", 

2211 stanza: Deb822ParagraphElement, 

2212 stanza_position: "TEPosition", 

2213 header_stanza: Deb822FileElement | None, 

2214 lint_state: LintState, 

2215 ) -> None: 

2216 missing_field_severity = self.missing_field_severity 

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

2218 return 

2219 

2220 if ( 

2221 self.inheritable_from_other_stanza 

2222 and header_stanza is not None 

2223 and self.name in header_stanza 

2224 ): 

2225 return 

2226 

2227 lint_state.emit_diagnostic( 

2228 representation_field_range, 

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

2230 missing_field_severity, 

2231 self.missing_field_authority, 

2232 ) 

2233 

2234 async def field_diagnostics( 

2235 self, 

2236 deb822_file: Deb822FileElement, 

2237 kvpair: Deb822KeyValuePairElement, 

2238 stanza: Deb822ParagraphElement, 

2239 stanza_position: "TEPosition", 

2240 kvpair_range_te: "TERange", 

2241 lint_state: LintState, 

2242 *, 

2243 field_name_typo_reported: bool = False, 

2244 ) -> None: 

2245 field_name_token = kvpair.field_token 

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

2247 kvpair_range_te.start_pos 

2248 ) 

2249 field_name = field_name_token.text 

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

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

2252 # in one but not the other. 

2253 field_value = stanza[field_name] 

2254 self._diagnostics_for_field_name( 

2255 kvpair_range_te, 

2256 field_name_token, 

2257 field_name_range_te, 

2258 field_name_typo_reported, 

2259 lint_state, 

2260 ) 

2261 if self.custom_field_check is not None: 

2262 self.custom_field_check( 

2263 self, 

2264 deb822_file, 

2265 kvpair, 

2266 kvpair_range_te, 

2267 field_name_range_te, 

2268 stanza, 

2269 stanza_position, 

2270 lint_state, 

2271 ) 

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

2273 if self.spellcheck_value: 

2274 words = kvpair.interpret_as(LIST_SPACE_SEPARATED_INTERPRETATION) 

2275 spell_checker = lint_state.spellchecker() 

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

2277 kvpair_range_te.start_pos 

2278 ) 

2279 async for word_ref in lint_state.slow_iter( 

2280 words.iter_value_references(), yield_every=25 

2281 ): 

2282 token = word_ref.value 

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

2284 corrections = spell_checker.provide_corrections_for(word) 

2285 if not corrections: 

2286 continue 

2287 word_loc = word_ref.locatable 

2288 word_pos_te = word_loc.position_in_parent().relative_to( 

2289 value_position 

2290 ) 

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

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

2293 word_size = TERange( 

2294 START_POSITION, 

2295 TEPosition(0, endpos - pos), 

2296 ) 

2297 lint_state.emit_diagnostic( 

2298 TERange.from_position_and_size(word_pos_te, word_size), 

2299 f'Spelling "{word}"', 

2300 "spelling", 

2301 "debputy", 

2302 quickfixes=[ 

2303 propose_correct_text_quick_fix(c) for c in corrections 

2304 ], 

2305 enable_non_interactive_auto_fix=False, 

2306 ) 

2307 else: 

2308 self._known_value_diagnostics( 

2309 kvpair, 

2310 kvpair_range_te.start_pos, 

2311 lint_state, 

2312 ) 

2313 

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

2315 lint_state.emit_diagnostic( 

2316 kvpair_range_te, 

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

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

2319 "warning", 

2320 "debputy", 

2321 ) 

2322 

2323 def _diagnostics_for_field_name( 

2324 self, 

2325 kvpair_range: "TERange", 

2326 token: Deb822FieldNameToken, 

2327 token_range: "TERange", 

2328 typo_detected: bool, 

2329 lint_state: LintState, 

2330 ) -> None: 

2331 field_name = token.text 

2332 # Defeat the case-insensitivity from python-debian 

2333 field_name_cased = str(field_name) 

2334 if self.deprecated_with_no_replacement: 

2335 lint_state.emit_diagnostic( 

2336 kvpair_range, 

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

2338 "warning", 

2339 "debputy", 

2340 quickfixes=[propose_remove_range_quick_fix()], 

2341 tags=[DiagnosticTag.Deprecated], 

2342 ) 

2343 elif self.replaced_by is not None: 

2344 lint_state.emit_diagnostic( 

2345 token_range, 

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

2347 "warning", 

2348 "debputy", 

2349 tags=[DiagnosticTag.Deprecated], 

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

2351 ) 

2352 

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

2354 lint_state.emit_diagnostic( 

2355 token_range, 

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

2357 "pedantic", 

2358 self.unknown_value_authority, 

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

2360 ) 

2361 

2362 def _dep5_file_list_diagnostics( 

2363 self, 

2364 kvpair: Deb822KeyValuePairElement, 

2365 kvpair_position: "TEPosition", 

2366 lint_state: LintState, 

2367 ) -> None: 

2368 source_root = lint_state.source_root 

2369 if ( 

2370 self.field_value_class != FieldValueClass.DEP5_FILE_LIST 

2371 or source_root is None 

2372 ): 

2373 return 

2374 interpreter = self.field_value_class.interpreter() 

2375 values = kvpair.interpret_as(interpreter) 

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

2377 kvpair_position 

2378 ) 

2379 

2380 assert interpreter is not None 

2381 

2382 for token in values.iter_parts(): 

2383 if token.is_whitespace: 

2384 continue 

2385 text = token.convert_to_text() 

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

2387 # TODO: We should validate these as well 

2388 continue 

2389 matched_path, missing_part = source_root.attempt_lookup(text) 

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

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

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

2393 # do not have the infrastructure for). 

2394 if not missing_part and matched_path.is_dir and self.name == "Files": 

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

2396 lint_state.emit_diagnostic( 

2397 path_range_te, 

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

2399 "warning", 

2400 self.unknown_value_authority, 

2401 quickfixes=[ 

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

2403 ], 

2404 ) 

2405 

2406 def _known_value_diagnostics( 

2407 self, 

2408 kvpair: Deb822KeyValuePairElement, 

2409 kvpair_position: "TEPosition", 

2410 lint_state: LintState, 

2411 ) -> None: 

2412 unknown_value_severity = self.unknown_value_severity 

2413 interpreter = self.field_value_class.interpreter() 

2414 if interpreter is None: 

2415 return 

2416 try: 

2417 values = kvpair.interpret_as(interpreter) 

2418 except ValueError: 

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

2420 kvpair_position 

2421 ) 

2422 lint_state.emit_diagnostic( 

2423 value_range, 

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

2425 "pedantic", 

2426 "debputy", 

2427 ) 

2428 return 

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

2430 kvpair_position 

2431 ) 

2432 

2433 last_token_non_ws_sep_token: TE | None = None 

2434 for token in values.iter_parts(): 

2435 if token.is_whitespace: 

2436 continue 

2437 if not token.is_separator: 

2438 last_token_non_ws_sep_token = None 

2439 continue 

2440 if last_token_non_ws_sep_token is not None: 

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

2442 lint_state.emit_diagnostic( 

2443 sep_range_te, 

2444 "Duplicate separator", 

2445 "error", 

2446 self.unknown_value_authority, 

2447 ) 

2448 last_token_non_ws_sep_token = token 

2449 

2450 allowed_values = self.known_values 

2451 if not allowed_values: 

2452 return 

2453 

2454 first_value = None 

2455 first_exclusive_value_ref = None 

2456 first_exclusive_value = None 

2457 has_emitted_for_exclusive = False 

2458 

2459 for value_ref in values.iter_value_references(): 

2460 value = value_ref.value 

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

2462 first_value is not None 

2463 and self.field_value_class == FieldValueClass.SINGLE_VALUE 

2464 ): 

2465 value_loc = value_ref.locatable 

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

2467 lint_state.emit_diagnostic( 

2468 range_position_te, 

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

2470 "error", 

2471 self.unknown_value_authority, 

2472 ) 

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

2474 continue 

2475 

2476 if first_exclusive_value_ref is not None and not has_emitted_for_exclusive: 

2477 assert first_exclusive_value is not None 

2478 value_loc = first_exclusive_value_ref.locatable 

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

2480 lint_state.emit_diagnostic( 

2481 value_range_te, 

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

2483 "error", 

2484 self.unknown_value_authority, 

2485 ) 

2486 

2487 known_value, unknown_value_message, unknown_severity, typo_fix_data = ( 

2488 _unknown_value_check( 

2489 self.name, 

2490 value, 

2491 self.known_values, 

2492 unknown_value_severity, 

2493 ) 

2494 ) 

2495 value_loc = value_ref.locatable 

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

2497 

2498 if known_value and known_value.is_exclusive: 

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

2500 first_exclusive_value_ref = value_ref 

2501 if first_value is not None: 

2502 has_emitted_for_exclusive = True 

2503 lint_state.emit_diagnostic( 

2504 value_range, 

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

2506 "error", 

2507 self.unknown_value_authority, 

2508 ) 

2509 

2510 if first_value is None: 

2511 first_value = value 

2512 

2513 if unknown_value_message is not None: 

2514 assert unknown_severity is not None 

2515 lint_state.emit_diagnostic( 

2516 value_range, 

2517 unknown_value_message, 

2518 unknown_severity, 

2519 self.unknown_value_authority, 

2520 quickfixes=typo_fix_data, 

2521 ) 

2522 

2523 if known_value is not None and known_value.is_deprecated: 

2524 replacement = known_value.replaced_by 

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

2526 obsolete_value_message = ( 

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

2528 ) 

2529 obsolete_fix_data = [propose_correct_text_quick_fix(replacement)] 

2530 else: 

2531 obsolete_value_message = ( 

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

2533 ) 

2534 obsolete_fix_data = None 

2535 lint_state.emit_diagnostic( 

2536 value_range, 

2537 obsolete_value_message, 

2538 "warning", 

2539 "debputy", 

2540 quickfixes=obsolete_fix_data, 

2541 ) 

2542 

2543 def _reformat_field_name( 

2544 self, 

2545 effective_preference: "EffectiveFormattingPreference", 

2546 stanza_range: TERange, 

2547 kvpair: Deb822KeyValuePairElement, 

2548 position_codec: LintCapablePositionCodec, 

2549 lines: list[str], 

2550 ) -> Iterable[TextEdit]: 

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

2552 return 

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

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

2555 return 

2556 

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

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

2559 ) 

2560 

2561 edit_range = position_codec.range_to_client_units( 

2562 lines, 

2563 Range( 

2564 Position( 

2565 field_name_range_te.start_pos.line_position, 

2566 field_name_range_te.start_pos.cursor_position, 

2567 ), 

2568 Position( 

2569 field_name_range_te.start_pos.line_position, 

2570 field_name_range_te.end_pos.cursor_position, 

2571 ), 

2572 ), 

2573 ) 

2574 yield TextEdit( 

2575 edit_range, 

2576 self.name, 

2577 ) 

2578 

2579 def reformat_field( 

2580 self, 

2581 effective_preference: "EffectiveFormattingPreference", 

2582 stanza_range: TERange, 

2583 kvpair: Deb822KeyValuePairElement, 

2584 formatter: FormatterCallback, 

2585 position_codec: LintCapablePositionCodec, 

2586 lines: list[str], 

2587 ) -> Iterable[TextEdit]: 

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

2589 yield from self._reformat_field_name( 

2590 effective_preference, 

2591 stanza_range, 

2592 kvpair, 

2593 position_codec, 

2594 lines, 

2595 ) 

2596 return trim_end_of_line_whitespace( 

2597 position_codec, 

2598 lines, 

2599 line_range=range( 

2600 kvpair_range.start_pos.line_position, 

2601 kvpair_range.end_pos.line_position, 

2602 ), 

2603 ) 

2604 

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

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

2607 

2608 

2609@dataclasses.dataclass(slots=True) 

2610class DctrlLikeKnownField(Deb822KnownField): 

2611 

2612 def reformat_field( 

2613 self, 

2614 effective_preference: "EffectiveFormattingPreference", 

2615 stanza_range: TERange, 

2616 kvpair: Deb822KeyValuePairElement, 

2617 formatter: FormatterCallback, 

2618 position_codec: LintCapablePositionCodec, 

2619 lines: list[str], 

2620 ) -> Iterable[TextEdit]: 

2621 interpretation = self.field_value_class.interpreter() 

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

2623 not effective_preference.deb822_normalize_field_content 

2624 or interpretation is None 

2625 ): 

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

2627 effective_preference, 

2628 stanza_range, 

2629 kvpair, 

2630 formatter, 

2631 position_codec, 

2632 lines, 

2633 ) 

2634 return 

2635 if not self.reformattable_field: 

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

2637 effective_preference, 

2638 stanza_range, 

2639 kvpair, 

2640 formatter, 

2641 position_codec, 

2642 lines, 

2643 ) 

2644 return 

2645 

2646 # Preserve the name fixes from the super call. 

2647 yield from self._reformat_field_name( 

2648 effective_preference, 

2649 stanza_range, 

2650 kvpair, 

2651 position_codec, 

2652 lines, 

2653 ) 

2654 

2655 seen: set[str] = set() 

2656 old_kvpair_range = kvpair.range_in_parent() 

2657 sort = self.is_sortable_field 

2658 

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

2660 field_content = kvpair.interpret_as(interpretation) 

2661 old_value = field_content.convert_to_text(with_field_name=False) 

2662 for package_ref in field_content.iter_value_references(): 

2663 value = package_ref.value 

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

2665 stanza_range.start_pos 

2666 ) 

2667 sublines = lines[ 

2668 value_range.start_pos.line_position : value_range.end_pos.line_position 

2669 ] 

2670 

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

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

2673 return 

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

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

2676 else: 

2677 new_value = value 

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

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

2680 package_ref.value = new_value 

2681 seen.add(new_value) 

2682 else: 

2683 package_ref.remove() 

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

2685 field_content.sort(key=_sort_packages_key) 

2686 field_content.value_formatter(formatter) 

2687 field_content.reformat_when_finished() 

2688 

2689 new_value = field_content.convert_to_text(with_field_name=False) 

2690 if new_value != old_value: 

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

2692 old_kvpair_range.start_pos 

2693 ) 

2694 range_server_units = te_range_to_lsp( 

2695 value_range.relative_to(stanza_range.start_pos) 

2696 ) 

2697 yield TextEdit( 

2698 position_codec.range_to_client_units(lines, range_server_units), 

2699 new_value, 

2700 ) 

2701 

2702 @property 

2703 def reformattable_field(self) -> bool: 

2704 return self.is_relationship_field or self.is_sortable_field 

2705 

2706 @property 

2707 def is_relationship_field(self) -> bool: 

2708 return False 

2709 

2710 @property 

2711 def is_sortable_field(self) -> bool: 

2712 return self.is_relationship_field 

2713 

2714 

2715@dataclasses.dataclass(slots=True) 

2716class DTestsCtrlKnownField(DctrlLikeKnownField): 

2717 @property 

2718 def is_relationship_field(self) -> bool: 

2719 return self.name == "Depends" 

2720 

2721 @property 

2722 def is_sortable_field(self) -> bool: 

2723 return self.is_relationship_field or self.name in ( 

2724 "Features", 

2725 "Restrictions", 

2726 "Tests", 

2727 ) 

2728 

2729 

2730@dataclasses.dataclass(slots=True) 

2731class DctrlKnownField(DctrlLikeKnownField): 

2732 

2733 def field_omitted_diagnostics( 

2734 self, 

2735 deb822_file: Deb822FileElement, 

2736 representation_field_range: "TERange", 

2737 stanza: Deb822ParagraphElement, 

2738 stanza_position: "TEPosition", 

2739 header_stanza: Deb822FileElement | None, 

2740 lint_state: LintState, 

2741 ) -> None: 

2742 missing_field_severity = self.missing_field_severity 

2743 if missing_field_severity is None: 

2744 return 

2745 

2746 if ( 

2747 self.inheritable_from_other_stanza 

2748 and header_stanza is not None 

2749 and self.name in header_stanza 

2750 ): 

2751 return 

2752 

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

2754 stanzas = list(deb822_file)[1:] 

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

2756 return 

2757 

2758 lint_state.emit_diagnostic( 

2759 representation_field_range, 

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

2761 missing_field_severity, 

2762 self.missing_field_authority, 

2763 ) 

2764 

2765 def reformat_field( 

2766 self, 

2767 effective_preference: "EffectiveFormattingPreference", 

2768 stanza_range: TERange, 

2769 kvpair: Deb822KeyValuePairElement, 

2770 formatter: FormatterCallback, 

2771 position_codec: LintCapablePositionCodec, 

2772 lines: list[str], 

2773 ) -> Iterable[TextEdit]: 

2774 if ( 

2775 self.name == "Architecture" 

2776 and effective_preference.deb822_normalize_field_content 

2777 ): 

2778 interpretation = self.field_value_class.interpreter() 

2779 assert interpretation is not None 

2780 interpreted = kvpair.interpret_as(interpretation) 

2781 archs = list(interpreted) 

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

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

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

2785 reformat_edits = list( 

2786 self._reformat_field_name( 

2787 effective_preference, 

2788 stanza_range, 

2789 kvpair, 

2790 position_codec, 

2791 lines, 

2792 ) 

2793 ) 

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

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

2796 kvpair.range_in_parent().start_pos 

2797 ) 

2798 kvpair_range = te_range_to_lsp( 

2799 value_range.relative_to(stanza_range.start_pos) 

2800 ) 

2801 reformat_edits.append( 

2802 TextEdit( 

2803 position_codec.range_to_client_units(lines, kvpair_range), 

2804 new_value, 

2805 ) 

2806 ) 

2807 return reformat_edits 

2808 

2809 return super(DctrlKnownField, self).reformat_field( 

2810 effective_preference, 

2811 stanza_range, 

2812 kvpair, 

2813 formatter, 

2814 position_codec, 

2815 lines, 

2816 ) 

2817 

2818 @property 

2819 def is_relationship_field(self) -> bool: 

2820 name_lc = self.name.lower() 

2821 return ( 

2822 name_lc in all_package_relationship_fields() 

2823 or name_lc in all_source_relationship_fields() 

2824 ) 

2825 

2826 @property 

2827 def reformattable_field(self) -> bool: 

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

2829 

2830 

2831@dataclasses.dataclass(slots=True) 

2832class DctrlRelationshipKnownField(DctrlKnownField): 

2833 allowed_version_operators: frozenset[str] = frozenset() 

2834 supports_or_relation: bool = True 

2835 

2836 @property 

2837 def is_relationship_field(self) -> bool: 

2838 return True 

2839 

2840 

2841SOURCE_FIELDS = _fields( 

2842 DctrlKnownField( 

2843 "Source", 

2844 FieldValueClass.SINGLE_VALUE, 

2845 custom_field_check=_combined_custom_field_check( 

2846 _each_value_match_regex_validation(PKGNAME_REGEX), 

2847 _has_packaging_expected_file( 

2848 "copyright", 

2849 "No copyright file (package license)", 

2850 severity="warning", 

2851 ), 

2852 _has_packaging_expected_file( 

2853 "changelog", 

2854 "No Debian changelog file", 

2855 severity="error", 

2856 ), 

2857 _has_build_instructions, 

2858 ), 

2859 ), 

2860 DctrlKnownField( 

2861 "Standards-Version", 

2862 FieldValueClass.SINGLE_VALUE, 

2863 custom_field_check=_sv_field_validation, 

2864 ), 

2865 DctrlKnownField( 

2866 "Section", 

2867 FieldValueClass.SINGLE_VALUE, 

2868 known_values=ALL_SECTIONS, 

2869 ), 

2870 DctrlKnownField( 

2871 "Priority", 

2872 FieldValueClass.SINGLE_VALUE, 

2873 ), 

2874 DctrlKnownField( 

2875 "Maintainer", 

2876 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2877 custom_field_check=_combined_custom_field_check( 

2878 _maintainer_field_validator, 

2879 _canonical_maintainer_name, 

2880 ), 

2881 ), 

2882 DctrlKnownField( 

2883 "Uploaders", 

2884 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2885 custom_field_check=_canonical_maintainer_name, 

2886 ), 

2887 DctrlRelationshipKnownField( 

2888 "Build-Depends", 

2889 FieldValueClass.COMMA_SEPARATED_LIST, 

2890 custom_field_check=_dctrl_validate_dep, 

2891 ), 

2892 DctrlRelationshipKnownField( 

2893 "Build-Depends-Arch", 

2894 FieldValueClass.COMMA_SEPARATED_LIST, 

2895 custom_field_check=_dctrl_validate_dep, 

2896 ), 

2897 DctrlRelationshipKnownField( 

2898 "Build-Depends-Indep", 

2899 FieldValueClass.COMMA_SEPARATED_LIST, 

2900 custom_field_check=_dctrl_validate_dep, 

2901 ), 

2902 DctrlRelationshipKnownField( 

2903 "Build-Conflicts", 

2904 FieldValueClass.COMMA_SEPARATED_LIST, 

2905 supports_or_relation=False, 

2906 custom_field_check=_dctrl_validate_dep, 

2907 ), 

2908 DctrlRelationshipKnownField( 

2909 "Build-Conflicts-Arch", 

2910 FieldValueClass.COMMA_SEPARATED_LIST, 

2911 supports_or_relation=False, 

2912 custom_field_check=_dctrl_validate_dep, 

2913 ), 

2914 DctrlRelationshipKnownField( 

2915 "Build-Conflicts-Indep", 

2916 FieldValueClass.COMMA_SEPARATED_LIST, 

2917 supports_or_relation=False, 

2918 custom_field_check=_dctrl_validate_dep, 

2919 ), 

2920 DctrlKnownField( 

2921 "Rules-Requires-Root", 

2922 FieldValueClass.SPACE_SEPARATED_LIST, 

2923 custom_field_check=_rrr_build_driver_mismatch, 

2924 ), 

2925 DctrlKnownField( 

2926 "X-Style", 

2927 FieldValueClass.SINGLE_VALUE, 

2928 known_values=ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

2929 ), 

2930 DctrlKnownField( 

2931 "Homepage", 

2932 FieldValueClass.SINGLE_VALUE, 

2933 custom_field_check=_validate_homepage_field, 

2934 ), 

2935) 

2936 

2937 

2938BINARY_FIELDS = _fields( 

2939 DctrlKnownField( 

2940 "Package", 

2941 FieldValueClass.SINGLE_VALUE, 

2942 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

2943 ), 

2944 DctrlKnownField( 

2945 "Architecture", 

2946 FieldValueClass.SPACE_SEPARATED_LIST, 

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

2948 known_values=allowed_values(dpkg_arch_and_wildcards()), 

2949 ), 

2950 DctrlKnownField( 

2951 "Pre-Depends", 

2952 FieldValueClass.COMMA_SEPARATED_LIST, 

2953 custom_field_check=_dctrl_validate_dep, 

2954 ), 

2955 DctrlKnownField( 

2956 "Depends", 

2957 FieldValueClass.COMMA_SEPARATED_LIST, 

2958 custom_field_check=_dctrl_validate_dep, 

2959 ), 

2960 DctrlKnownField( 

2961 "Recommends", 

2962 FieldValueClass.COMMA_SEPARATED_LIST, 

2963 custom_field_check=_dctrl_validate_dep, 

2964 ), 

2965 DctrlKnownField( 

2966 "Suggests", 

2967 FieldValueClass.COMMA_SEPARATED_LIST, 

2968 custom_field_check=_dctrl_validate_dep, 

2969 ), 

2970 DctrlKnownField( 

2971 "Enhances", 

2972 FieldValueClass.COMMA_SEPARATED_LIST, 

2973 custom_field_check=_dctrl_validate_dep, 

2974 ), 

2975 DctrlRelationshipKnownField( 

2976 "Provides", 

2977 FieldValueClass.COMMA_SEPARATED_LIST, 

2978 custom_field_check=_dctrl_validate_dep, 

2979 supports_or_relation=False, 

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

2981 ), 

2982 DctrlRelationshipKnownField( 

2983 "Conflicts", 

2984 FieldValueClass.COMMA_SEPARATED_LIST, 

2985 custom_field_check=_dctrl_validate_dep, 

2986 supports_or_relation=False, 

2987 ), 

2988 DctrlRelationshipKnownField( 

2989 "Breaks", 

2990 FieldValueClass.COMMA_SEPARATED_LIST, 

2991 custom_field_check=_dctrl_validate_dep, 

2992 supports_or_relation=False, 

2993 ), 

2994 DctrlRelationshipKnownField( 

2995 "Replaces", 

2996 FieldValueClass.COMMA_SEPARATED_LIST, 

2997 custom_field_check=_dctrl_validate_dep, 

2998 ), 

2999 DctrlKnownField( 

3000 "Build-Profiles", 

3001 FieldValueClass.BUILD_PROFILES_LIST, 

3002 ), 

3003 DctrlKnownField( 

3004 "Section", 

3005 FieldValueClass.SINGLE_VALUE, 

3006 known_values=ALL_SECTIONS, 

3007 ), 

3008 DctrlRelationshipKnownField( 

3009 "Built-Using", 

3010 FieldValueClass.COMMA_SEPARATED_LIST, 

3011 custom_field_check=_arch_not_all_only_field_validation, 

3012 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3013 supports_or_relation=False, 

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

3015 ), 

3016 DctrlRelationshipKnownField( 

3017 "Static-Built-Using", 

3018 FieldValueClass.COMMA_SEPARATED_LIST, 

3019 custom_field_check=_arch_not_all_only_field_validation, 

3020 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3021 supports_or_relation=False, 

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

3023 ), 

3024 DctrlKnownField( 

3025 "Multi-Arch", 

3026 FieldValueClass.SINGLE_VALUE, 

3027 custom_field_check=_combined_custom_field_check( 

3028 _not_applicable_to_udeb_field_validation, _dctrl_ma_field_validation 

3029 ), 

3030 known_values=allowed_values( 

3031 ( 

3032 Keyword( 

3033 "same", 

3034 can_complete_keyword_in_stanza=_complete_only_in_arch_dep_pkgs, 

3035 ), 

3036 ), 

3037 ), 

3038 ), 

3039 DctrlKnownField( 

3040 "XB-Installer-Menu-Item", 

3041 FieldValueClass.SINGLE_VALUE, 

3042 can_complete_field_in_stanza=_complete_only_for_udeb_pkgs, 

3043 custom_field_check=_combined_custom_field_check( 

3044 _udeb_only_field_validation, 

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

3046 ), 

3047 ), 

3048 DctrlKnownField( 

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

3050 FieldValueClass.SINGLE_VALUE, 

3051 custom_field_check=_arch_not_all_only_field_validation, 

3052 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3053 ), 

3054 DctrlKnownField( 

3055 "X-Doc-Main-Package", 

3056 FieldValueClass.SINGLE_VALUE, 

3057 custom_field_check=_binary_package_from_same_source, 

3058 ), 

3059 DctrlKnownField( 

3060 "X-Time64-Compat", 

3061 FieldValueClass.SINGLE_VALUE, 

3062 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3063 custom_field_check=_combined_custom_field_check( 

3064 _each_value_match_regex_validation(PKGNAME_REGEX), 

3065 _arch_not_all_only_field_validation, 

3066 ), 

3067 ), 

3068 DctrlKnownField( 

3069 "Description", 

3070 FieldValueClass.FREE_TEXT_FIELD, 

3071 custom_field_check=dctrl_description_validator, 

3072 ), 

3073 DctrlKnownField( 

3074 "XB-Cnf-Visible-Pkgname", 

3075 FieldValueClass.SINGLE_VALUE, 

3076 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

3077 ), 

3078 DctrlKnownField( 

3079 "Homepage", 

3080 FieldValueClass.SINGLE_VALUE, 

3081 show_as_inherited=False, 

3082 custom_field_check=_validate_homepage_field, 

3083 ), 

3084) 

3085_DEP5_HEADER_FIELDS = _fields( 

3086 Deb822KnownField( 

3087 "Format", 

3088 FieldValueClass.SINGLE_VALUE, 

3089 custom_field_check=_use_https_instead_of_http, 

3090 ), 

3091) 

3092_DEP5_FILES_FIELDS = _fields( 

3093 Deb822KnownField( 

3094 "Files", 

3095 FieldValueClass.DEP5_FILE_LIST, 

3096 custom_field_check=_dep5_files_check, 

3097 ), 

3098) 

3099_DEP5_LICENSE_FIELDS = _fields( 

3100 Deb822KnownField( 

3101 "License", 

3102 FieldValueClass.FREE_TEXT_FIELD, 

3103 ), 

3104) 

3105 

3106_DTESTSCTRL_FIELDS = _fields( 

3107 DTestsCtrlKnownField( 

3108 "Architecture", 

3109 FieldValueClass.SPACE_SEPARATED_LIST, 

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

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

3112 ), 

3113) 

3114_DWATCH_HEADER_FIELDS = _fields() 

3115_DWATCH_TEMPLATE_FIELDS = _fields() 

3116_DWATCH_SOURCE_FIELDS = _fields() 

3117 

3118 

3119@dataclasses.dataclass(slots=True) 

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

3121 stanza_type_name: str 

3122 stanza_fields: Mapping[str, F] 

3123 is_substvars_allowed_in_stanza: bool 

3124 

3125 async def stanza_diagnostics( 

3126 self, 

3127 deb822_file: Deb822FileElement, 

3128 stanza: Deb822ParagraphElement, 

3129 stanza_position_in_file: "TEPosition", 

3130 lint_state: LintState, 

3131 *, 

3132 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3133 confusable_with_stanza_name: str | None = None, 

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

3135 ) -> None: 

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

3137 confusable_with_stanza_metadata is None 

3138 ): 

3139 raise ValueError( 

3140 "confusable_with_stanza_name and confusable_with_stanza_metadata must be used together" 

3141 ) 

3142 _, representation_field_range = self.stanza_representation( 

3143 stanza, 

3144 stanza_position_in_file, 

3145 ) 

3146 known_fields = self.stanza_fields 

3147 self.omitted_field_diagnostics( 

3148 lint_state, 

3149 deb822_file, 

3150 stanza, 

3151 stanza_position_in_file, 

3152 inherit_from_stanza=inherit_from_stanza, 

3153 representation_field_range=representation_field_range, 

3154 ) 

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

3156 

3157 async for kvpair_range, kvpair in lint_state.slow_iter( 

3158 with_range_in_continuous_parts( 

3159 stanza.iter_parts(), 

3160 start_relative_to=stanza_position_in_file, 

3161 ), 

3162 yield_every=1, 

3163 ): 

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

3165 continue 

3166 field_name_token = kvpair.field_token 

3167 field_name = field_name_token.text 

3168 field_name_lc = field_name.lower() 

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

3170 field_name = str(field_name) 

3171 normalized_field_name_lc = self.normalize_field_name(field_name_lc) 

3172 known_field = known_fields.get(normalized_field_name_lc) 

3173 field_value = stanza[field_name] 

3174 kvpair_range_te = kvpair.range_in_parent().relative_to( 

3175 stanza_position_in_file 

3176 ) 

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

3178 kvpair_range_te.start_pos 

3179 ) 

3180 field_position_te = field_range.start_pos 

3181 field_name_typo_detected = False 

3182 dup_field_key = ( 

3183 known_field.name 

3184 if known_field is not None 

3185 else normalized_field_name_lc 

3186 ) 

3187 existing_field_range = seen_fields.get(dup_field_key) 

3188 if existing_field_range is not None: 

3189 existing_field_range[3].append(field_range) 

3190 existing_field_range[4].add(field_name) 

3191 else: 

3192 normalized_field_name = self.normalize_field_name(field_name) 

3193 seen_fields[dup_field_key] = ( 

3194 known_field.name if known_field else field_name, 

3195 normalized_field_name, 

3196 field_range, 

3197 [], 

3198 {field_name}, 

3199 ) 

3200 

3201 if known_field is None: 

3202 candidates = detect_possible_typo( 

3203 normalized_field_name_lc, known_fields 

3204 ) 

3205 if candidates: 

3206 known_field = known_fields[candidates[0]] 

3207 field_range = TERange.from_position_and_size( 

3208 field_position_te, kvpair.field_token.size() 

3209 ) 

3210 field_name_typo_detected = True 

3211 lint_state.emit_diagnostic( 

3212 field_range, 

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

3214 "warning", 

3215 "debputy", 

3216 quickfixes=[ 

3217 propose_correct_text_quick_fix(known_fields[m].name) 

3218 for m in candidates 

3219 ], 

3220 ) 

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

3222 lint_state.emit_diagnostic( 

3223 field_range, 

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

3225 "error", 

3226 "Policy 5.1", 

3227 ) 

3228 continue 

3229 if known_field is None: 

3230 known_else_where = confusable_with_stanza_metadata.stanza_fields.get( 

3231 normalized_field_name_lc 

3232 ) 

3233 if known_else_where is not None: 

3234 msg = ( 

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

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

3237 f" or in case of two run-in stanzas add an empty newline to separate them" 

3238 ) 

3239 lint_state.emit_diagnostic( 

3240 field_range, 

3241 msg, 

3242 "error", 

3243 known_else_where.missing_field_authority, 

3244 ) 

3245 continue 

3246 await known_field.field_diagnostics( 

3247 deb822_file, 

3248 kvpair, 

3249 stanza, 

3250 stanza_position_in_file, 

3251 kvpair_range_te, 

3252 lint_state, 

3253 field_name_typo_reported=field_name_typo_detected, 

3254 ) 

3255 

3256 inherit_value = ( 

3257 inherit_from_stanza.get(field_name) if inherit_from_stanza else None 

3258 ) 

3259 

3260 if ( 

3261 known_field.inheritable_from_other_stanza 

3262 and inherit_value is not None 

3263 and field_value == inherit_value 

3264 ): 

3265 quick_fix = propose_remove_range_quick_fix( 

3266 proposed_title="Remove redundant definition" 

3267 ) 

3268 lint_state.emit_diagnostic( 

3269 kvpair_range_te, 

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

3271 "informational", 

3272 "debputy", 

3273 quickfixes=[quick_fix], 

3274 ) 

3275 for ( 

3276 field_name, 

3277 normalized_field_name, 

3278 field_range, 

3279 duplicates, 

3280 used_fields, 

3281 ) in seen_fields.values(): 

3282 if not duplicates: 

3283 continue 

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

3285 via_aliases_msg = " (via aliases)" 

3286 else: 

3287 via_aliases_msg = "" 

3288 for dup_range in duplicates: 

3289 lint_state.emit_diagnostic( 

3290 dup_range, 

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

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

3293 "error", 

3294 "Policy 5.1", 

3295 related_information=[ 

3296 lint_state.related_diagnostic_information( 

3297 field_range, 

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

3299 ), 

3300 ], 

3301 ) 

3302 

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

3304 key_lc = key.lower() 

3305 key_norm = normalize_dctrl_field_name(key_lc) 

3306 return self.stanza_fields[key_norm] 

3307 

3308 def __len__(self) -> int: 

3309 return len(self.stanza_fields) 

3310 

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

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

3313 

3314 def omitted_field_diagnostics( 

3315 self, 

3316 lint_state: LintState, 

3317 deb822_file: Deb822FileElement, 

3318 stanza: Deb822ParagraphElement, 

3319 stanza_position: "TEPosition", 

3320 *, 

3321 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3322 representation_field_range: Range | None = None, 

3323 ) -> None: 

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

3325 _, representation_field_range = self.stanza_representation( 

3326 stanza, 

3327 stanza_position, 

3328 ) 

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

3330 if known_field.name in stanza: 

3331 continue 

3332 

3333 known_field.field_omitted_diagnostics( 

3334 deb822_file, 

3335 representation_field_range, 

3336 stanza, 

3337 stanza_position, 

3338 inherit_from_stanza, 

3339 lint_state, 

3340 ) 

3341 

3342 def _paragraph_representation_field( 

3343 self, 

3344 paragraph: Deb822ParagraphElement, 

3345 ) -> Deb822KeyValuePairElement: 

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

3347 

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

3349 return field_name 

3350 

3351 def stanza_representation( 

3352 self, 

3353 stanza: Deb822ParagraphElement, 

3354 stanza_position: TEPosition, 

3355 ) -> tuple[Deb822KeyValuePairElement, TERange]: 

3356 representation_field = self._paragraph_representation_field(stanza) 

3357 representation_field_range = representation_field.range_in_parent().relative_to( 

3358 stanza_position 

3359 ) 

3360 return representation_field, representation_field_range 

3361 

3362 def reformat_stanza( 

3363 self, 

3364 effective_preference: "EffectiveFormattingPreference", 

3365 stanza: Deb822ParagraphElement, 

3366 stanza_range: TERange, 

3367 formatter: FormatterCallback, 

3368 position_codec: LintCapablePositionCodec, 

3369 lines: list[str], 

3370 ) -> Iterable[TextEdit]: 

3371 for field_name in stanza: 

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

3373 if known_field is None: 

3374 continue 

3375 kvpair = stanza.get_kvpair_element(field_name) 

3376 yield from known_field.reformat_field( 

3377 effective_preference, 

3378 stanza_range, 

3379 kvpair, 

3380 formatter, 

3381 position_codec, 

3382 lines, 

3383 ) 

3384 

3385 

3386@dataclasses.dataclass(slots=True) 

3387class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3388 pass 

3389 

3390 

3391@dataclasses.dataclass(slots=True) 

3392class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]): 

3393 

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

3395 return normalize_dctrl_field_name(field_name) 

3396 

3397 

3398@dataclasses.dataclass(slots=True) 

3399class DTestsCtrlStanzaMetadata(StanzaMetadata[DTestsCtrlKnownField]): 

3400 

3401 def omitted_field_diagnostics( 

3402 self, 

3403 lint_state: LintState, 

3404 deb822_file: Deb822FileElement, 

3405 stanza: Deb822ParagraphElement, 

3406 stanza_position: "TEPosition", 

3407 *, 

3408 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3409 representation_field_range: Range | None = None, 

3410 ) -> None: 

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

3412 _, representation_field_range = self.stanza_representation( 

3413 stanza, 

3414 stanza_position, 

3415 ) 

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

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

3418 lint_state.emit_diagnostic( 

3419 representation_field_range, 

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

3421 "error", 

3422 # TODO: Better authority_reference 

3423 auth_ref, 

3424 ) 

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

3426 lint_state.emit_diagnostic( 

3427 representation_field_range, 

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

3429 "error", 

3430 # TODO: Better authority_reference 

3431 auth_ref, 

3432 ) 

3433 

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

3435 # always do the super call. 

3436 super(DTestsCtrlStanzaMetadata, self).omitted_field_diagnostics( 

3437 lint_state, 

3438 deb822_file, 

3439 stanza, 

3440 stanza_position, 

3441 representation_field_range=representation_field_range, 

3442 inherit_from_stanza=inherit_from_stanza, 

3443 ) 

3444 

3445 

3446@dataclasses.dataclass(slots=True) 

3447class DebianWatchStanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3448 

3449 def omitted_field_diagnostics( 

3450 self, 

3451 lint_state: LintState, 

3452 deb822_file: Deb822FileElement, 

3453 stanza: Deb822ParagraphElement, 

3454 stanza_position: "TEPosition", 

3455 *, 

3456 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3457 representation_field_range: Range | None = None, 

3458 ) -> None: 

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

3460 _, representation_field_range = self.stanza_representation( 

3461 stanza, 

3462 stanza_position, 

3463 ) 

3464 

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

3466 self.stanza_type_name != "Header" 

3467 and "Source" not in stanza 

3468 and "Template" not in stanza 

3469 ): 

3470 lint_state.emit_diagnostic( 

3471 representation_field_range, 

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

3473 "error", 

3474 # TODO: Better authority_reference 

3475 "debputy", 

3476 ) 

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

3478 # call until this error is resolved. 

3479 return 

3480 

3481 super(DebianWatchStanzaMetadata, self).omitted_field_diagnostics( 

3482 lint_state, 

3483 deb822_file, 

3484 stanza, 

3485 stanza_position, 

3486 representation_field_range=representation_field_range, 

3487 inherit_from_stanza=inherit_from_stanza, 

3488 ) 

3489 

3490 

3491def lsp_reference_data_dir() -> str: 

3492 return os.path.join( 

3493 os.path.dirname(__file__), 

3494 "data", 

3495 ) 

3496 

3497 

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

3499 

3500 def __init__(self) -> None: 

3501 self._is_initialized = False 

3502 self._data: Deb822ReferenceData | None = None 

3503 

3504 @property 

3505 def reference_data_basename(self) -> str: 

3506 raise NotImplementedError 

3507 

3508 def _new_field( 

3509 self, 

3510 name: str, 

3511 field_value_type: FieldValueClass, 

3512 ) -> F: 

3513 raise NotImplementedError 

3514 

3515 def _reference_data(self) -> Deb822ReferenceData: 

3516 ref = self._data 

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

3518 return ref 

3519 

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

3521 self.reference_data_basename 

3522 ) 

3523 

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

3525 raw = MANIFEST_YAML.load(fd) 

3526 

3527 attr_path = AttributePath.root_path(p) 

3528 try: 

3529 ref = DEB822_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

3530 except ManifestParseException as e: 

3531 raise ValueError( 

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

3533 ) from e 

3534 self._data = ref 

3535 return ref 

3536 

3537 @property 

3538 def is_initialized(self) -> bool: 

3539 return self._is_initialized 

3540 

3541 def ensure_initialized(self) -> None: 

3542 if self.is_initialized: 

3543 return 

3544 # Enables us to use __getitem__ 

3545 self._is_initialized = True 

3546 ref_data = self._reference_data() 

3547 ref_defs = ref_data.get("definitions") 

3548 variables = {} 

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

3550 for ref_variable in ref_variables: 

3551 name = ref_variable["name"] 

3552 fallback = ref_variable["fallback"] 

3553 variables[name] = fallback 

3554 

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

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

3557 return None 

3558 try: 

3559 return template.format(**variables) 

3560 except ValueError as e: 

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

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

3563 

3564 for ref_stanza_type in ref_data["stanza_types"]: 

3565 stanza_name = ref_stanza_type["stanza_name"] 

3566 stanza = self[stanza_name] 

3567 stanza_fields = dict(stanza.stanza_fields) 

3568 stanza.stanza_fields = stanza_fields 

3569 for ref_field in ref_stanza_type["fields"]: 

3570 _resolve_field( 

3571 ref_field, 

3572 stanza_fields, 

3573 self._new_field, 

3574 _resolve_doc, 

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

3576 ) 

3577 

3578 def file_metadata_applies_to_file( 

3579 self, 

3580 deb822_file: Deb822FileElement | None, 

3581 ) -> bool: 

3582 return deb822_file is not None 

3583 

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

3585 return self.guess_stanza_classification_by_idx(stanza_idx) 

3586 

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

3588 raise NotImplementedError 

3589 

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

3591 raise NotImplementedError 

3592 

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

3594 raise NotImplementedError 

3595 

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

3597 try: 

3598 return self[item] 

3599 except KeyError: 

3600 return None 

3601 

3602 def reformat( 

3603 self, 

3604 effective_preference: "EffectiveFormattingPreference", 

3605 deb822_file: Deb822FileElement, 

3606 formatter: FormatterCallback, 

3607 _content: str, 

3608 position_codec: LintCapablePositionCodec, 

3609 lines: list[str], 

3610 ) -> Iterable[TextEdit]: 

3611 stanza_idx = -1 

3612 for token_or_element in deb822_file.iter_parts(): 

3613 if isinstance(token_or_element, Deb822ParagraphElement): 

3614 stanza_range = token_or_element.range_in_parent() 

3615 stanza_idx += 1 

3616 stanza_metadata = self.classify_stanza(token_or_element, stanza_idx) 

3617 yield from stanza_metadata.reformat_stanza( 

3618 effective_preference, 

3619 token_or_element, 

3620 stanza_range, 

3621 formatter, 

3622 position_codec, 

3623 lines, 

3624 ) 

3625 else: 

3626 token_range = token_or_element.range_in_parent() 

3627 yield from trim_end_of_line_whitespace( 

3628 position_codec, 

3629 lines, 

3630 line_range=range( 

3631 token_range.start_pos.line_position, 

3632 token_range.end_pos.line_position, 

3633 ), 

3634 ) 

3635 

3636 

3637_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata( 

3638 "Source", 

3639 SOURCE_FIELDS, 

3640 is_substvars_allowed_in_stanza=False, 

3641) 

3642_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata( 

3643 "Package", 

3644 BINARY_FIELDS, 

3645 is_substvars_allowed_in_stanza=True, 

3646) 

3647 

3648_DEP5_HEADER_STANZA = Dep5StanzaMetadata( 

3649 "Header", 

3650 _DEP5_HEADER_FIELDS, 

3651 is_substvars_allowed_in_stanza=False, 

3652) 

3653_DEP5_FILES_STANZA = Dep5StanzaMetadata( 

3654 "Files", 

3655 _DEP5_FILES_FIELDS, 

3656 is_substvars_allowed_in_stanza=False, 

3657) 

3658_DEP5_LICENSE_STANZA = Dep5StanzaMetadata( 

3659 "License", 

3660 _DEP5_LICENSE_FIELDS, 

3661 is_substvars_allowed_in_stanza=False, 

3662) 

3663 

3664_DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata( 

3665 "Tests", 

3666 _DTESTSCTRL_FIELDS, 

3667 is_substvars_allowed_in_stanza=False, 

3668) 

3669 

3670_WATCH_HEADER_HEADER_STANZA = DebianWatchStanzaMetadata( 

3671 "Header", 

3672 _DWATCH_HEADER_FIELDS, 

3673 is_substvars_allowed_in_stanza=False, 

3674) 

3675_WATCH_SOURCE_STANZA = DebianWatchStanzaMetadata( 

3676 "Source", 

3677 _DWATCH_SOURCE_FIELDS, 

3678 is_substvars_allowed_in_stanza=False, 

3679) 

3680 

3681 

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

3683 

3684 @property 

3685 def reference_data_basename(self) -> str: 

3686 return "debian_copyright_reference_data.yaml" 

3687 

3688 def _new_field( 

3689 self, 

3690 name: str, 

3691 field_value_type: FieldValueClass, 

3692 ) -> F: 

3693 return Deb822KnownField(name, field_value_type) 

3694 

3695 def file_metadata_applies_to_file( 

3696 self, 

3697 deb822_file: Deb822FileElement | None, 

3698 ) -> bool: 

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

3700 return False 

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

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

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

3704 return False 

3705 

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

3707 if part.is_error: 

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

3709 return False 

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

3711 break 

3712 return True 

3713 

3714 def classify_stanza( 

3715 self, 

3716 stanza: Deb822ParagraphElement, 

3717 stanza_idx: int, 

3718 ) -> Dep5StanzaMetadata: 

3719 self.ensure_initialized() 

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

3721 return _DEP5_HEADER_STANZA 

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

3723 if "Files" in stanza: 

3724 return _DEP5_FILES_STANZA 

3725 return _DEP5_LICENSE_STANZA 

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

3727 

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

3729 self.ensure_initialized() 

3730 if stanza_idx == 0: 

3731 return _DEP5_HEADER_STANZA 

3732 if stanza_idx > 0: 

3733 return _DEP5_FILES_STANZA 

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

3735 

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

3737 self.ensure_initialized() 

3738 # Order assumption made in the LSP code. 

3739 yield _DEP5_HEADER_STANZA 

3740 yield _DEP5_FILES_STANZA 

3741 yield _DEP5_LICENSE_STANZA 

3742 

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

3744 self.ensure_initialized() 

3745 if item == "Header": 

3746 return _DEP5_HEADER_STANZA 

3747 if item == "Files": 

3748 return _DEP5_FILES_STANZA 

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

3750 return _DEP5_LICENSE_STANZA 

3751 raise KeyError(item) 

3752 

3753 

3754class DebianWatch5FileMetadata( 

3755 Deb822FileMetadata[DebianWatchStanzaMetadata, Deb822KnownField] 

3756): 

3757 

3758 @property 

3759 def reference_data_basename(self) -> str: 

3760 return "debian_watch_reference_data.yaml" 

3761 

3762 def _new_field( 

3763 self, 

3764 name: str, 

3765 field_value_type: FieldValueClass, 

3766 ) -> F: 

3767 return Deb822KnownField(name, field_value_type) 

3768 

3769 def file_metadata_applies_to_file( 

3770 self, deb822_file: Deb822FileElement | None 

3771 ) -> bool: 

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

3773 return False 

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

3775 

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

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

3778 return False 

3779 

3780 try: 

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

3782 return False 

3783 except (ValueError, IndexError, TypeError): 

3784 return False 

3785 

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

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

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

3789 return False 

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

3791 break 

3792 return True 

3793 

3794 def classify_stanza( 

3795 self, 

3796 stanza: Deb822ParagraphElement, 

3797 stanza_idx: int, 

3798 ) -> DebianWatchStanzaMetadata: 

3799 self.ensure_initialized() 

3800 if stanza_idx == 0: 

3801 return _WATCH_HEADER_HEADER_STANZA 

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

3803 return _WATCH_SOURCE_STANZA 

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

3805 

3806 def guess_stanza_classification_by_idx( 

3807 self, stanza_idx: int 

3808 ) -> DebianWatchStanzaMetadata: 

3809 self.ensure_initialized() 

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

3811 return _WATCH_HEADER_HEADER_STANZA 

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

3813 return _WATCH_SOURCE_STANZA 

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

3815 

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

3817 self.ensure_initialized() 

3818 # Order assumption made in the LSP code. 

3819 yield _WATCH_HEADER_HEADER_STANZA 

3820 yield _WATCH_SOURCE_STANZA 

3821 

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

3823 self.ensure_initialized() 

3824 if item == "Header": 

3825 return _WATCH_HEADER_HEADER_STANZA 

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

3827 return _WATCH_SOURCE_STANZA 

3828 raise KeyError(item) 

3829 

3830 

3831def _resolve_keyword( 

3832 ref_value: StaticValue, 

3833 known_values: dict[str, Keyword], 

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

3835 translation_context: str, 

3836) -> None: 

3837 value_key = ref_value["value"] 

3838 changes = { 

3839 "translation_context": translation_context, 

3840 } 

3841 try: 

3842 known_value = known_values[value_key] 

3843 except KeyError: 

3844 known_value = Keyword(value_key) 

3845 known_values[value_key] = known_value 

3846 else: 

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

3848 raise ValueError( 

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

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

3851 ) 

3852 value_doc = ref_value.get("documentation") 

3853 if value_doc is not None: 

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

3855 changes["long_description"] = resolve_template( 

3856 value_doc.get("long_description") 

3857 ) 

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

3859 changes["is_exclusive"] = is_exclusive 

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

3861 changes["sort_text"] = sort_key 

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

3863 changes["usage_hint"] = usage_hint 

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

3865 known_value = known_value.replace(**changes) 

3866 known_values[value_key] = known_value 

3867 

3868 _expand_aliases( 

3869 known_value, 

3870 known_values, 

3871 operator.attrgetter("value"), 

3872 ref_value.get("aliases"), 

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

3874 ) 

3875 

3876 

3877def _resolve_field( 

3878 ref_field: Deb822Field, 

3879 stanza_fields: dict[str, F], 

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

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

3882 translation_context: str, 

3883) -> None: 

3884 field_name = ref_field["canonical_name"] 

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

3886 doc = ref_field.get("documentation") 

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

3888 norm_field_name = normalize_dctrl_field_name(field_name.lower()) 

3889 

3890 try: 

3891 field = stanza_fields[norm_field_name] 

3892 except KeyError: 

3893 field = field_constructor( 

3894 field_name, 

3895 field_value_type, 

3896 ) 

3897 stanza_fields[norm_field_name] = field 

3898 else: 

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

3900 _error( 

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

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

3903 ) 

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

3905 _error( 

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

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

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

3909 ) 

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

3911 raise ValueError( 

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

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

3914 ) 

3915 

3916 if doc is not None: 

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

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

3919 

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

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

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

3923 field.deprecated_with_no_replacement = ref_field.get( 

3924 "is_obsolete_without_replacement", False 

3925 ) 

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

3927 field.translation_context = translation_context 

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

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

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

3931 field.unknown_value_severity = ( 

3932 None if unknown_value_severity == "none" else unknown_value_severity 

3933 ) 

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

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

3936 field.is_substvars_disabled_even_if_allowed_by_stanza = not ref_field.get( 

3937 "supports_substvars", 

3938 True, 

3939 ) 

3940 field.inheritable_from_other_stanza = ref_field.get( 

3941 "inheritable_from_other_stanza", 

3942 False, 

3943 ) 

3944 

3945 known_values = field.known_values 

3946 if known_values is None: 

3947 known_values = {} 

3948 else: 

3949 known_values = dict(known_values) 

3950 

3951 for ref_value in ref_values: 

3952 _resolve_keyword(ref_value, known_values, resolve_template, translation_context) 

3953 

3954 if known_values: 

3955 field.known_values = known_values 

3956 

3957 _expand_aliases( 

3958 field, 

3959 stanza_fields, 

3960 operator.attrgetter("name"), 

3961 ref_field.get("aliases"), 

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

3963 ) 

3964 

3965 

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

3967 

3968 

3969def _expand_aliases( 

3970 item: A, 

3971 item_container: dict[str, A], 

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

3973 aliases_ref: list[Alias] | None, 

3974 doc_template: str, 

3975) -> None: 

3976 if aliases_ref is None: 

3977 return 

3978 name = canonical_name_resolver(item) 

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

3980 for alias_ref in aliases_ref: 

3981 alias_name = alias_ref["alias"] 

3982 alias_doc = item.long_description 

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

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

3985 if alias_doc: 

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

3987 else: 

3988 alias_doc = doc_suffix 

3989 alias_field = item.replace( 

3990 long_description=alias_doc, 

3991 is_alias_of=name, 

3992 is_completion_suggestion=is_completion_suggestion, 

3993 ) 

3994 alias_key = alias_name.lower() 

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

3996 existing_name = canonical_name_resolver(item_container[alias_key]) 

3997 assert ( 

3998 existing_name is not None 

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

4000 raise ValueError( 

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

4002 ) 

4003 item_container[alias_key] = alias_field 

4004 

4005 

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

4007 

4008 @property 

4009 def reference_data_basename(self) -> str: 

4010 return "debian_control_reference_data.yaml" 

4011 

4012 def _new_field( 

4013 self, 

4014 name: str, 

4015 field_value_type: FieldValueClass, 

4016 ) -> F: 

4017 return DctrlKnownField(name, field_value_type) 

4018 

4019 def guess_stanza_classification_by_idx( 

4020 self, 

4021 stanza_idx: int, 

4022 ) -> DctrlStanzaMetadata: 

4023 self.ensure_initialized() 

4024 if stanza_idx == 0: 

4025 return _DCTRL_SOURCE_STANZA 

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

4027 return _DCTRL_PACKAGE_STANZA 

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

4029 

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

4031 self.ensure_initialized() 

4032 # Order assumption made in the LSP code. 

4033 yield _DCTRL_SOURCE_STANZA 

4034 yield _DCTRL_PACKAGE_STANZA 

4035 

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

4037 self.ensure_initialized() 

4038 if item == "Source": 

4039 return _DCTRL_SOURCE_STANZA 

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

4041 return _DCTRL_PACKAGE_STANZA 

4042 raise KeyError(item) 

4043 

4044 def reformat( 

4045 self, 

4046 effective_preference: "EffectiveFormattingPreference", 

4047 deb822_file: Deb822FileElement, 

4048 formatter: FormatterCallback, 

4049 content: str, 

4050 position_codec: LintCapablePositionCodec, 

4051 lines: list[str], 

4052 ) -> Iterable[TextEdit]: 

4053 edits = list( 

4054 super().reformat( 

4055 effective_preference, 

4056 deb822_file, 

4057 formatter, 

4058 content, 

4059 position_codec, 

4060 lines, 

4061 ) 

4062 ) 

4063 

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

4065 not effective_preference.deb822_normalize_stanza_order 

4066 or deb822_file.find_first_error_element() is not None 

4067 ): 

4068 return edits 

4069 names = [] 

4070 for idx, stanza in enumerate(deb822_file): 

4071 if idx < 2: 

4072 continue 

4073 name = stanza.get("Package") 

4074 if name is None: 

4075 return edits 

4076 names.append(name) 

4077 

4078 reordered = sorted(names) 

4079 if names == reordered: 

4080 return edits 

4081 

4082 if edits: 

4083 content = apply_text_edits(content, lines, edits) 

4084 lines = content.splitlines(keepends=True) 

4085 deb822_file = parse_deb822_file( 

4086 lines, 

4087 accept_files_with_duplicated_fields=True, 

4088 accept_files_with_error_tokens=True, 

4089 ) 

4090 

4091 stanzas = list(deb822_file) 

4092 reordered_stanza = stanzas[:2] + sorted( 

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

4094 ) 

4095 bits = [] 

4096 stanza_idx = 0 

4097 for token_or_element in deb822_file.iter_parts(): 

4098 if isinstance(token_or_element, Deb822ParagraphElement): 

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

4100 stanza_idx += 1 

4101 else: 

4102 bits.append(token_or_element.convert_to_text()) 

4103 

4104 new_content = "".join(bits) 

4105 

4106 return [ 

4107 TextEdit( 

4108 Range( 

4109 Position(0, 0), 

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

4111 ), 

4112 new_content, 

4113 ) 

4114 ] 

4115 

4116 

4117class DTestsCtrlFileMetadata( 

4118 Deb822FileMetadata[DTestsCtrlStanzaMetadata, DTestsCtrlKnownField] 

4119): 

4120 

4121 @property 

4122 def reference_data_basename(self) -> str: 

4123 return "debian_tests_control_reference_data.yaml" 

4124 

4125 def _new_field( 

4126 self, 

4127 name: str, 

4128 field_value_type: FieldValueClass, 

4129 ) -> F: 

4130 return DTestsCtrlKnownField(name, field_value_type) 

4131 

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

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

4134 self.ensure_initialized() 

4135 return _DTESTSCTRL_STANZA 

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

4137 

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

4139 self.ensure_initialized() 

4140 yield _DTESTSCTRL_STANZA 

4141 

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

4143 self.ensure_initialized() 

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

4145 return _DTESTSCTRL_STANZA 

4146 raise KeyError(item) 

4147 

4148 

4149TRANSLATABLE_DEB822_FILE_METADATA: Sequence[ 

4150 Callable[[], Deb822FileMetadata[Any, Any]] 

4151] = [ 

4152 DctrlFileMetadata, 

4153 Dep5FileMetadata, 

4154 DTestsCtrlFileMetadata, 

4155]