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

1395 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-09-06 13:40 +0000

1import collections 

2import dataclasses 

3import functools 

4import importlib.resources 

5import itertools 

6import operator 

7import os.path 

8import re 

9import textwrap 

10from abc import ABC 

11from typing import ( 

12 FrozenSet, 

13 Optional, 

14 cast, 

15 List, 

16 Generic, 

17 TypeVar, 

18 Union, 

19 Tuple, 

20 Any, 

21 Set, 

22 TYPE_CHECKING, 

23 Dict, 

24 Self, 

25) 

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

27 

28from debian.debian_support import DpkgArchTable, Version 

29 

30import debputy.lsp.data.deb822_data as deb822_ref_data_dir 

31from debputy.filesystem_scan import VirtualPathBase 

32from debputy.linting.lint_util import LintState, with_range_in_continuous_parts 

33from debputy.linting.lint_util import te_range_to_lsp 

34from debputy.lsp.diagnostics import LintSeverity 

35from debputy.lsp.lsp_reference_keyword import ( 

36 Keyword, 

37 allowed_values, 

38 format_comp_item_synopsis_doc, 

39 LSP_DATA_DOMAIN, 

40 ALL_PUBLIC_NAMED_STYLES_AS_KEYWORDS, 

41) 

42from debputy.lsp.quickfixes import ( 

43 propose_correct_text_quick_fix, 

44 propose_remove_range_quick_fix, 

45) 

46from debputy.lsp.ref_models.deb822_reference_parse_models import ( 

47 Deb822ReferenceData, 

48 DEB822_REFERENCE_DATA_PARSER, 

49 FieldValueClass, 

50 StaticValue, 

51 Deb822Field, 

52 UsageHint, 

53 Alias, 

54) 

55from debputy.lsp.text_edit import apply_text_edits 

56from debputy.lsp.text_util import ( 

57 normalize_dctrl_field_name, 

58 LintCapablePositionCodec, 

59 trim_end_of_line_whitespace, 

60) 

61from debian._deb822_repro.parsing import ( 

62 Deb822KeyValuePairElement, 

63 LIST_SPACE_SEPARATED_INTERPRETATION, 

64 Deb822ParagraphElement, 

65 Deb822FileElement, 

66 Interpretation, 

67 parse_deb822_file, 

68 Deb822ParsedTokenList, 

69 Deb822ValueLineElement, 

70) 

71from debian._deb822_repro.tokens import ( 

72 Deb822FieldNameToken, 

73) 

74from debian._deb822_repro.types import FormatterCallback, TE 

75from debputy.lsp.vendoring.wrap_and_sort import _sort_packages_key 

76from debputy.lsprotocol.types import ( 

77 DiagnosticTag, 

78 Range, 

79 TextEdit, 

80 Position, 

81 CompletionItem, 

82 MarkupContent, 

83 CompletionItemTag, 

84 MarkupKind, 

85 CompletionItemKind, 

86 CompletionItemLabelDetails, 

87) 

88from debputy.manifest_parser.exceptions import ManifestParseException 

89from debputy.manifest_parser.util import AttributePath 

90from debputy.path_matcher import BasenameGlobMatch 

91from debputy.plugin.api import VirtualPath 

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

93from debputy.yaml import MANIFEST_YAML 

94 

95try: 

96 from debian._deb822_repro.locatable import ( 

97 Position as TEPosition, 

98 Range as TERange, 

99 START_POSITION, 

100 ) 

101except ImportError: 

102 pass 

103 

104 

105if TYPE_CHECKING: 

106 from debputy.lsp.maint_prefs import EffectiveFormattingPreference 

107 from debputy.lsp.debputy_ls import DebputyLanguageServer 

108 

109 

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

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

112 

113 

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

115 

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

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

118_RE_SYNOPSIS_IS_TEMPLATE = re.compile( 

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

120) 

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

122CURRENT_STANDARDS_VERSION = Version("4.7.4") 

123 

124 

125CustomFieldCheck = Callable[ 

126 [ 

127 "F", 

128 Deb822FileElement, 

129 Deb822KeyValuePairElement, 

130 "TERange", 

131 "TERange", 

132 Deb822ParagraphElement, 

133 "TEPosition", 

134 LintState, 

135 ], 

136 None, 

137] 

138 

139 

140@functools.lru_cache 

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

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

143 return { 

144 f.lower(): f 

145 for f in ( 

146 "Pre-Depends", 

147 "Depends", 

148 "Recommends", 

149 "Suggests", 

150 "Enhances", 

151 "Conflicts", 

152 "Breaks", 

153 "Replaces", 

154 "Provides", 

155 "Built-Using", 

156 "Static-Built-Using", 

157 ) 

158 } 

159 

160 

161@functools.lru_cache 

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

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

164 return { 

165 f.lower(): f 

166 for f in ( 

167 "Build-Depends", 

168 "Build-Depends-Arch", 

169 "Build-Depends-Indep", 

170 "Build-Conflicts", 

171 "Build-Conflicts-Arch", 

172 "Build-Conflicts-Indep", 

173 ) 

174 } 

