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-04-19 20:37 +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 This is an architecture-dependent package, and needs to be 

305 compiled for each and every architecture. 

306 

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

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

309 dpkg. 

310 """), 

311 ) 

312 yield Keyword( 

313 "all", 

314 is_exclusive=True, 

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

316 long_description=textwrap.dedent("""\ 

317 The package is an architecture independent package. This is 

318 typically appropriate for packages containing only scripts, 

319 data or documentation. 

320 

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

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

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

324 """), 

325 ) 

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

327 yield arch_name 

328 if allow_negations: 

329 yield f"!{arch_name}" 

330 cpu_wc = "any-" + quad_tuple.cpu_name 

331 os_wc = quad_tuple.os_name + "-any" 

332 if cpu_wc not in wildcards: 

333 yield cpu_wc 

334 if allow_negations: 

335 yield f"!{cpu_wc}" 

336 wildcards.add(cpu_wc) 

337 if os_wc not in wildcards: 

338 yield os_wc 

339 if allow_negations: 

340 yield f"!{os_wc}" 

341 wildcards.add(os_wc) 

342 # Add the remaining wildcards 

343 

344 

345@functools.lru_cache 

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

347 dpkg_arch_table = DpkgArchTable.load_arch_table() 

348 return frozenset( 

349 all_architectures_and_wildcards( 

350 dpkg_arch_table._arch2table, 

351 allow_negations=allow_negations, 

352 ) 

353 ) 

354 

355 

356def extract_first_value_and_position( 

357 kvpair: Deb822KeyValuePairElement, 

358 stanza_pos: "TEPosition", 

359 *, 

360 interpretation: Interpretation[ 

361 Deb822ParsedTokenList[Any, Any] 

362 ] = LIST_SPACE_SEPARATED_INTERPRETATION, 

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

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

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

366 kvpair_pos 

367 ) 

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

369 v = value_ref.value 

370 section_value_loc = value_ref.locatable 

371 value_range_te = section_value_loc.range_in_parent().relative_to( 

372 value_element_pos 

373 ) 

374 return v, value_range_te 

375 return None, None 

376 

377 

378def _sv_field_validation( 

379 known_field: "F", 

380 _deb822_file: Deb822FileElement, 

381 kvpair: Deb822KeyValuePairElement, 

382 _kvpair_range: "TERange", 

383 _field_name_range_te: "TERange", 

384 _stanza: Deb822ParagraphElement, 

385 stanza_position: "TEPosition", 

386 lint_state: LintState, 

387) -> None: 

388 sv_value, sv_value_range = extract_first_value_and_position( 

389 kvpair, 

390 stanza_position, 

391 ) 

392 m = _RE_SV.fullmatch(sv_value) 

393 if m is None: 

394 lint_state.emit_diagnostic( 

395 sv_value_range, 

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

397 "warning", 

398 known_field.unknown_value_authority, 

399 ) 

400 return 

401 

402 sv_version = Version(sv_value) 

403 if sv_version < CURRENT_STANDARDS_VERSION: 

404 lint_state.emit_diagnostic( 

405 sv_value_range, 

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

407 "informational", 

408 known_field.unknown_value_authority, 

409 ) 

410 return 

411 extra = m.group(2) 

412 if extra: 

413 extra_len = lint_state.position_codec.client_num_units(extra) 

414 lint_state.emit_diagnostic( 

415 TERange.between( 

416 TEPosition( 

417 sv_value_range.end_pos.line_position, 

418 sv_value_range.end_pos.cursor_position - extra_len, 

419 ), 

420 sv_value_range.end_pos, 

421 ), 

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

423 "informational", 

424 known_field.unknown_value_authority, 

425 quickfixes=[ 

426 propose_remove_range_quick_fix( 

427 proposed_title="Remove unnecessary version part" 

428 ) 

429 ], 

430 ) 

431 

432 

433def _dctrl_ma_field_validation( 

434 _known_field: "F", 

435 _deb822_file: Deb822FileElement, 

436 _kvpair: Deb822KeyValuePairElement, 

437 _kvpair_range: "TERange", 

438 _field_name_range: "TERange", 

439 stanza: Deb822ParagraphElement, 

440 stanza_position: "TEPosition", 

441 lint_state: LintState, 

442) -> None: 

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

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

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

446 ma_value, ma_value_range = extract_first_value_and_position( 

447 ma_kvpair, 

448 stanza_position, 

449 ) 

450 if ma_value == "same": 

451 lint_state.emit_diagnostic( 

452 ma_value_range, 

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

454 "error", 

455 "debputy", 

456 ) 

457 

458 

459def _udeb_only_field_validation( 

460 known_field: "F", 

461 _deb822_file: Deb822FileElement, 

462 _kvpair: Deb822KeyValuePairElement, 

463 _kvpair_range: "TERange", 

464 field_name_range: "TERange", 

465 stanza: Deb822ParagraphElement, 

466 _stanza_position: "TEPosition", 

467 lint_state: LintState, 

468) -> None: 

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

470 if package_type != "udeb": 

471 lint_state.emit_diagnostic( 

472 field_name_range, 

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

474 "warning", 

475 "debputy", 

476 ) 

477 

478 

479def _complete_only_in_arch_dep_pkgs( 

480 stanza_parts: Iterable[Deb822ParagraphElement], 

481) -> bool: 

482 for stanza in stanza_parts: 

483 arch = stanza.get("Architecture") 

484 if arch is None: 

485 continue 

486 archs = arch.split() 

487 return "all" not in archs 

488 return False 

489 

490 

491def _complete_only_for_udeb_pkgs( 

492 stanza_parts: Iterable[Deb822ParagraphElement], 

493) -> bool: 

494 for stanza in stanza_parts: 

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

496 pkg_type = stanza.get(option) 

497 if pkg_type is not None: 

498 return pkg_type == "udeb" 

499 return False 

500 

501 

502def _arch_not_all_only_field_validation( 

503 known_field: "F", 

504 _deb822_file: Deb822FileElement, 

505 _kvpair: Deb822KeyValuePairElement, 

506 _kvpair_range_te: "TERange", 

507 field_name_range_te: "TERange", 

508 stanza: Deb822ParagraphElement, 

509 _stanza_position: "TEPosition", 

510 lint_state: LintState, 

511) -> None: 

512 architecture = stanza.get("Architecture") 

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

514 lint_state.emit_diagnostic( 

515 field_name_range_te, 

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

517 "warning", 

518 "debputy", 

519 ) 

520 

521 

522def _binary_package_from_same_source( 

523 known_field: "F", 

524 _deb822_file: Deb822FileElement, 

525 _kvpair: Deb822KeyValuePairElement, 

526 kvpair_range: "TERange", 

527 _field_name_range: "TERange", 

528 stanza: Deb822ParagraphElement, 

529 stanza_position: "TEPosition", 

530 lint_state: LintState, 

531) -> None: 

532 doc_main_package_kvpair = stanza.get_kvpair_element( 

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

534 ) 

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

536 lint_state.emit_diagnostic( 

537 kvpair_range, 

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

539 "warning", 

540 "debputy", 

541 quickfixes=[propose_remove_range_quick_fix()], 

542 ) 

543 return 

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

545 doc_main_package, value_range = extract_first_value_and_position( 

546 doc_main_package_kvpair, 

547 stanza_position, 

548 ) 

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

550 return 

551 lint_state.emit_diagnostic( 

552 value_range, 

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

554 "error", 

555 "debputy", 

556 quickfixes=[ 

557 propose_correct_text_quick_fix(name) 

558 for name in lint_state.binary_packages 

559 ], 

560 ) 

561 

562 

563def _single_line_span_range_relative_to_pos( 

564 span: tuple[int, int], 

565 relative_to: "TEPosition", 

566) -> Range: 

567 return TERange( 

568 TEPosition( 

569 relative_to.line_position, 

570 relative_to.cursor_position + span[0], 

571 ), 

572 TEPosition( 

573 relative_to.line_position, 

574 relative_to.cursor_position + span[1], 

575 ), 

576 ) 

577 

578 

579def _check_extended_description_line( 

580 description_value_line: Deb822ValueLineElement, 

581 description_line_range_te: "TERange", 

582 package: str | None, 

583 lint_state: LintState, 

584) -> None: 

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

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

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

588 return 

589 description_line_with_leading_space = ( 

590 description_value_line.convert_to_text().rstrip() 

591 ) 

592 try: 

593 idx = description_line_with_leading_space.index( 

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

595 ) 

596 except ValueError: 

597 pass 

598 else: 

599 template_span = idx, idx + len( 

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

601 ) 

602 lint_state.emit_diagnostic( 

603 _single_line_span_range_relative_to_pos( 

604 template_span, 

605 description_line_range_te.start_pos, 

606 ), 

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

608 "error", 

609 "debputy", 

610 ) 

611 if len(description_line_with_leading_space) > 80: 

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

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

614 # 

615 # See also debputy#122 

616 span = 80, len(description_line_with_leading_space) 

617 lint_state.emit_diagnostic( 

618 _single_line_span_range_relative_to_pos( 

619 span, 

620 description_line_range_te.start_pos, 

621 ), 

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

623 "warning", 

624 "debputy", 

625 ) 

626 

627 

628def _check_synopsis( 

629 synopsis_value_line: Deb822ValueLineElement, 

630 synopsis_range_te: "TERange", 

631 field_name_range_te: "TERange", 

632 package: str | None, 

633 lint_state: LintState, 

634) -> None: 

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

636 assert synopsis_value_line.comment_element is None 

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

638 if not synopsis_text_with_leading_space: 

639 lint_state.emit_diagnostic( 

640 field_name_range_te, 

641 "Package synopsis is missing", 

642 "warning", 

643 "debputy", 

644 ) 

645 return 

646 synopsis_text_trimmed = synopsis_text_with_leading_space.lstrip() 

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

648 starts_with_article = _RE_SYNOPSIS_STARTS_WITH_ARTICLE.search( 

649 synopsis_text_with_leading_space 

650 ) 

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

652 if starts_with_article: 

653 lint_state.emit_diagnostic( 

654 _single_line_span_range_relative_to_pos( 

655 starts_with_article.span(1), 

656 synopsis_range_te.start_pos, 

657 ), 

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

659 "warning", 

660 "DevRef 6.2.2", 

661 ) 

662 if len(synopsis_text_trimmed) >= 80: 

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

664 span = synopsis_offset + 79, len(synopsis_text_with_leading_space) 

665 lint_state.emit_diagnostic( 

666 _single_line_span_range_relative_to_pos( 

667 span, 

668 synopsis_range_te.start_pos, 

669 ), 

670 "Package synopsis is too long.", 

671 "warning", 

672 "Policy 3.4.1", 

673 ) 

674 if template_match := _RE_SYNOPSIS_IS_TEMPLATE.match( 

675 synopsis_text_with_leading_space 

676 ): 

677 lint_state.emit_diagnostic( 

678 _single_line_span_range_relative_to_pos( 

679 template_match.span(1), 

680 synopsis_range_te.start_pos, 

681 ), 

682 "Package synopsis is a placeholder", 

683 "warning", 

684 "debputy", 

685 ) 

686 elif too_short_match := _RE_SYNOPSIS_IS_TOO_SHORT.match( 

687 synopsis_text_with_leading_space 

688 ): 

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

690 lint_state.emit_diagnostic( 

691 _single_line_span_range_relative_to_pos( 

692 too_short_match.span(1), 

693 synopsis_range_te.start_pos, 

694 ), 

695 "Package synopsis is too short", 

696 "warning", 

697 "debputy", 

698 ) 

699 

700 

701def dctrl_description_validator( 

702 _known_field: "F", 

703 _deb822_file: Deb822FileElement, 

704 kvpair: Deb822KeyValuePairElement, 

705 kvpair_range_te: "TERange", 

706 _field_name_range: "TERange", 

707 stanza: Deb822ParagraphElement, 

708 _stanza_position: "TEPosition", 

709 lint_state: LintState, 

710) -> None: 

711 value_lines = kvpair.value_element.value_lines 

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

713 return 

714 package = stanza.get("Package") 

715 synopsis_value_line = value_lines[0] 

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

717 kvpair_range_te.start_pos 

718 ) 

719 synopsis_line_range_te = synopsis_value_line.range_in_parent().relative_to( 

720 value_range_te.start_pos 

721 ) 

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

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

724 kvpair_range_te.start_pos 

725 ) 

726 _check_synopsis( 

727 synopsis_value_line, 

728 synopsis_line_range_te, 

729 field_name_range_te, 

730 package, 

731 lint_state, 

732 ) 

733 description_lines = value_lines[1:] 

734 else: 

735 description_lines = value_lines 

736 for description_line in description_lines: 

737 description_line_range_te = description_line.range_in_parent().relative_to( 

738 value_range_te.start_pos 

739 ) 

740 _check_extended_description_line( 

741 description_line, 

742 description_line_range_te, 

743 package, 

744 lint_state, 

745 ) 

746 

747 

748def _has_packaging_expected_file( 

749 name: str, 

750 msg: str, 

751 severity: LintSeverity = "error", 

752) -> CustomFieldCheck: 

753 

754 def _impl( 

755 _known_field: "F", 

756 _deb822_file: Deb822FileElement, 

757 _kvpair: Deb822KeyValuePairElement, 

758 kvpair_range_te: "TERange", 

759 _field_name_range_te: "TERange", 

760 _stanza: Deb822ParagraphElement, 

761 _stanza_position: "TEPosition", 

762 lint_state: LintState, 

763 ) -> None: 

764 debian_dir = lint_state.debian_dir 

765 if debian_dir is None: 

766 return 

767 cpy = debian_dir.lookup(name) 

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

769 lint_state.emit_diagnostic( 

770 kvpair_range_te, 

771 msg, 

772 severity, 

773 "debputy", 

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

775 ) 

776 

777 return _impl 

778 

779 

780_check_missing_debian_rules = _has_packaging_expected_file( 

781 "rules", 

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

783) 

784 

785 

786def _has_build_instructions( 

787 known_field: "F", 

788 deb822_file: Deb822FileElement, 

789 kvpair: Deb822KeyValuePairElement, 

790 kvpair_range_te: "TERange", 

791 field_name_range_te: "TERange", 

792 stanza: Deb822ParagraphElement, 

793 stanza_position: "TEPosition", 

794 lint_state: LintState, 

795) -> None: 

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

797 return 

798 

799 _check_missing_debian_rules( 

800 known_field, 

801 deb822_file, 

802 kvpair, 

803 kvpair_range_te, 

804 field_name_range_te, 

805 stanza, 

806 stanza_position, 

807 lint_state, 

808 ) 

809 

810 

811def _canonical_maintainer_name( 

812 known_field: "F", 

813 _deb822_file: Deb822FileElement, 

814 kvpair: Deb822KeyValuePairElement, 

815 kvpair_range_te: "TERange", 

816 _field_name_range_te: "TERange", 

817 _stanza: Deb822ParagraphElement, 

818 _stanza_position: "TEPosition", 

819 lint_state: LintState, 

820) -> None: 

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

822 kvpair_range_te.start_pos 

823 ) 

824 try: 

825 interpreted_value = kvpair.interpret_as( 

826 known_field.field_value_class.interpreter() 

827 ) 

828 except ValueError: 

829 return 

830 

831 for part in interpreted_value.iter_parts(): 

832 if part.is_separator or part.is_whitespace or part.is_whitespace: 

833 continue 

834 name_and_email = part.convert_to_text() 

835 try: 

836 email_start = name_and_email.rindex("<") 

837 email_end = name_and_email.rindex(">") 

838 email = name_and_email[email_start + 1 : email_end] 

839 except IndexError: 

840 continue 

841 

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

843 if pref is None or not pref.canonical_name: 

844 continue 

845 

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

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

848 continue 

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

850 lint_state.emit_diagnostic( 

851 value_range_te, 

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

853 "informational", 

854 "debputy", 

855 quickfixes=[propose_correct_text_quick_fix(expected)], 

856 ) 

857 

858 

859def _maintainer_field_validator( 

860 known_field: "F", 

861 _deb822_file: Deb822FileElement, 

862 kvpair: Deb822KeyValuePairElement, 

863 kvpair_range_te: "TERange", 

864 _field_name_range_te: "TERange", 

865 _stanza: Deb822ParagraphElement, 

866 _stanza_position: "TEPosition", 

867 lint_state: LintState, 

868) -> None: 

869 

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

871 kvpair_range_te.start_pos 

872 ) 

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

874 for part in interpreted_value.iter_parts(): 

875 if not part.is_separator: 

876 continue 

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

878 severity = known_field.unknown_value_severity 

879 assert severity is not None 

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

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

882 lint_state.emit_diagnostic( 

883 value_range_te, 

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

885 severity, 

886 known_field.unknown_value_authority, 

887 ) 

888 

889 

890def _use_https_instead_of_http( 

891 known_field: "F", 

892 _deb822_file: Deb822FileElement, 

893 kvpair: Deb822KeyValuePairElement, 

894 kvpair_range_te: "TERange", 

895 _field_name_range_te: "TERange", 

896 _stanza: Deb822ParagraphElement, 

897 _stanza_position: "TEPosition", 

898 lint_state: LintState, 

899) -> None: 

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

901 kvpair_range_te.start_pos 

902 ) 

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

904 for part in interpreted_value.iter_parts(): 

905 value = part.convert_to_text() 

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

907 continue 

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

909 problem_range_te = TERange.between( 

910 value_range_te.start_pos, 

911 TEPosition( 

912 value_range_te.start_pos.line_position, 

913 value_range_te.start_pos.cursor_position + 7, 

914 ), 

915 ) 

916 lint_state.emit_diagnostic( 

917 problem_range_te, 

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

919 "warning", 

920 "debputy", 

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

922 ) 

923 

924 

925def _each_value_match_regex_validation( 

926 regex: re.Pattern, 

927 *, 

928 diagnostic_severity: LintSeverity = "error", 

929 authority_reference: str | None = None, 

930) -> CustomFieldCheck: 

931 

932 def _validator( 

933 known_field: "F", 

934 _deb822_file: Deb822FileElement, 

935 kvpair: Deb822KeyValuePairElement, 

936 kvpair_range_te: "TERange", 

937 _field_name_range_te: "TERange", 

938 _stanza: Deb822ParagraphElement, 

939 _stanza_position: "TEPosition", 

940 lint_state: LintState, 

941 ) -> None: 

942 nonlocal authority_reference 

943 interpreter = known_field.field_value_class.interpreter() 

944 if interpreter is None: 

945 raise AssertionError( 

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

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

948 ) 

949 auth_ref = ( 

950 authority_reference 

951 if authority_reference is not None 

952 else known_field.unknown_value_authority 

953 ) 

954 

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

956 kvpair_range_te.start_pos 

957 ) 

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

959 v = value_ref.value 

960 m = regex.fullmatch(v) 

961 if m is not None: 

962 continue 

963 

964 if "${" in v: 

965 # Ignore substvars 

966 continue 

967 

968 section_value_loc = value_ref.locatable 

969 value_range_te = section_value_loc.range_in_parent().relative_to( 

970 value_element_pos 

971 ) 

972 lint_state.emit_diagnostic( 

973 value_range_te, 

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

975 diagnostic_severity, 

976 auth_ref, 

977 ) 

978 

979 return _validator 

980 

981 

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

983_DEP_RELATION_CLAUSE = re.compile( 

984 r""" 

