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

1402 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-03-22 09:00 +0000

1import collections 

2import dataclasses 

3import functools 

4import importlib.resources 

5import itertools 

6import operator 

7import os.path 

8import re 

9import textwrap 

10from abc import ABC 

11from typing import ( 

12 FrozenSet, 

13 Optional, 

14 cast, 

15 List, 

16 Generic, 

17 TypeVar, 

18 Union, 

19 Tuple, 

20 Any, 

21 Set, 

22 TYPE_CHECKING, 

23 Dict, 

24 Self, 

25) 

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

27 

28from debian.debian_support import DpkgArchTable, Version 

29 

30import debputy.lsp.data.deb822_data as deb822_ref_data_dir 

31from debputy.filesystem_scan import VirtualPathBase 

32from debputy.linting.lint_util import LintState, with_range_in_continuous_parts 

33from debputy.linting.lint_util import te_range_to_lsp 

34from debputy.lsp.diagnostics import LintSeverity 

35from debputy.lsp.lsp_reference_keyword import ( 

36 Keyword, 

37 allowed_values, 

38 format_comp_item_synopsis_doc, 

39 LSP_DATA_DOMAIN, 

40 ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

41) 

42from debputy.lsp.quickfixes import ( 

43 propose_correct_text_quick_fix, 

44 propose_remove_range_quick_fix, 

45) 

46from debputy.lsp.ref_models.deb822_reference_parse_models import ( 

47 Deb822ReferenceData, 

48 DEB822_REFERENCE_DATA_PARSER, 

49 FieldValueClass, 

50 StaticValue, 

51 Deb822Field, 

52 UsageHint, 

53 Alias, 

54) 

55from debputy.lsp.text_edit import apply_text_edits 

56from debputy.lsp.text_util import ( 

57 normalize_dctrl_field_name, 

58 LintCapablePositionCodec, 

59 trim_end_of_line_whitespace, 

60) 

61from debian._deb822_repro.parsing import ( 

62 Deb822KeyValuePairElement, 

63 LIST_SPACE_SEPARATED_INTERPRETATION, 

64 Deb822ParagraphElement, 

65 Deb822FileElement, 

66 Interpretation, 

67 parse_deb822_file, 

68 Deb822ParsedTokenList, 

69 Deb822ValueLineElement, 

70) 

71from debian._deb822_repro.tokens import ( 

72 Deb822FieldNameToken, 

73) 

74from debian._deb822_repro.types import FormatterCallback, TE 

75from debputy.lsp.vendoring.wrap_and_sort import _sort_packages_key 

76from debputy.lsprotocol.types import ( 

77 DiagnosticTag, 

78 Range, 

79 TextEdit, 

80 Position, 

81 CompletionItem, 

82 MarkupContent, 

83 CompletionItemTag, 

84 MarkupKind, 

85 CompletionItemKind, 

86 CompletionItemLabelDetails, 

87) 

88from debputy.manifest_parser.exceptions import ManifestParseException 

89from debputy.manifest_parser.util import AttributePath 

90from debputy.path_matcher import BasenameGlobMatch 

91from debputy.plugin.api import VirtualPath 

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

93from debputy.yaml import MANIFEST_YAML 

94 

95try: 

96 from debian._deb822_repro.locatable import ( 

97 Position as TEPosition, 

98 Range as TERange, 

99 START_POSITION, 

100 ) 

101except ImportError: 

102 pass 

103 

104 

105if TYPE_CHECKING: 

106 from debputy.lsp.maint_prefs import EffectiveFormattingPreference 

107 from debputy.lsp.debputy_ls import DebputyLanguageServer 

108 

109 

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

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

112 

113 

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

115 

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

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

118_RE_SYNOPSIS_IS_TEMPLATE = re.compile( 

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

120) 

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

122CURRENT_STANDARDS_VERSION = Version("4.7.2") 

123 

124 

125CustomFieldCheck = Callable[ 

126 [ 

127 "F", 

128 Deb822FileElement, 

129 Deb822KeyValuePairElement, 

130 "TERange", 

131 "TERange", 

132 Deb822ParagraphElement, 

133 "TEPosition", 

134 LintState, 

135 ], 

136 None, 

137] 

138 

139 

140@functools.lru_cache 

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

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

143 return { 

144 f.lower(): f 

145 for f in ( 

146 "Pre-Depends", 

147 "Depends", 

148 "Recommends", 

149 "Suggests", 

150 "Enhances", 

151 "Conflicts", 

152 "Breaks", 

153 "Replaces", 

154 "Provides", 

155 "Built-Using", 

156 "Static-Built-Using", 

157 ) 

158 } 

159 

160 

161@functools.lru_cache 

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

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

164 return { 

165 f.lower(): f 

166 for f in ( 

167 "Build-Depends", 

168 "Build-Depends-Arch", 

169 "Build-Depends-Indep", 

170 "Build-Conflicts", 

171 "Build-Conflicts-Arch", 

172 "Build-Conflicts-Indep", 

173 ) 

174 } 

175 

176 

177ALL_SECTIONS_WITHOUT_COMPONENT = frozenset( 

178 [ 

179 "admin", 

180 "cli-mono", 

181 "comm", 

182 "database", 

183 "debian-installer", 

184 "debug", 

185 "devel", 

186 "doc", 

187 "editors", 

188 "education", 

189 "electronics", 

190 "embedded", 

191 "fonts", 

192 "games", 

193 "gnome", 

194 "gnu-r", 

195 "gnustep", 

196 "golang", 

197 "graphics", 

198 "hamradio", 

199 "haskell", 

200 "httpd", 

201 "interpreters", 

202 "introspection", 

203 "java", 

204 "javascript", 

205 "kde", 

206 "kernel", 

207 "libdevel", 

208 "libs", 

209 "lisp", 

210 "localization", 

211 "mail", 

212 "math", 

213 "metapackages", 

214 "misc", 

215 "net", 

216 "news", 

217 "ocaml", 

218 "oldlibs", 

219 "otherosfs", 

220 "perl", 

221 "php", 

222 "python", 

223 "ruby", 

224 "rust", 

225 "science", 

226 "shells", 

227 "sound", 

228 "tasks", 

229 "tex", 

230 "text", 

231 "utils", 

232 "vcs", 

233 "video", 

234 "virtual", 

235 "web", 

236 "x11", 

237 "xfce", 

238 "zope", 

239 ] 

240) 

241 

242ALL_COMPONENTS = frozenset( 

243 [ 

244 "main", 

245 "restricted", # Ubuntu 

246 "non-free", 

247 "non-free-firmware", 

248 "contrib", 

249 ] 

250) 

251 

252 

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

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

255 

256 

257def _complete_section_sort_hint( 

258 keyword: Keyword, 

259 _lint_state: LintState, 

260 stanza_parts: Sequence[Deb822ParagraphElement], 

261 _value_being_completed: str, 

262) -> str | None: 

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

264 pkg = stanza.get("Package") 

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

266 break 

267 else: 

268 return None 

269 section = package_name_to_section(pkg) 

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

271 keyword_section = value_parts[-1] 

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

273 if section is None: 

274 if keyword_component == "": 

275 return keyword_section 

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

277 if keyword_section != section: 

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

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

280 

281 

282ALL_SECTIONS = allowed_values( 

283 Keyword( 

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

285 sort_text=_complete_section_sort_hint, 

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

287 ) 

288 for c, s in itertools.product( 

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

290 ALL_SECTIONS_WITHOUT_COMPONENT, 

291 ) 

292) 

293 

294 

295def all_architectures_and_wildcards( 

296 arch2table, *, allow_negations: bool = False 

297) -> Iterable[str | Keyword]: 

298 wildcards = set() 

299 yield Keyword( 

300 "any", 

301 is_exclusive=True, 

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

303 long_description=textwrap.dedent( 

304 """\ 

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

306 compiled for each and every architecture. 

307 

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

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

310 dpkg. 

311 """ 

312 ), 

313 ) 

314 yield Keyword( 

315 "all", 

316 is_exclusive=True, 

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

318 long_description=textwrap.dedent( 

319 """\ 

320 The package is an architecture independent package. This is 

321 typically appropriate for packages containing only scripts, 

322 data or documentation. 

323 

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

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

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

327 """ 

328 ), 

329 ) 

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

331 yield arch_name 

332 if allow_negations: 

333 yield f"!{arch_name}" 

334 cpu_wc = "any-" + quad_tuple.cpu_name 

335 os_wc = quad_tuple.os_name + "-any" 

336 if cpu_wc not in wildcards: 

337 yield cpu_wc 

338 if allow_negations: 

339 yield f"!{cpu_wc}" 

340 wildcards.add(cpu_wc) 

341 if os_wc not in wildcards: 

342 yield os_wc 

343 if allow_negations: 

344 yield f"!{os_wc}" 

345 wildcards.add(os_wc) 

346 # Add the remaining wildcards 

347 

348 

349@functools.lru_cache 

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

351 dpkg_arch_table = DpkgArchTable.load_arch_table() 

352 return frozenset( 

353 all_architectures_and_wildcards( 

354 dpkg_arch_table._arch2table, 

355 allow_negations=allow_negations, 

356 ) 

357 ) 

358 

359 

360def extract_first_value_and_position( 

361 kvpair: Deb822KeyValuePairElement, 

362 stanza_pos: "TEPosition", 

363 *, 

364 interpretation: Interpretation[ 

365 Deb822ParsedTokenList[Any, Any] 

366 ] = LIST_SPACE_SEPARATED_INTERPRETATION, 

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

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

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

370 kvpair_pos 

371 ) 

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

373 v = value_ref.value 

374 section_value_loc = value_ref.locatable 

375 value_range_te = section_value_loc.range_in_parent().relative_to( 

376 value_element_pos 

377 ) 

378 return v, value_range_te 

379 return None, None 

380 

381 

382def _sv_field_validation( 

383 known_field: "F", 

384 _deb822_file: Deb822FileElement, 

385 kvpair: Deb822KeyValuePairElement, 

386 _kvpair_range: "TERange", 

387 _field_name_range_te: "TERange", 

388 _stanza: Deb822ParagraphElement, 

389 stanza_position: "TEPosition", 

390 lint_state: LintState, 

391) -> None: 

392 sv_value, sv_value_range = extract_first_value_and_position( 

393 kvpair, 

394 stanza_position, 

395 ) 

396 m = _RE_SV.fullmatch(sv_value) 

397 if m is None: 

398 lint_state.emit_diagnostic( 

399 sv_value_range, 

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

401 "warning", 

402 known_field.unknown_value_authority, 

403 ) 

404 return 

405 

406 sv_version = Version(sv_value) 

407 if sv_version < CURRENT_STANDARDS_VERSION: 

408 lint_state.emit_diagnostic( 

409 sv_value_range, 

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

411 "informational", 

412 known_field.unknown_value_authority, 

413 ) 

414 return 

415 extra = m.group(2) 

416 if extra: 

417 extra_len = lint_state.position_codec.client_num_units(extra) 

418 lint_state.emit_diagnostic( 

419 TERange.between( 

420 TEPosition( 

421 sv_value_range.end_pos.line_position, 

422 sv_value_range.end_pos.cursor_position - extra_len, 

423 ), 

424 sv_value_range.end_pos, 

425 ), 

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

427 "informational", 

428 known_field.unknown_value_authority, 

429 quickfixes=[ 

430 propose_remove_range_quick_fix( 

431 proposed_title="Remove unnecessary version part" 

432 ) 

433 ], 

434 ) 

435 

436 

437def _dctrl_ma_field_validation( 

438 _known_field: "F", 

439 _deb822_file: Deb822FileElement, 

440 _kvpair: Deb822KeyValuePairElement, 

441 _kvpair_range: "TERange", 

442 _field_name_range: "TERange", 

443 stanza: Deb822ParagraphElement, 

444 stanza_position: "TEPosition", 

445 lint_state: LintState, 

446) -> None: 

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

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

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

450 ma_value, ma_value_range = extract_first_value_and_position( 

451 ma_kvpair, 

452 stanza_position, 

453 ) 

454 if ma_value == "same": 

455 lint_state.emit_diagnostic( 

456 ma_value_range, 

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

458 "error", 

459 "debputy", 

460 ) 

461 

462 

463def _udeb_only_field_validation( 

464 known_field: "F", 

465 _deb822_file: Deb822FileElement, 

466 _kvpair: Deb822KeyValuePairElement, 

467 _kvpair_range: "TERange", 

468 field_name_range: "TERange", 

469 stanza: Deb822ParagraphElement, 

470 _stanza_position: "TEPosition", 

471 lint_state: LintState, 

472) -> None: 

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

474 if package_type != "udeb": 

475 lint_state.emit_diagnostic( 

476 field_name_range, 

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

478 "warning", 

479 "debputy", 

480 ) 

481 

482 

483def _complete_only_in_arch_dep_pkgs( 

484 stanza_parts: Iterable[Deb822ParagraphElement], 

485) -> bool: 

486 for stanza in stanza_parts: 

487 arch = stanza.get("Architecture") 

488 if arch is None: 

489 continue 

490 archs = arch.split() 

491 return "all" not in archs 

492 return False 

493 

494 

495def _complete_only_for_udeb_pkgs( 

496 stanza_parts: Iterable[Deb822ParagraphElement], 

497) -> bool: 

498 for stanza in stanza_parts: 

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

500 pkg_type = stanza.get(option) 

501 if pkg_type is not None: 

502 return pkg_type == "udeb" 

503 return False 

504 

505 

506def _arch_not_all_only_field_validation( 

507 known_field: "F", 

508 _deb822_file: Deb822FileElement, 

509 _kvpair: Deb822KeyValuePairElement, 

510 _kvpair_range_te: "TERange", 

511 field_name_range_te: "TERange", 

512 stanza: Deb822ParagraphElement, 

513 _stanza_position: "TEPosition", 

514 lint_state: LintState, 

515) -> None: 

516 architecture = stanza.get("Architecture") 

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

518 lint_state.emit_diagnostic( 

519 field_name_range_te, 

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

521 "warning", 

522 "debputy", 

523 ) 

524 

525 

526def _binary_package_from_same_source( 

527 known_field: "F", 

528 _deb822_file: Deb822FileElement, 

529 _kvpair: Deb822KeyValuePairElement, 

530 kvpair_range: "TERange", 

531 _field_name_range: "TERange", 

532 stanza: Deb822ParagraphElement, 

533 stanza_position: "TEPosition", 

534 lint_state: LintState, 

535) -> None: 

536 doc_main_package_kvpair = stanza.get_kvpair_element( 

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

538 ) 

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

540 lint_state.emit_diagnostic( 

541 kvpair_range, 

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

543 "warning", 

544 "debputy", 

545 quickfixes=[propose_remove_range_quick_fix()], 

546 ) 

547 return 

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

549 doc_main_package, value_range = extract_first_value_and_position( 

550 doc_main_package_kvpair, 

551 stanza_position, 

552 ) 

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