175 

176 

177ALL_SECTIONS_WITHOUT_COMPONENT = frozenset( 

178 [ 

179 "admin", 

180 "cli-mono", 

181 "comm", 

182 "database", 

183 "debian-installer", 

184 "debug", 

185 "devel", 

186 "doc", 

187 "editors", 

188 "education", 

189 "electronics", 

190 "embedded", 

191 "fonts", 

192 "games", 

193 "gnome", 

194 "gnu-r", 

195 "gnustep", 

196 "golang", 

197 "graphics", 

198 "hamradio", 

199 "haskell", 

200 "httpd", 

201 "interpreters", 

202 "introspection", 

203 "java", 

204 "javascript", 

205 "kde", 

206 "kernel", 

207 "libdevel", 

208 "libs", 

209 "lisp", 

210 "localization", 

211 "mail", 

212 "math", 

213 "metapackages", 

214 "misc", 

215 "net", 

216 "news", 

217 "ocaml", 

218 "oldlibs", 

219 "otherosfs", 

220 "perl", 

221 "php", 

222 "python", 

223 "ruby", 

224 "rust", 

225 "science", 

226 "shells", 

227 "sound", 

228 "tasks", 

229 "tex", 

230 "text", 

231 "utils", 

232 "vcs", 

233 "video", 

234 "virtual", 

235 "web", 

236 "x11", 

237 "xfce", 

238 "zope", 

239 ] 

240) 

241 

242ALL_COMPONENTS = frozenset( 

243 [ 

244 "main", 

245 "restricted", # Ubuntu 

246 "non-free", 

247 "non-free-firmware", 

248 "contrib", 

249 ] 

250) 

251 

252 

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

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

255 

256 

257def _complete_section_sort_hint( 

258 keyword: Keyword, 

259 _lint_state: LintState, 

260 stanza_parts: Sequence[Deb822ParagraphElement], 

261 _value_being_completed: str, 

262) -> str | None: 

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

264 pkg = stanza.get("Package") 

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

266 break 

267 else: 

268 return None 

269 section = package_name_to_section(pkg) 

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

271 keyword_section = value_parts[-1] 

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

273 if section is None: 

274 if keyword_component == "": 

275 return keyword_section 

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

277 if keyword_section != section: 

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

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

280 

281 

282ALL_SECTIONS = allowed_values( 

283 Keyword( 

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

285 sort_text=_complete_section_sort_hint, 

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

287 ) 

288 for c, s in itertools.product( 

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

290 ALL_SECTIONS_WITHOUT_COMPONENT, 

291 ) 

292) 

293 

294 

295def all_architectures_and_wildcards( 

296 arch2table, *, allow_negations: bool = False 

297) -> Iterable[str | Keyword]: 

298 wildcards = set() 

299 yield Keyword( 

300 "any", 

301 is_exclusive=True, 

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

303 long_description=textwrap.dedent("""\ 

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

305 compiled for each and every architecture. 

306 

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

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

309 dpkg. 

310 """), 

311 ) 

312 yield Keyword( 

313 "all", 

314 is_exclusive=True, 

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

316 long_description=textwrap.dedent("""\ 

317 The package is an architecture independent package. This is 

318 typically appropriate for packages containing only scripts, 

319 data or documentation. 

320 

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

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

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

324 """), 

325 ) 

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

327 yield arch_name 

328 if allow_negations: 

329 yield f"!{arch_name}" 

330 cpu_wc = "any-" + quad_tuple.cpu_name 

331 os_wc = quad_tuple.os_name + "-any" 

332 if cpu_wc not in wildcards: 

333 yield cpu_wc 

334 if allow_negations: 

335 yield f"!{cpu_wc}" 

336 wildcards.add(cpu_wc) 

337 if os_wc not in wildcards: 

338 yield os_wc 

339 if allow_negations: 

340 yield f"!{os_wc}" 

341 wildcards.add(os_wc) 

342 # Add the remaining wildcards 

343 

344 

345@functools.lru_cache 

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

347 dpkg_arch_table = DpkgArchTable.load_arch_table() 

348 return frozenset( 

349 all_architectures_and_wildcards( 

350 dpkg_arch_table._arch2table, 

351 allow_negations=allow_negations, 

352 ) 

353 ) 

354 

355 

356def extract_first_value_and_position( 

357 kvpair: Deb822KeyValuePairElement, 

358 stanza_pos: "TEPosition", 

359 *, 

360 interpretation: Interpretation[ 

361 Deb822ParsedTokenList[Any, Any] 

362 ] = LIST_SPACE_SEPARATED_INTERPRETATION, 

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

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

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

366 kvpair_pos 

367 ) 

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

369 v = value_ref.value 

370 section_value_loc = value_ref.locatable 

371 value_range_te = section_value_loc.range_in_parent().relative_to( 

372 value_element_pos 

373 ) 

374 return v, value_range_te 

375 return None, None 

376 

377 