985 ^ 

986 \s* 

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

988 \s* 

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

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

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

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

993 $ 

994""", 

995 re.VERBOSE | re.MULTILINE, 

996) 

997 

998 

999def _span_to_te_range( 

1000 text: str, 

1001 start_pos: int, 

1002 end_pos: int, 

1003) -> TERange: 

1004 prefix = text[0:start_pos] 

1005 prefix_plus_text = text[0:end_pos] 

1006 

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

1008 if start_line: 

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

1010 # +1 to skip past the newline 

1011 start_cursor_pos = start_pos - (start_newline_offset + 1) 

1012 else: 

1013 start_cursor_pos = start_pos 

1014 

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

1016 if end_line == start_line: 

1017 end_cursor_pos = start_cursor_pos + (end_pos - start_pos) 

1018 else: 

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

1020 end_cursor_pos = end_pos - (end_newline_offset + 1) 

1021 

1022 return TERange( 

1023 TEPosition( 

1024 start_line, 

1025 start_cursor_pos, 

1026 ), 

1027 TEPosition( 

1028 end_line, 

1029 end_cursor_pos, 

1030 ), 

1031 ) 

1032 

1033 

1034def _split_w_spans( 

1035 v: str, 

1036 sep: str, 

1037 *, 

1038 offset: int = 0, 

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

1040 separator_size = len(sep) 

1041 parts = v.split(sep) 

1042 for part in parts: 

1043 size = len(part) 

1044 end_offset = offset + size 

1045 yield part, offset, end_offset 

1046 offset = end_offset + separator_size 

1047 

1048 

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

1050 

1051 

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

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

1054 

1055 

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

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

1058 if not newlines: 

1059 return TEPosition( 

1060 newlines, 

1061 len(text), 

1062 ) 

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

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

1065 return TEPosition( 

1066 newlines, 

1067 line_offset, 

1068 ) 

1069 

1070 

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

1072class Relation: 

1073 name: str 

1074 arch_qual: str | None = None 

1075 version_operator: str | None = None 

1076 version: str | None = None 

1077 arch_restriction: str | None = None 

1078 build_profile_restriction: str | None = None 

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

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

1081 # an example). 

1082 content_display_offset: int = -1 

1083 content_display_end_offset: int = -1 

1084 

1085 

1086def relation_key_variations( 

1087 relation: Relation, 

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

1089 operator_variants = ( 

1090 [relation.version_operator, None] 

1091 if relation.version_operator is not None 

1092 else [None] 

1093 ) 

1094 arch_qual_variants = ( 

1095 [relation.arch_qual, None] 

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

1097 else [None] 

1098 ) 

1099 for arch_qual, version_operator in itertools.product( 

1100 arch_qual_variants, 

1101 operator_variants, 

1102 ): 

1103 yield relation.name, arch_qual, version_operator 

1104 

1105 

1106def dup_check_relations( 

1107 known_field: "F", 

1108 relations: Sequence[Relation], 

1109 raw_value_masked_comments: str, 

1110 value_element_pos: "TEPosition", 

1111 lint_state: LintState, 

1112) -> None: 

1113 overlap_table = {} 

1114 for relation in relations: 

1115 version_operator = relation.version_operator 

1116 arch_qual = relation.arch_qual 

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

1118 continue 

1119 

1120 for relation_key in relation_key_variations(relation): 

1121 prev_relation = overlap_table.get(relation_key) 

1122 if prev_relation is None: 

1123 overlap_table[relation_key] = relation 

1124 else: 

1125 prev_version_operator = prev_relation.version_operator 

1126 

1127 if ( 

1128 prev_version_operator 

1129 and version_operator 

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

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

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

1133 ): 

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

1135 continue 

1136 

1137 prev_arch_qual = prev_relation.arch_qual 

1138 if ( 

1139 arch_qual != prev_arch_qual 

1140 and prev_arch_qual != "any" 

1141 and arch_qual != "any" 

1142 ): 

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

1144 # 

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

1146 continue 

1147 

1148 if ( 

1149 known_field.name == "Provides" 

1150 and version_operator == "=" 

1151 and prev_version_operator == version_operator 

1152 and relation.version != prev_relation.version 

1153 ): 

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

1155 continue 

1156 

1157 orig_relation_range = TERange( 

1158 _text_to_te_position( 

1159 raw_value_masked_comments[ 

1160 : prev_relation.content_display_offset 

1161 ] 

1162 ), 

1163 _text_to_te_position( 

1164 raw_value_masked_comments[ 

1165 : prev_relation.content_display_end_offset 

1166 ] 

1167 ), 

1168 ).relative_to(value_element_pos) 

1169 

1170 duplicate_relation_range = TERange( 

1171 _text_to_te_position( 

1172 raw_value_masked_comments[: relation.content_display_offset] 

1173 ), 

1174 _text_to_te_position( 

1175 raw_value_masked_comments[: relation.content_display_end_offset] 

1176 ), 

1177 ).relative_to(value_element_pos) 

1178 

1179 lint_state.emit_diagnostic( 

1180 duplicate_relation_range, 

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

1182 "warning", 

1183 known_field.unknown_value_authority, 

1184 related_information=[ 

1185 lint_state.related_diagnostic_information( 

1186 orig_relation_range, 

1187 "The previous definition", 

1188 ), 

1189 ], 

1190 ) 

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

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

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

1194 break 

1195 

1196 

1197def _dctrl_check_dep_version_operator( 

1198 known_field: "F", 

1199 version_operator: str, 

1200 version_operator_span: tuple[int, int], 

1201 version_operators: frozenset[str], 

1202 raw_value_masked_comments: str, 

1203 offset: int, 

1204 value_element_pos: "TEPosition", 

1205 lint_state: LintState, 

1206) -> bool: 

1207 if ( 

1208 version_operators 

1209 and version_operator is not None 

1210 and version_operator not in version_operators 

1211 ): 

1212 v_start_offset = offset + version_operator_span[0] 

1213 v_end_offset = offset + version_operator_span[1] 

1214 version_problem_range_te = TERange( 

1215 _text_to_te_position(raw_value_masked_comments[:v_start_offset]), 

1216 _text_to_te_position(raw_value_masked_comments[:v_end_offset]), 

1217 ).relative_to(value_element_pos) 

1218 

1219 sorted_version_operators = sorted(version_operators) 

1220 

1221 excluding_equal = f"{version_operator}{version_operator}" 

1222 including_equal = f"{version_operator}=" 

1223 

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

1225 excluding_equal in version_operators or including_equal in version_operators 

1226 ): 

1227 lint_state.emit_diagnostic( 

1228 version_problem_range_te, 

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

1230 "error", 

1231 "Policy 7.1", 

1232 quickfixes=[ 

1233 propose_correct_text_quick_fix(n) 

1234 for n in (excluding_equal, including_equal) 

1235 if not version_operators or n in version_operators 

1236 ], 

1237 ) 

1238 else: 

1239 lint_state.emit_diagnostic( 

1240 version_problem_range_te, 

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

1242 "error", 

1243 known_field.unknown_value_authority, 

1244 quickfixes=[ 

1245 propose_correct_text_quick_fix(n) for n in sorted_version_operators 

1246 ], 

1247 ) 

1248 return True 

1249 return False 

1250 

1251 

1252def _dctrl_validate_dep( 

1253 known_field: "DF", 

1254 _deb822_file: Deb822FileElement, 

1255 kvpair: Deb822KeyValuePairElement, 

1256 kvpair_range_te: "TERange", 

1257 _field_name_range: "TERange", 

1258 _stanza: Deb822ParagraphElement, 

1259 _stanza_position: "TEPosition", 

1260 lint_state: LintState, 

1261) -> None: 

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

1263 kvpair_range_te.start_pos 

1264 ) 

1265 raw_value_with_comments = kvpair.value_element.convert_to_text() 

1266 raw_value_masked_comments = "".join( 

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

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

1269 ) 

1270 if isinstance(known_field, DctrlRelationshipKnownField): 

1271 version_operators = known_field.allowed_version_operators 

1272 supports_or_relation = known_field.supports_or_relation 

1273 else: 

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

1275 supports_or_relation = True 

1276 

1277 relation_dup_table = collections.defaultdict(list) 

1278 

1279 for rel, rel_offset, rel_end_offset in _split_w_spans( 

1280 raw_value_masked_comments, "," 

1281 ): 

1282 sub_relations = [] 

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

1284 if or_rel.isspace(): 

1285 continue 

1286 if sub_relations and not supports_or_relation: 

1287 separator_range_te = TERange( 

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

1289 _text_to_te_position(raw_value_masked_comments[:offset]), 

1290 ).relative_to(value_element_pos) 

1291 lint_state.emit_diagnostic( 

1292 separator_range_te, 

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

1294 "error", 

1295 known_field.unknown_value_authority, 

1296 ) 

1297 m = _DEP_RELATION_CLAUSE.fullmatch(or_rel) 

1298 

1299 if m is not None: 

1300 garbage = m.group("garbage") 

1301 version_operator = m.group("operator") 

1302 version_operator_span = m.span("operator") 

1303 if _dctrl_check_dep_version_operator( 

1304 known_field, 

1305 version_operator, 

1306 version_operator_span, 

1307 version_operators, 

1308 raw_value_masked_comments, 

1309 offset, 

1310 value_element_pos, 

1311 lint_state, 

1312 ): 

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

1314 else: 

1315 name_arch_qual = m.group("name_arch_qual") 

1316 if ":" in name_arch_qual: 

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

1318 else: 

1319 name = name_arch_qual 

1320 arch_qual = None 

1321 sub_relations.append( 

1322 Relation( 

1323 name, 

1324 arch_qual=arch_qual, 

1325 version_operator=version_operator, 

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

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

1328 build_profile_restriction=m.group( 

1329 "build_profile_restriction" 

1330 ), 

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

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

1333 content_display_end_offset=rel_end_offset, 

1334 ) 

1335 ) 

1336 else: 

1337 garbage = None 

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

1339 

1340 if m is not None and not garbage: 

1341 continue 

1342 if m is not None: 

1343 garbage_span = m.span("garbage") 

1344 garbage_start, garbage_end = garbage_span 

1345 error_start_offset = offset + garbage_start 

1346 error_end_offset = offset + garbage_end 

1347 garbage_part = raw_value_masked_comments[ 

1348 error_start_offset:error_end_offset 

1349 ] 

1350 else: 

1351 garbage_part = None 

1352 error_start_offset = offset 

1353 error_end_offset = end_offset 

1354 

1355 problem_range_te = TERange( 

1356 _text_to_te_position(raw_value_masked_comments[:error_start_offset]), 

1357 _text_to_te_position(raw_value_masked_comments[:error_end_offset]), 

1358 ).relative_to(value_element_pos) 

1359 

1360 if garbage_part is not None: 

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

1362 msg = ( 

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

1364 " Is a separator missing before this part?" 

1365 ) 

1366 else: 

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

1368 lint_state.emit_diagnostic( 

1369 problem_range_te, 

1370 msg, 

1371 "error", 

1372 known_field.unknown_value_authority, 

1373 ) 

1374 else: 

1375 dep = _cleanup_rel( 

1376 raw_value_masked_comments[error_start_offset:error_end_offset] 

1377 ) 

1378 lint_state.emit_diagnostic( 

1379 problem_range_te, 

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

1381 "error", 

1382 known_field.unknown_value_authority, 

1383 ) 

1384 if ( 

1385 len(sub_relations) == 1 

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

1387 ): 

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

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

1390 

1391 for relations in relation_dup_table.values(): 

1392 if len(relations) > 1: 

1393 dup_check_relations( 

1394 known_field, 

1395 relations, 

1396 raw_value_masked_comments, 

1397 value_element_pos, 

1398 lint_state, 

1399 ) 

1400 

1401 

1402def _rrr_build_driver_mismatch( 

1403 _known_field: "F", 

1404 _deb822_file: Deb822FileElement, 

1405 _kvpair: Deb822KeyValuePairElement, 

1406 kvpair_range_te: "TERange", 

1407 _field_name_range: "TERange", 

1408 stanza: Deb822ParagraphElement, 

1409 _stanza_position: "TEPosition", 

1410 lint_state: LintState, 

1411) -> None: 

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

1413 if dr != "debian-rules": 

1414 lint_state.emit_diagnostic( 

1415 kvpair_range_te, 

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

1417 "informational", 

1418 "debputy", 

1419 quickfixes=[ 

1420 propose_remove_range_quick_fix( 

1421 proposed_title="Remove Rules-Requires-Root" 

1422 ) 

1423 ], 

1424 ) 

1425 

1426 

1427class Dep5Matcher(BasenameGlobMatch): 

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

1429 super().__init__( 

1430 basename_glob, 

1431 only_when_in_directory=None, 

1432 path_type=None, 

1433 recursive_match=False, 

1434 ) 

1435 

1436 

1437def _match_dep5_segment( 

1438 current_dir: VirtualPathBase, basename_glob: str 

1439) -> Iterable[VirtualPathBase]: 

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

1441 return Dep5Matcher(basename_glob).finditer(current_dir) 

1442 else: 

1443 res = current_dir.get(basename_glob) 

1444 if res is None: 

1445 return tuple() 

1446 return (res,) 

1447 

1448 

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

1450 

1451 

1452def _dep5_unnecessary_symbols( 

1453 value: str, 

1454 value_range: TERange, 

1455 lint_state: LintState, 

1456) -> None: 

1457 slash_check_index = 0 

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

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

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

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

1462 prefix_len = slashes_end 

1463 

1464 slash_check_index = prefix_len 

1465 prefix_range = TERange( 

1466 value_range.start_pos, 

1467 TEPosition( 

1468 value_range.start_pos.line_position, 

1469 value_range.start_pos.cursor_position + prefix_len, 

1470 ), 

1471 ) 

1472 lint_state.emit_diagnostic( 

1473 prefix_range, 

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

1475 "warning", 

1476 "debputy", 

1477 quickfixes=[ 

1478 propose_remove_range_quick_fix( 

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

1480 ) 

1481 ], 

1482 ) 

1483 

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

1485 m_start, m_end = m.span(0) 

1486 

1487 prefix_range = TERange( 

1488 TEPosition( 

1489 value_range.start_pos.line_position, 

1490 value_range.start_pos.cursor_position + m_start, 

1491 ), 

1492 TEPosition( 

1493 value_range.start_pos.line_position, 

1494 value_range.start_pos.cursor_position + m_end, 

1495 ), 

1496 ) 

1497 lint_state.emit_diagnostic( 

1498 prefix_range, 

1499 'Simplify to a single "/"', 

1500 "warning", 

1501 "debputy", 

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

1503 ) 

1504 

1505 

1506def _dep5_files_check( 

1507 known_field: "F", 

1508 _deb822_file: Deb822FileElement, 

1509 kvpair: Deb822KeyValuePairElement, 

1510 kvpair_range_te: "TERange", 

1511 _field_name_range: "TERange", 

1512 _stanza: Deb822ParagraphElement, 

1513 _stanza_position: "TEPosition", 

1514 lint_state: LintState, 

1515) -> None: 

1516 interpreter = known_field.field_value_class.interpreter() 

1517 assert interpreter is not None 

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

1519 kvpair_range_te.start_pos 

1520 ) 

1521 values_with_ranges = [] 

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

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

1524 full_value_range.start_pos 

1525 ) 

1526 value = value_ref.value 

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

1528 _dep5_unnecessary_symbols(value, value_range, lint_state) 

1529 

1530 source_root = lint_state.source_root 

1531 if source_root is None: 

1532 return 

1533 i = 0 

1534 limit = len(values_with_ranges) 

1535 while i < limit: 

1536 value, value_range = values_with_ranges[i] 

1537 i += 1 

1538 

1539 

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

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

1542_KNOWN_HTTPS_HOSTS = frozenset( 

1543 [ 

1544 "debian.org", 

1545 "bioconductor.org", 

1546 "cran.r-project.org", 

1547 "github.com", 

1548 "gitlab.com", 

1549 "metacpan.org", 

1550 "gnu.org", 

1551 ] 

1552) 

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

1554_NO_DOT_GIT_HOMEPAGE_HOSTS = frozenset( 

1555 { 

1556 "salsa.debian.org", 

1557 "github.com", 

1558 "gitlab.com", 

1559 } 

1560) 

1561 

1562 

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

1564 if host in known_hosts: 

1565 return True 

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

1567 try: 

1568 idx = host.index(".") 

1569 host = host[idx + 1 :] 

1570 except ValueError: 

1571 break 

1572 if host in known_hosts: 

1573 return True 

1574 return False 

1575 

1576 

1577def _validate_homepage_field( 

1578 _known_field: "F", 

1579 _deb822_file: Deb822FileElement, 

1580 kvpair: Deb822KeyValuePairElement, 

1581 kvpair_range_te: "TERange", 

1582 _field_name_range_te: "TERange", 

1583 _stanza: Deb822ParagraphElement, 

1584 _stanza_position: "TEPosition", 

1585 lint_state: LintState, 

1586) -> None: 

1587 value = kvpair.value_element.convert_to_text() 

1588 offset = 0 

1589 homepage = value 

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

1591 expected_value = m.group(1) 

1592 quickfixes = [] 

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

1594 homepage = expected_value.strip() 

1595 offset = m.start(1) 

1596 quickfixes.append(propose_correct_text_quick_fix(expected_value)) 

1597 lint_state.emit_diagnostic( 

1598 _single_line_span_range_relative_to_pos( 

1599 m.span(), 

1600 kvpair.value_element.position_in_parent().relative_to( 

1601 kvpair_range_te.start_pos 

1602 ), 

1603 ), 

1604 "Superfluous URL/URI wrapping", 

1605 "informational", 

1606 "Policy 5.6.23", 

1607 quickfixes=quickfixes, 

1608 ) 

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

1610 m = _URI_RE.search(homepage) 

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

1612 return 

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

1614 protocol = m.group("protocol") 

1615 host = m.group("host") 

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

1617 if _is_known_host(host, _REPLACED_HOSTS): 

1618 span = m.span("host") 

1619 lint_state.emit_diagnostic( 

1620 _single_line_span_range_relative_to_pos( 

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

1622 kvpair.value_element.position_in_parent().relative_to( 

1623 kvpair_range_te.start_pos 

1624 ), 

1625 ), 

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

1627 "warning", 

1628 "debputy", 

1629 ) 

1630 return 

1631 if ( 

1632 protocol == "ftp" 

1633 or protocol == "http" 

1634 and _is_known_host(host, _KNOWN_HTTPS_HOSTS) 

1635 ): 

1636 span = m.span("protocol") 

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

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

1639 quickfixes = [] 

1640 else: 

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

1642 quickfixes = [propose_correct_text_quick_fix("https")] 

1643 lint_state.emit_diagnostic( 

1644 _single_line_span_range_relative_to_pos( 

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

1646 kvpair.value_element.position_in_parent().relative_to( 

1647 kvpair_range_te.start_pos 

1648 ), 

1649 ), 

1650 msg, 

1651 "pedantic", 

1652 "debputy", 

1653 quickfixes=quickfixes, 

1654 ) 

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

1656 span = m.span("path") 

1657 msg = "Unnecessary suffix" 

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

1659 lint_state.emit_diagnostic( 

1660 _single_line_span_range_relative_to_pos( 

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

1662 kvpair.value_element.position_in_parent().relative_to( 

1663 kvpair_range_te.start_pos 

1664 ), 

1665 ), 

1666 msg, 

1667 "pedantic", 

1668 "debputy", 

1669 quickfixes=quickfixes, 

1670 ) 

1671 

1672 

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

1674 def _validator( 

1675 known_field: "F", 

1676 deb822_file: Deb822FileElement, 

1677 kvpair: Deb822KeyValuePairElement, 

1678 kvpair_range_te: "TERange", 

1679 field_name_range_te: "TERange", 

1680 stanza: Deb822ParagraphElement, 

1681 stanza_position: "TEPosition", 

1682 lint_state: LintState, 

1683 ) -> None: 

1684 for check in checks: 

1685 check( 

1686 known_field, 

1687 deb822_file, 

1688 kvpair, 

1689 kvpair_range_te, 

1690 field_name_range_te, 

1691 stanza, 

1692 stanza_position, 

1693 lint_state, 

1694 ) 

1695 

1696 return _validator 

1697 

1698 

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

1700class PackageNameSectionRule: 

1701 section: str 

1702 check: Callable[[str], bool] 

1703 

1704 

1705def _package_name_section_rule( 

1706 section: str, 

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

1708 *, 

1709 confirm_re: re.Pattern | None = None, 

1710) -> PackageNameSectionRule: 

1711 if confirm_re is not None: 

1712 assert callable(check) 

1713 

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

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

1716 

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

1718 

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

1720 return check.search(v) is not None 

1721 

1722 else: 

1723 _impl = check 

1724 

1725 return PackageNameSectionRule(section, _impl) 

1726 

1727 

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

1729_PKGNAME_VS_SECTION_RULES = [ 

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

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

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

1733 _package_name_section_rule( 

1734 "httpd", 

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

1736 ), 

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

1738 _package_name_section_rule( 

1739 "gnustep", 

1740 lambda n: n.endswith( 

1741 ( 

1742 ".framework", 

1743 ".framework-common", 

1744 ".tool", 

1745 ".tool-common", 

1746 ".app", 

1747 ".app-common", 

1748 ) 

1749 ), 

1750 ), 

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

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

1753 _package_name_section_rule( 

1754 "zope", 

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

1756 ), 

1757 _package_name_section_rule( 

1758 "python", 

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

1760 ), 

1761 _package_name_section_rule( 

1762 "gnu-r", 

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

1764 ), 

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

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

1767 _package_name_section_rule( 

1768 "lisp", 

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

1770 ), 

1771 _package_name_section_rule( 

1772 "lisp", 

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

1774 ), 

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

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

1777 _package_name_section_rule( 

1778 "perl", 

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

1780 ), 

1781 _package_name_section_rule( 

1782 "cli-mono", 

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

1784 ), 

1785 _package_name_section_rule( 

1786 "java", 

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

1788 ), 

1789 _package_name_section_rule( 

1790 "php", 

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

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

1793 ), 

1794 _package_name_section_rule( 

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

1796 ), 

1797 _package_name_section_rule( 

1798 "haskell", 

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

1800 ), 

1801 _package_name_section_rule( 

1802 "ruby", 

1803 lambda n: "-ruby" in n, 

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

1805 ), 

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

1807 _package_name_section_rule( 

1808 "rust", 

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

1810 ), 

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

1812 _package_name_section_rule( 

1813 "ocaml", 

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

1815 ), 

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

1817 _package_name_section_rule( 

1818 "interpreters", 

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

1820 ), 

1821 _package_name_section_rule( 

1822 "introspection", 

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

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

1825 ), 

1826 _package_name_section_rule( 

1827 "fonts", 

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

1829 ), 

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

1831 _package_name_section_rule( 

1832 "localization", 

1833 lambda n: n.startswith( 

1834 ( 

1835 "aspell-", 

1836 "hunspell-", 

1837 "myspell-", 

1838 "mythes-", 

1839 "dict-freedict-", 

1840 "gcompris-sound-", 

1841 ) 

1842 ), 

1843 ), 

1844 _package_name_section_rule( 

1845 "localization", 

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

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

1848 ), 

1849 _package_name_section_rule( 

1850 "localization", 

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

1852 ), 

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

1854 _package_name_section_rule( 

1855 "libdevel", 

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

1857 ), 

1858 _package_name_section_rule( 

1859 "libs", 

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

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

1862 ), 

1863] 

1864 

1865 

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

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

1868@functools.lru_cache(64) 

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

1870 for rule in _PKGNAME_VS_SECTION_RULES: 

1871 if rule.check(name): 

1872 return rule.section 

1873 return None 

1874 

1875 

1876def _unknown_value_check( 

1877 field_name: str, 

1878 value: str, 

1879 known_values: Mapping[str, Keyword], 

1880 unknown_value_severity: LintSeverity | None, 

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

1882 known_value = known_values.get(value) 

1883 message = None 

1884 severity = unknown_value_severity 

1885 fix_data = None 

1886 if known_value is None: 

1887 candidates = detect_possible_typo( 

1888 value, 

1889 known_values, 

1890 ) 

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

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

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

1894 else: 

1895 hint_text = "" 

1896 fix_data = None 

1897 severity = unknown_value_severity 

1898 fix_text = hint_text 

1899 if candidates: 

1900 match = candidates[0] 

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

1902 known_value = known_values[match] 

1903 fix_text = ( 

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

1905 ) 

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

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

1908 return None, None, None, None 

1909 if severity is None: 

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

1911 # It always has leading whitespace 

1912 message = fix_text.strip() 

1913 else: 

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

1915 return known_value, message, severity, fix_data 

1916 

1917 

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

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

1920 

1921 

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

1923 return path 

1924 

1925 

1926def _should_ignore_dir( 

1927 path: VirtualPath, 

1928 *, 

1929 supports_dir_match: bool = False, 

1930 match_non_persistent_paths: bool = False, 

1931) -> bool: 

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

1933 return True 

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

1935 if ( 

1936 not match_non_persistent_paths 

1937 and cachedir_tag is not None 

1938 and cachedir_tag.is_file 

1939 ): 

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

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

1942 start = fd.read(43) 

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

1944 return True 

1945 return False 

1946 

1947 

1948@dataclasses.dataclass(slots=True) 

1949class Deb822KnownField: 

1950 name: str 

1951 field_value_class: FieldValueClass 

1952 warn_if_default: bool = True 

1953 unknown_value_authority: str = "debputy" 

1954 missing_field_authority: str = "debputy" 

1955 replaced_by: str | None = None 

1956 deprecated_with_no_replacement: bool = False 

1957 missing_field_severity: LintSeverity | None = None 

1958 default_value: str | None = None 

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

1960 unknown_value_severity: LintSeverity | None = "error" 

1961 translation_context: str = "" 

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

1963 synopsis: str | None = None 

1964 usage_hint: UsageHint | None = None 

1965 long_description: str | None = None 

1966 spellcheck_value: bool = False 

1967 inheritable_from_other_stanza: bool = False 

1968 show_as_inherited: bool = True 

1969 custom_field_check: CustomFieldCheck | None = None 

1970 can_complete_field_in_stanza: None | ( 

1971 Callable[[Iterable[Deb822ParagraphElement]], bool] 

1972 ) = None 

1973 is_substvars_disabled_even_if_allowed_by_stanza: bool = False 

1974 is_alias_of: str | None = None 

1975 is_completion_suggestion: bool = True 

1976 

1977 def synopsis_translated( 

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

1979 ) -> str | None: 

1980 if self.synopsis is None: 

1981 return None 

1982 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1983 self.translation_context, 

1984 self.synopsis, 

1985 ) 

1986 

1987 def long_description_translated( 

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

1989 ) -> str | None: 

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

1991 return None 

1992 return translation_provider.translation(LSP_DATA_DOMAIN).pgettext( 

1993 self.translation_context, 

1994 self.long_description, 

1995 ) 

1996 

1997 def _can_complete_field_in_stanza( 

1998 self, 

1999 stanza_parts: Sequence[Deb822ParagraphElement], 

2000 ) -> bool: 

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

2002 return False 

2003 return ( 

2004 self.can_complete_field_in_stanza is None 

2005 or self.can_complete_field_in_stanza(stanza_parts) 

2006 ) 

2007 

2008 def complete_field( 

2009 self, 

2010 lint_state: LintState, 

2011 stanza_parts: Sequence[Deb822ParagraphElement], 

2012 markdown_kind: MarkupKind, 

2013 ) -> CompletionItem | None: 

2014 if not self._can_complete_field_in_stanza(stanza_parts): 

2015 return None 

2016 name = self.name 

2017 complete_as = name + ": " 

2018 options = self.value_options_for_completer( 

2019 lint_state, 

2020 stanza_parts, 

2021 "", 

2022 markdown_kind, 

2023 is_completion_for_field=True, 

2024 ) 

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

2026 value = options[0].insert_text 

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

2028 complete_as += value 

2029 tags = [] 

2030 is_deprecated = False 

2031 if self.replaced_by or self.deprecated_with_no_replacement: 

2032 is_deprecated = True 

2033 tags.append(CompletionItemTag.Deprecated) 

2034 

2035 doc = self.long_description 

2036 if doc: 

2037 doc = MarkupContent( 

2038 value=doc, 

2039 kind=markdown_kind, 

2040 ) 

2041 else: 

2042 doc = None 

2043 

2044 return CompletionItem( 

2045 name, 

2046 insert_text=complete_as, 

2047 deprecated=is_deprecated, 

2048 tags=tags, 

2049 detail=format_comp_item_synopsis_doc( 

2050 self.usage_hint, 

2051 self.synopsis_translated(lint_state), 

2052 is_deprecated, 

2053 ), 

2054 documentation=doc, 

2055 ) 

2056 

2057 def _complete_files( 

2058 self, 

2059 base_dir: VirtualPathBase | None, 

2060 value_being_completed: str, 

2061 *, 

2062 is_dep5_file_list: bool = False, 

2063 supports_dir_match: bool = False, 

2064 supports_spaces_in_filename: bool = False, 

2065 match_non_persistent_paths: bool = False, 

2066 ) -> Sequence[CompletionItem] | None: 

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

2068 if base_dir is None or not base_dir.is_dir: 

2069 return None 

2070 

2071 if is_dep5_file_list: 

2072 supports_spaces_in_filename = True 

2073 supports_dir_match = False 

2074 match_non_persistent_paths = False 

2075 

2076 if value_being_completed == "": 

2077 current_dir = base_dir 

2078 unmatched_parts: Sequence[str] = () 

2079 else: 

2080 current_dir, unmatched_parts = base_dir.attempt_lookup( 

2081 value_being_completed 

2082 ) 

2083 

2084 if len(unmatched_parts) > 1: 

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

2086 return None 

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

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

2089 return None 

2090 items = [] 

2091 

2092 path_escaper = _dep5_escape_path if is_dep5_file_list else _noop_escape_path 

2093 

2094 for child in current_dir.iterdir(): 

2095 if child.is_symlink and is_dep5_file_list: 

2096 continue 

2097 if not supports_spaces_in_filename and ( 

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

2099 ): 

2100 continue 

2101 sort_text = ( 

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

2103 ) 

2104 if child.is_dir: 

2105 if _should_ignore_dir( 

2106 child, 

2107 supports_dir_match=supports_dir_match, 

2108 match_non_persistent_paths=match_non_persistent_paths, 

2109 ): 

2110 continue 

2111 items.append( 

2112 CompletionItem( 

2113 f"{child.path}/", 

2114 label_details=CompletionItemLabelDetails( 

2115 description=child.path, 

2116 ), 

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

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

2119 sort_text=sort_text, 

2120 kind=CompletionItemKind.Folder, 

2121 ) 

2122 ) 

2123 else: 

2124 items.append( 

2125 CompletionItem( 

2126 child.path, 

2127 label_details=CompletionItemLabelDetails( 

2128 description=child.path, 

2129 ), 

2130 insert_text=path_escaper(child.path), 

2131 filter_text=child.path, 

2132 sort_text=sort_text, 

2133 kind=CompletionItemKind.File, 

2134 ) 

2135 ) 

2136 return items 

2137 

2138 def value_options_for_completer( 

2139 self, 

2140 lint_state: LintState, 

2141 stanza_parts: Sequence[Deb822ParagraphElement], 

2142 value_being_completed: str, 

2143 markdown_kind: MarkupKind, 

2144 *, 

2145 is_completion_for_field: bool = False, 

2146 ) -> Sequence[CompletionItem] | None: 

2147 known_values = self.known_values 

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

2149 if is_completion_for_field: 

2150 return None 

2151 return self._complete_files( 

2152 lint_state.source_root, 

2153 value_being_completed, 

2154 is_dep5_file_list=True, 

2155 ) 

2156 

2157 if known_values is None: 

2158 return None 

2159 if is_completion_for_field and ( 

2160 len(known_values) == 1 

2161 or ( 

2162 len(known_values) == 2 

2163 and self.warn_if_default 

2164 and self.default_value is not None 

2165 ) 

2166 ): 

2167 value = next( 

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

2169 None, 

2170 ) 

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

2172 return None 

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

2174 return [ 

2175 keyword.as_completion_item( 

2176 lint_state, 

2177 stanza_parts, 

2178 value_being_completed, 

2179 markdown_kind, 

2180 ) 

2181 for keyword in known_values.values() 

2182 if keyword.is_keyword_valid_completion_in_stanza(stanza_parts) 

2183 and keyword.is_completion_suggestion 

2184 ] 

2185 

2186 def field_omitted_diagnostics( 

2187 self, 

2188 deb822_file: Deb822FileElement, 

2189 representation_field_range: "TERange", 

2190 stanza: Deb822ParagraphElement, 

2191 stanza_position: "TEPosition", 

2192 header_stanza: Deb822FileElement | None, 

2193 lint_state: LintState, 

2194 ) -> None: 

2195 missing_field_severity = self.missing_field_severity 

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

2197 return 

2198 

2199 if ( 

2200 self.inheritable_from_other_stanza 

2201 and header_stanza is not None 

2202 and self.name in header_stanza 

2203 ): 

2204 return 

2205 

2206 lint_state.emit_diagnostic( 

2207 representation_field_range, 

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

2209 missing_field_severity, 

2210 self.missing_field_authority, 

2211 ) 

2212 

2213 async def field_diagnostics( 

2214 self, 

2215 deb822_file: Deb822FileElement, 

2216 kvpair: Deb822KeyValuePairElement, 

2217 stanza: Deb822ParagraphElement, 

2218 stanza_position: "TEPosition", 

2219 kvpair_range_te: "TERange", 

2220 lint_state: LintState, 

2221 *, 

2222 field_name_typo_reported: bool = False, 

2223 ) -> None: 

2224 field_name_token = kvpair.field_token 

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

2226 kvpair_range_te.start_pos 

2227 ) 

2228 field_name = field_name_token.text 

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

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

2231 # in one but not the other. 

2232 field_value = stanza[field_name] 

2233 self._diagnostics_for_field_name( 

2234 kvpair_range_te, 

2235 field_name_token, 

2236 field_name_range_te, 

2237 field_name_typo_reported, 

2238 lint_state, 

2239 ) 

2240 if self.custom_field_check is not None: 

2241 self.custom_field_check( 

2242 self, 

2243 deb822_file, 

2244 kvpair, 

2245 kvpair_range_te, 

2246 field_name_range_te, 

2247 stanza, 

2248 stanza_position, 

2249 lint_state, 

2250 ) 

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

2252 if self.spellcheck_value: 

2253 words = kvpair.interpret_as(LIST_SPACE_SEPARATED_INTERPRETATION) 

2254 spell_checker = lint_state.spellchecker() 

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

2256 kvpair_range_te.start_pos 

2257 ) 

2258 async for word_ref in lint_state.slow_iter( 

2259 words.iter_value_references(), yield_every=25 

2260 ): 

2261 token = word_ref.value 

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

2263 corrections = spell_checker.provide_corrections_for(word) 

2264 if not corrections: 

2265 continue 

2266 word_loc = word_ref.locatable 

2267 word_pos_te = word_loc.position_in_parent().relative_to( 

2268 value_position 

2269 ) 

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

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

2272 word_size = TERange( 

2273 START_POSITION, 

2274 TEPosition(0, endpos - pos), 

2275 ) 

2276 lint_state.emit_diagnostic( 

2277 TERange.from_position_and_size(word_pos_te, word_size), 

2278 f'Spelling "{word}"', 

2279 "spelling", 

2280 "debputy", 

2281 quickfixes=[ 

2282 propose_correct_text_quick_fix(c) for c in corrections 

2283 ], 

2284 enable_non_interactive_auto_fix=False, 

2285 ) 

2286 else: 

2287 self._known_value_diagnostics( 

2288 kvpair, 

2289 kvpair_range_te.start_pos, 

2290 lint_state, 

2291 ) 

2292 

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

2294 lint_state.emit_diagnostic( 

2295 kvpair_range_te, 

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

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

2298 "warning", 

2299 "debputy", 

2300 ) 

2301 

2302 def _diagnostics_for_field_name( 

2303 self, 

2304 kvpair_range: "TERange", 

2305 token: Deb822FieldNameToken, 

2306 token_range: "TERange", 

2307 typo_detected: bool, 

2308 lint_state: LintState, 

2309 ) -> None: 

2310 field_name = token.text 

2311 # Defeat the case-insensitivity from python-debian 

2312 field_name_cased = str(field_name) 

2313 if self.deprecated_with_no_replacement: 

2314 lint_state.emit_diagnostic( 

2315 kvpair_range, 

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

2317 "warning", 

2318 "debputy", 

2319 quickfixes=[propose_remove_range_quick_fix()], 

2320 tags=[DiagnosticTag.Deprecated], 

2321 ) 

2322 elif self.replaced_by is not None: 

2323 lint_state.emit_diagnostic( 

2324 token_range, 

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

2326 "warning", 

2327 "debputy", 

2328 tags=[DiagnosticTag.Deprecated], 

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

2330 ) 

2331 

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

2333 lint_state.emit_diagnostic( 

2334 token_range, 

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

2336 "pedantic", 

2337 self.unknown_value_authority, 

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

2339 ) 

2340 

2341 def _dep5_file_list_diagnostics( 

2342 self, 

2343 kvpair: Deb822KeyValuePairElement, 

2344 kvpair_position: "TEPosition", 

2345 lint_state: LintState, 

2346 ) -> None: 

2347 source_root = lint_state.source_root 

2348 if ( 

2349 self.field_value_class != FieldValueClass.DEP5_FILE_LIST 

2350 or source_root is None 

2351 ): 

2352 return 

2353 interpreter = self.field_value_class.interpreter() 

2354 values = kvpair.interpret_as(interpreter) 

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

2356 kvpair_position 

2357 ) 

2358 

2359 assert interpreter is not None 

2360 

2361 for token in values.iter_parts(): 

2362 if token.is_whitespace: 

2363 continue 

2364 text = token.convert_to_text() 

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

2366 # TODO: We should validate these as well 

2367 continue 

2368 matched_path, missing_part = source_root.attempt_lookup(text) 

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

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

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

2372 # do not have the infrastructure for). 

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

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

2375 lint_state.emit_diagnostic( 

2376 path_range_te, 

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

2378 "warning", 

2379 self.unknown_value_authority, 

2380 quickfixes=[ 

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

2382 ], 

2383 ) 

2384 

2385 def _known_value_diagnostics( 

2386 self, 

2387 kvpair: Deb822KeyValuePairElement, 

2388 kvpair_position: "TEPosition", 

2389 lint_state: LintState, 

2390 ) -> None: 

2391 unknown_value_severity = self.unknown_value_severity 

2392 interpreter = self.field_value_class.interpreter() 

2393 if interpreter is None: 

2394 return 

2395 try: 

2396 values = kvpair.interpret_as(interpreter) 

2397 except ValueError: 

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

2399 kvpair_position 

2400 ) 

2401 lint_state.emit_diagnostic( 

2402 value_range, 

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

2404 "pedantic", 

2405 "debputy", 

2406 ) 

2407 return 

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

2409 kvpair_position 

2410 ) 

2411 

2412 last_token_non_ws_sep_token: TE | None = None 

2413 for token in values.iter_parts(): 

2414 if token.is_whitespace: 

2415 continue 

2416 if not token.is_separator: 

2417 last_token_non_ws_sep_token = None 

2418 continue 

2419 if last_token_non_ws_sep_token is not None: 

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

2421 lint_state.emit_diagnostic( 

2422 sep_range_te, 

2423 "Duplicate separator", 

2424 "error", 

2425 self.unknown_value_authority, 

2426 ) 

2427 last_token_non_ws_sep_token = token 

2428 

2429 allowed_values = self.known_values 

2430 if not allowed_values: 

2431 return 

2432 

2433 first_value = None 

2434 first_exclusive_value_ref = None 

2435 first_exclusive_value = None 

2436 has_emitted_for_exclusive = False 

2437 

2438 for value_ref in values.iter_value_references(): 

2439 value = value_ref.value 

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

2441 first_value is not None 

2442 and self.field_value_class == FieldValueClass.SINGLE_VALUE 

2443 ): 

2444 value_loc = value_ref.locatable 

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

2446 lint_state.emit_diagnostic( 

2447 range_position_te, 

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

2449 "error", 

2450 self.unknown_value_authority, 

2451 ) 

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

2453 continue 

2454 

2455 if first_exclusive_value_ref is not None and not has_emitted_for_exclusive: 

2456 assert first_exclusive_value is not None 

2457 value_loc = first_exclusive_value_ref.locatable 

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

2459 lint_state.emit_diagnostic( 

2460 value_range_te, 

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

2462 "error", 

2463 self.unknown_value_authority, 

2464 ) 

2465 

2466 known_value, unknown_value_message, unknown_severity, typo_fix_data = ( 

2467 _unknown_value_check( 

2468 self.name, 

2469 value, 

2470 self.known_values, 

2471 unknown_value_severity, 

2472 ) 

2473 ) 

2474 value_loc = value_ref.locatable 

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

2476 

2477 if known_value and known_value.is_exclusive: 

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

2479 first_exclusive_value_ref = value_ref 

2480 if first_value is not None: 

2481 has_emitted_for_exclusive = True 

2482 lint_state.emit_diagnostic( 

2483 value_range, 

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

2485 "error", 

2486 self.unknown_value_authority, 

2487 ) 

2488 

2489 if first_value is None: 

2490 first_value = value 

2491 

2492 if unknown_value_message is not None: 

2493 assert unknown_severity is not None 

2494 lint_state.emit_diagnostic( 

2495 value_range, 

2496 unknown_value_message, 

2497 unknown_severity, 

2498 self.unknown_value_authority, 

2499 quickfixes=typo_fix_data, 

2500 ) 

2501 

2502 if known_value is not None and known_value.is_deprecated: 

2503 replacement = known_value.replaced_by 

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

2505 obsolete_value_message = ( 

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

2507 ) 

2508 obsolete_fix_data = [propose_correct_text_quick_fix(replacement)] 

2509 else: 

2510 obsolete_value_message = ( 

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

2512 ) 

2513 obsolete_fix_data = None 

2514 lint_state.emit_diagnostic( 

2515 value_range, 

2516 obsolete_value_message, 

2517 "warning", 

2518 "debputy", 

2519 quickfixes=obsolete_fix_data, 

2520 ) 

2521 

2522 def _reformat_field_name( 

2523 self, 

2524 effective_preference: "EffectiveFormattingPreference", 

2525 stanza_range: TERange, 

2526 kvpair: Deb822KeyValuePairElement, 

2527 position_codec: LintCapablePositionCodec, 

2528 lines: list[str], 

2529 ) -> Iterable[TextEdit]: 

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

2531 return 

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

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

2534 return 

2535 

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

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

2538 ) 

2539 

2540 edit_range = position_codec.range_to_client_units( 

2541 lines, 

2542 Range( 

2543 Position( 

2544 field_name_range_te.start_pos.line_position, 

2545 field_name_range_te.start_pos.cursor_position, 

2546 ), 

2547 Position( 

2548 field_name_range_te.start_pos.line_position, 

2549 field_name_range_te.end_pos.cursor_position, 

2550 ), 

2551 ), 

2552 ) 

2553 yield TextEdit( 

2554 edit_range, 

2555 self.name, 

2556 ) 

2557 

2558 def reformat_field( 

2559 self, 

2560 effective_preference: "EffectiveFormattingPreference", 

2561 stanza_range: TERange, 

2562 kvpair: Deb822KeyValuePairElement, 

2563 formatter: FormatterCallback, 

2564 position_codec: LintCapablePositionCodec, 

2565 lines: list[str], 

2566 ) -> Iterable[TextEdit]: 

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

2568 yield from self._reformat_field_name( 

2569 effective_preference, 

2570 stanza_range, 

2571 kvpair, 

2572 position_codec, 

2573 lines, 

2574 ) 

2575 return trim_end_of_line_whitespace( 

2576 position_codec, 

2577 lines, 

2578 line_range=range( 

2579 kvpair_range.start_pos.line_position, 

2580 kvpair_range.end_pos.line_position, 

2581 ), 

2582 ) 

2583 

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

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

2586 

2587 

2588@dataclasses.dataclass(slots=True) 

2589class DctrlLikeKnownField(Deb822KnownField): 

2590 

2591 def reformat_field( 

2592 self, 

2593 effective_preference: "EffectiveFormattingPreference", 

2594 stanza_range: TERange, 

2595 kvpair: Deb822KeyValuePairElement, 

2596 formatter: FormatterCallback, 

2597 position_codec: LintCapablePositionCodec, 

2598 lines: list[str], 

2599 ) -> Iterable[TextEdit]: 

2600 interpretation = self.field_value_class.interpreter() 

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

2602 not effective_preference.deb822_normalize_field_content 

2603 or interpretation is None 

2604 ): 

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

2606 effective_preference, 

2607 stanza_range, 

2608 kvpair, 

2609 formatter, 

2610 position_codec, 

2611 lines, 

2612 ) 

2613 return 

2614 if not self.reformattable_field: 

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

2616 effective_preference, 

2617 stanza_range, 

2618 kvpair, 

2619 formatter, 

2620 position_codec, 

2621 lines, 

2622 ) 

2623 return 

2624 

2625 # Preserve the name fixes from the super call. 

2626 yield from self._reformat_field_name( 

2627 effective_preference, 

2628 stanza_range, 

2629 kvpair, 

2630 position_codec, 

2631 lines, 

2632 ) 

2633 

2634 seen: set[str] = set() 

2635 old_kvpair_range = kvpair.range_in_parent() 

2636 sort = self.is_sortable_field 

2637 

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

2639 field_content = kvpair.interpret_as(interpretation) 

2640 old_value = field_content.convert_to_text(with_field_name=False) 

2641 for package_ref in field_content.iter_value_references(): 

2642 value = package_ref.value 

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

2644 stanza_range.start_pos 

2645 ) 

2646 sublines = lines[ 

2647 value_range.start_pos.line_position : value_range.end_pos.line_position 

2648 ] 

2649 

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

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

2652 return 

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

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

2655 else: 

2656 new_value = value 

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

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

2659 package_ref.value = new_value 

2660 seen.add(new_value) 

2661 else: 

2662 package_ref.remove() 

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

2664 field_content.sort(key=_sort_packages_key) 

2665 field_content.value_formatter(formatter) 

2666 field_content.reformat_when_finished() 

2667 

2668 new_value = field_content.convert_to_text(with_field_name=False) 

2669 if new_value != old_value: 

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

2671 old_kvpair_range.start_pos 

2672 ) 

2673 range_server_units = te_range_to_lsp( 

2674 value_range.relative_to(stanza_range.start_pos) 

2675 ) 

2676 yield TextEdit( 

2677 position_codec.range_to_client_units(lines, range_server_units), 

2678 new_value, 

2679 ) 

2680 

2681 @property 

2682 def reformattable_field(self) -> bool: 

2683 return self.is_relationship_field or self.is_sortable_field 

2684 

2685 @property 

2686 def is_relationship_field(self) -> bool: 

2687 return False 

2688 

2689 @property 

2690 def is_sortable_field(self) -> bool: 

2691 return self.is_relationship_field 

2692 

2693 

2694@dataclasses.dataclass(slots=True) 

2695class DTestsCtrlKnownField(DctrlLikeKnownField): 

2696 @property 

2697 def is_relationship_field(self) -> bool: 

2698 return self.name == "Depends" 

2699 

2700 @property 

2701 def is_sortable_field(self) -> bool: 

2702 return self.is_relationship_field or self.name in ( 

2703 "Features", 

2704 "Restrictions", 

2705 "Tests", 

2706 ) 

2707 

2708 

2709@dataclasses.dataclass(slots=True) 

2710class DctrlKnownField(DctrlLikeKnownField): 

2711 

2712 def field_omitted_diagnostics( 

2713 self, 

2714 deb822_file: Deb822FileElement, 

2715 representation_field_range: "TERange", 

2716 stanza: Deb822ParagraphElement, 

2717 stanza_position: "TEPosition", 

2718 header_stanza: Deb822FileElement | None, 

2719 lint_state: LintState, 

2720 ) -> None: 

2721 missing_field_severity = self.missing_field_severity 

2722 if missing_field_severity is None: 

2723 return 

2724 

2725 if ( 

2726 self.inheritable_from_other_stanza 

2727 and header_stanza is not None 

2728 and self.name in header_stanza 

2729 ): 

2730 return 

2731 

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

2733 stanzas = list(deb822_file)[1:] 

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

2735 return 

2736 

2737 lint_state.emit_diagnostic( 

2738 representation_field_range, 

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

2740 missing_field_severity, 

2741 self.missing_field_authority, 

2742 ) 

2743 

2744 def reformat_field( 

2745 self, 

2746 effective_preference: "EffectiveFormattingPreference", 

2747 stanza_range: TERange, 

2748 kvpair: Deb822KeyValuePairElement, 

2749 formatter: FormatterCallback, 

2750 position_codec: LintCapablePositionCodec, 

2751 lines: list[str], 

2752 ) -> Iterable[TextEdit]: 

2753 if ( 

2754 self.name == "Architecture" 

2755 and effective_preference.deb822_normalize_field_content 

2756 ): 

2757 interpretation = self.field_value_class.interpreter() 

2758 assert interpretation is not None 

2759 interpreted = kvpair.interpret_as(interpretation) 

2760 archs = list(interpreted) 

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

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

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

2764 reformat_edits = list( 

2765 self._reformat_field_name( 

2766 effective_preference, 

2767 stanza_range, 

2768 kvpair, 

2769 position_codec, 

2770 lines, 

2771 ) 

2772 ) 

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

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

2775 kvpair.range_in_parent().start_pos 

2776 ) 

2777 kvpair_range = te_range_to_lsp( 

2778 value_range.relative_to(stanza_range.start_pos) 

2779 ) 

2780 reformat_edits.append( 

2781 TextEdit( 

2782 position_codec.range_to_client_units(lines, kvpair_range), 

2783 new_value, 

2784 ) 

2785 ) 

2786 return reformat_edits 

2787 

2788 return super(DctrlKnownField, self).reformat_field( 

2789 effective_preference, 

2790 stanza_range, 

2791 kvpair, 

2792 formatter, 

2793 position_codec, 

2794 lines, 

2795 ) 

2796 

2797 @property 

2798 def is_relationship_field(self) -> bool: 

2799 name_lc = self.name.lower() 

2800 return ( 

2801 name_lc in all_package_relationship_fields() 

2802 or name_lc in all_source_relationship_fields() 

2803 ) 

2804 

2805 @property 

2806 def reformattable_field(self) -> bool: 

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

2808 

2809 

2810@dataclasses.dataclass(slots=True) 

2811class DctrlRelationshipKnownField(DctrlKnownField): 

2812 allowed_version_operators: frozenset[str] = frozenset() 

2813 supports_or_relation: bool = True 

2814 

2815 @property 

2816 def is_relationship_field(self) -> bool: 

2817 return True 

2818 

2819 

2820SOURCE_FIELDS = _fields( 

2821 DctrlKnownField( 

2822 "Source", 

2823 FieldValueClass.SINGLE_VALUE, 

2824 custom_field_check=_combined_custom_field_check( 

2825 _each_value_match_regex_validation(PKGNAME_REGEX), 

2826 _has_packaging_expected_file( 

2827 "copyright", 

2828 "No copyright file (package license)", 

2829 severity="warning", 

2830 ), 

2831 _has_packaging_expected_file( 

2832 "changelog", 

2833 "No Debian changelog file", 

2834 severity="error", 

2835 ), 

2836 _has_build_instructions, 

2837 ), 

2838 ), 

2839 DctrlKnownField( 

2840 "Standards-Version", 

2841 FieldValueClass.SINGLE_VALUE, 

2842 custom_field_check=_sv_field_validation, 

2843 ), 

2844 DctrlKnownField( 

2845 "Section", 

2846 FieldValueClass.SINGLE_VALUE, 

2847 known_values=ALL_SECTIONS, 

2848 ), 

2849 DctrlKnownField( 

2850 "Priority", 

2851 FieldValueClass.SINGLE_VALUE, 

2852 ), 

2853 DctrlKnownField( 

2854 "Maintainer", 

2855 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2856 custom_field_check=_combined_custom_field_check( 

2857 _maintainer_field_validator, 

2858 _canonical_maintainer_name, 

2859 ), 

2860 ), 

2861 DctrlKnownField( 

2862 "Uploaders", 

2863 FieldValueClass.COMMA_SEPARATED_EMAIL_LIST, 

2864 custom_field_check=_canonical_maintainer_name, 

2865 ), 

2866 DctrlRelationshipKnownField( 

2867 "Build-Depends", 

2868 FieldValueClass.COMMA_SEPARATED_LIST, 

2869 custom_field_check=_dctrl_validate_dep, 

2870 ), 

2871 DctrlRelationshipKnownField( 

2872 "Build-Depends-Arch", 

2873 FieldValueClass.COMMA_SEPARATED_LIST, 

2874 custom_field_check=_dctrl_validate_dep, 

2875 ), 

2876 DctrlRelationshipKnownField( 

2877 "Build-Depends-Indep", 

2878 FieldValueClass.COMMA_SEPARATED_LIST, 

2879 custom_field_check=_dctrl_validate_dep, 

2880 ), 

2881 DctrlRelationshipKnownField( 

2882 "Build-Conflicts", 

2883 FieldValueClass.COMMA_SEPARATED_LIST, 

2884 supports_or_relation=False, 

2885 custom_field_check=_dctrl_validate_dep, 

2886 ), 

2887 DctrlRelationshipKnownField( 

2888 "Build-Conflicts-Arch", 

2889 FieldValueClass.COMMA_SEPARATED_LIST, 

2890 supports_or_relation=False, 

2891 custom_field_check=_dctrl_validate_dep, 

2892 ), 

2893 DctrlRelationshipKnownField( 

2894 "Build-Conflicts-Indep", 

2895 FieldValueClass.COMMA_SEPARATED_LIST, 

2896 supports_or_relation=False, 

2897 custom_field_check=_dctrl_validate_dep, 

2898 ), 

2899 DctrlKnownField( 

2900 "Rules-Requires-Root", 

2901 FieldValueClass.SPACE_SEPARATED_LIST, 

2902 custom_field_check=_rrr_build_driver_mismatch, 

2903 ), 

2904 DctrlKnownField( 

2905 "X-Style", 

2906 FieldValueClass.SINGLE_VALUE, 

2907 known_values=ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

2908 ), 

2909 DctrlKnownField( 

2910 "Homepage", 

2911 FieldValueClass.SINGLE_VALUE, 

2912 custom_field_check=_validate_homepage_field, 

2913 ), 

2914) 

2915 

2916 

2917BINARY_FIELDS = _fields( 

2918 DctrlKnownField( 

2919 "Package", 

2920 FieldValueClass.SINGLE_VALUE, 

2921 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

2922 ), 

2923 DctrlKnownField( 

2924 "Architecture", 

2925 FieldValueClass.SPACE_SEPARATED_LIST, 

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

2927 known_values=allowed_values(dpkg_arch_and_wildcards()), 

2928 ), 

2929 DctrlKnownField( 

2930 "Pre-Depends", 

2931 FieldValueClass.COMMA_SEPARATED_LIST, 

2932 custom_field_check=_dctrl_validate_dep, 

2933 ), 

2934 DctrlKnownField( 

2935 "Depends", 

2936 FieldValueClass.COMMA_SEPARATED_LIST, 

2937 custom_field_check=_dctrl_validate_dep, 

2938 ), 

2939 DctrlKnownField( 

2940 "Recommends", 

2941 FieldValueClass.COMMA_SEPARATED_LIST, 

2942 custom_field_check=_dctrl_validate_dep, 

2943 ), 

2944 DctrlKnownField( 

2945 "Suggests", 

2946 FieldValueClass.COMMA_SEPARATED_LIST, 

2947 custom_field_check=_dctrl_validate_dep, 

2948 ), 

2949 DctrlKnownField( 

2950 "Enhances", 

2951 FieldValueClass.COMMA_SEPARATED_LIST, 

2952 custom_field_check=_dctrl_validate_dep, 

2953 ), 

2954 DctrlRelationshipKnownField( 

2955 "Provides", 

2956 FieldValueClass.COMMA_SEPARATED_LIST, 

2957 custom_field_check=_dctrl_validate_dep, 

2958 supports_or_relation=False, 

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

2960 ), 

2961 DctrlRelationshipKnownField( 

2962 "Conflicts", 

2963 FieldValueClass.COMMA_SEPARATED_LIST, 

2964 custom_field_check=_dctrl_validate_dep, 

2965 supports_or_relation=False, 

2966 ), 

2967 DctrlRelationshipKnownField( 

2968 "Breaks", 

2969 FieldValueClass.COMMA_SEPARATED_LIST, 

2970 custom_field_check=_dctrl_validate_dep, 

2971 supports_or_relation=False, 

2972 ), 

2973 DctrlRelationshipKnownField( 

2974 "Replaces", 

2975 FieldValueClass.COMMA_SEPARATED_LIST, 

2976 custom_field_check=_dctrl_validate_dep, 

2977 ), 

2978 DctrlKnownField( 

2979 "Build-Profiles", 

2980 FieldValueClass.BUILD_PROFILES_LIST, 

2981 ), 

2982 DctrlKnownField( 

2983 "Section", 

2984 FieldValueClass.SINGLE_VALUE, 

2985 known_values=ALL_SECTIONS, 

2986 ), 

2987 DctrlRelationshipKnownField( 

2988 "Built-Using", 

2989 FieldValueClass.COMMA_SEPARATED_LIST, 

2990 custom_field_check=_arch_not_all_only_field_validation, 

2991 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

2992 supports_or_relation=False, 

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

2994 ), 

2995 DctrlRelationshipKnownField( 

2996 "Static-Built-Using", 

2997 FieldValueClass.COMMA_SEPARATED_LIST, 

2998 custom_field_check=_arch_not_all_only_field_validation, 

2999 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3000 supports_or_relation=False, 

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

3002 ), 

3003 DctrlKnownField( 

3004 "Multi-Arch", 

3005 FieldValueClass.SINGLE_VALUE, 

3006 custom_field_check=_dctrl_ma_field_validation, 

3007 known_values=allowed_values( 

3008 ( 

3009 Keyword( 

3010 "same", 

3011 can_complete_keyword_in_stanza=_complete_only_in_arch_dep_pkgs, 

3012 ), 

3013 ), 

3014 ), 

3015 ), 

3016 DctrlKnownField( 

3017 "XB-Installer-Menu-Item", 

3018 FieldValueClass.SINGLE_VALUE, 

3019 can_complete_field_in_stanza=_complete_only_for_udeb_pkgs, 

3020 custom_field_check=_combined_custom_field_check( 

3021 _udeb_only_field_validation, 

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

3023 ), 

3024 ), 

3025 DctrlKnownField( 

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

3027 FieldValueClass.SINGLE_VALUE, 

3028 custom_field_check=_arch_not_all_only_field_validation, 

3029 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3030 ), 

3031 DctrlKnownField( 

3032 "X-Doc-Main-Package", 

3033 FieldValueClass.SINGLE_VALUE, 

3034 custom_field_check=_binary_package_from_same_source, 

3035 ), 

3036 DctrlKnownField( 

3037 "X-Time64-Compat", 

3038 FieldValueClass.SINGLE_VALUE, 

3039 can_complete_field_in_stanza=_complete_only_in_arch_dep_pkgs, 

3040 custom_field_check=_combined_custom_field_check( 

3041 _each_value_match_regex_validation(PKGNAME_REGEX), 

3042 _arch_not_all_only_field_validation, 

3043 ), 

3044 ), 

3045 DctrlKnownField( 

3046 "Description", 

3047 FieldValueClass.FREE_TEXT_FIELD, 

3048 custom_field_check=dctrl_description_validator, 

3049 ), 

3050 DctrlKnownField( 

3051 "XB-Cnf-Visible-Pkgname", 

3052 FieldValueClass.SINGLE_VALUE, 

3053 custom_field_check=_each_value_match_regex_validation(PKGNAME_REGEX), 

3054 ), 

3055 DctrlKnownField( 

3056 "Homepage", 

3057 FieldValueClass.SINGLE_VALUE, 

3058 show_as_inherited=False, 

3059 custom_field_check=_validate_homepage_field, 

3060 ), 

3061) 

3062_DEP5_HEADER_FIELDS = _fields( 

3063 Deb822KnownField( 

3064 "Format", 

3065 FieldValueClass.SINGLE_VALUE, 

3066 custom_field_check=_use_https_instead_of_http, 

3067 ), 

3068) 

3069_DEP5_FILES_FIELDS = _fields( 

3070 Deb822KnownField( 

3071 "Files", 

3072 FieldValueClass.DEP5_FILE_LIST, 

3073 custom_field_check=_dep5_files_check, 

3074 ), 

3075) 

3076_DEP5_LICENSE_FIELDS = _fields( 

3077 Deb822KnownField( 

3078 "License", 

3079 FieldValueClass.FREE_TEXT_FIELD, 

3080 ), 

3081) 

3082 

3083_DTESTSCTRL_FIELDS = _fields( 

3084 DTestsCtrlKnownField( 

3085 "Architecture", 

3086 FieldValueClass.SPACE_SEPARATED_LIST, 

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

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

3089 ), 

3090) 

3091_DWATCH_HEADER_FIELDS = _fields() 

3092_DWATCH_TEMPLATE_FIELDS = _fields() 

3093_DWATCH_SOURCE_FIELDS = _fields() 

3094 

3095 

3096@dataclasses.dataclass(slots=True) 

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

3098 stanza_type_name: str 

3099 stanza_fields: Mapping[str, F] 

3100 is_substvars_allowed_in_stanza: bool 

3101 

3102 async def stanza_diagnostics( 

3103 self, 

3104 deb822_file: Deb822FileElement, 

3105 stanza: Deb822ParagraphElement, 

3106 stanza_position_in_file: "TEPosition", 

3107 lint_state: LintState, 

3108 *, 

3109 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3110 confusable_with_stanza_name: str | None = None, 

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

3112 ) -> None: 

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

3114 confusable_with_stanza_metadata is None 

3115 ): 

3116 raise ValueError( 

3117 "confusable_with_stanza_name and confusable_with_stanza_metadata must be used together" 

3118 ) 

3119 _, representation_field_range = self.stanza_representation( 

3120 stanza, 

3121 stanza_position_in_file, 

3122 ) 

3123 known_fields = self.stanza_fields 

3124 self.omitted_field_diagnostics( 

3125 lint_state, 

3126 deb822_file, 

3127 stanza, 

3128 stanza_position_in_file, 

3129 inherit_from_stanza=inherit_from_stanza, 

3130 representation_field_range=representation_field_range, 

3131 ) 

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

3133 

3134 async for kvpair_range, kvpair in lint_state.slow_iter( 

3135 with_range_in_continuous_parts( 

3136 stanza.iter_parts(), 

3137 start_relative_to=stanza_position_in_file, 

3138 ), 

3139 yield_every=1, 

3140 ): 

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

3142 continue 

3143 field_name_token = kvpair.field_token 

3144 field_name = field_name_token.text 

3145 field_name_lc = field_name.lower() 

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

3147 field_name = str(field_name) 

3148 normalized_field_name_lc = self.normalize_field_name(field_name_lc) 

3149 known_field = known_fields.get(normalized_field_name_lc) 

3150 field_value = stanza[field_name] 

3151 kvpair_range_te = kvpair.range_in_parent().relative_to( 

3152 stanza_position_in_file 

3153 ) 

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

3155 kvpair_range_te.start_pos 

3156 ) 

3157 field_position_te = field_range.start_pos 

3158 field_name_typo_detected = False 

3159 dup_field_key = ( 

3160 known_field.name 

3161 if known_field is not None 

3162 else normalized_field_name_lc 

3163 ) 

3164 existing_field_range = seen_fields.get(dup_field_key) 

3165 if existing_field_range is not None: 

3166 existing_field_range[3].append(field_range) 

3167 existing_field_range[4].add(field_name) 

3168 else: 

3169 normalized_field_name = self.normalize_field_name(field_name) 

3170 seen_fields[dup_field_key] = ( 

3171 known_field.name if known_field else field_name, 

3172 normalized_field_name, 

3173 field_range, 

3174 [], 

3175 {field_name}, 

3176 ) 

3177 

3178 if known_field is None: 

3179 candidates = detect_possible_typo( 

3180 normalized_field_name_lc, known_fields 

3181 ) 

3182 if candidates: 

3183 known_field = known_fields[candidates[0]] 

3184 field_range = TERange.from_position_and_size( 

3185 field_position_te, kvpair.field_token.size() 

3186 ) 

3187 field_name_typo_detected = True 

3188 lint_state.emit_diagnostic( 

3189 field_range, 

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

3191 "warning", 

3192 "debputy", 

3193 quickfixes=[ 

3194 propose_correct_text_quick_fix(known_fields[m].name) 

3195 for m in candidates 

3196 ], 

3197 ) 

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

3199 lint_state.emit_diagnostic( 

3200 field_range, 

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

3202 "error", 

3203 "Policy 5.1", 

3204 ) 

3205 continue 

3206 if known_field is None: 

3207 known_else_where = confusable_with_stanza_metadata.stanza_fields.get( 

3208 normalized_field_name_lc 

3209 ) 

3210 if known_else_where is not None: 

3211 lint_state.emit_diagnostic( 

3212 field_range, 

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

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

3215 "error", 

3216 known_else_where.missing_field_authority, 

3217 ) 

3218 continue 

3219 await known_field.field_diagnostics( 

3220 deb822_file, 

3221 kvpair, 

3222 stanza, 

3223 stanza_position_in_file, 

3224 kvpair_range_te, 

3225 lint_state, 

3226 field_name_typo_reported=field_name_typo_detected, 

3227 ) 

3228 

3229 inherit_value = ( 

3230 inherit_from_stanza.get(field_name) if inherit_from_stanza else None 

3231 ) 

3232 

3233 if ( 

3234 known_field.inheritable_from_other_stanza 

3235 and inherit_value is not None 

3236 and field_value == inherit_value 

3237 ): 

3238 quick_fix = propose_remove_range_quick_fix( 

3239 proposed_title="Remove redundant definition" 

3240 ) 

3241 lint_state.emit_diagnostic( 

3242 kvpair_range_te, 

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

3244 "informational", 

3245 "debputy", 

3246 quickfixes=[quick_fix], 

3247 ) 

3248 for ( 

3249 field_name, 

3250 normalized_field_name, 

3251 field_range, 

3252 duplicates, 

3253 used_fields, 

3254 ) in seen_fields.values(): 

3255 if not duplicates: 

3256 continue 

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

3258 via_aliases_msg = " (via aliases)" 

3259 else: 

3260 via_aliases_msg = "" 

3261 for dup_range in duplicates: 

3262 lint_state.emit_diagnostic( 

3263 dup_range, 

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

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

3266 "error", 

3267 "Policy 5.1", 

3268 related_information=[ 

3269 lint_state.related_diagnostic_information( 

3270 field_range, 

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

3272 ), 

3273 ], 

3274 ) 

3275 

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

3277 key_lc = key.lower() 

3278 key_norm = normalize_dctrl_field_name(key_lc) 

3279 return self.stanza_fields[key_norm] 

3280 

3281 def __len__(self) -> int: 

3282 return len(self.stanza_fields) 

3283 

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

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

3286 

3287 def omitted_field_diagnostics( 

3288 self, 

3289 lint_state: LintState, 

3290 deb822_file: Deb822FileElement, 

3291 stanza: Deb822ParagraphElement, 

3292 stanza_position: "TEPosition", 

3293 *, 

3294 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3295 representation_field_range: Range | None = None, 

3296 ) -> None: 

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

3298 _, representation_field_range = self.stanza_representation( 

3299 stanza, 

3300 stanza_position, 

3301 ) 

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

3303 if known_field.name in stanza: 

3304 continue 

3305 

3306 known_field.field_omitted_diagnostics( 

3307 deb822_file, 

3308 representation_field_range, 

3309 stanza, 

3310 stanza_position, 

3311 inherit_from_stanza, 

3312 lint_state, 

3313 ) 

3314 

3315 def _paragraph_representation_field( 

3316 self, 

3317 paragraph: Deb822ParagraphElement, 

3318 ) -> Deb822KeyValuePairElement: 

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

3320 

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

3322 return field_name 

3323 

3324 def stanza_representation( 

3325 self, 

3326 stanza: Deb822ParagraphElement, 

3327 stanza_position: TEPosition, 

3328 ) -> tuple[Deb822KeyValuePairElement, TERange]: 

3329 representation_field = self._paragraph_representation_field(stanza) 

3330 representation_field_range = representation_field.range_in_parent().relative_to( 

3331 stanza_position 

3332 ) 

3333 return representation_field, representation_field_range 

3334 

3335 def reformat_stanza( 

3336 self, 

3337 effective_preference: "EffectiveFormattingPreference", 

3338 stanza: Deb822ParagraphElement, 

3339 stanza_range: TERange, 

3340 formatter: FormatterCallback, 

3341 position_codec: LintCapablePositionCodec, 

3342 lines: list[str], 

3343 ) -> Iterable[TextEdit]: 

3344 for field_name in stanza: 

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

3346 if known_field is None: 

3347 continue 

3348 kvpair = stanza.get_kvpair_element(field_name) 

3349 yield from known_field.reformat_field( 

3350 effective_preference, 

3351 stanza_range, 

3352 kvpair, 

3353 formatter, 

3354 position_codec, 

3355 lines, 

3356 ) 

3357 

3358 

3359@dataclasses.dataclass(slots=True) 

3360class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3361 pass 

3362 

3363 

3364@dataclasses.dataclass(slots=True) 

3365class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]): 

3366 

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

3368 return normalize_dctrl_field_name(field_name) 

3369 

3370 

3371@dataclasses.dataclass(slots=True) 

3372class DTestsCtrlStanzaMetadata(StanzaMetadata[DTestsCtrlKnownField]): 

3373 

3374 def omitted_field_diagnostics( 

3375 self, 

3376 lint_state: LintState, 

3377 deb822_file: Deb822FileElement, 

3378 stanza: Deb822ParagraphElement, 

3379 stanza_position: "TEPosition", 

3380 *, 

3381 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3382 representation_field_range: Range | None = None, 

3383 ) -> None: 

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

3385 _, representation_field_range = self.stanza_representation( 

3386 stanza, 

3387 stanza_position, 

3388 ) 

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

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

3391 lint_state.emit_diagnostic( 

3392 representation_field_range, 

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

3394 "error", 

3395 # TODO: Better authority_reference 

3396 auth_ref, 

3397 ) 

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

3399 lint_state.emit_diagnostic( 

3400 representation_field_range, 

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

3402 "error", 

3403 # TODO: Better authority_reference 

3404 auth_ref, 

3405 ) 

3406 

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

3408 # always do the super call. 

3409 super(DTestsCtrlStanzaMetadata, self).omitted_field_diagnostics( 

3410 lint_state, 

3411 deb822_file, 

3412 stanza, 

3413 stanza_position, 

3414 representation_field_range=representation_field_range, 

3415 inherit_from_stanza=inherit_from_stanza, 

3416 ) 

3417 

3418 

3419@dataclasses.dataclass(slots=True) 

3420class DebianWatchStanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3421 

3422 def omitted_field_diagnostics( 

3423 self, 

3424 lint_state: LintState, 

3425 deb822_file: Deb822FileElement, 

3426 stanza: Deb822ParagraphElement, 

3427 stanza_position: "TEPosition", 

3428 *, 

3429 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3430 representation_field_range: Range | None = None, 

3431 ) -> None: 

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

3433 _, representation_field_range = self.stanza_representation( 

3434 stanza, 

3435 stanza_position, 

3436 ) 

3437 

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

3439 self.stanza_type_name != "Header" 

3440 and "Source" not in stanza 

3441 and "Template" not in stanza 

3442 ): 

3443 lint_state.emit_diagnostic( 

3444 representation_field_range, 

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

3446 "error", 

3447 # TODO: Better authority_reference 

3448 "debputy", 

3449 ) 

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

3451 # call until this error is resolved. 

3452 return 

3453 

3454 super(DebianWatchStanzaMetadata, self).omitted_field_diagnostics( 

3455 lint_state, 

3456 deb822_file, 

3457 stanza, 

3458 stanza_position, 

3459 representation_field_range=representation_field_range, 

3460 inherit_from_stanza=inherit_from_stanza, 

3461 ) 

3462 

3463 

3464def lsp_reference_data_dir() -> str: 

3465 return os.path.join( 

3466 os.path.dirname(__file__), 

3467 "data", 

3468 ) 

3469 

3470 

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

3472 

3473 def __init__(self) -> None: 

3474 self._is_initialized = False 

3475 self._data: Deb822ReferenceData | None = None 

3476 

3477 @property 

3478 def reference_data_basename(self) -> str: 

3479 raise NotImplementedError 

3480 

3481 def _new_field( 

3482 self, 

3483 name: str, 

3484 field_value_type: FieldValueClass, 

3485 ) -> F: 

3486 raise NotImplementedError 

3487 

3488 def _reference_data(self) -> Deb822ReferenceData: 

3489 ref = self._data 

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

3491 return ref 

3492 

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

3494 self.reference_data_basename 

3495 ) 

3496 

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

3498 raw = MANIFEST_YAML.load(fd) 

3499 

3500 attr_path = AttributePath.root_path(p) 

3501 try: 

3502 ref = DEB822_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

3503 except ManifestParseException as e: 

3504 raise ValueError( 

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

3506 ) from e 

3507 self._data = ref 

3508 return ref 

3509 

3510 @property 

3511 def is_initialized(self) -> bool: 

3512 return self._is_initialized 

3513 

3514 def ensure_initialized(self) -> None: 

3515 if self.is_initialized: 

3516 return 

3517 # Enables us to use __getitem__ 

3518 self._is_initialized = True 

3519 ref_data = self._reference_data() 

3520 ref_defs = ref_data.get("definitions") 

3521 variables = {} 

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

3523 for ref_variable in ref_variables: 

3524 name = ref_variable["name"] 

3525 fallback = ref_variable["fallback"] 

3526 variables[name] = fallback 

3527 

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

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

3530 return None 

3531 try: 

3532 return template.format(**variables) 

3533 except ValueError as e: 

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

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

3536 

3537 for ref_stanza_type in ref_data["stanza_types"]: 

3538 stanza_name = ref_stanza_type["stanza_name"] 

3539 stanza = self[stanza_name] 

3540 stanza_fields = dict(stanza.stanza_fields) 

3541 stanza.stanza_fields = stanza_fields 

3542 for ref_field in ref_stanza_type["fields"]: 

3543 _resolve_field( 

3544 ref_field, 

3545 stanza_fields, 

3546 self._new_field, 

3547 _resolve_doc, 

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

3549 ) 

3550 

3551 def file_metadata_applies_to_file( 

3552 self, 

3553 deb822_file: Deb822FileElement | None, 

3554 ) -> bool: 

3555 return deb822_file is not None 

3556 

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

3558 return self.guess_stanza_classification_by_idx(stanza_idx) 

3559 

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

3561 raise NotImplementedError 

3562 

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

3564 raise NotImplementedError 

3565 

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

3567 raise NotImplementedError 

3568 

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

3570 try: 

3571 return self[item] 

3572 except KeyError: 

3573 return None 

3574 

3575 def reformat( 

3576 self, 

3577 effective_preference: "EffectiveFormattingPreference", 

3578 deb822_file: Deb822FileElement, 

3579 formatter: FormatterCallback, 

3580 _content: str, 

3581 position_codec: LintCapablePositionCodec, 

3582 lines: list[str], 

3583 ) -> Iterable[TextEdit]: 

3584 stanza_idx = -1 

3585 for token_or_element in deb822_file.iter_parts(): 

3586 if isinstance(token_or_element, Deb822ParagraphElement): 

3587 stanza_range = token_or_element.range_in_parent() 

3588 stanza_idx += 1 

3589 stanza_metadata = self.classify_stanza(token_or_element, stanza_idx) 

3590 yield from stanza_metadata.reformat_stanza( 

3591 effective_preference, 

3592 token_or_element, 

3593 stanza_range, 

3594 formatter, 

3595 position_codec, 

3596 lines, 

3597 ) 

3598 else: 

3599 token_range = token_or_element.range_in_parent() 

3600 yield from trim_end_of_line_whitespace( 

3601 position_codec, 

3602 lines, 

3603 line_range=range( 

3604 token_range.start_pos.line_position, 

3605 token_range.end_pos.line_position, 

3606 ), 

3607 ) 

3608 

3609 

3610_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata( 

3611 "Source", 

3612 SOURCE_FIELDS, 

3613 is_substvars_allowed_in_stanza=False, 

3614) 

3615_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata( 

3616 "Package", 

3617 BINARY_FIELDS, 

3618 is_substvars_allowed_in_stanza=True, 

3619) 

3620 

3621_DEP5_HEADER_STANZA = Dep5StanzaMetadata( 

3622 "Header", 

3623 _DEP5_HEADER_FIELDS, 

3624 is_substvars_allowed_in_stanza=False, 

3625) 

3626_DEP5_FILES_STANZA = Dep5StanzaMetadata( 

3627 "Files", 

3628 _DEP5_FILES_FIELDS, 

3629 is_substvars_allowed_in_stanza=False, 

3630) 

3631_DEP5_LICENSE_STANZA = Dep5StanzaMetadata( 

3632 "License", 

3633 _DEP5_LICENSE_FIELDS, 

3634 is_substvars_allowed_in_stanza=False, 

3635) 

3636 

3637_DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata( 

3638 "Tests", 

3639 _DTESTSCTRL_FIELDS, 

3640 is_substvars_allowed_in_stanza=False, 

3641) 

3642 

3643_WATCH_HEADER_HEADER_STANZA = DebianWatchStanzaMetadata( 

3644 "Header", 

3645 _DWATCH_HEADER_FIELDS, 

3646 is_substvars_allowed_in_stanza=False, 

3647) 

3648_WATCH_SOURCE_STANZA = DebianWatchStanzaMetadata( 

3649 "Source", 

3650 _DWATCH_SOURCE_FIELDS, 

3651 is_substvars_allowed_in_stanza=False, 

3652) 

3653 

3654 

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

3656 

3657 @property 

3658 def reference_data_basename(self) -> str: 

3659 return "debian_copyright_reference_data.yaml" 

3660 

3661 def _new_field( 

3662 self, 

3663 name: str, 

3664 field_value_type: FieldValueClass, 

3665 ) -> F: 

3666 return Deb822KnownField(name, field_value_type) 

3667 

3668 def file_metadata_applies_to_file( 

3669 self, 

3670 deb822_file: Deb822FileElement | None, 

3671 ) -> bool: 

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

3673 return False 

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

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

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

3677 return False 

3678 

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

3680 if part.is_error: 

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

3682 return False 

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

3684 break 

3685 return True 

3686 

3687 def classify_stanza( 

3688 self, 

3689 stanza: Deb822ParagraphElement, 

3690 stanza_idx: int, 

3691 ) -> Dep5StanzaMetadata: 

3692 self.ensure_initialized() 

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

3694 return _DEP5_HEADER_STANZA 

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

3696 if "Files" in stanza: 

3697 return _DEP5_FILES_STANZA 

3698 return _DEP5_LICENSE_STANZA 

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

3700 

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

3702 self.ensure_initialized() 

3703 if stanza_idx == 0: 

3704 return _DEP5_HEADER_STANZA 

3705 if stanza_idx > 0: 

3706 return _DEP5_FILES_STANZA 

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

3708 

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

3710 self.ensure_initialized() 

3711 # Order assumption made in the LSP code. 

3712 yield _DEP5_HEADER_STANZA 

3713 yield _DEP5_FILES_STANZA 

3714 yield _DEP5_LICENSE_STANZA 

3715 

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

3717 self.ensure_initialized() 

3718 if item == "Header": 

3719 return _DEP5_HEADER_STANZA 

3720 if item == "Files": 

3721 return _DEP5_FILES_STANZA 

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

3723 return _DEP5_LICENSE_STANZA 

3724 raise KeyError(item) 

3725 

3726 

3727class DebianWatch5FileMetadata( 

3728 Deb822FileMetadata[DebianWatchStanzaMetadata, Deb822KnownField] 

3729): 

3730 

3731 @property 

3732 def reference_data_basename(self) -> str: 

3733 return "debian_watch_reference_data.yaml" 

3734 

3735 def _new_field( 

3736 self, 

3737 name: str, 

3738 field_value_type: FieldValueClass, 

3739 ) -> F: 

3740 return Deb822KnownField(name, field_value_type) 

3741 

3742 def file_metadata_applies_to_file( 

3743 self, deb822_file: Deb822FileElement | None 

3744 ) -> bool: 

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

3746 return False 

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

3748 

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

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

3751 return False 

3752 

3753 try: 

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

3755 return False 

3756 except (ValueError, IndexError, TypeError): 

3757 return False 

3758 

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

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

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

3762 return False 

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

3764 break 

3765 return True 

3766 

3767 def classify_stanza( 

3768 self, 

3769 stanza: Deb822ParagraphElement, 

3770 stanza_idx: int, 

3771 ) -> DebianWatchStanzaMetadata: 

3772 self.ensure_initialized() 

3773 if stanza_idx == 0: 

3774 return _WATCH_HEADER_HEADER_STANZA 

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

3776 return _WATCH_SOURCE_STANZA 

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

3778 

3779 def guess_stanza_classification_by_idx( 

3780 self, stanza_idx: int 

3781 ) -> DebianWatchStanzaMetadata: 

3782 self.ensure_initialized() 

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

3784 return _WATCH_HEADER_HEADER_STANZA 

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

3786 return _WATCH_SOURCE_STANZA 

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

3788 

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

3790 self.ensure_initialized() 

3791 # Order assumption made in the LSP code. 

3792 yield _WATCH_HEADER_HEADER_STANZA 

3793 yield _WATCH_SOURCE_STANZA 

3794 

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

3796 self.ensure_initialized() 

3797 if item == "Header": 

3798 return _WATCH_HEADER_HEADER_STANZA 

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

3800 return _WATCH_SOURCE_STANZA 

3801 raise KeyError(item) 

3802 

3803 

3804def _resolve_keyword( 

3805 ref_value: StaticValue, 

3806 known_values: dict[str, Keyword], 

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

3808 translation_context: str, 

3809) -> None: 

3810 value_key = ref_value["value"] 

3811 changes = { 

3812 "translation_context": translation_context, 

3813 } 

3814 try: 

3815 known_value = known_values[value_key] 

3816 except KeyError: 

3817 known_value = Keyword(value_key) 

3818 known_values[value_key] = known_value 

3819 else: 

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

3821 raise ValueError( 

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

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

3824 ) 

3825 value_doc = ref_value.get("documentation") 

3826 if value_doc is not None: 

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

3828 changes["long_description"] = resolve_template( 

3829 value_doc.get("long_description") 

3830 ) 

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

3832 changes["is_exclusive"] = is_exclusive 

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

3834 changes["sort_text"] = sort_key 

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

3836 changes["usage_hint"] = usage_hint 

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

3838 known_value = known_value.replace(**changes) 

3839 known_values[value_key] = known_value 

3840 

3841 _expand_aliases( 

3842 known_value, 

3843 known_values, 

3844 operator.attrgetter("value"), 

3845 ref_value.get("aliases"), 

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

3847 ) 

3848 

3849 

3850def _resolve_field( 

3851 ref_field: Deb822Field, 

3852 stanza_fields: dict[str, F], 

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

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

3855 translation_context: str, 

3856) -> None: 

3857 field_name = ref_field["canonical_name"] 

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

3859 doc = ref_field.get("documentation") 

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

3861 norm_field_name = normalize_dctrl_field_name(field_name.lower()) 

3862 

3863 try: 

3864 field = stanza_fields[norm_field_name] 

3865 except KeyError: 

3866 field = field_constructor( 

3867 field_name, 

3868 field_value_type, 

3869 ) 

3870 stanza_fields[norm_field_name] = field 

3871 else: 

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

3873 _error( 

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

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

3876 ) 

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

3878 _error( 

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

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

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

3882 ) 

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

3884 raise ValueError( 

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

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

3887 ) 

3888 

3889 if doc is not None: 

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

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

3892 

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

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

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

3896 field.deprecated_with_no_replacement = ref_field.get( 

3897 "is_obsolete_without_replacement", False 

3898 ) 

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

3900 field.translation_context = translation_context 

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

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

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

3904 field.unknown_value_severity = ( 

3905 None if unknown_value_severity == "none" else unknown_value_severity 

3906 ) 

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

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

3909 field.is_substvars_disabled_even_if_allowed_by_stanza = not ref_field.get( 

3910 "supports_substvars", 

3911 True, 

3912 ) 

3913 field.inheritable_from_other_stanza = ref_field.get( 

3914 "inheritable_from_other_stanza", 

3915 False, 

3916 ) 

3917 

3918 known_values = field.known_values 

3919 if known_values is None: 

3920 known_values = {} 

3921 else: 

3922 known_values = dict(known_values) 

3923 

3924 for ref_value in ref_values: 

3925 _resolve_keyword(ref_value, known_values, resolve_template, translation_context) 

3926 

3927 if known_values: 

3928 field.known_values = known_values 

3929 

3930 _expand_aliases( 

3931 field, 

3932 stanza_fields, 

3933 operator.attrgetter("name"), 

3934 ref_field.get("aliases"), 

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

3936 ) 

3937 

3938 

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

3940 

3941 

3942def _expand_aliases( 

3943 item: A, 

3944 item_container: dict[str, A], 

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

3946 aliases_ref: list[Alias] | None, 

3947 doc_template: str, 

3948) -> None: 

3949 if aliases_ref is None: 

3950 return 

3951 name = canonical_name_resolver(item) 

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

3953 for alias_ref in aliases_ref: 

3954 alias_name = alias_ref["alias"] 

3955 alias_doc = item.long_description 

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

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

3958 if alias_doc: 

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

3960 else: 

3961 alias_doc = doc_suffix 

3962 alias_field = item.replace( 

3963 long_description=alias_doc, 

3964 is_alias_of=name, 

3965 is_completion_suggestion=is_completion_suggestion, 

3966 ) 

3967 alias_key = alias_name.lower() 

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

3969 existing_name = canonical_name_resolver(item_container[alias_key]) 

3970 assert ( 

3971 existing_name is not None 

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

3973 raise ValueError( 

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

3975 ) 

3976 item_container[alias_key] = alias_field 

3977 

3978 

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

3980 

3981 @property 

3982 def reference_data_basename(self) -> str: 

3983 return "debian_control_reference_data.yaml" 

3984 

3985 def _new_field( 

3986 self, 

3987 name: str, 

3988 field_value_type: FieldValueClass, 

3989 ) -> F: 

3990 return DctrlKnownField(name, field_value_type) 

3991 

3992 def guess_stanza_classification_by_idx( 

3993 self, 

3994 stanza_idx: int, 

3995 ) -> DctrlStanzaMetadata: 

3996 self.ensure_initialized() 

3997 if stanza_idx == 0: 

3998 return _DCTRL_SOURCE_STANZA 

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

4000 return _DCTRL_PACKAGE_STANZA 

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

4002 

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

4004 self.ensure_initialized() 

4005 # Order assumption made in the LSP code. 

4006 yield _DCTRL_SOURCE_STANZA 

4007 yield _DCTRL_PACKAGE_STANZA 

4008 

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

4010 self.ensure_initialized() 

4011 if item == "Source": 

4012 return _DCTRL_SOURCE_STANZA 

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

4014 return _DCTRL_PACKAGE_STANZA 

4015 raise KeyError(item) 

4016 

4017 def reformat( 

4018 self, 

4019 effective_preference: "EffectiveFormattingPreference", 

4020 deb822_file: Deb822FileElement, 

4021 formatter: FormatterCallback, 

4022 content: str, 

4023 position_codec: LintCapablePositionCodec, 

4024 lines: list[str], 

4025 ) -> Iterable[TextEdit]: 

4026 edits = list( 

4027 super().reformat( 

4028 effective_preference, 

4029 deb822_file, 

4030 formatter, 

4031 content, 

4032 position_codec, 

4033 lines, 

4034 ) 

4035 ) 

4036 

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

4038 not effective_preference.deb822_normalize_stanza_order 

4039 or deb822_file.find_first_error_element() is not None 

4040 ): 

4041 return edits 

4042 names = [] 

4043 for idx, stanza in enumerate(deb822_file): 

4044 if idx < 2: 

4045 continue 

4046 name = stanza.get("Package") 

4047 if name is None: 

4048 return edits 

4049 names.append(name) 

4050 

4051 reordered = sorted(names) 

4052 if names == reordered: 

4053 return edits 

4054 

4055 if edits: 

4056 content = apply_text_edits(content, lines, edits) 

4057 lines = content.splitlines(keepends=True) 

4058 deb822_file = parse_deb822_file( 

4059 lines, 

4060 accept_files_with_duplicated_fields=True, 

4061 accept_files_with_error_tokens=True, 

4062 ) 

4063 

4064 stanzas = list(deb822_file) 

4065 reordered_stanza = stanzas[:2] + sorted( 

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

4067 ) 

4068 bits = [] 

4069 stanza_idx = 0 

4070 for token_or_element in deb822_file.iter_parts(): 

4071 if isinstance(token_or_element, Deb822ParagraphElement): 

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

4073 stanza_idx += 1 

4074 else: 

4075 bits.append(token_or_element.convert_to_text()) 

4076 

4077 new_content = "".join(bits) 

4078 

4079 return [ 

4080 TextEdit( 

4081 Range( 

4082 Position(0, 0), 

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

4084 ), 

4085 new_content, 

4086 ) 

4087 ] 

4088 

4089 

4090class DTestsCtrlFileMetadata( 

4091 Deb822FileMetadata[DTestsCtrlStanzaMetadata, DTestsCtrlKnownField] 

4092): 

4093 

4094 @property 

4095 def reference_data_basename(self) -> str: 

4096 return "debian_tests_control_reference_data.yaml" 

4097 

4098 def _new_field( 

4099 self, 

4100 name: str, 

4101 field_value_type: FieldValueClass, 

4102 ) -> F: 

4103 return DTestsCtrlKnownField(name, field_value_type) 

4104 

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

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

4107 self.ensure_initialized() 

4108 return _DTESTSCTRL_STANZA 

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

4110 

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

4112 self.ensure_initialized() 

4113 yield _DTESTSCTRL_STANZA 

4114 

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

4116 self.ensure_initialized() 

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

4118 return _DTESTSCTRL_STANZA 

4119 raise KeyError(item) 

4120 

4121 

4122TRANSLATABLE_DEB822_FILE_METADATA: Sequence[ 

4123 Callable[[], Deb822FileMetadata[Any, Any]] 

4124] = [ 

4125 DctrlFileMetadata, 

4126 Dep5FileMetadata, 

4127 DTestsCtrlFileMetadata, 

4128]