554 return 

555 lint_state.emit_diagnostic( 

556 value_range, 

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

558 "error", 

559 "debputy", 

560 quickfixes=[ 

561 propose_correct_text_quick_fix(name) 

562 for name in lint_state.binary_packages 

563 ], 

564 ) 

565 

566 

567def _single_line_span_range_relative_to_pos( 

568 span: tuple[int, int], 

569 relative_to: "TEPosition", 

570) -> Range: 

571 return TERange( 

572 TEPosition( 

573 relative_to.line_position, 

574 relative_to.cursor_position + span[0], 

575 ), 

576 TEPosition( 

577 relative_to.line_position, 

578 relative_to.cursor_position + span[1], 

579 ), 

580 ) 

581 

582 

583def _check_extended_description_line( 

584 description_value_line: Deb822ValueLineElement, 

585 description_line_range_te: "TERange", 

586 package: str | None, 

587 lint_state: LintState, 

588) -> None: 

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

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

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

592 return 

593 description_line_with_leading_space = ( 

594 description_value_line.convert_to_text().rstrip() 

595 ) 

596 try: 

597 idx = description_line_with_leading_space.index( 

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

599 ) 

600 except ValueError: 

601 pass 

602 else: 

603 template_span = idx, idx + len( 

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

605 ) 

606 lint_state.emit_diagnostic( 

607 _single_line_span_range_relative_to_pos( 

608 template_span, 

609 description_line_range_te.start_pos, 

610 ), 

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

612 "error", 

613 "debputy", 

614 ) 

615 if len(description_line_with_leading_space) > 80: 

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

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

618 # 

619 # See also debputy#122 

620 span = 80, len(description_line_with_leading_space) 

621 lint_state.emit_diagnostic( 

622 _single_line_span_range_relative_to_pos( 

623 span, 

624 description_line_range_te.start_pos, 

625 ), 

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

627 "warning", 

628 "debputy", 

629 ) 

630 

631 

632def _check_synopsis( 

633 synopsis_value_line: Deb822ValueLineElement, 

634 synopsis_range_te: "TERange", 

635 field_name_range_te: "TERange", 

636 package: str | None, 

637 lint_state: LintState, 

638) -> None: 

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

640 assert synopsis_value_line.comment_element is None 

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

642 if not synopsis_text_with_leading_space: 

643 lint_state.emit_diagnostic( 

644 field_name_range_te, 

645 "Package synopsis is missing", 

646 "warning", 

647 "debputy", 

648 ) 

649 return 

650 synopsis_text_trimmed = synopsis_text_with_leading_space.lstrip() 

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

652 starts_with_article = _RE_SYNOPSIS_STARTS_WITH_ARTICLE.search( 

653 synopsis_text_with_leading_space 

654 ) 

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

656 if starts_with_article: 

657 lint_state.emit_diagnostic( 

658 _single_line_span_range_relative_to_pos( 

659 starts_with_article.span(1), 

660 synopsis_range_te.start_pos, 

661 ), 

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

663 "warning", 

664 "DevRef 6.2.2", 

665 ) 

666 if len(synopsis_text_trimmed) >= 80: 

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

668 span = synopsis_offset + 79, len(synopsis_text_with_leading_space) 

669 lint_state.emit_diagnostic( 

670 _single_line_span_range_relative_to_pos( 

671 span, 

672 synopsis_range_te.start_pos, 

673 ), 

674 "Package synopsis is too long.", 

675 "warning", 

676 "Policy 3.4.1", 

677 ) 

678 if template_match := _RE_SYNOPSIS_IS_TEMPLATE.match( 

679 synopsis_text_with_leading_space 

680 ): 

681 lint_state.emit_diagnostic( 

682 _single_line_span_range_relative_to_pos( 

683 template_match.span(1), 

684 synopsis_range_te.start_pos, 

685 ), 

686 "Package synopsis is a placeholder", 

687 "warning", 

688 "debputy", 

689 ) 

690 elif too_short_match := _RE_SYNOPSIS_IS_TOO_SHORT.match( 

691 synopsis_text_with_leading_space 

692 ): 

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

694 lint_state.emit_diagnostic( 

695 _single_line_span_range_relative_to_pos( 

696 too_short_match.span(1), 

697 synopsis_range_te.start_pos, 

698 ), 

699 "Package synopsis is too short", 

700 "warning", 

701 "debputy", 

702 ) 

703 

704 

705def dctrl_description_validator( 

706 _known_field: "F", 

707 _deb822_file: Deb822FileElement, 

708 kvpair: Deb822KeyValuePairElement, 

709 kvpair_range_te: "TERange", 

710 _field_name_range: "TERange", 

711 stanza: Deb822ParagraphElement, 

712 _stanza_position: "TEPosition", 

713 lint_state: LintState, 

714) -> None: 

715 value_lines = kvpair.value_element.value_lines 

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

717 return 

718 package = stanza.get("Package") 

719 synopsis_value_line = value_lines[0] 

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

721 kvpair_range_te.start_pos 

722 ) 

723 synopsis_line_range_te = synopsis_value_line.range_in_parent().relative_to( 

724 value_range_te.start_pos 

725 ) 

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

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

728 kvpair_range_te.start_pos 

729 ) 

730 _check_synopsis( 

731 synopsis_value_line, 

732 synopsis_line_range_te, 

733 field_name_range_te, 

734 package, 

735 lint_state, 

736 ) 

737 description_lines = value_lines[1:] 

738 else: 

739 description_lines = value_lines 

740 for description_line in description_lines: 

741 description_line_range_te = description_line.range_in_parent().relative_to( 

742 value_range_te.start_pos 

743 ) 

744 _check_extended_description_line( 

745 description_line, 

746 description_line_range_te, 

747 package, 

748 lint_state, 

749 ) 

750 

751 

752def _has_packaging_expected_file( 

753 name: str, 

754 msg: str, 

755 severity: LintSeverity = "error", 

756) -> CustomFieldCheck: 

757 

758 def _impl( 

759 _known_field: "F", 

760 _deb822_file: Deb822FileElement, 

761 _kvpair: Deb822KeyValuePairElement, 

762 kvpair_range_te: "TERange", 

763 _field_name_range_te: "TERange", 

764 _stanza: Deb822ParagraphElement, 

765 _stanza_position: "TEPosition", 

766 lint_state: LintState, 

767 ) -> None: 

768 debian_dir = lint_state.debian_dir 

769 if debian_dir is None: 

770 return 

771 cpy = debian_dir.lookup(name) 

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

773 lint_state.emit_diagnostic( 

774 kvpair_range_te, 

775 msg, 

776 severity, 

777 "debputy", 

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

779 ) 

780 

781 return _impl 

782 

783 

784_check_missing_debian_rules = _has_packaging_expected_file( 

785 "rules", 

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

787) 

788 

789 

790def _has_build_instructions( 

791 known_field: "F", 

792 deb822_file: Deb822FileElement, 

793 kvpair: Deb822KeyValuePairElement, 

794 kvpair_range_te: "TERange", 

795 field_name_range_te: "TERange", 

796 stanza: Deb822ParagraphElement, 

797 stanza_position: "TEPosition", 

798 lint_state: LintState, 

799) -> None: 

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

801 return 

802 

803 _check_missing_debian_rules( 

804 known_field, 

805 deb822_file, 

806 kvpair, 

807 kvpair_range_te, 

808 field_name_range_te, 

809 stanza, 

810 stanza_position, 

811 lint_state, 

812 ) 

813 

814 

815def _canonical_maintainer_name( 

816 known_field: "F", 

817 _deb822_file: Deb822FileElement, 

818 kvpair: Deb822KeyValuePairElement, 

819 kvpair_range_te: "TERange", 

820 _field_name_range_te: "TERange", 

821 _stanza: Deb822ParagraphElement, 

822 _stanza_position: "TEPosition", 

823 lint_state: LintState, 

824) -> None: 

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

826 kvpair_range_te.start_pos 

827 ) 

828 try: 

829 interpreted_value = kvpair.interpret_as( 

830 known_field.field_value_class.interpreter() 

831 ) 

832 except ValueError: 

833 return 

834 

835 for part in interpreted_value.iter_parts(): 

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

837 continue 

838 name_and_email = part.convert_to_text() 

839 try: 

840 email_start = name_and_email.rindex("<") 

841 email_end = name_and_email.rindex(">") 

842 email = name_and_email[email_start + 1 : email_end] 

843 except IndexError: 

844 continue 

845 

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

847 if pref is None or not pref.canonical_name: 

848 continue 

849 

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

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

852 continue 

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

854 lint_state.emit_diagnostic( 

855 value_range_te, 

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

857 "informational", 

858 "debputy", 

859 quickfixes=[propose_correct_text_quick_fix(expected)], 

860 ) 

861 

862 

863def _maintainer_field_validator( 

864 known_field: "F", 

865 _deb822_file: Deb822FileElement, 

866 kvpair: Deb822KeyValuePairElement, 

867 kvpair_range_te: "TERange", 

868 _field_name_range_te: "TERange", 

869 _stanza: Deb822ParagraphElement, 

870 _stanza_position: "TEPosition", 

871 lint_state: LintState, 

872) -> None: 

873 

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

875 kvpair_range_te.start_pos 

876 ) 

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

878 for part in interpreted_value.iter_parts(): 

879 if not part.is_separator: 

880 continue 

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

882 severity = known_field.unknown_value_severity 

883 assert severity is not None 

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

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

886 lint_state.emit_diagnostic( 

887 value_range_te, 

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

889 severity, 

890 known_field.unknown_value_authority, 

891 ) 

892 

893 

894def _use_https_instead_of_http( 

895 known_field: "F", 

896 _deb822_file: Deb822FileElement, 

897 kvpair: Deb822KeyValuePairElement, 

898 kvpair_range_te: "TERange", 

899 _field_name_range_te: "TERange", 

900 _stanza: Deb822ParagraphElement, 

901 _stanza_position: "TEPosition", 

902 lint_state: LintState, 

903) -> None: 

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

905 kvpair_range_te.start_pos 

906 ) 

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

908 for part in interpreted_value.iter_parts(): 

909 value = part.convert_to_text() 

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

911 continue 

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

913 problem_range_te = TERange.between( 

914 value_range_te.start_pos, 

915 TEPosition( 

916 value_range_te.start_pos.line_position, 

917 value_range_te.start_pos.cursor_position + 7, 

918 ), 

919 ) 

920 lint_state.emit_diagnostic( 

921 problem_range_te, 

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

923 "warning", 

924 "debputy", 

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

926 ) 

927 

928 

929def _each_value_match_regex_validation( 

930 regex: re.Pattern, 

931 *, 

932 diagnostic_severity: LintSeverity = "error", 

933 authority_reference: str | None = None, 

934) -> CustomFieldCheck: 

935 

936 def _validator( 

937 known_field: "F", 

938 _deb822_file: Deb822FileElement, 

939 kvpair: Deb822KeyValuePairElement, 

940 kvpair_range_te: "TERange", 

941 _field_name_range_te: "TERange", 

942 _stanza: Deb822ParagraphElement, 

943 _stanza_position: "TEPosition", 

944 lint_state: LintState, 

945 ) -> None: 

946 nonlocal authority_reference 

947 interpreter = known_field.field_value_class.interpreter() 

948 if interpreter is None: 

949 raise AssertionError( 

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

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

952 ) 

953 auth_ref = ( 

954 authority_reference 

955 if authority_reference is not None 

956 else known_field.unknown_value_authority 

957 ) 

958 

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

960 kvpair_range_te.start_pos 

961 ) 

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

963 v = value_ref.value 

964 m = regex.fullmatch(v) 

965 if m is not None: 

966 continue 

967 

968 if "${" in v: 

969 # Ignore substvars 

970 continue 

971 

972 section_value_loc = value_ref.locatable 

973 value_range_te = section_value_loc.range_in_parent().relative_to( 

974 value_element_pos 

975 ) 

976 lint_state.emit_diagnostic( 

977 value_range_te, 

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

979 diagnostic_severity, 

980 auth_ref, 

981 ) 

982 

983 return _validator 

984 

985 

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

987_DEP_RELATION_CLAUSE = re.compile( 

988 r""" 

989 ^ 

990 \s* 

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

992 \s* 

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

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

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

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

997 $ 

998""", 

999 re.VERBOSE | re.MULTILINE, 

1000) 

1001 

1002 

1003def _span_to_te_range( 

1004 text: str, 

1005 start_pos: int, 

1006 end_pos: int, 

1007) -> TERange: 

1008 prefix = text[0:start_pos] 

1009 prefix_plus_text = text[0:end_pos] 

1010 

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

1012 if start_line: 

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

1014 # +1 to skip past the newline 

1015 start_cursor_pos = start_pos - (start_newline_offset + 1) 

1016 else: 

1017 start_cursor_pos = start_pos 

1018 

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

1020 if end_line == start_line: 

1021 end_cursor_pos = start_cursor_pos + (end_pos - start_pos) 

1022 else: 

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

1024 end_cursor_pos = end_pos - (end_newline_offset + 1) 

1025 

1026 return TERange( 

1027 TEPosition( 

1028 start_line, 

1029 start_cursor_pos, 

1030 ), 

1031 TEPosition( 

1032 end_line, 

1033 end_cursor_pos, 

1034 ), 

1035 ) 

1036 

1037 