378def _sv_field_validation( 

379 known_field: "F", 

380 _deb822_file: Deb822FileElement, 

381 kvpair: Deb822KeyValuePairElement, 

382 _kvpair_range: "TERange", 

383 _field_name_range_te: "TERange", 

384 _stanza: Deb822ParagraphElement, 

385 stanza_position: "TEPosition", 

386 lint_state: LintState, 

387) -> None: 

388 sv_value, sv_value_range = extract_first_value_and_position( 

389 kvpair, 

390 stanza_position, 

391 ) 

392 m = _RE_SV.fullmatch(sv_value) 

393 if m is None: 

394 lint_state.emit_diagnostic( 

395 sv_value_range, 

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

397 "warning", 

398 known_field.unknown_value_authority, 

399 ) 

400 return 

401 

402 sv_version = Version(sv_value) 

403 if sv_version < CURRENT_STANDARDS_VERSION: 

404 lint_state.emit_diagnostic( 

405 sv_value_range, 

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

407 "informational", 

408 known_field.unknown_value_authority, 

409 ) 

410 return 

411 extra = m.group(2) 

412 if extra: 

413 extra_len = lint_state.position_codec.client_num_units(extra) 

414 lint_state.emit_diagnostic( 

415 TERange.between( 

416 TEPosition( 

417 sv_value_range.end_pos.line_position, 

418 sv_value_range.end_pos.cursor_position - extra_len, 

419 ), 

420 sv_value_range.end_pos, 

421 ), 

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

423 "informational", 

424 known_field.unknown_value_authority, 

425 quickfixes=[ 

426 propose_remove_range_quick_fix( 

427 proposed_title="Remove unnecessary version part" 

428 ) 

429 ], 

430 ) 

431 

432 

433def _dctrl_ma_field_validation( 

434 _known_field: "F", 

435 _deb822_file: Deb822FileElement, 

436 _kvpair: Deb822KeyValuePairElement, 

437 _kvpair_range: "TERange", 

438 _field_name_range: "TERange", 

439 stanza: Deb822ParagraphElement, 

440 stanza_position: "TEPosition", 

441 lint_state: LintState, 

442) -> None: 

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

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

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

446 ma_value, ma_value_range = extract_first_value_and_position( 

447 ma_kvpair, 

448 stanza_position, 

449 ) 

450 if ma_value == "same": 

451 lint_state.emit_diagnostic( 

452 ma_value_range, 

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

454 "error", 

455 "debputy", 

456 ) 

457 

458 

459def _udeb_only_field_validation( 

460 known_field: "F", 

461 _deb822_file: Deb822FileElement, 

462 _kvpair: Deb822KeyValuePairElement, 

463 _kvpair_range: "TERange", 

464 field_name_range: "TERange", 

465 stanza: Deb822ParagraphElement, 

466 _stanza_position: "TEPosition", 

467 lint_state: LintState, 

468) -> None: 

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

470 if package_type != "udeb": 

471 lint_state.emit_diagnostic( 

472 field_name_range, 

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

474 "warning", 

475 "debputy", 

476 ) 

477 

478 

479def _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 msg = ( 

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

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

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

3215 ) 

3216 lint_state.emit_diagnostic( 

3217 field_range, 

3218 msg, 

3219 "error", 

3220 known_else_where.missing_field_authority, 

3221 ) 

3222 continue 

3223 await known_field.field_diagnostics( 

3224 deb822_file, 

3225 kvpair, 

3226 stanza, 

3227 stanza_position_in_file, 

3228 kvpair_range_te, 

3229 lint_state, 

3230 field_name_typo_reported=field_name_typo_detected, 

3231 ) 

3232 

3233 inherit_value = ( 

3234 inherit_from_stanza.get(field_name) if inherit_from_stanza else None 

3235 ) 

3236 

3237 if ( 

3238 known_field.inheritable_from_other_stanza 

3239 and inherit_value is not None 

3240 and field_value == inherit_value 

3241 ): 

3242 quick_fix = propose_remove_range_quick_fix( 

3243 proposed_title="Remove redundant definition" 

3244 ) 

3245 lint_state.emit_diagnostic( 

3246 kvpair_range_te, 

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

3248 "informational", 

3249 "debputy", 

3250 quickfixes=[quick_fix], 

3251 ) 

3252 for ( 

3253 field_name, 

3254 normalized_field_name, 

3255 field_range, 

3256 duplicates, 

3257 used_fields, 

3258 ) in seen_fields.values(): 

3259 if not duplicates: 

3260 continue 

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

3262 via_aliases_msg = " (via aliases)" 

3263 else: 

3264 via_aliases_msg = "" 

3265 for dup_range in duplicates: 

3266 lint_state.emit_diagnostic( 

3267 dup_range, 

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

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

3270 "error", 

3271 "Policy 5.1", 

3272 related_information=[ 

3273 lint_state.related_diagnostic_information( 

3274 field_range, 

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

3276 ), 

3277 ], 

3278 ) 

3279 

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

3281 key_lc = key.lower() 

3282 key_norm = normalize_dctrl_field_name(key_lc) 

3283 return self.stanza_fields[key_norm] 

3284 

3285 def __len__(self) -> int: 

3286 return len(self.stanza_fields) 

3287 

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

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

3290 

3291 def omitted_field_diagnostics( 

3292 self, 

3293 lint_state: LintState, 

3294 deb822_file: Deb822FileElement, 

3295 stanza: Deb822ParagraphElement, 

3296 stanza_position: "TEPosition", 

3297 *, 

3298 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3299 representation_field_range: Range | None = None, 

3300 ) -> None: 

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

3302 _, representation_field_range = self.stanza_representation( 

3303 stanza, 

3304 stanza_position, 

3305 ) 

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

3307 if known_field.name in stanza: 

3308 continue 

3309 

3310 known_field.field_omitted_diagnostics( 

3311 deb822_file, 

3312 representation_field_range, 

3313 stanza, 

3314 stanza_position, 

3315 inherit_from_stanza, 

3316 lint_state, 

3317 ) 

3318 

3319 def _paragraph_representation_field( 

3320 self, 

3321 paragraph: Deb822ParagraphElement, 

3322 ) -> Deb822KeyValuePairElement: 

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

3324 

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

3326 return field_name 

3327 

3328 def stanza_representation( 

3329 self, 

3330 stanza: Deb822ParagraphElement, 

3331 stanza_position: TEPosition, 

3332 ) -> tuple[Deb822KeyValuePairElement, TERange]: 

3333 representation_field = self._paragraph_representation_field(stanza) 

3334 representation_field_range = representation_field.range_in_parent().relative_to( 

3335 stanza_position 

3336 ) 

3337 return representation_field, representation_field_range 

3338 

3339 def reformat_stanza( 

3340 self, 

3341 effective_preference: "EffectiveFormattingPreference", 

3342 stanza: Deb822ParagraphElement, 

3343 stanza_range: TERange, 

3344 formatter: FormatterCallback, 

3345 position_codec: LintCapablePositionCodec, 

3346 lines: list[str], 

3347 ) -> Iterable[TextEdit]: 

3348 for field_name in stanza: 

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

3350 if known_field is None: 

3351 continue 

3352 kvpair = stanza.get_kvpair_element(field_name) 

3353 yield from known_field.reformat_field( 

3354 effective_preference, 

3355 stanza_range, 

3356 kvpair, 

3357 formatter, 

3358 position_codec, 

3359 lines, 

3360 ) 

3361 

3362 

3363@dataclasses.dataclass(slots=True) 

3364class Dep5StanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3365 pass 

3366 

3367 

3368@dataclasses.dataclass(slots=True) 

3369class DctrlStanzaMetadata(StanzaMetadata[DctrlKnownField]): 

3370 

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

3372 return normalize_dctrl_field_name(field_name) 

3373 

3374 

3375@dataclasses.dataclass(slots=True) 

3376class DTestsCtrlStanzaMetadata(StanzaMetadata[DTestsCtrlKnownField]): 

3377 

3378 def omitted_field_diagnostics( 

3379 self, 

3380 lint_state: LintState, 

3381 deb822_file: Deb822FileElement, 

3382 stanza: Deb822ParagraphElement, 

3383 stanza_position: "TEPosition", 

3384 *, 

3385 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3386 representation_field_range: Range | None = None, 

3387 ) -> None: 

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

3389 _, representation_field_range = self.stanza_representation( 

3390 stanza, 

3391 stanza_position, 

3392 ) 

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

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

3395 lint_state.emit_diagnostic( 

3396 representation_field_range, 

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

3398 "error", 

3399 # TODO: Better authority_reference 

3400 auth_ref, 

3401 ) 

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

3403 lint_state.emit_diagnostic( 

3404 representation_field_range, 

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

3406 "error", 

3407 # TODO: Better authority_reference 

3408 auth_ref, 

3409 ) 

3410 

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

3412 # always do the super call. 

3413 super(DTestsCtrlStanzaMetadata, self).omitted_field_diagnostics( 

3414 lint_state, 

3415 deb822_file, 

3416 stanza, 

3417 stanza_position, 

3418 representation_field_range=representation_field_range, 

3419 inherit_from_stanza=inherit_from_stanza, 

3420 ) 

3421 

3422 

3423@dataclasses.dataclass(slots=True) 

3424class DebianWatchStanzaMetadata(StanzaMetadata[Deb822KnownField]): 

3425 

3426 def omitted_field_diagnostics( 

3427 self, 

3428 lint_state: LintState, 

3429 deb822_file: Deb822FileElement, 

3430 stanza: Deb822ParagraphElement, 

3431 stanza_position: "TEPosition", 

3432 *, 

3433 inherit_from_stanza: Deb822ParagraphElement | None = None, 

3434 representation_field_range: Range | None = None, 

3435 ) -> None: 

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

3437 _, representation_field_range = self.stanza_representation( 

3438 stanza, 

3439 stanza_position, 

3440 ) 

3441 

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

3443 self.stanza_type_name != "Header" 