1038def _split_w_spans( 

1039 v: str, 

1040 sep: str, 

1041 *, 

1042 offset: int = 0, 

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

1044 separator_size = len(sep) 

1045 parts = v.split(sep) 

1046 for part in parts: 

1047 size = len(part) 

1048 end_offset = offset + size 

1049 yield part, offset, end_offset 

1050 offset = end_offset + separator_size 

1051 

1052 

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

1054 

1055 

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

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

1058 

1059 

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

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

1062 if not newlines: 

1063 return TEPosition( 

1064 newlines, 

1065 len(text), 

1066 ) 

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

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

1069 return TEPosition( 

1070 newlines, 

1071 line_offset, 

1072 ) 

1073 

1074 

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

1076class Relation: 

1077 name: str 

1078 arch_qual: str | None = None 

1079 version_operator: str | None = None 

1080 version: str | None = None 

1081 arch_restriction: str | None = None 

1082 build_profile_restriction: str | None = None 

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

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

1085 # an example). 

1086 content_display_offset: int = -1 

1087 content_display_end_offset: int = -1 

1088 

1089 

1090def relation_key_variations( 

1091 relation: Relation, 

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

1093 operator_variants = ( 

1094 [relation.version_operator, None] 

1095 if relation.version_operator is not None 

1096 else [None] 

1097 ) 

1098 arch_qual_variants = ( 

1099 [relation.arch_qual, None] 

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

1101 else [None] 

1102 ) 

1103 for arch_qual, version_operator in itertools.product( 

1104 arch_qual_variants, 

1105 operator_variants, 

1106 ): 

1107 yield relation.name, arch_qual, version_operator 

1108 

1109 

1110def dup_check_relations( 

1111 known_field: "F", 

1112 relations: Sequence[Relation], 

1113 raw_value_masked_comments: str, 

1114 value_element_pos: "TEPosition", 

1115 lint_state: LintState, 

1116) -> None: 

1117 overlap_table = {} 

1118 for relation in relations: 

1119 version_operator = relation.version_operator 

1120 arch_qual = relation.arch_qual 

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

1122 continue 

1123 

1124 for relation_key in relation_key_variations(relation): 

1125 prev_relation = overlap_table.get(relation_key) 

1126 if prev_relation is None: 

1127 overlap_table[relation_key] = relation 

1128 else: 

1129 prev_version_operator = prev_relation.version_operator 

1130 

1131 if ( 

1132 prev_version_operator 

1133 and version_operator 

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

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

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

1137 ): 

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

1139 continue 

1140 

1141 prev_arch_qual = prev_relation.arch_qual 

1142 if ( 

1143 arch_qual != prev_arch_qual 

1144 and prev_arch_qual != "any" 

1145 and arch_qual != "any" 

1146 ): 

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

1148 # 

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

1150 continue 

1151 

1152 if ( 

1153 known_field.name == "Provides" 

1154 and version_operator == "=" 

1155 and prev_version_operator == version_operator 

1156 and relation.version != prev_relation.version 

1157 ): 

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

1159 continue 

1160 

1161 orig_relation_range = TERange( 

1162 _text_to_te_position( 

1163 raw_value_masked_comments[ 

1164 : prev_relation.content_display_offset 

1165 ] 

1166 ), 

1167 _text_to_te_position( 

1168 raw_value_masked_comments[ 

1169 : prev_relation.content_display_end_offset 

1170 ] 

1171 ), 

1172 ).relative_to(value_element_pos) 

1173 

1174 duplicate_relation_range = TERange( 

1175 _text_to_te_position( 

1176 raw_value_masked_comments[: relation.content_display_offset] 

1177 ), 

1178 _text_to_te_position( 

1179 raw_value_masked_comments[: relation.content_display_end_offset] 

1180 ), 

1181 ).relative_to(value_element_pos) 

1182 

1183 lint_state.emit_diagnostic( 

1184 duplicate_relation_range, 

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

1186 "warning", 

1187 known_field.unknown_value_authority, 

1188 related_information=[ 

1189 lint_state.related_diagnostic_information( 

1190 orig_relation_range, 

1191 "The previous definition", 

1192 ), 

1193 ], 

1194 ) 

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

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

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

1198 break 

1199 

1200 

1201def _dctrl_check_dep_version_operator( 

1202 known_field: "F", 

1203 version_operator: str, 

1204 version_operator_span: tuple[int, int], 

1205 version_operators: frozenset[str], 

1206 raw_value_masked_comments: str, 

1207 offset: int, 

1208 value_element_pos: "TEPosition", 

1209 lint_state: LintState, 

1210) -> bool: 

1211 if ( 

1212 version_operators 

1213 and version_operator is not None 

1214 and version_operator not in version_operators 

1215 ): 

1216 v_start_offset = offset + version_operator_span[0] 

1217 v_end_offset = offset + version_operator_span[1] 

1218 version_problem_range_te = TERange( 

1219 _text_to_te_position(raw_value_masked_comments[:v_start_offset]), 

1220 _text_to_te_position(raw_value_masked_comments[:v_end_offset]), 

1221 ).relative_to(value_element_pos) 

1222 

1223 sorted_version_operators = sorted(version_operators) 

1224 

1225 excluding_equal = f"{version_operator}{version_operator}" 

1226 including_equal = f"{version_operator}=" 

1227 

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

1229 excluding_equal in version_operators or including_equal in version_operators 

1230 ): 

1231 lint_state.emit_diagnostic( 

1232 version_problem_range_te, 

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

1234 "error", 

1235 "Policy 7.1", 

1236 quickfixes=[ 

1237 propose_correct_text_quick_fix(n) 

1238 for n in (excluding_equal, including_equal) 

1239 if not version_operators or n in version_operators 

1240 ], 

1241 ) 

1242 else: 

1243 lint_state.emit_diagnostic( 

1244 version_problem_range_te, 

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

1246 "error", 

1247 known_field.unknown_value_authority, 

1248 quickfixes=[ 

1249 propose_correct_text_quick_fix(n) for n in sorted_version_operators 

1250 ], 

1251 ) 

1252 return True 

1253 return False 

1254 

1255 

1256def _dctrl_validate_dep( 

1257 known_field: "DF", 

1258 _deb822_file: Deb822FileElement, 

1259 kvpair: Deb822KeyValuePairElement, 

1260 kvpair_range_te: "TERange", 

1261 _field_name_range: "TERange", 

1262 _stanza: Deb822ParagraphElement, 

1263 _stanza_position: "TEPosition", 

1264 lint_state: LintState, 

1265) -> None: 

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

1267 kvpair_range_te.start_pos 

1268 ) 

1269 raw_value_with_comments = kvpair.value_element.convert_to_text() 

1270 raw_value_masked_comments = "".join( 

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

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

1273 ) 

1274 if isinstance(known_field, DctrlRelationshipKnownField): 

1275 version_operators = known_field.allowed_version_operators 

1276 supports_or_relation = known_field.supports_or_relation 

1277 else: 

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

1279 supports_or_relation = True 

1280 

1281 relation_dup_table = collections.defaultdict(list) 

1282 

1283 for rel, rel_offset, rel_end_offset in _split_w_spans( 

1284 raw_value_masked_comments, "," 

1285 ): 

1286 sub_relations = [] 

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

1288 if or_rel.isspace(): 

1289 continue 

1290 if sub_relations and not supports_or_relation: 

1291 separator_range_te = TERange( 

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

1293 _text_to_te_position(raw_value_masked_comments[:offset]), 

1294 ).relative_to(value_element_pos) 

1295 lint_state.emit_diagnostic( 

1296 separator_range_te, 

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

1298 "error", 

1299 known_field.unknown_value_authority, 

1300 ) 

1301 m = _DEP_RELATION_CLAUSE.fullmatch(or_rel) 

1302 

1303 if m is not None: 

1304 garbage = m.group("garbage") 

1305 version_operator = m.group("operator") 

1306 version_operator_span = m.span("operator") 

1307 if _dctrl_check_dep_version_operator( 

1308 known_field, 

1309 version_operator, 

1310 version_operator_span, 

1311 version_operators, 

1312 raw_value_masked_comments, 

1313 offset, 

1314 value_element_pos, 

1315 lint_state, 

1316 ): 

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

1318 else: 

1319 name_arch_qual = m.group("name_arch_qual") 

1320 if ":" in name_arch_qual: 

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

1322 else: 

1323 name = name_arch_qual 

1324 arch_qual = None 

1325 sub_relations.append( 

1326 Relation( 

1327 name, 

1328 arch_qual=arch_qual, 

1329 version_operator=version_operator, 

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

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

1332 build_profile_restriction=m.group( 

1333 "build_profile_restriction" 

1334 ), 

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

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

1337 content_display_end_offset=rel_end_offset, 

1338 ) 

1339 ) 

1340 else: 

1341 garbage = None 

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

1343 

1344 if m is not None and not garbage: 

1345 continue 

1346 if m is not None: 

1347 garbage_span = m.span("garbage") 

1348 garbage_start, garbage_end = garbage_span 

1349 error_start_offset = offset + garbage_start 

1350 error_end_offset = offset + garbage_end 

1351 garbage_part = raw_value_masked_comments[ 

1352 error_start_offset:error_end_offset 

1353 ] 

1354 else: 

1355 garbage_part = None 

1356 error_start_offset = offset 

1357 error_end_offset = end_offset 

1358 

1359 problem_range_te = TERange( 

1360 _text_to_te_position(raw_value_masked_comments[:error_start_offset]), 

1361 _text_to_te_position(raw_value_masked_comments[:error_end_offset]), 

1362 ).relative_to(value_element_pos) 

1363 

1364 if garbage_part is not None: 

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

1366 msg = ( 

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

1368 " Is a separator missing before this part?" 

1369 ) 

1370 else: 

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

1372 lint_state.emit_diagnostic( 

1373 problem_range_te, 

1374 msg, 

1375 "error", 

1376 known_field.unknown_value_authority, 

1377 ) 

1378 else: 

1379 dep = _cleanup_rel( 

1380 raw_value_masked_comments[error_start_offset:error_end_offset] 

1381 ) 

1382 lint_state.emit_diagnostic( 

1383 problem_range_te, 

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

1385 "error", 

1386 known_field.unknown_value_authority, 

1387 ) 

1388 if ( 

1389 len(sub_relations) == 1 

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

1391 ): 

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

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

1394 

1395 for relations in relation_dup_table.values(): 

1396 if len(relations) > 1: 

1397 dup_check_relations( 

1398 known_field, 

1399 relations, 

1400 raw_value_masked_comments, 

1401 value_element_pos, 

1402 lint_state, 

1403 ) 

1404 

1405 

1406def _rrr_build_driver_mismatch( 

1407 _known_field: "F", 

1408 _deb822_file: Deb822FileElement, 

1409 _kvpair: Deb822KeyValuePairElement, 

1410 kvpair_range_te: "TERange", 

1411 _field_name_range: "TERange", 

1412 stanza: Deb822ParagraphElement, 

1413 _stanza_position: "TEPosition", 

1414 lint_state: LintState, 

1415) -> None: 

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

1417 if dr != "debian-rules": 

1418 lint_state.emit_diagnostic( 

1419 kvpair_range_te, 

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

1421 "informational", 

1422 "debputy", 

1423 quickfixes=[ 

1424 propose_remove_range_quick_fix( 

1425 proposed_title="Remove Rules-Requires-Root" 

1426 ) 

1427 ], 

1428 ) 

1429 

1430 

1431class Dep5Matcher(BasenameGlobMatch): 

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

1433 super().__init__( 

1434 basename_glob, 

1435 only_when_in_directory=None, 

1436 path_type=None, 

1437 recursive_match=False, 

1438 ) 

1439 

1440 

1441def _match_dep5_segment( 

1442 current_dir: VirtualPathBase, basename_glob: str 

1443) -> Iterable[VirtualPathBase]: 

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

1445 return Dep5Matcher(basename_glob).finditer(current_dir) 

1446 else: 

1447 res = current_dir.get(basename_glob) 

1448 if res is None: 

1449 return tuple() 

1450 return (res,) 

1451 

1452 

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

1454 

1455 

1456def _dep5_unnecessary_symbols( 

1457 value: str, 

1458 value_range: TERange, 

1459 lint_state: LintState, 

1460) -> None: 

1461 slash_check_index = 0 

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

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

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

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

1466 prefix_len = slashes_end 

1467 

1468 slash_check_index = prefix_len 

1469 prefix_range = TERange( 

1470 value_range.start_pos, 

1471 TEPosition( 

1472 value_range.start_pos.line_position, 

1473 value_range.start_pos.cursor_position + prefix_len, 

1474 ), 

1475 ) 

1476 lint_state.emit_diagnostic( 

1477 prefix_range, 

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

1479 "warning", 

1480 "debputy", 

1481 quickfixes=[ 

1482 propose_remove_range_quick_fix( 

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

1484 ) 

1485 ], 

1486 ) 

1487 

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

1489 m_start, m_end = m.span(0) 

1490 

1491 prefix_range = TERange( 

1492 TEPosition( 

1493 value_range.start_pos.line_position, 

1494 value_range.start_pos.cursor_position + m_start, 

1495 ), 

1496 TEPosition( 

1497 value_range.start_pos.line_position, 

1498 value_range.start_pos.cursor_position + m_end, 

1499 ), 

1500 ) 

1501 lint_state.emit_diagnostic( 

1502 prefix_range, 

1503 'Simplify to a single "/"', 

1504 "warning", 

1505 "debputy", 

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

1507 ) 

1508 

1509 

1510def _dep5_files_check( 

1511 known_field: "F", 

1512 _deb822_file: Deb822FileElement, 

1513 kvpair: Deb822KeyValuePairElement, 

1514 kvpair_range_te: "TERange", 

1515 _field_name_range: "TERange", 

1516 _stanza: Deb822ParagraphElement, 

1517 _stanza_position: "TEPosition", 

1518 lint_state: LintState, 

1519) -> None: 

1520 interpreter = known_field.field_value_class.interpreter() 

1521 assert interpreter is not None 

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

1523 kvpair_range_te.start_pos 

1524 ) 

1525 values_with_ranges = [] 

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

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

1528 full_value_range.start_pos 

1529 ) 

1530 value = value_ref.value 

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

1532 _dep5_unnecessary_symbols(value, value_range, lint_state) 

1533 

1534 source_root = lint_state.source_root 

1535 if source_root is None: 

1536 return 

1537 i = 0 

1538 limit = len(values_with_ranges) 

1539 while i < limit: 

1540 value, value_range = values_with_ranges[i] 

1541 i += 1 

1542 

1543 

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

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

1546_KNOWN_HTTPS_HOSTS = frozenset( 

1547 [ 

1548 "debian.org", 

1549 "bioconductor.org", 

1550 "cran.r-project.org", 

1551 "github.com", 

1552 "gitlab.com", 

1553 "metacpan.org", 

1554 "gnu.org", 

1555 ] 

1556) 

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

1558_NO_DOT_GIT_HOMEPAGE_HOSTS = frozenset( 

1559 { 

1560 "salsa.debian.org", 

1561 "github.com", 

1562 "gitlab.com", 

1563 } 

1564) 

1565 

1566 

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

1568 if host in known_hosts: 

1569 return True 

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

1571 try: 

1572 idx = host.index(".") 

1573 host = host[idx + 1 :] 

1574 except ValueError: 

1575 break 

1576 if host in known_hosts: 

1577 return True 

1578 return False 

1579 

1580 

1581def _validate_homepage_field( 

1582 _known_field: "F", 

1583 _deb822_file: Deb822FileElement, 

1584 kvpair: Deb822KeyValuePairElement, 

1585 kvpair_range_te: "TERange", 

1586 _field_name_range_te: "TERange", 

1587 _stanza: Deb822ParagraphElement, 

1588 _stanza_position: "TEPosition", 

1589 lint_state: LintState, 

1590) -> None: 

1591 value = kvpair.value_element.convert_to_text() 

1592 offset = 0 

1593 homepage = value 

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

1595 expected_value = m.group(1) 

1596 quickfixes = [] 

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

1598 homepage = expected_value.strip() 

1599 offset = m.start(1) 

1600 quickfixes.append(propose_correct_text_quick_fix(expected_value)) 