3444 and "Source" not in stanza 

3445 and "Template" not in stanza 

3446 ): 

3447 lint_state.emit_diagnostic( 

3448 representation_field_range, 

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

3450 "error", 

3451 # TODO: Better authority_reference 

3452 "debputy", 

3453 ) 

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

3455 # call until this error is resolved. 

3456 return 

3457 

3458 super(DebianWatchStanzaMetadata, self).omitted_field_diagnostics( 

3459 lint_state, 

3460 deb822_file, 

3461 stanza, 

3462 stanza_position, 

3463 representation_field_range=representation_field_range, 

3464 inherit_from_stanza=inherit_from_stanza, 

3465 ) 

3466 

3467 

3468def lsp_reference_data_dir() -> str: 

3469 return os.path.join( 

3470 os.path.dirname(__file__), 

3471 "data", 

3472 ) 

3473 

3474 

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

3476 

3477 def __init__(self) -> None: 

3478 self._is_initialized = False 

3479 self._data: Deb822ReferenceData | None = None 

3480 

3481 @property 

3482 def reference_data_basename(self) -> str: 

3483 raise NotImplementedError 

3484 

3485 def _new_field( 

3486 self, 

3487 name: str, 

3488 field_value_type: FieldValueClass, 

3489 ) -> F: 

3490 raise NotImplementedError 

3491 

3492 def _reference_data(self) -> Deb822ReferenceData: 

3493 ref = self._data 

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

3495 return ref 

3496 

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

3498 self.reference_data_basename 

3499 ) 

3500 

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

3502 raw = MANIFEST_YAML.load(fd) 

3503 

3504 attr_path = AttributePath.root_path(p) 

3505 try: 

3506 ref = DEB822_REFERENCE_DATA_PARSER.parse_input(raw, attr_path) 

3507 except ManifestParseException as e: 

3508 raise ValueError( 

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

3510 ) from e 

3511 self._data = ref 

3512 return ref 

3513 

3514 @property 

3515 def is_initialized(self) -> bool: 

3516 return self._is_initialized 

3517 

3518 def ensure_initialized(self) -> None: 

3519 if self.is_initialized: 

3520 return 

3521 # Enables us to use __getitem__ 

3522 self._is_initialized = True 

3523 ref_data = self._reference_data() 

3524 ref_defs = ref_data.get("definitions") 

3525 variables = {} 

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

3527 for ref_variable in ref_variables: 

3528 name = ref_variable["name"] 

3529 fallback = ref_variable["fallback"] 

3530 variables[name] = fallback 

3531 

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

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

3534 return None 

3535 try: 

3536 return template.format(**variables) 

3537 except ValueError as e: 

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

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

3540 

3541 for ref_stanza_type in ref_data["stanza_types"]: 

3542 stanza_name = ref_stanza_type["stanza_name"] 

3543 stanza = self[stanza_name] 

3544 stanza_fields = dict(stanza.stanza_fields) 

3545 stanza.stanza_fields = stanza_fields 

3546 for ref_field in ref_stanza_type["fields"]: 

3547 _resolve_field( 

3548 ref_field, 

3549 stanza_fields, 

3550 self._new_field, 

3551 _resolve_doc, 

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

3553 ) 

3554 

3555 def file_metadata_applies_to_file( 

3556 self, 

3557 deb822_file: Deb822FileElement | None, 

3558 ) -> bool: 

3559 return deb822_file is not None 

3560 

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

3562 return self.guess_stanza_classification_by_idx(stanza_idx) 

3563 

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

3565 raise NotImplementedError 

3566 

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

3568 raise NotImplementedError 

3569 

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

3571 raise NotImplementedError 

3572 

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

3574 try: 

3575 return self[item] 

3576 except KeyError: 

3577 return None 

3578 

3579 def reformat( 

3580 self, 

3581 effective_preference: "EffectiveFormattingPreference", 

3582 deb822_file: Deb822FileElement, 

3583 formatter: FormatterCallback, 

3584 _content: str, 

3585 position_codec: LintCapablePositionCodec, 

3586 lines: list[str], 

3587 ) -> Iterable[TextEdit]: 

3588 stanza_idx = -1 

3589 for token_or_element in deb822_file.iter_parts(): 

3590 if isinstance(token_or_element, Deb822ParagraphElement): 

3591 stanza_range = token_or_element.range_in_parent() 

3592 stanza_idx += 1 

3593 stanza_metadata = self.classify_stanza(token_or_element, stanza_idx) 

3594 yield from stanza_metadata.reformat_stanza( 

3595 effective_preference, 

3596 token_or_element, 

3597 stanza_range, 

3598 formatter, 

3599 position_codec, 

3600 lines, 

3601 ) 

3602 else: 

3603 token_range = token_or_element.range_in_parent() 

3604 yield from trim_end_of_line_whitespace( 

3605 position_codec, 

3606 lines, 

3607 line_range=range( 

3608 token_range.start_pos.line_position, 

3609 token_range.end_pos.line_position, 

3610 ), 

3611 ) 