1601 lint_state.emit_diagnostic( 

1602 _single_line_span_range_relative_to_pos( 

1603 m.span(), 

1604 kvpair.value_element.position_in_parent().relative_to( 

1605 kvpair_range_te.start_pos 

1606 ), 

1607 ), 

1608 "Superfluous URL/URI wrapping", 

1609 "informational", 

1610 "Policy 5.6.23", 

1611 quickfixes=quickfixes, 

1612 ) 

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

1614 m = _URI_RE.search(homepage) 

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

1616 return 

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

1618 protocol = m.group("protocol") 

1619 host = m.group("host") 

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

1621 if _is_known_host(host, _REPLACED_HOSTS): 

1622 span = m.span("host") 

1623 lint_state.emit_diagnostic( 

1624 _single_line_span_range_relative_to_pos( 

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

1626 kvpair.value_element.position_in_parent().relative_to( 

1627 kvpair_range_te.start_pos 

1628 ), 

1629 ), 

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

1631 "warning", 

1632 "debputy", 

1633 ) 

1634 return 

1635 if ( 

1636 protocol == "ftp" 

1637 or protocol == "http" 

1638 and _is_known_host(host, _KNOWN_HTTPS_HOSTS) 

1639 ): 

1640 span = m.span("protocol") 

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

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

1643 quickfixes = [] 

1644 else: 

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

1646 quickfixes = [propose_correct_text_quick_fix("https")] 

1647 lint_state.emit_diagnostic( 

1648 _single_line_span_range_relative_to_pos( 

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

1650 kvpair.value_element.position_in_parent().relative_to( 

1651 kvpair_range_te.start_pos 

1652 ), 

1653 ), 

1654 msg, 

1655 "pedantic", 

1656 "debputy", 

1657 quickfixes=quickfixes, 

1658 ) 

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

1660 span = m.span("path") 

1661 msg = "Unnecessary suffix" 

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

1663 lint_state.emit_diagnostic( 

1664 _single_line_span_range_relative_to_pos( 

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

1666 kvpair.value_element.position_in_parent().relative_to( 

1667 kvpair_range_te.start_pos 

1668 ), 

1669 ), 

1670 msg, 

1671 "pedantic", 

1672 "debputy", 

1673 quickfixes=quickfixes, 

1674 ) 

1675 

1676 

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

1678 def _validator( 

1679 known_field: "F", 

1680 deb822_file: Deb822FileElement, 

1681 kvpair: Deb822KeyValuePairElement, 

1682 kvpair_range_te: "TERange", 

1683 field_name_range_te: "TERange", 

1684 stanza: Deb822ParagraphElement, 

1685 stanza_position: "TEPosition", 

1686 lint_state: LintState, 

1687 ) -> None: 

1688 for check in checks: 

1689 check( 

1690 known_field, 

1691 deb822_file, 

1692 kvpair, 

1693 kvpair_range_te, 

1694 field_name_range_te, 

1695 stanza, 

1696 stanza_position, 

1697 lint_state, 

1698 ) 

1699 

1700 return _validator 

1701 

1702 

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

1704class PackageNameSectionRule: 

1705 section: str 

1706 check: Callable[[str], bool] 

1707 

1708 

1709def _package_name_section_rule( 

1710 section: str, 

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

1712 *, 

1713 confirm_re: re.Pattern | None = None, 

1714) -> PackageNameSectionRule: 

1715 if confirm_re is not None: 

1716 assert callable(check) 

1717 

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

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

1720 

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

1722 

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

1724 return check.search(v) is not None 

1725 

1726 else: 

1727 _impl = check 

1728 

1729 return PackageNameSectionRule(section, _impl) 

1730 

1731 

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

1733_PKGNAME_VS_SECTION_RULES = [ 

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

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

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

1737 _package_name_section_rule( 

1738 "httpd", 

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

1740 ), 

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

1742 _package_name_section_rule( 

1743 "gnustep", 

1744 lambda n: n.endswith( 

1745 ( 

1746 ".framework", 

1747 ".framework-common", 

1748 ".tool", 

1749 ".tool-common", 

1750 ".app", 

1751 ".app-common", 

1752 ) 

1753 ), 

1754 ), 

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

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

1757 _package_name_section_rule( 

1758 "zope", 

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

1760 ), 

1761 _package_name_section_rule( 

1762 "python", 

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

1764 ), 

1765 _package_name_section_rule( 

1766 "gnu-r", 

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

1768 ), 

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

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

1771 _package_name_section_rule( 

1772 "lisp", 

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

1774 ), 

1775 _package_name_section_rule( 

1776 "lisp", 

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

1778 ), 

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

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

1781 _package_name_section_rule( 

1782 "perl", 

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

1784 ), 

1785 _package_name_section_rule( 

1786 "cli-mono", 

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

1788 ), 

1789 _package_name_section_rule( 

1790 "java", 

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

1792 ), 

1793 _package_name_section_rule( 

1794 "php", 

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

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

1797 ), 

1798 _package_name_section_rule( 

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

1800 ), 

1801 _package_name_section_rule( 

1802 "haskell", 

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

1804 ), 

1805 _package_name_section_rule( 

1806 "ruby", 

1807 lambda n: "-ruby" in n, 

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

1809 ), 

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

1811 _package_name_section_rule( 

1812 "rust", 

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

1814 ), 

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

1816 _package_name_section_rule( 

1817 "ocaml", 

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

1819 ), 

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

1821 _package_name_section_rule( 

1822 "interpreters", 

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

1824 ), 

1825 _package_name_section_rule( 

1826 "introspection", 

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

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

1829 ), 

1830 _package_name_section_rule( 

1831 "fonts", 

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

1833 ), 

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

1835 _package_name_section_rule( 

1836 "localization", 

1837 lambda n: n.startswith( 

1838 ( 

1839 "aspell-", 

1840 "hunspell-", 

1841 "myspell-", 

1842 "mythes-", 

1843 "dict-freedict-", 

1844 "gcompris-sound-", 

1845 ) 

1846 ), 

1847 ), 

1848 _package_name_section_rule( 

1849 "localization", 

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

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

1852 ), 

1853 _package_name_section_rule( 

1854 "localization", 

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

1856 ), 

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

1858 _package_name_section_rule( 

1859 "libdevel", 

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

1861 ), 

1862 _package_name_section_rule( 

1863 "libs", 

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

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

1866 ), 

1867] 

1868 

1869 

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

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

1872@functools.lru_cache(64) 

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

1874 for rule in _PKGNAME_VS_SECTION_RULES: 

1875 if rule.check(name): 

1876 return rule.section 

1877 return None 

1878 

1879 

1880def _unknown_value_check( 

1881 field_name: str, 

1882 value: str, 

1883 known_values: Mapping[str, Keyword], 

1884 unknown_value_severity: LintSeverity | None, 

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

1886 known_value = known_values.get(value) 

1887 message = None 

1888 severity = unknown_value_severity 

1889 fix_data = None 

1890 if known_value is None: 

1891 candidates = detect_possible_typo( 

1892 value, 

1893 known_values, 

1894 ) 

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

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

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

1898 else: 

1899 hint_text = "" 

1900 fix_data = None 

1901 severity = unknown_value_severity 

1902 fix_text = hint_text 

1903 if candidates: 

1904 match = candidates[0] 

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

1906 known_value = known_values[match] 

1907 fix_text = ( 

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

1909 ) 

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

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

1912 return None, None, None, None 

1913 if severity is None: 

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

1915 # It always has leading whitespace 

1916 message = fix_text.strip() 

1917 else: 

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

1919 return known_value, message, severity, fix_data 

1920 

1921 

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

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

1924 

1925 

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

1927 return path 

1928 

1929 

1930def _should_ignore_dir( 

1931 path: VirtualPath, 

1932 *, 

1933 supports_dir_match: bool = False, 

1934 match_non_persistent_paths: bool = False, 

1935) -> bool: 

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

1937 return True 

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

1939 if ( 

1940 not match_non_persistent_paths 

1941 and cachedir_tag is not None 

1942 and cachedir_tag.is_file 

1943 ): 

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

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

1946 start = fd.read(43) 

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

1948 return True 

1949 return False 

1950 

1951 

1952@dataclasses.dataclass(slots=True) 

1953class Deb822KnownField: 

1954 name: str 

1955 field_value_class: FieldValueClass 

1956 warn_if_default: bool = True 

1957 unknown_value_authority: str = "debputy" 

1958 missing_field_authority: str = "debputy" 

1959 replaced_by: str | None = None 

1960 deprecated_with_no_replacement: bool = False 

1961 missing_field_severity: LintSeverity | None = None 

1962 default_value: str | None = None 

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

1964 unknown_value_severity: LintSeverity | None = "error" 

1965 translation_context: str = "" 

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

1967 synopsis: str | None = None 

1968 usage_hint: UsageHint | None = None 

1969 long_description: str | None = None 

1970 spellcheck_value: bool = False 

1971 inheritable_from_other_stanza: bool = False 

1972 show_as_inherited: bool = True 

1973 custom_field_check: CustomFieldCheck | None = None 

1974 can_complete_field_in_stanza: None | ( 

1975 Callable[[Iterable[Deb822ParagraphElement]], bool] 

1976 ) = None 

1977 is_substvars_disabled_even_if_allowed_by_stanza: bool = False 

1978 is_alias_of: str | None = None 

1979 is_completion_suggestion: bool = True 

1980 

1981 def synopsis_translated( 

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

1983 ) -> str | None: 

1984 if self.synopsis is None: 

1985 return None 

1986 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1987 self.translation_context, 

1988 self.synopsis, 

1989 ) 

1990 

1991 def long_description_translated( 

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

1993 ) -> str | None: 

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

1995 return None 

1996 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1997 self.translation_context, 

1998 self.long_description, 

1999 ) 

2000 

2001 def _can_complete_field_in_stanza( 

2002 self, 

2003 stanza_parts: Sequence[Deb822ParagraphElement], 

2004 ) -> bool: 

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

2006 return False 

2007 return ( 

2008 self.can_complete_field_in_stanza is None 

2009 or self.can_complete_field_in_stanza(stanza_parts) 

2010 ) 

2011 

2012 def complete_field( 

2013 self, 

2014 lint_state: LintState, 

2015 stanza_parts: Sequence[Deb822ParagraphElement], 

2016 markdown_kind: MarkupKind, 

2017 ) -> CompletionItem | None: 

2018 if not self._can_complete_field_in_stanza(stanza_parts): 

2019 return None 

2020 name = self.name 

2021 complete_as = name + ": " 

2022 options = self.value_options_for_completer( 

2023 lint_state, 

2024 stanza_parts, 

2025 "", 

2026 markdown_kind, 

2027 is_completion_for_field=True, 

2028 ) 

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

2030 value = options[0].insert_text 

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

2032 complete_as += value 

2033 tags = [] 

2034 is_deprecated = False 

2035 if self.replaced_by or self.deprecated_with_no_replacement: 

2036 is_deprecated = True 

2037 tags.append(CompletionItemTag.Deprecated) 

2038 

2039 doc = self.long_description 

2040 if doc: 

2041 doc = MarkupContent( 

2042 value=doc, 

2043 kind=markdown_kind, 

2044 ) 

2045 else: 

2046 doc = None 

2047 

2048 return CompletionItem( 

2049 name, 

2050 insert_text=complete_as, 

2051 deprecated=is_deprecated, 

2052 tags=tags, 

2053 detail=format_comp_item_synopsis_doc( 

2054 self.usage_hint, 

2055 self.synopsis_translated(lint_state), 

2056 is_deprecated, 

2057 ), 

2058 documentation=doc, 

2059 ) 

2060 

2061 def _complete_files( 

2062 self, 

2063 base_dir: VirtualPathBase | None, 

2064 value_being_completed: str, 

2065 *, 

2066 is_dep5_file_list: bool = False, 

2067 supports_dir_match: bool = False, 

2068 supports_spaces_in_filename: bool = False, 

2069 match_non_persistent_paths: bool = False, 

2070 ) -> Sequence[CompletionItem] | None: 

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

2072 if base_dir is None or not base_dir.is_dir: 

2073 return None 

2074 

2075 if is_dep5_file_list: 

2076 supports_spaces_in_filename = True 

2077 supports_dir_match = False 

2078 match_non_persistent_paths = False 

2079 

2080 if value_being_completed == "": 

2081 current_dir = base_dir 

2082 unmatched_parts: Sequence[str] = () 

2083 else: 

2084 current_dir, unmatched_parts = base_dir.attempt_lookup( 

2085 value_being_completed 

2086 ) 

2087 

2088 if len(unmatched_parts) > 1: 

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

2090 return None 

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

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

2093 return None 

2094 items = [] 

2095 

2096 path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path 

2097 

2098 for child in current_dir.iterdir(): 

2099 if child.is_symlink and is_dep5_file_list: 

2100 continue 

2101 if not supports_spaces_in_filename and ( 

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

2103 ): 

2104 continue 

2105 sort_text = ( 

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

2107 ) 

2108 if child.is_dir: 

2109 if _should_ignore_dir( 

2110 child, 

2111 supports_dir_match=supports_dir_match, 

2112 match_non_persistent_paths=match_non_persistent_paths, 

2113 ): 

2114 continue 

2115 items.append( 

2116 CompletionItem( 

2117 f"{child.path}/", 

2118 label_details=CompletionItemLabelDetails( 

2119 description=child.path, 

2120 ), 

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

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

2123 sort_text=sort_text, 

2124 kind=CompletionItemKind.Folder, 

2125 ) 

2126 ) 

2127 else: 

2128 items.append( 

2129 CompletionItem( 

2130 child.path, 

2131 label_details=CompletionItemLabelDetails( 

2132 description=child.path, 

2133 ), 

2134 insert_text=path_escaper(child.path), 

2135 filter_text=child.path, 

2136 sort_text=sort_text, 

2137 kind=CompletionItemKind.File, 

2138 ) 

2139 ) 

2140 return items 

2141 

2142 def value_options_for_completer( 

2143 self, 

2144 lint_state: LintState, 

2145 stanza_parts: Sequence[Deb822ParagraphElement], 

2146 value_being_completed: str, 

2147 markdown_kind: MarkupKind, 

2148 *, 

2149 is_completion_for_field: bool = False, 

2150 ) -> Sequence[CompletionItem] | None: 

2151 known_values = self.known_values 

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

2153 if is_completion_for_field: 

2154 return None 

2155 return self._complete_files( 

2156 lint_state.source_root, 

2157 value_being_completed, 

2158 is_dep5_file_list=True, 

2159 ) 

2160 

2161 if known_values is None: 

2162 return None 

2163 if is_completion_for_field and ( 

2164 len(known_values) == 1 

2165 or ( 

2166 len(known_values) == 2 

2167 and self.warn_if_default 

2168 and self.default_value is not None 

2169 ) 

2170 ): 

2171 value = next( 

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

2173 None, 

2174 ) 

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

2176 return None 

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