3612 

3613 

3614_DCTRL_SOURCE_STANZA = DctrlStanzaMetadata( 

3615 "Source", 

3616 SOURCE_FIELDS, 

3617 is_substvars_allowed_in_stanza=False, 

3618) 

3619_DCTRL_PACKAGE_STANZA = DctrlStanzaMetadata( 

3620 "Package", 

3621 BINARY_FIELDS, 

3622 is_substvars_allowed_in_stanza=True, 

3623) 

3624 

3625_DEP5_HEADER_STANZA = Dep5StanzaMetadata( 

3626 "Header", 

3627 _DEP5_HEADER_FIELDS, 

3628 is_substvars_allowed_in_stanza=False, 

3629) 

3630_DEP5_FILES_STANZA = Dep5StanzaMetadata( 

3631 "Files", 

3632 _DEP5_FILES_FIELDS, 

3633 is_substvars_allowed_in_stanza=False, 

3634) 

3635_DEP5_LICENSE_STANZA = Dep5StanzaMetadata( 

3636 "License", 

3637 _DEP5_LICENSE_FIELDS, 

3638 is_substvars_allowed_in_stanza=False, 

3639) 

3640 

3641_DTESTSCTRL_STANZA = DTestsCtrlStanzaMetadata( 

3642 "Tests", 

3643 _DTESTSCTRL_FIELDS, 

3644 is_substvars_allowed_in_stanza=False, 

3645) 

3646 

3647_WATCH_HEADER_HEADER_STANZA = DebianWatchStanzaMetadata( 

3648 "Header", 

3649 _DWATCH_HEADER_FIELDS, 

3650 is_substvars_allowed_in_stanza=False, 

3651) 

3652_WATCH_SOURCE_STANZA = DebianWatchStanzaMetadata( 

3653 "Source", 

3654 _DWATCH_SOURCE_FIELDS, 

3655 is_substvars_allowed_in_stanza=False, 

3656) 

3657 

3658 

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

3660 

3661 @property 

3662 def reference_data_basename(self) -> str: 

3663 return "debian_copyright_reference_data.yaml" 

3664 

3665 def _new_field( 

3666 self, 

3667 name: str, 

3668 field_value_type: FieldValueClass, 

3669 ) -> F: 

3670 return Deb822KnownField(name, field_value_type) 

3671 

3672 def file_metadata_applies_to_file( 

3673 self, 

3674 deb822_file: Deb822FileElement | None, 

3675 ) -> bool: 

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

3677 return False 

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

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

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

3681 return False 

3682 

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

3684 if part.is_error: 

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

3686 return False 

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

3688 break 

3689 return True 

3690 

3691 def classify_stanza( 

3692 self, 

3693 stanza: Deb822ParagraphElement, 

3694 stanza_idx: int, 

3695 ) -> Dep5StanzaMetadata: 

3696 self.ensure_initialized() 

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

3698 return _DEP5_HEADER_STANZA 

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

3700 if "Files" in stanza: 

3701 return _DEP5_FILES_STANZA 

3702 return _DEP5_LICENSE_STANZA 

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

3704 

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

3706 self.ensure_initialized() 

3707 if stanza_idx == 0: 

3708 return _DEP5_HEADER_STANZA 

3709 if stanza_idx > 0: 

3710 return _DEP5_FILES_STANZA 

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

3712 

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

3714 self.ensure_initialized() 

3715 # Order assumption made in the LSP code. 

3716 yield _DEP5_HEADER_STANZA 

3717 yield _DEP5_FILES_STANZA 

3718 yield _DEP5_LICENSE_STANZA 

3719 

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

3721 self.ensure_initialized() 

3722 if item == "Header": 

3723 return _DEP5_HEADER_STANZA 

3724 if item == "Files": 

3725 return _DEP5_FILES_STANZA 

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

3727 return _DEP5_LICENSE_STANZA 

3728 raise KeyError(item) 

3729 

3730 

3731class DebianWatch5FileMetadata( 

3732 Deb822FileMetadata[DebianWatchStanzaMetadata, Deb822KnownField] 

3733): 

3734 

3735 @property 

3736 def reference_data_basename(self) -> str: 

3737 return "debian_watch_reference_data.yaml" 

3738 

3739 def _new_field( 

3740 self, 

3741 name: str, 

3742 field_value_type: FieldValueClass, 

3743 ) -> F: 

3744 return Deb822KnownField(name, field_value_type) 

3745 

3746 def file_metadata_applies_to_file( 

3747 self, deb822_file: Deb822FileElement | None 

3748 ) -> bool: 

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

3750 return False 

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

3752 

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

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

3755 return False 

3756 

3757 try: 

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

3759 return False 

3760 except (ValueError, IndexError, TypeError): 

3761 return False 

3762 

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

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

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

3766 return False 

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

3768 break 

3769 return True 

3770 

3771 def classify_stanza( 

3772 self, 

3773 stanza: Deb822ParagraphElement, 

3774 stanza_idx: int, 

3775 ) -> DebianWatchStanzaMetadata: 