2178 return [ 

2179 keyword.as_completion_item( 

2180 lint_state, 

2181 stanza_parts, 

2182 value_being_completed, 

2183 markdown_kind, 

2184 ) 

2185 for keyword in known_values.values() 

2186 if keyword.is_keyword_valid_completion_in_stanza(stanza_parts) 

2187 and keyword.is_completion_suggestion 

2188 ] 

2189 

2190 def field_omitted_diagnostics( 

2191 self, 

2192 deb822_file: Deb822FileElement, 

2193 representation_field_range: "TERange", 

2194 stanza: Deb822ParagraphElement, 

2195 stanza_position: "TEPosition", 

2196 header_stanza: Deb822FileElement | None, 

2197 lint_state: LintState, 

2198 ) -> None: 

2199 missing_field_severity = self.missing_field_severity 

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

2201 return 

2202 

2203 if ( 

2204 self.inheritable_from_other_stanza 

2205 and header_stanza is not None 

2206 and self.name in header_stanza 

2207 ): 

2208 return 

2209 

2210 lint_state.emit_diagnostic( 

2211 representation_field_range, 

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

2213 missing_field_severity, 

2214 self.missing_field_authority, 

2215 ) 

2216 

2217 async def field_diagnostics( 

2218 self, 

2219 deb822_file: Deb822FileElement, 

2220 kvpair: Deb822KeyValuePairElement, 

2221 stanza: Deb822ParagraphElement, 

2222 stanza_position: "TEPosition", 

2223 kvpair_range_te: "TERange", 

2224 lint_state: LintState, 

2225 *, 

2226 field_name_typo_reported: bool = False, 

2227 ) -> None: 

2228 field_name_token = kvpair.field_token 

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

2230 kvpair_range_te.start_pos 

2231 ) 

2232 field_name = field_name_token.text 

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

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

2235 # in one but not the other. 

2236 field_value = stanza[field_name] 

2237 self._diagnostics_for_field_name( 

2238 kvpair_range_te, 

2239 field_name_token, 

2240 field_name_range_te, 

2241 field_name_typo_reported, 

2242 lint_state, 

2243 ) 

2244 if self.custom_field_check is not None: 

2245 self.custom_field_check( 

2246 self, 

2247 deb822_file, 

2248 kvpair, 

2249 kvpair_range_te, 

2250 field_name_range_te, 

2251 stanza, 

2252 stanza_position, 

2253 lint_state, 

2254 ) 

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

2256 if self.spellcheck_value: 

2257 words = kvpair.interpret_as(LIST_SPACE_SEPARATED_INTERPRETATION) 

2258 spell_checker = lint_state.spellchecker() 

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

2260 kvpair_range_te.start_pos 

2261 ) 

2262 async for word_ref in lint_state.slow_iter( 

2263 words.iter_value_references(), yield_every=25 

2264 ): 

2265 token = word_ref.value 

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

2267 corrections = spell_checker.provide_corrections_for(word) 

2268 if not corrections: 

2269 continue 

2270 word_loc = word_ref.locatable 

2271 word_pos_te = word_loc.position_in_parent().relative_to( 

2272 value_position 

2273 ) 

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

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

2276 word_size = TERange( 

2277 START_POSITION, 

2278 TEPosition(0, endpos - pos), 

2279 ) 

2280 lint_state.emit_diagnostic( 

2281 TERange.from_position_and_size(word_pos_te, word_size), 

2282 f'Spelling "{word}"', 

2283 "spelling", 

2284 "debputy", 

2285 quickfixes=[ 

2286 propose_correct_text_quick_fix(c) for c in corrections 

2287 ], 

2288 enable_non_interactive_auto_fix=False, 

2289 ) 

2290 else: 

2291 self._known_value_diagnostics( 

2292 kvpair, 

2293 kvpair_range_te.start_pos, 

2294 lint_state, 

2295 ) 

2296 

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

2298 lint_state.emit_diagnostic( 

2299 kvpair_range_te, 

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

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

2302 "warning", 

2303 "debputy", 

2304 ) 

2305 

2306 def _diagnostics_for_field_name( 

2307 self, 

2308 kvpair_range: "TERange", 

2309 token: Deb822FieldNameToken, 

2310 token_range: "TERange", 

2311 typo_detected: bool, 

2312 lint_state: LintState, 

2313 ) -> None: 

2314 field_name = token.text 

2315 # Defeat the case-insensitivity from python-debian 

2316 field_name_cased = str(field_name) 

2317 if self.deprecated_with_no_replacement: 

2318 lint_state.emit_diagnostic( 

2319 kvpair_range, 

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

2321 "warning", 

2322 "debputy", 

2323 quickfixes=[propose_remove_range_quick_fix()], 

2324 tags=[DiagnosticTag.Deprecated], 

2325 ) 

2326 elif self.replaced_by is not None: 

2327 lint_state.emit_diagnostic( 

2328 token_range, 

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

2330 "warning", 

2331 "debputy", 

2332 tags=[DiagnosticTag.Deprecated], 

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

2334 ) 

2335 

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

2337 lint_state.emit_diagnostic( 

2338 token_range, 

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

2340 "pedantic", 

2341 self.unknown_value_authority, 

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

2343 ) 

2344 

2345 def _dep5_file_list_diagnostics( 

2346 self, 

2347 kvpair: Deb822KeyValuePairElement, 

2348 kvpair_position: "TEPosition", 

2349 lint_state: LintState, 

2350 ) -> None: 

2351 source_root = lint_state.source_root 

2352 if ( 

2353 self.field_value_class != FieldValueClass.DEP5_FILE_LIST 

2354 or source_root is None 

2355 ): 

2356 return 

2357 interpreter = self.field_value_class.interpreter() 

2358 values = kvpair.interpret_as(interpreter) 

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

2360 kvpair_position 

2361 ) 

2362 

2363 assert interpreter is not None 

2364 

2365 for token in values.iter_parts(): 

2366 if token.is_whitespace: 

2367 continue 

2368 text = token.convert_to_text() 

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

2370 # TODO: We should validate these as well 

2371 continue 

2372 matched_path, missing_part = source_root.attempt_lookup(text) 

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

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

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

2376 # do not have the infrastructure for). 

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

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

2379 lint_state.emit_diagnostic( 

2380 path_range_te, 

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

2382 "warning", 

2383 self.unknown_value_authority, 

2384 quickfixes=[ 

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

2386 ], 

2387 ) 

2388 

2389 def _known_value_diagnostics( 

2390 self, 

2391 kvpair: Deb822KeyValuePairElement, 

2392 kvpair_position: "TEPosition", 

2393 lint_state: LintState, 

2394 ) -> None: 

2395 unknown_value_severity = self.unknown_value_severity 

2396 interpreter = self.field_value_class.interpreter() 

2397 if interpreter is None: 

2398 return 

2399 try: 

2400 values = kvpair.interpret_as(interpreter) 

2401 except ValueError: 

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

2403 kvpair_position 

2404 ) 

2405 lint_state.emit_diagnostic( 

2406 value_range, 

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

2408 "pedantic", 

2409 "debputy", 

2410 ) 

2411 return 

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

2413 kvpair_position 

2414 ) 

2415 

2416 last_token_non_ws_sep_token: TE | None = None 

2417 for token in values.iter_parts(): 

2418 if token.is_whitespace: 

2419 continue 

2420 if not token.is_separator: 

2421 last_token_non_ws_sep_token = None 

2422 continue 

2423 if last_token_non_ws_sep_token is not None: 

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

2425 lint_state.emit_diagnostic( 

2426 sep_range_te, 

2427 "Duplicate separator", 

2428 "error", 

2429 self.unknown_value_authority, 

2430 ) 

2431 last_token_non_ws_sep_token = token 

2432 

2433 allowed_values = self.known_values 

2434 if not allowed_values: 

2435 return 

2436 

2437 first_value = None 

2438 first_exclusive_value_ref = None 

2439 first_exclusive_value = None 

2440 has_emitted_for_exclusive = False 

2441 

2442 for value_ref in values.iter_value_references(): 

2443 value = value_ref.value 

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

2445 first_value is not None 

2446 and self.field_value_class == FieldValueClass.SINGLE_VALUE 

2447 ): 

2448 value_loc = value_ref.locatable 

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

2450 lint_state.emit_diagnostic( 

2451 range_position_te, 

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

2453 "error", 

2454 self.unknown_value_authority, 

2455 ) 

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

2457 continue 

2458 

2459 if first_exclusive_value_ref is not None and not has_emitted_for_exclusive: 

2460 assert first_exclusive_value is not None 

2461 value_loc = first_exclusive_value_ref.locatable 

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

2463 lint_state.emit_diagnostic( 

2464 value_range_te, 

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

2466 "error", 

2467 self.unknown_value_authority, 

2468 ) 

2469 

2470 known_value, unknown_value_message, unknown_severity, typo_fix_data = ( 

2471 _unknown_value_check( 

2472 self.name, 

2473 value, 

2474 self.known_values, 

2475 unknown_value_severity, 

2476 ) 

2477 ) 

2478 value_loc = value_ref.locatable 

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

2480 

2481 if known_value and known_value.is_exclusive: 

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

2483 first_exclusive_value_ref = value_ref 

2484 if first_value is not None: 

2485 has_emitted_for_exclusive = True 

2486 lint_state.emit_diagnostic( 

2487 value_range, 

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

2489 "error", 

2490 self.unknown_value_authority, 

2491 ) 

2492 

2493 if first_value is None: 

2494 first_value = value 

2495 

2496 if unknown_value_message is not None: 

2497 assert unknown_severity is not None 

2498 lint_state.emit_diagnostic( 

2499 value_range, 

2500 unknown_value_message, 

2501 unknown_severity, 

2502 self.unknown_value_authority, 

2503 quickfixes=typo_fix_data, 

2504 ) 

2505 

2506 if known_value is not None and known_value.is_deprecated: 

2507 replacement = known_value.replaced_by 

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

2509 obsolete_value_message = ( 

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

2511 ) 

2512 obsolete_fix_data = [propose_correct_text_quick_fix(replacement)] 

2513 else: 

2514 obsolete_value_message = ( 

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

2516 ) 

2517 obsolete_fix_data = None 

2518 lint_state.emit_diagnostic( 

2519 value_range, 

2520 obsolete_value_message, 

2521 "warning", 

2522 "debputy", 

2523 quickfixes=obsolete_fix_data, 

2524 ) 

2525 

2526 def _reformat_field_name( 

2527 self, 

2528 effective_preference: "EffectiveFormattingPreference", 

2529 stanza_range: TERange, 

2530 kvpair: Deb822KeyValuePairElement, 

2531 position_codec: LintCapablePositionCodec, 

2532 lines: list[str], 

2533 ) -> Iterable[TextEdit]: 

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

2535 return 

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

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

2538 return 

2539 

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

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

2542 ) 

2543 

2544 edit_range = position_codec.range_to_client_units( 

2545 lines, 

2546 Range( 

2547 Position( 

2548 field_name_range_te.start_pos.line_position, 

2549 field_name_range_te.start_pos.cursor_position, 

2550 ), 

2551 Position( 

2552 field_name_range_te.start_pos.line_position, 

2553 field_name_range_te.end_pos.cursor_position, 

2554 ), 

2555 ), 

2556 ) 

2557 yield TextEdit( 

2558 edit_range, 

2559 self.name, 

2560 ) 

2561 

2562 def reformat_field( 

2563 self, 

2564 effective_preference: "EffectiveFormattingPreference", 

2565 stanza_range: TERange, 

2566 kvpair: Deb822KeyValuePairElement, 

2567 formatter: FormatterCallback, 

2568 position_codec: LintCapablePositionCodec, 

2569 lines: list[str], 

2570 ) -> Iterable[TextEdit]: 

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

2572 yield from self._reformat_field_name( 

2573 effective_preference, 

2574 stanza_range, 

2575 kvpair, 

2576 position_codec, 

2577 lines, 

2578 ) 

2579 return trim_end_of_line_whitespace( 

2580 position_codec, 

2581 lines, 

2582 line_range=range( 

2583 kvpair_range.start_pos.line_position, 

2584 kvpair_range.end_pos.line_position, 

2585 ), 

2586 ) 

2587 

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

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

2590 

2591 

2592@dataclasses.dataclass(slots=True) 

2593class DctrlLikeKnownField(Deb822KnownField): 

2594 

2595 def reformat_field( 

2596 self, 

2597 effective_preference: "EffectiveFormattingPreference", 

2598 stanza_range: TERange, 

2599 kvpair: Deb822KeyValuePairElement, 

2600 formatter: FormatterCallback, 

2601 position_codec: LintCapablePositionCodec, 

2602 lines: list[str], 

2603 ) -> Iterable[TextEdit]: 

2604 interpretation = self.field_value_class.interpreter() 

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

2606 not effective_preference.deb822_normalize_field_content 

2607 or interpretation is None 

2608 ): 

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

2610 effective_preference, 

2611 stanza_range, 

2612 kvpair, 

2613 formatter, 

2614 position_codec, 

2615 lines, 

2616 ) 

2617 return 

2618 if not self.reformattable_field: 

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

2620 effective_preference, 

2621 stanza_range, 

2622 kvpair, 

2623 formatter, 

2624 position_codec, 

2625 lines, 

2626 ) 

2627 return 

2628 

2629 # Preserve the name fixes from the super call. 

2630 yield from self._reformat_field_name( 

2631 effective_preference, 

2632 stanza_range, 

2633 kvpair, 

2634 position_codec, 

2635 lines, 

2636 ) 

2637 

2638 seen: set[str] = set() 

2639 old_kvpair_range = kvpair.range_in_parent() 

2640 sort = self.is_sortable_field 

2641 

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

2643 field_content = kvpair.interpret_as(interpretation) 

2644 old_value = field_content.convert_to_text(with_field_name=False) 

2645 for package_ref in field_content.iter_value_references(): 

2646 value = package_ref.value 

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

2648 stanza_range.start_pos 

2649 ) 

2650 sublines = lines[ 

2651 value_range.start_pos.line_position : value_range.end_pos.line_position 

2652 ] 

2653 

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

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

2656 return 

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

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

2659 else: 

2660 new_value = value 

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

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

2663 package_ref.value = new_value 

2664 seen.add(new_value) 

2665 else: 

2666 package_ref.remove() 

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

2668 field_content.sort(key=_sort_packages_key) 

2669 field_content.value_formatter(formatter) 

2670 field_content.reformat_when_finished() 

2671 

2672 new_value = field_content.convert_to_text(with_field_name=False) 

2673 if new_value != old_value: 

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

2675 old_kvpair_range.start_pos 

2676 ) 

2677 range_server_units = te_range_to_lsp( 

2678 value_range.relative_to(stanza_range.start_pos) 

2679 ) 

2680 yield TextEdit( 

2681 position_codec.range_to_client_units(lines, range_server_units), 

2682 new_value, 

2683 ) 

2684 

2685 @property 

2686 def reformattable_field(self) -> bool: 

2687 return self.is_relationship_field or self.is_sortable_field 

2688 

2689 @property 

2690 def is_relationship_field(self) -> bool: 

2691 return False 

2692 

2693 @property 

2694 def is_sortable_field(self) -> bool: 

2695 return self.is_relationship_field 

2696 

2697 

2698@dataclasses.dataclass(slots=True) 

2699class DTestsCtrlKnownField(DctrlLikeKnownField): 

2700 @property 

2701 def is_relationship_field(self) -> bool: 

2702 return self.name == "Depends" 

2703 

2704 @property 

2705 def is_sortable_field(self) -> bool: 

2706 return self.is_relationship_field or self.name in ( 

2707 "Features", 

2708 "Restrictions", 

2709 "Tests", 

2710 ) 

2711 

2712 

2713@dataclasses.dataclass(slots=True) 

2714class DctrlKnownField(DctrlLikeKnownField): 

2715 

2716 def field_omitted_diagnostics( 

2717 self, 

2718 deb822_file: Deb822FileElement, 

2719 representation_field_range: "TERange", 

2720 stanza: Deb822ParagraphElement, 

2721 stanza_position: "TEPosition", 

2722 header_stanza: Deb822FileElement | None, 

2723 lint_state: LintState, 

2724 ) -> None: 

2725 missing_field_severity = self.missing_field_severity 

2726 if missing_field_severity is None: 

2727 return 

2728 

2729 if ( 

2730 self.inheritable_from_other_stanza 

2731 and header_stanza is not None 

2732 and self.name in header_stanza 

2733 ): 

2734 return 

2735 

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

2737 stanzas = list(deb822_file)[1:] 

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

2739 return 

2740 

2741 lint_state.emit_diagnostic( 

2742 representation_field_range, 

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

2744 missing_field_severity, 

2745 self.missing_field_authority, 

2746 ) 

2747 

2748 def reformat_field( 

2749 self, 

2750 effective_preference: "EffectiveFormattingPreference", 

2751 stanza_range: TERange, 

2752 kvpair: Deb822KeyValuePairElement, 

2753 formatter: FormatterCallback, 

2754 position_codec: LintCapablePositionCodec, 

2755 lines: list[str], 

2756 ) -> Iterable[TextEdit]: 

2757 if ( 

2758 self.name == "Architecture" 

2759 and effective_preference.deb822_normalize_field_content 

2760 ): 

2761 interpretation = self.field_value_class.interpreter() 

2762 assert interpretation is not None 

2763 interpreted = kvpair.interpret_as(interpretation) 

2764 archs = list(interpreted) 

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

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

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

2768 reformat_edits = list( 

2769 self._reformat_field_name( 

2770 effective_preference, 

2771 stanza_range, 

2772 kvpair, 

2773 position_codec, 

2774 lines, 

2775 ) 

2776 ) 

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

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

2779 kvpair.range_in_parent().start_pos 

2780 ) 

2781 kvpair_range = te_range_to_lsp( 

2782 value_range.relative_to(stanza_range.start_pos) 

2783 ) 

2784 reformat_edits.append( 

2785 TextEdit( 

2786 position_codec.range_to_client_units(lines, kvpair_range), 

2787 new_value, 

2788 ) 

2789 ) 

2790 return reformat_edits 

2791 

2792 return super(DctrlKnownField, self).reformat_field( 

2793 effective_preference, 

2794 stanza_range, 

2795 kvpair, 

2796 formatter, 

2797 position_codec, 

2798 lines, 

2799 ) 

2800 

2801 @property 

2802 def is_relationship_field(self) -> bool: 

2803 name_lc = self.name.lower() 

2804 return ( 

2805 name_lc in all_package_relationship_fields() 

2806 or name_lc in all_source_relationship_fields() 

2807 ) 

2808 

2809 @property 

2810 def reformattable_field(self) -> bool: 

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

2812 

2813 

2814@dataclasses.dataclass(slots=True) 

2815class DctrlRelationshipKnownField(DctrlKnownField): 

2816 allowed_version_operators: frozenset[str] = frozenset() 

2817 supports_or_relation: bool = True 

2818 

2819 @property 

2820 def is_relationship_field(self) -> bool: 

2821 return True 

2822 

2823 

2824SOURCE_FIELDS = _fields( 

2825 DctrlKnownField( 

2826 "Source", 

2827 FieldValueClass.SINGLE_VALUE, 

2828 custom_field_check=_combined_custom_field_check( 

2829 _each_value_match_regex_validation(PKGNAME_REGEX), 

2830 _has_packaging_expected_file( 

2831 "copyright", 

2832 "No copyright file (package license)", 

2833 severity="warning", 

2834 ), 

2835 _has_packaging_expected_file( 

2836 "changelog", 

2837 "No Debian changelog file", 

2838 severity="error", 

2839 ), 

2840 _has_build_instructions, 

2841 ), 

2842 ), 

2843 DctrlKnownField( 

2844 "Standards-Version", 

2845 FieldValueClass.SINGLE_VALUE, 

2846 custom_field_check=_sv_field_validation, 

2847 ), 

2848 DctrlKnownField( 

2849 "Section", 

2850 FieldValueClass.SINGLE_VALUE, 

2851 known_values=ALL_SECTIONS, 

2852 ), 

2853 DctrlKnownField( 

2854 "Priority", 

2855 FieldValueClass.SINGLE_VALUE, 

2856 ), 

2857 DctrlKnownField( 

2858 "Maintainer", 

2859 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2860 custom_field_check=_combined_custom_field_check( 

2861 _maintainer_field_validator, 

2862 _canonical_maintainer_name, 

2863 ), 

2864 ), 

2865 DctrlKnownField( 

2866 "Uploaders", 

2867 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2868 custom_field_check=_canonical_maintainer_name, 

2869 ), 

2870 DctrlRelationshipKnownField( 

2871 "Build-Depends", 

2872 FieldValueClass.COMMA_SEPARATED_LIST, 

2873 custom_field_check=_dctrl_validate_dep, 

2874 ), 

2875 DctrlRelationshipKnownField( 

2876 "Build-Depends-Arch", 

2877 FieldValueClass.COMMA_SEPARATED_LIST, 

2878 custom_field_check=_dctrl_validate_dep, 

2879 ), 

2880 DctrlRelationshipKnownField( 

2881 "Build-Depends-Indep", 

2882 FieldValueClass.COMMA_SEPARATED_LIST, 

2883 custom_field_check=_dctrl_validate_dep, 

2884 ), 

2885 DctrlRelationshipKnownField( 

2886 "Build-Conflicts", 

2887 FieldValueClass.COMMA_SEPARATED_LIST, 

2888 supports_or_relation=False, 

2889 custom_field_check=_dctrl_validate_dep, 

2890 ), 

2891 DctrlRelationshipKnownField( 

2892 "Build-Conflicts-Arch", 

2893 FieldValueClass.COMMA_SEPARATED_LIST, 

2894 supports_or_relation=False, 

2895 custom_field_check=_dctrl_validate_dep, 

2896 ), 

2897 DctrlRelationshipKnownField( 

2898 "Build-Conflicts-Indep", 

2899 FieldValueClass.COMMA_SEPARATED_LIST, 

2900 supports_or_relation=False, 

2901 custom_field_check=_dctrl_validate_dep, 

2902 ), 

2903 DctrlKnownField( 

2904 "Rules-Requires-Root", 

2905 FieldValueClass.SPACE_SEPARATED_LIST, 

2906 custom_field_check=_rrr_build_driver_mismatch, 

2907 ), 

2908 DctrlKnownField( 

2909 "X-Style", 

2910 FieldValueClass.SINGLE_VALUE, 

2911 known_values=ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

2912 ), 

2913 DctrlKnownField( 

2914 "Homepage", 

2915 FieldValueClass.SINGLE_VALUE, 

2916 custom_field_check=_validate_homepage_field, 

2917 ), 

2918) 

2919 

2920 

2921BINARY_FIELDS = _fields( 

2922 DctrlKnownField( 

2923 "Package", 

2924 FieldValueClass.SINGLE_VALUE, 

2925 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

2926 ), 

2927 DctrlKnownField( 

2928 "Architecture", 

2929 FieldValueClass.SPACE_SEPARATED_LIST, 

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

2931 known_values=allowed_values(dpkg_arch_and_wildcards()), 

2932 ), 

2933 DctrlKnownField( 

2934 "Pre-Depends", 

2935 FieldValueClass.COMMA_SEPARATED_LIST, 

2936 custom_field_check=_dctrl_validate_dep, 

2937 ), 

2938 DctrlKnownField( 

2939 "Depends", 

2940 FieldValueClass.COMMA_SEPARATED_LIST, 

2941 custom_field_check=_dctrl_validate_dep, 

2942 ), 

2943 DctrlKnownField( 

2944 "Recommends", 

2945 FieldValueClass.COMMA_SEPARATED_LIST, 

2946 custom_field_check=_dctrl_validate_dep, 

2947 ), 

2948 DctrlKnownField( 

2949 "Suggests", 

2950 FieldValueClass.COMMA_SEPARATED_LIST, 

2951 custom_field_check=_dctrl_validate_dep, 

2952 ), 

2953 DctrlKnownField( 

2954 "Enhances", 

2955 FieldValueClass.COMMA_SEPARATED_LIST, 

2956 custom_field_check=_dctrl_validate_dep, 

2957 ), 

2958 DctrlRelationshipKnownField( 

2959 "Provides", 

2960 FieldValueClass.COMMA_SEPARATED_LIST, 

2961 custom_field_check=_dctrl_validate_dep, 

2962 supports_or_relation=False, 

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

2964 ), 

2965 DctrlRelationshipKnownField( 

2966 "Conflicts", 

2967 FieldValueClass.COMMA_SEPARATED_LIST, 

2968 custom_field_check=_dctrl_validate_dep, 

2969 supports_or_relation=False, 

2970 ), 

2971 DctrlRelationshipKnownField( 

2972 "Breaks", 

2973 FieldValueClass.COMMA_SEPARATED_LIST, 

2974 custom_field_check=_dctrl_validate_dep, 

2975 supports_or_relation=False, 

2976 ), 

2977 DctrlRelationshipKnownField( 

2978 "Replaces", 

2979 FieldValueClass.COMMA_SEPARATED_LIST, 

2980 custom_field_check=_dctrl_validate_dep, 

2981 ), 

2982 DctrlKnownField( 

2983 "Build-Profiles", 

2984 FieldValueClass.BUILD_PROFILES_LIST, 

2985 ), 

2986 DctrlKnownField( 

2987 "Section", 

2988 FieldValueClass.SINGLE_VALUE, 

2989 known_values=ALL_SECTIONS, 

2990 ), 

2991 DctrlRelationshipKnownField( 

2992 "Built-Using", 

2993 FieldValueClass.COMMA_SEPARATED_LIST, 

2994 custom_field_check=_arch_not_all_only_field_validation, 

2995 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

2996 supports_or_relation=False, 

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

2998 ), 

2999 DctrlRelationshipKnownField( 

3000 "Static-Built-Using", 

3001 FieldValueClass.COMMA_SEPARATED_LIST, 

3002 custom_field_check=_arch_not_all_only_field_validation, 

3003 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3004 supports_or_relation=False, 

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

3006 ), 

3007 DctrlKnownField( 

3008 "Multi-Arch", 

3009 FieldValueClass.SINGLE_VALUE, 

3010 custom_field_check=_dctrl_ma_field_validation, 

3011 known_values=allowed_values( 

3012 ( 

3013 Keyword( 

3014 "same", 

3015 can_complete_keyword_in_stanza=_complete_only_in_arch_dep_pkgs, 

3016 ), 

3017 ), 

3018 ), 

3019 ), 

3020 DctrlKnownField( 

3021 "XB-Installer-Menu-Item", 

3022 FieldValueClass.SINGLE_VALUE, 

3023 can_complete_field_in_stanza=_complete_only_for_udeb_pkgs, 

3024 custom_field_check=_combined_custom_field_check( 

3025 _udeb_only_field_validation, 

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

3027 ), 

3028 ), 

3029 DctrlKnownField( 

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

3031 FieldValueClass.SINGLE_VALUE, 

3032 custom_field_check=_arch_not_all_only_field_validation, 

3033 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3034 ), 

3035 DctrlKnownField( 

3036 "X-Doc-Main-Package", 

3037 FieldValueClass.SINGLE_VALUE, 

3038 custom_field_check=_binary_package_from_same_source, 

3039 ), 

3040 DctrlKnownField( 

3041 "X-Time64-Compat", 

3042 FieldValueClass.SINGLE_VALUE, 

3043 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3044 custom_field_check=_combined_custom_field_check( 

3045 _each_value_match_regex_validation(PKGNAME_REGEX), 

3046 _arch_not_all_only_field_validation, 

3047 ), 

3048 ), 

3049 DctrlKnownField( 

3050 "Description", 

3051 FieldValueClass.FREE_TEXT_FIELD, 

3052 custom_field_check=dctrl_description_validator, 

3053 ), 

3054 DctrlKnownField( 

3055 "XB-Cnf-Visible-Pkgname", 

3056 FieldValueClass.SINGLE_VALUE, 

3057 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

3058 ), 

3059 DctrlKnownField( 

3060 "Homepage", 

3061 FieldValueClass.SINGLE_VALUE, 

3062 show_as_inherited=False, 

3063 custom_field_check=_validate_homepage_field, 

3064 ), 

3065) 

3066_DEP5_HEADER_FIELDS = _fields( 

3067 Deb822KnownField( 

3068 "Format", 

3069 FieldValueClass.SINGLE_VALUE, 

3070 custom_field_check=_use_https_instead_of_http, 

3071 ), 

3072) 

3073_DEP5_FILES_FIELDS = _fields( 

3074 Deb822KnownField( 

3075 "Files", 

3076 FieldValueClass.DEP5_FILE_LIST, 

3077 custom_field_check=_dep5_files_check, 

3078 ), 

3079) 

3080_DEP5_LICENSE_FIELDS = _fields( 

3081 Deb822KnownField( 

3082 "License", 

3083 FieldValueClass.FREE_TEXT_FIELD, 

3084 ), 

3085) 

3086 

3087_DTESTSCTRL_FIELDS = _fields( 

3088 DTestsCtrlKnownField( 

3089 "Architecture", 

3090 FieldValueClass.SPACE_SEPARATED_LIST, 

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

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

3093 ), 

3094) 

3095_DWATCH_HEADER_FIELDS = _fields() 

3096_DWATCH_TEMPLATE_FIELDS = _fields() 

3097_DWATCH_SOURCE_FIELDS = _fields() 

3098 

3099 

3100@dataclasses.dataclass(slots=True) 

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

3102 stanza_type_name: str 

3103 stanza_fields: Mapping[str, F] 

3104 is_substvars_allowed_in_stanza: bool 