3776 self.ensure_initialized() 

3777 if stanza_idx == 0: 

3778 return _WATCH_HEADER_HEADER_STANZA 

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

3780 return _WATCH_SOURCE_STANZA 

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

3782 

3783 def guess_stanza_classification_by_idx( 

3784 self, stanza_idx: int 

3785 ) -> DebianWatchStanzaMetadata: 

3786 self.ensure_initialized() 

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

3788 return _WATCH_HEADER_HEADER_STANZA 

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

3790 return _WATCH_SOURCE_STANZA 

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

3792 

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

3794 self.ensure_initialized() 

3795 # Order assumption made in the LSP code. 

3796 yield _WATCH_HEADER_HEADER_STANZA 

3797 yield _WATCH_SOURCE_STANZA 

3798 

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

3800 self.ensure_initialized() 

3801 if item == "Header": 

3802 return _WATCH_HEADER_HEADER_STANZA 

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

3804 return _WATCH_SOURCE_STANZA 

3805 raise KeyError(item) 

3806 

3807 

3808def _resolve_keyword( 

3809 ref_value: StaticValue, 

3810 known_values: dict[str, Keyword], 

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

3812 translation_context: str, 

3813) -> None: 

3814 value_key = ref_value["value"] 

3815 changes = { 

3816 "translation_context": translation_context, 

3817 } 

3818 try: 

3819 known_value = known_values[value_key] 

3820 except KeyError: 

3821 known_value = Keyword(value_key) 

3822 known_values[value_key] = known_value 

3823 else: 

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

3825 raise ValueError( 

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

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

3828 ) 

3829 value_doc = ref_value.get("documentation") 

3830 if value_doc is not None: 

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

3832 changes["long_description"] = resolve_template( 

3833 value_doc.get("long_description") 

3834 ) 

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

3836 changes["is_exclusive"] = is_exclusive 

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

3838 changes["sort_text"] = sort_key 

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

3840 changes["usage_hint"] = usage_hint 

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

3842 known_value = known_value.replace(**changes) 

3843 known_values[value_key] = known_value 

3844 

3845 _expand_aliases( 

3846 known_value, 

3847 known_values, 

3848 operator.attrgetter("value"), 

3849 ref_value.get("aliases"), 

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

3851 ) 

3852 

3853 

3854def _resolve_field( 

3855 ref_field: Deb822Field, 

3856 stanza_fields: dict[str, F], 

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

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

3859 translation_context: str, 

3860) -> None: 

3861 field_name = ref_field["canonical_name"] 

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

3863 doc = ref_field.get("documentation") 

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

3865 norm_field_name = normalize_dctrl_field_name(field_name.lower()) 

3866 

3867 try: 

3868 field = stanza_fields[norm_field_name] 

3869 except KeyError: 

3870 field = field_constructor( 

3871 field_name, 

3872 field_value_type, 

3873 ) 

3874 stanza_fields[norm_field_name] = field 

3875 else: 

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

3877 _error( 

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

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

3880 ) 

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

3882 _error( 

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

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

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

3886 ) 

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

3888 raise ValueError( 

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

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

3891 ) 

3892 

3893 if doc is not None: 

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

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

3896 

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

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

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

3900 field.deprecated_with_no_replacement = ref_field.get( 

3901 "is_obsolete_without_replacement", False 

3902 ) 

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

3904 field.translation_context = translation_context 

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

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

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

3908 field.unknown_value_severity = ( 

3909 None if unknown_value_severity == "none" else unknown_value_severity 

3910 ) 

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

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

3913 field.is_substvars_disabled_even_if_allowed_by_stanza = not ref_field.get( 

3914 "supports_substvars", 

3915 True, 

3916 ) 

3917 field.inheritable_from_other_stanza = ref_field.get( 

3918 "inheritable_from_other_stanza", 

3919 False, 

3920 ) 

3921 

3922 known_values = field.known_values 

3923 if known_values is None: 

3924 known_values = {} 

3925 else: 

3926 known_values = dict(known_values) 

3927 

3928 for ref_value in ref_values: 

3929 _resolve_keyword(ref_value, known_values, resolve_template, translation_context) 

3930 

3931 if known_values: 

3932 field.known_values = known_values 

3933 

3934 _expand_aliases( 

3935 field, 

3936 stanza_fields, 

3937 operator.attrgetter("name"), 

3938 ref_field.get("aliases"), 

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

3940 ) 

3941 

3942 

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

3944 

3945 

3946def _expand_aliases( 

3947 item: A, 

3948 item_container: dict[str, A], 

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

3950 aliases_ref: list[Alias] | None, 

3951 doc_template: str, 

3952) -> None: 

3953 if aliases_ref is None: 

3954 return 

3955 name = canonical_name_resolver(item) 

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

3957 for alias_ref in aliases_ref: 

3958 alias_name = alias_ref["alias"] 

3959 alias_doc = item.long_description 

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

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

3962 if alias_doc: 

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

3964 else: 

3965 alias_doc = doc_suffix 