3105 

3106 async def stanza_diagnostics( 

3107 self, 

3108 deb822_file: Deb822FileElement, 

3109 stanza: Deb822ParagraphElement, 

3110 stanza_position_in_file: "TEPosition", 

3111 lint_state: LintState, 

3112 *, 

3113 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3114 confusable_with_stanza_name: str | None = None, 

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

3116 ) -> None: 

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

3118 confusable_with_stanza_metadata is None 

3119 ): 

3120 raise ValueError( 

3121 "confusable_with_stanza_name and confusable_with_stanza_metadata must be used together" 

3122 ) 

3123 _, representation_field_range = self.stanza_representation( 

3124 stanza, 

3125 stanza_position_in_file, 

3126 ) 

3127 known_fields = self.stanza_fields 

3128 self.omitted_field_diagnostics( 

3129 lint_state, 

3130 deb822_file, 

3131 stanza, 

3132 stanza_position_in_file, 

3133 inherit_from_stanza=inherit_from_stanza, 

3134 representation_field_range=representation_field_range, 

3135 ) 

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

3137 

3138 async for kvpair_range, kvpair in lint_state.slow_iter( 

3139 with_range_in_continuous_parts( 

3140 stanza.iter_parts(), 

3141 start_relative_to=stanza_position_in_file, 

3142 ), 

3143 yield_every=1, 

3144 ): 

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

3146 continue 

3147 field_name_token = kvpair.field_token 

3148 field_name = field_name_token.text 

3149 field_name_lc = field_name.lower() 

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

3151 field_name = str(field_name) 

3152 normalized_field_name_lc = self.normalize_field_name(field_name_lc) 

3153 known_field = known_fields.get(normalized_field_name_lc) 

3154 field_value = stanza[field_name] 

3155 kvpair_range_te = kvpair.range_in_parent().relative_to( 

3156 stanza_position_in_file 

3157 ) 

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

3159 kvpair_range_te.start_pos 

3160 ) 

3161 field_position_te = field_range.start_pos 

3162 field_name_typo_detected = False 

3163 dup_field_key = ( 

3164 known_field.name 

3165 if known_field is not None 

3166 else normalized_field_name_lc 

3167 ) 

3168 existing_field_range = seen_fields.get(dup_field_key) 

3169 if existing_field_range is not None: 

3170 existing_field_range[3].append(field_range) 

3171 existing_field_range[4].add(field_name) 

3172 else: 

3173 normalized_field_name = self.normalize_field_name(field_name) 

3174 seen_fields[dup_field_key] = ( 

3175 known_field.name if known_field else field_name, 

3176 normalized_field_name, 

3177 field_range, 

3178 [], 

3179 {field_name}, 

3180 ) 

3181 

3182 if known_field is None: 

3183 candidates = detect_possible_typo( 

3184 normalized_field_name_lc, known_fields 

3185 ) 

3186 if candidates: 

3187 known_field = known_fields[candidates[0]] 

3188 field_range = TERange.from_position_and_size( 

3189 field_position_te, kvpair.field_token.size() 

3190 ) 

3191 field_name_typo_detected = True 

3192 lint_state.emit_diagnostic( 

3193 field_range, 

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

3195 "warning", 

3196 "debputy", 

3197 quickfixes=[ 

3198 propose_correct_text_quick_fix(known_fields[m].name) 

3199 for m in candidates 

3200 ], 

3201 ) 

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

3203 lint_state.emit_diagnostic( 

3204 field_range, 

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

3206 "error", 

3207 "Policy 5.1", 

3208 ) 

3209 continue 

3210 if known_field is None: 

3211 known_else_where = confusable_with_stanza_metadata.stanza_fields.get( 

3212 normalized_field_name_lc 

3213 ) 

3214 if known_else_where is not None: 

3215 lint_state.emit_diagnostic( 

3216 field_range, 

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

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

3219 "error", 

3220 known_else_where.missing_field_authority, 

3221 ) 

3222 continue 

3223 await known_field.field_diagnostics( 

3224 deb822_file, 

3225 kvpair, 

3226 stanza, 

3227 stanza_position_in_file, 

3228 kvpair_range_te, 

3229 lint_state, 

3230 field_name_typo_reported=field_name_typo_detected, 

3231 ) 

3232 

3233 inherit_value = ( 

3234 inherit_from_stanza.get(field_name) if inherit_from_stanza else None 

3235 ) 

3236 

3237 if ( 

3238 known_field.inheritable_from_other_stanza 

3239 and inherit_value is not None 

3240 and field_value == inherit_value 

3241 ): 

3242 quick_fix = propose_remove_range_quick_fix( 

3243 proposed_title="Remove redundant definition" 

3244 ) 

3245 lint_state.emit_diagnostic( 

3246 kvpair_range_te, 

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

3248 "informational", 

3249 "debputy", 

3250 quickfixes=[quick_fix], 

3251 ) 

3252 for ( 

3253 field_name, 

3254 normalized_field_name, 

3255 field_range, 

3256 duplicates, 

3257 used_fields, 

3258 ) in seen_fields.values(): 

3259 if not duplicates: 

3260 continue 

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

3262 via_aliases_msg = " (via aliases)" 

3263 else: 

3264 via_aliases_msg = "" 

3265 for dup_range in duplicates: 

3266 lint_state.emit_diagnostic( 

3267 dup_range, 

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

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

3270 "error", 

3271 "Policy 5.1", 

3272 related_information=[ 

3273 lint_state.related_diagnostic_information( 

3274 field_range, 

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

3276 ), 

3277 ], 

3278 ) 

3279 

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

3281 key_lc = key.lower() 

3282 key_norm = normalize_dctrl_field_name(key_lc) 

3283 return self.stanza_fields[key_norm] 

3284 

3285 def __len__(self) -> int: 

3286 return len(self.stanza_fields) 

3287 

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

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

3290 

3291 def omitted_field_diagnostics( 

3292 self, 

3293 lint_state: LintState, 

3294 deb822_file: Deb822FileElement, 

3295 stanza: Deb822ParagraphElement, 

3296 stanza_position: "TEPosition", 

3297 *, 

3298 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3299 representation_field_range: Range | None = None, 

3300 ) -> None: 

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

3302 _, representation_field_range = self.stanza_representation( 

3303 stanza, 

3304 stanza_position, 

3305 ) 

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

3307 if known_field.name in stanza: 

3308 continue 

3309 

3310 known_field.field_omitted_diagnostics( 

3311 deb822_file, 

3312 representation_field_range, 

3313 stanza, 

3314 stanza_position, 

3315 inherit_from_stanza, 

3316 lint_state, 

3317 ) 

3318 

3319 def _paragraph_representation_field( 

3320 self, 

3321 paragraph: Deb822ParagraphElement, 

3322 ) -> Deb822KeyValuePairElement: 

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

3324 

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

3326 return field_name 

3327 

3328 def stanza_representation( 

3329 self, 

3330 stanza: Deb822ParagraphElement, 

3331 stanza_position: TEPosition, 

3332 ) -> tuple[Deb822KeyValuePairElement, TERange]: 

3333 representation_field = self._paragraph_representation_field(stanza) 

3334 representation_field_range = representation_field.range_in_parent().relative_to( 

3335 stanza_position 

3336 ) 

3337 return representation_field, representation_field_range 

3338 

3339 def reformat_stanza( 

3340 self, 

3341 effective_preference: "EffectiveFormattingPreference", 

3342 stanza: Deb822ParagraphElement, 

3343 stanza_range: TERange, 

3344 formatter: FormatterCallback, 

3345 position_codec: LintCapablePositionCodec, 

3346 lines: list[str], 

3347 ) -> Iterable[TextEdit]: 

3348 for field_name in stanza: 

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

3350 if known_field is None: 

3351 continue 

3352 kvpair = stanza.get_kvpair_element(field_name) 

3353 yield from known_field.reformat_field( 

3354 effective_preference, 

3355 stanza_range, 

3356 kvpair, 

3357 formatter, 

3358 position_codec, 

3359 lines, 

3360 ) 

3361 

3362 

3363@dataclasses.dataclass(slots=True) 

3364class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3365 pass 

3366 

3367 

3368@dataclasses.dataclass(slots=True) 

3369class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]): 

3370 

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

3372 return normalize_dctrl_field_name(field_name) 

3373 

3374 

3375@dataclasses.dataclass(slots=True) 

3376class DTestsCtrlStanzaMetadata(StanzaMetadata[DTestsCtrlKnownField]): 

3377 

3378 def omitted_field_diagnostics( 

3379 self, 

3380 lint_state: LintState, 

3381 deb822_file: Deb822FileElement, 

3382 stanza: Deb822ParagraphElement, 

3383 stanza_position: "TEPosition", 

3384 *, 

3385 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3386 representation_field_range: Range | None = None, 

3387 ) -> None: 

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

3389 _, representation_field_range = self.stanza_representation( 

3390 stanza, 

3391 stanza_position, 

3392 ) 

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

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

3395 lint_state.emit_diagnostic( 

3396 representation_field_range, 

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

3398 "error", 

3399 # TODO: Better authority_reference 

3400 auth_ref, 

3401 ) 

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

3403 lint_state.emit_diagnostic( 

3404 representation_field_range, 

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

3406 "error", 

3407 # TODO: Better authority_reference 

3408 auth_ref, 

3409 ) 

3410 

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

3412 # always do the super call. 

3413 super(DTestsCtrlStanzaMetadata, self).omitted_field_diagnostics( 

3414 lint_state, 

3415 deb822_file, 

3416 stanza, 

3417 stanza_position, 

3418 representation_field_range=representation_field_range, 

3419 inherit_from_stanza=inherit_from_stanza, 

3420 ) 

3421 

3422 

3423@dataclasses.dataclass(slots=True) 

3424class DebianWatchStanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3425 

3426 def omitted_field_diagnostics( 

3427 self, 

3428 lint_state: LintState, 

3429 deb822_file: Deb822FileElement, 

3430 stanza: Deb822ParagraphElement, 

3431 stanza_position: "TEPosition", 

3432 *, 

3433 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3434 representation_field_range: Range | None = None, 

3435 ) -> None: 

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

3437 _, representation_field_range = self.stanza_representation( 

3438 stanza, 

3439 stanza_position, 

3440 ) 

3441 

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

3443 self.stanza_type_name != "Header" 

3444 and "Source" not in stanza 

3445 and "Template" not in stanza 

3446 ): 

3447 lint_state.emit_diagnostic( 

3448 representation_field_range, 

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

3450 "error", 

3451 # TODO: Better authority_reference 

3452 "debputy", 

3453 ) 

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

3455 # call until this error is resolved. 

3456 return 

3457 

3458 super(DebianWatchStanzaMetadata, self).omitted_field_diagnostics( 

3459 lint_state, 

3460 deb822_file, 

3461 stanza, 

3462 stanza_position, 

3463 representation_field_range=representation_field_range, 

3464 inherit_from_stanza=inherit_from_stanza, 

3465 ) 

3466 

3467 

3468def lsp_reference_data_dir() -> str: 

3469 return os.path.join( 

3470 os.path.dirname(__file__), 

3471 "data", 

3472 ) 

3473 

3474 

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

3476 

3477 def __init__(self) -> None: 

3478 self._is_initialized = False 

3479 self._data: Deb822ReferenceData | None = None 

3480 

3481 @property 

3482 def reference_data_basename(self) -> str: 

3483 raise NotImplementedError 

3484 

3485 def _new_field( 

3486 self, 

3487 name: str, 

3488 field_value_type: FieldValueClass, 

3489 ) -> F: 

3490 raise NotImplementedError 

3491 

3492 def _reference_data(self) -> Deb822ReferenceData: 

3493 ref = self._data 

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

3495 return ref 

3496 

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

3498 self.reference_data_basename 

3499 ) 

3500 

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

3502 raw = MANIFEST_YAML.load(fd) 

3503 

3504 attr_path = AttributePath.root_path(p) 

3505 try: 

3506 ref = DEB822_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

3507 except ManifestParseException as e: 

3508 raise ValueError( 

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

3510 ) from e 

3511 self._data = ref 

3512 return ref 

3513 

3514 @property 

3515 def is_initialized(self) -> bool: 

3516 return self._is_initialized 

3517 

3518 def ensure_initialized(self) -> None: 

3519 if self.is_initialized: 

3520 return 

3521 # Enables us to use __getitem__ 

3522 self._is_initialized = True 

3523 ref_data = self._reference_data() 

3524 ref_defs = ref_data.get("definitions") 

3525 variables = {} 

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

3527 for ref_variable in ref_variables: 

3528 name = ref_variable["name"] 

3529 fallback = ref_variable["fallback"] 

3530 variables[name] = fallback 

3531 

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

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

3534 return None 

3535 try: 

3536 return template.format(**variables) 

3537 except ValueError as e: 

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

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

3540 

3541 for ref_stanza_type in ref_data["stanza_types"]: 

3542 stanza_name = ref_stanza_type["stanza_name"] 

3543 stanza = self[stanza_name] 

3544 stanza_fields = dict(stanza.stanza_fields) 

3545 stanza.stanza_fields = stanza_fields 

3546 for ref_field in ref_stanza_type["fields"]: 

3547 _resolve_field( 

3548 ref_field, 

3549 stanza_fields, 

3550 self._new_field, 

3551 _resolve_doc, 

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

3553 ) 

3554 

3555 def file_metadata_applies_to_file( 

3556 self, 

3557 deb822_file: Deb822FileElement | None, 

3558 ) -> bool: 

3559 return deb822_file is not None 

3560 

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

3562 return self.guess_stanza_classification_by_idx(stanza_idx) 

3563 

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

3565 raise NotImplementedError 

3566 

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

3568 raise NotImplementedError 

3569 

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

3571 raise NotImplementedError 

3572 

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

3574 try: 

3575 return self[item] 

3576 except KeyError: 

3577 return None 

3578 

3579 def reformat( 

3580 self, 

3581 effective_preference: "EffectiveFormattingPreference", 

3582 deb822_file: Deb822FileElement, 

3583 formatter: FormatterCallback, 

3584 _content: str, 

3585 position_codec: LintCapablePositionCodec, 

3586 lines: list[str], 

3587 ) -> Iterable[TextEdit]: 

3588 stanza_idx = -1 

3589 for token_or_element in deb822_file.iter_parts(): 

3590 if isinstance(token_or_element, Deb822ParagraphElement): 

3591 stanza_range = token_or_element.range_in_parent() 

3592 stanza_idx += 1 

3593 stanza_metadata = self.classify_stanza(token_or_element, stanza_idx) 

3594 yield from stanza_metadata.reformat_stanza( 

3595 effective_preference, 

3596 token_or_element, 

3597 stanza_range, 

3598 formatter, 

3599 position_codec, 

3600 lines, 

3601 ) 

3602 else: 

3603 token_range = token_or_element.range_in_parent() 

3604 yield from trim_end_of_line_whitespace( 

3605 position_codec, 

3606 lines, 

3607 line_range=range( 

3608 token_range.start_pos.line_position, 

3609 token_range.end_pos.line_position, 

3610 ), 

3611 ) 

3612 

3613 

3614_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata( 

3615 "Source", 

3616 SOURCE_FIELDS, 

3617 is_substvars_allowed_in_stanza=False, 

3618) 

3619_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata( 

3620 "Package", 

3621 BINARY_FIELDS, 

3622 is_substvars_allowed_in_stanza=True, 

3623) 

3624 

3625_DEP5_HEADER_STANZA = Dep5StanzaMetadata( 

3626 "Header", 

3627 _DEP5_HEADER_FIELDS, 

3628 is_substvars_allowed_in_stanza=False, 

3629) 

3630_DEP5_FILES_STANZA = Dep5StanzaMetadata( 

3631 "Files", 

3632 _DEP5_FILES_FIELDS, 

3633 is_substvars_allowed_in_stanza=False, 

3634) 

3635_DEP5_LICENSE_STANZA = Dep5StanzaMetadata( 

3636 "License", 

3637 _DEP5_LICENSE_FIELDS, 

3638 is_substvars_allowed_in_stanza=False, 

3639) 

3640 

3641_DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata( 

3642 "Tests", 

3643 _DTESTSCTRL_FIELDS, 

3644 is_substvars_allowed_in_stanza=False, 

3645) 

3646 

3647_WATCH_HEADER_HEADER_STANZA = DebianWatchStanzaMetadata( 

3648 "Header", 

3649 _DWATCH_HEADER_FIELDS, 

3650 is_substvars_allowed_in_stanza=False, 

3651) 

3652_WATCH_SOURCE_STANZA = DebianWatchStanzaMetadata( 

3653 "Source", 

3654 _DWATCH_SOURCE_FIELDS, 

3655 is_substvars_allowed_in_stanza=False, 

3656) 

3657 

3658 

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

3660 

3661 @property 

3662 def reference_data_basename(self) -> str: 

3663 return "debian_copyright_reference_data.yaml" 

3664 

3665 def _new_field( 

3666 self, 

3667 name: str, 

3668 field_value_type: FieldValueClass, 

3669 ) -> F: 

3670 return Deb822KnownField(name, field_value_type) 

3671 

3672 def file_metadata_applies_to_file( 

3673 self, 

3674 deb822_file: Deb822FileElement | None, 

3675 ) -> bool: 

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

3677 return False 

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

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

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

3681 return False 

3682 

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

3684 if part.is_error: 

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

3686 return False 

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

3688 break 

3689 return True 

3690 

3691 def classify_stanza( 

3692 self, 

3693 stanza: Deb822ParagraphElement, 

3694 stanza_idx: int, 

3695 ) -> Dep5StanzaMetadata: 

3696 self.ensure_initialized() 

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

3698 return _DEP5_HEADER_STANZA 

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

3700 if "Files" in stanza: 

3701 return _DEP5_FILES_STANZA 

3702 return _DEP5_LICENSE_STANZA 

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

3704 

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

3706 self.ensure_initialized() 

3707 if stanza_idx == 0: 

3708 return _DEP5_HEADER_STANZA 

3709 if stanza_idx > 0: 

3710 return _DEP5_FILES_STANZA 

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

3712 

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

3714 self.ensure_initialized() 

3715 # Order assumption made in the LSP code. 

3716 yield _DEP5_HEADER_STANZA 

3717 yield _DEP5_FILES_STANZA 

3718 yield _DEP5_LICENSE_STANZA 

3719 

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

3721 self.ensure_initialized() 

3722 if item == "Header": 

3723 return _DEP5_HEADER_STANZA 

3724 if item == "Files": 

3725 return _DEP5_FILES_STANZA 

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

3727 return _DEP5_LICENSE_STANZA 

3728 raise KeyError(item) 

3729 

3730 

3731class DebianWatch5FileMetadata( 

3732 Deb822FileMetadata[DebianWatchStanzaMetadata, Deb822KnownField] 

3733): 

3734 

3735 @property 

3736 def reference_data_basename(self) -> str: 

3737 return "debian_watch_reference_data.yaml" 

3738 

3739 def _new_field( 

3740 self, 

3741 name: str, 

3742 field_value_type: FieldValueClass, 

3743 ) -> F: 

3744 return Deb822KnownField(name, field_value_type) 

3745 

3746 def file_metadata_applies_to_file( 

3747 self, deb822_file: Deb822FileElement | None 

3748 ) -> bool: 

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

3750 return False 

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

3752 

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

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

3755 return False 

3756 

3757 try: 

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

3759 return False 

3760 except (ValueError, IndexError, TypeError): 

3761 return False 

3762 

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

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

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

3766 return False 

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

3768 break 

3769 return True 

3770 

3771 def classify_stanza( 

3772 self, 

3773 stanza: Deb822ParagraphElement, 

3774 stanza_idx: int, 

3775 ) -> DebianWatchStanzaMetadata: 

3776 self.ensure_initialized() 

3777 if stanza_idx == 0: 

3778 return _WATCH_HEADER_HEADER_STANZA 

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

3780 return _WATCH_SOURCE_STANZA 

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

3782 

3783 def guess_stanza_classification_by_idx( 

3784 self, stanza_idx: int 

3785 ) -> DebianWatchStanzaMetadata: 

3786 self.ensure_initialized() 

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

3788 return _WATCH_HEADER_HEADER_STANZA 

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

3790 return _WATCH_SOURCE_STANZA 

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

3792 

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

3794 self.ensure_initialized() 

3795 # Order assumption made in the LSP code. 

3796 yield _WATCH_HEADER_HEADER_STANZA 

3797 yield _WATCH_SOURCE_STANZA 

3798 

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

3800 self.ensure_initialized() 

3801 if item == "Header": 

3802 return _WATCH_HEADER_HEADER_STANZA 

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

3804 return _WATCH_SOURCE_STANZA 

3805 raise KeyError(item) 

3806 

3807 

3808def _resolve_keyword( 

3809 ref_value: StaticValue, 

3810 known_values: dict[str, Keyword], 

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

3812 translation_context: str, 

3813) -> None: 

3814 value_key = ref_value["value"] 

3815 changes = { 

3816 "translation_context": translation_context, 

3817 } 

3818 try: 

3819 known_value = known_values[value_key] 

3820 except KeyError: 

3821 known_value = Keyword(value_key) 

3822 known_values[value_key] = known_value 

3823 else: 

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

3825 raise ValueError( 

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

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

3828 ) 

3829 value_doc = ref_value.get("documentation") 

3830 if value_doc is not None: 

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

3832 changes["long_description"] = resolve_template( 

3833 value_doc.get("long_description") 

3834 ) 

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

3836 changes["is_exclusive"] = is_exclusive 

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

3838 changes["sort_text"] = sort_key 

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

3840 changes["usage_hint"] = usage_hint 

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

3842 known_value = known_value.replace(**changes) 

3843 known_values[value_key] = known_value 

3844 

3845 _expand_aliases( 

3846 known_value, 

3847 known_values, 

3848 operator.attrgetter("value"), 

3849 ref_value.get("aliases"), 

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

3851 ) 

3852 

3853 

3854def _resolve_field( 

3855 ref_field: Deb822Field, 

3856 stanza_fields: dict[str, F], 

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

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

3859 translation_context: str, 

3860) -> None: 

3861 field_name = ref_field["canonical_name"] 

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

3863 doc = ref_field.get("documentation") 

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

3865 norm_field_name = normalize_dctrl_field_name(field_name.lower()) 

3866 

3867 try: 

3868 field = stanza_fields[norm_field_name] 

3869 except KeyError: 

3870 field = field_constructor( 

3871 field_name, 

3872 field_value_type, 

3873 ) 

3874 stanza_fields[norm_field_name] = field 

3875 else: 

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

3877 _error( 

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

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

3880 ) 

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

3882 _error( 

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

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

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

3886 ) 

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

3888 raise ValueError( 

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

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

3891 ) 

3892 

3893 if doc is not None: 

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

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

3896 

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

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

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

3900 field.deprecated_with_no_replacement = ref_field.get( 

3901 "is_obsolete_without_replacement", False 

3902 ) 

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

3904 field.translation_context = translation_context 

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

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

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

3908 field.unknown_value_severity = ( 

3909 None if unknown_value_severity == "none" else unknown_value_severity 

3910 ) 

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

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

3913 field.is_substvars_disabled_even_if_allowed_by_stanza = not ref_field.get( 

3914 "supports_substvars", 

3915 True, 

3916 ) 

3917 field.inheritable_from_other_stanza = ref_field.get( 

3918 "inheritable_from_other_stanza", 

3919 False, 

3920 ) 

3921 

3922 known_values = field.known_values 

3923 if known_values is None: 

3924 known_values = {} 

3925 else: 

3926 known_values = dict(known_values) 

3927 

3928 for ref_value in ref_values: 

3929 _resolve_keyword(ref_value, known_values, resolve_template, translation_context) 

3930 

3931 if known_values: 

3932 field.known_values = known_values 

3933 

3934 _expand_aliases( 

3935 field, 

3936 stanza_fields, 

3937 operator.attrgetter("name"), 

3938 ref_field.get("aliases"), 

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

3940 ) 

3941 

3942 

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

3944 

3945 

3946def _expand_aliases( 

3947 item: A, 

3948 item_container: dict[str, A], 

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

3950 aliases_ref: list[Alias] | None, 

3951 doc_template: str, 

3952) -> None: 

3953 if aliases_ref is None: 

3954 return 

3955 name = canonical_name_resolver(item) 

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

3957 for alias_ref in aliases_ref: 

3958 alias_name = alias_ref["alias"] 

3959 alias_doc = item.long_description 

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

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

3962 if alias_doc: 

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

3964 else: 

3965 alias_doc = doc_suffix 

3966 alias_field = item.replace( 

3967 long_description=alias_doc, 

3968 is_alias_of=name, 

3969 is_completion_suggestion=is_completion_suggestion, 

3970 ) 

3971 alias_key = alias_name.lower() 

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

3973 existing_name = canonical_name_resolver(item_container[alias_key]) 

3974 assert ( 

3975 existing_name is not None 

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

3977 raise ValueError( 

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

3979 ) 

3980 item_container[alias_key] = alias_field 

3981 

3982 

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

3984 

3985 @property 

3986 def reference_data_basename(self) -> str: 

3987 return "debian_control_reference_data.yaml" 

3988 

3989 def _new_field( 

3990 self, 

3991 name: str, 

3992 field_value_type: FieldValueClass, 

3993 ) -> F: 

3994 return DctrlKnownField(name, field_value_type) 

3995 

3996 def guess_stanza_classification_by_idx( 

3997 self, 

3998 stanza_idx: int, 

3999 ) -> DctrlStanzaMetadata: 

4000 self.ensure_initialized() 

4001 if stanza_idx == 0: 

4002 return _DCTRL_SOURCE_STANZA 

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

4004 return _DCTRL_PACKAGE_STANZA 

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

4006 

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

4008 self.ensure_initialized() 

4009 # Order assumption made in the LSP code. 

4010 yield _DCTRL_SOURCE_STANZA 

4011 yield _DCTRL_PACKAGE_STANZA 

4012 

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

4014 self.ensure_initialized() 

4015 if item == "Source": 

4016 return _DCTRL_SOURCE_STANZA 

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

4018 return _DCTRL_PACKAGE_STANZA 

4019 raise KeyError(item) 

4020 

4021 def reformat( 

4022 self, 

4023 effective_preference: "EffectiveFormattingPreference", 

4024 deb822_file: Deb822FileElement, 

4025 formatter: FormatterCallback, 

4026 content: str, 

4027 position_codec: LintCapablePositionCodec, 

4028 lines: list[str], 

4029 ) -> Iterable[TextEdit]: 

4030 edits = list( 

4031 super().reformat( 

4032 effective_preference, 

4033 deb822_file, 

4034 formatter, 

4035 content, 

4036 position_codec, 

4037 lines, 

4038 ) 

4039 ) 

4040 

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

4042 not effective_preference.deb822_normalize_stanza_order 

4043 or deb822_file.find_first_error_element() is not None 

4044 ): 

4045 return edits 

4046 names = [] 

4047 for idx, stanza in enumerate(deb822_file): 

4048 if idx < 2: 

4049 continue 

4050 name = stanza.get("Package") 

4051 if name is None: 

4052 return edits 

4053 names.append(name) 

4054 

4055 reordered = sorted(names) 

4056 if names == reordered: 

4057 return edits 

4058 

4059 if edits: 

4060 content = apply_text_edits(content, lines, edits) 

4061 lines = content.splitlines(keepends=True) 

4062 deb822_file = parse_deb822_file( 

4063 lines, 

4064 accept_files_with_duplicated_fields=True, 

4065 accept_files_with_error_tokens=True, 

4066 ) 

4067 

4068 stanzas = list(deb822_file) 

4069 reordered_stanza = stanzas[:2] + sorted( 

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

4071 ) 

4072 bits = [] 

4073 stanza_idx = 0 

4074 for token_or_element in deb822_file.iter_parts(): 

4075 if isinstance(token_or_element, Deb822ParagraphElement): 

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

4077 stanza_idx += 1 

4078 else: 

4079 bits.append(token_or_element.convert_to_text()) 

4080 

4081 new_content = "".join(bits) 

4082 

4083 return [ 

4084 TextEdit( 

4085 Range( 

4086 Position(0, 0), 

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

4088 ), 

4089 new_content, 

4090 ) 

4091 ] 

4092 

4093 

4094class DTestsCtrlFileMetadata( 

4095 Deb822FileMetadata[DTestsCtrlStanzaMetadata, DTestsCtrlKnownField] 

4096): 

4097 

4098 @property 

4099 def reference_data_basename(self) -> str: 

4100 return "debian_tests_control_reference_data.yaml" 

4101 

4102 def _new_field( 

4103 self, 

4104 name: str, 

4105 field_value_type: FieldValueClass, 

4106 ) -> F: 

4107 return DTestsCtrlKnownField(name, field_value_type) 

4108 

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

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

4111 self.ensure_initialized() 

4112 return _DTESTSCTRL_STANZA 

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

4114 

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

4116 self.ensure_initialized() 

4117 yield _DTESTSCTRL_STANZA 

4118 

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

4120 self.ensure_initialized() 

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

4122 return _DTESTSCTRL_STANZA 

4123 raise KeyError(item) 

4124 

4125 

4126TRANSLATABLE_DEB822_FILE_METADATA: Sequence[ 

4127 Callable[[], Deb822FileMetadata[Any, Any]] 

4128] = [ 

4129 DctrlFileMetadata, 

4130 Dep5FileMetadata, 

4131 DTestsCtrlFileMetadata, 

4132]