3966 alias_field = item.replace( 

3967 long_description=alias_doc, 

3968 is_alias_of=name, 

3969 is_completion_suggestion=is_completion_suggestion, 

3970 ) 

3971 alias_key = alias_name.lower() 

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

3973 existing_name = canonical_name_resolver(item_container[alias_key]) 

3974 assert ( 

3975 existing_name is not None 

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

3977 raise ValueError( 

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

3979 ) 

3980 item_container[alias_key] = alias_field 

3981 

3982 

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

3984 

3985 @property 

3986 def reference_data_basename(self) -> str: 

3987 return "debian_control_reference_data.yaml" 

3988 

3989 def _new_field( 

3990 self, 

3991 name: str, 

3992 field_value_type: FieldValueClass, 

3993 ) -> F: 

3994 return DctrlKnownField(name, field_value_type) 

3995 

3996 def guess_stanza_classification_by_idx( 

3997 self, 

3998 stanza_idx: int, 

3999 ) -> DctrlStanzaMetadata: 

4000 self.ensure_initialized() 

4001 if stanza_idx == 0: 

4002 return _DCTRL_SOURCE_STANZA 

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

4004 return _DCTRL_PACKAGE_STANZA 

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

4006 

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

4008 self.ensure_initialized() 

4009 # Order assumption made in the LSP code. 

4010 yield _DCTRL_SOURCE_STANZA 

4011 yield _DCTRL_PACKAGE_STANZA 

4012 

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

4014 self.ensure_initialized() 

4015 if item == "Source": 

4016 return _DCTRL_SOURCE_STANZA 

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

4018 return _DCTRL_PACKAGE_STANZA 

4019 raise KeyError(item) 

4020 

4021 def reformat( 

4022 self, 

4023 effective_preference: "EffectiveFormattingPreference", 

4024 deb822_file: Deb822FileElement, 

4025 formatter: FormatterCallback, 

4026 content: str, 

4027 position_codec: LintCapablePositionCodec, 

4028 lines: list[str], 

4029 ) -> Iterable[TextEdit]: 

4030 edits = list( 

4031 super().reformat( 

4032 effective_preference, 

4033 deb822_file, 

4034 formatter, 

4035 content, 

4036 position_codec, 

4037 lines, 

4038 ) 

4039 ) 

4040 

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

4042 not effective_preference.deb822_normalize_stanza_order 

4043 or deb822_file.find_first_error_element() is not None 

4044 ): 

4045 return edits 

4046 names = [] 

4047 for idx, stanza in enumerate(deb822_file): 

4048 if idx < 2: 

4049 continue 

4050 name = stanza.get("Package") 

4051 if name is None: 

4052 return edits 

4053 names.append(name) 

4054 

4055 reordered = sorted(names) 

4056 if names == reordered: 

4057 return edits 

4058 

4059 if edits: 

4060 content = apply_text_edits(content, lines, edits) 

4061 lines = content.splitlines(keepends=True) 

4062 deb822_file = parse_deb822_file( 

4063 lines, 

4064 accept_files_with_duplicated_fields=True, 

4065 accept_files_with_error_tokens=True, 

4066 ) 

4067 

4068 stanzas = list(deb822_file) 

4069 reordered_stanza = stanzas[:2] + sorted( 

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

4071 ) 

4072 bits = [] 

4073 stanza_idx = 0 

4074 for token_or_element in deb822_file.iter_parts(): 

4075 if isinstance(token_or_element, Deb822ParagraphElement): 

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

4077 stanza_idx += 1 

4078 else: 

4079 bits.append(token_or_element.convert_to_text()) 

4080 

4081 new_content = "".join(bits) 

4082 

4083 return [ 

4084 TextEdit( 

4085 Range( 

4086 Position(0, 0), 

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

4088 ), 

4089 new_content, 

4090 ) 

4091 ] 

4092 

4093 

4094class DTestsCtrlFileMetadata( 

4095 Deb822FileMetadata[DTestsCtrlStanzaMetadata, DTestsCtrlKnownField] 

4096): 

4097 

4098 @property 

4099 def reference_data_basename(self) -> str: 

4100 return "debian_tests_control_reference_data.yaml" 

4101 

4102 def _new_field( 

4103 self, 

4104 name: str, 

4105 field_value_type: FieldValueClass, 

4106 ) -> F: 

4107 return DTestsCtrlKnownField(name, field_value_type) 

4108 

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

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

4111 self.ensure_initialized() 

4112 return _DTESTSCTRL_STANZA 

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

4114 

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

4116 self.ensure_initialized() 

4117 yield _DTESTSCTRL_STANZA 

4118 

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

4120 self.ensure_initialized() 

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

4122 return _DTESTSCTRL_STANZA 

4123 raise KeyError(item) 

4124 

4125 

4126TRANSLATABLE_DEB822_FILE_METADATA: Sequence[ 

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

4128] = [ 

4129 DctrlFileMetadata, 

4130 Dep5FileMetadata, 

4131 DTestsCtrlFileMetadata, 

4132]