Coverage for src/debputy/linting/lint_impl.py: 13%

346 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1import dataclasses 

2import os 

3import stat 

4import subprocess 

5import sys 

6import textwrap 

7from typing import Optional, List, Union, NoReturn, Mapping 

8 

9from debputy.commands.debputy_cmd.context import CommandContext 

10from debputy.commands.debputy_cmd.output import _output_styling, OutputStylingBase 

11from debputy.filesystem_scan import FSROOverlay 

12from debputy.linting.lint_util import ( 

13 LinterImpl, 

14 LintReport, 

15 LintStateImpl, 

16 FormatterImpl, 

17 TermLintReport, 

18 LintDiagnosticResultState, 

19) 

20from debputy.lsp.diagnostics import DiagnosticData 

21from debputy.lsp.lsp_debian_changelog import _lint_debian_changelog 

22from debputy.lsp.lsp_debian_control import ( 

23 _lint_debian_control, 

24 _reformat_debian_control, 

25) 

26from debputy.lsp.lsp_debian_copyright import ( 

27 _lint_debian_copyright, 

28 _reformat_debian_copyright, 

29) 

30from debputy.lsp.lsp_debian_debputy_manifest import _lint_debian_debputy_manifest 

31from debputy.lsp.lsp_debian_patches_series import _lint_debian_patches_series 

32from debputy.lsp.lsp_debian_rules import _lint_debian_rules 

33from debputy.lsp.lsp_debian_tests_control import ( 

34 _lint_debian_tests_control, 

35 _reformat_debian_tests_control, 

36) 

37from debputy.lsp.lsp_debian_upstream_metadata import _lint_debian_upstream_metadata 

38from debputy.lsp.maint_prefs import ( 

39 MaintainerPreferenceTable, 

40 EffectiveFormattingPreference, 

41 determine_effective_preference, 

42) 

43from debputy.lsp.quickfixes import provide_standard_quickfixes_from_diagnostics 

44from debputy.lsp.spellchecking import disable_spellchecking 

45from debputy.lsp.text_edit import ( 

46 get_well_formatted_edit, 

47 merge_sort_text_edits, 

48 apply_text_edits, 

49 OverLappingTextEditException, 

50) 

51from debputy.lsp.vendoring._deb822_repro import Deb822FileElement 

52from debputy.lsprotocol.types import ( 

53 CodeAction, 

54 Command, 

55 CodeActionParams, 

56 CodeActionContext, 

57 TextDocumentIdentifier, 

58 TextEdit, 

59 Position, 

60 Range, 

61 DiagnosticSeverity, 

62 Diagnostic, 

63) 

64from debputy.packages import SourcePackage, BinaryPackage 

65from debputy.plugin.api import VirtualPath 

66from debputy.plugin.api.feature_set import PluginProvidedFeatureSet 

67from debputy.util import _warn, _error, _info 

68from debputy.yaml import MANIFEST_YAML, YAMLError 

69from debputy.yaml.compat import CommentedMap 

70 

71LINTER_FORMATS = { 

72 "debian/changelog": _lint_debian_changelog, 

73 "debian/control": _lint_debian_control, 

74 "debian/copyright": _lint_debian_copyright, 

75 "debian/debputy.manifest": _lint_debian_debputy_manifest, 

76 "debian/upstream/metadata": _lint_debian_upstream_metadata, 

77 "debian/rules": _lint_debian_rules, 

78 "debian/patches/series": _lint_debian_patches_series, 

79 "debian/tests/control": _lint_debian_tests_control, 

80} 

81 

82 

83REFORMAT_FORMATS = { 

84 "debian/control": _reformat_debian_control, 

85 "debian/copyright": _reformat_debian_copyright, 

86 "debian/tests/control": _reformat_debian_tests_control, 

87} 

88 

89 

90@dataclasses.dataclass(slots=True) 

91class LintContext: 

92 plugin_feature_set: PluginProvidedFeatureSet 

93 maint_preference_table: MaintainerPreferenceTable 

94 source_root: Optional[VirtualPath] 

95 debian_dir: Optional[VirtualPath] 

96 parsed_deb822_file_content: Optional[Deb822FileElement] = None 

97 source_package: Optional[SourcePackage] = None 

98 binary_packages: Optional[Mapping[str, BinaryPackage]] = None 

99 effective_preference: Optional[EffectiveFormattingPreference] = None 

100 style_tool: Optional[str] = None 

101 unsupported_preference_reason: Optional[str] = None 

102 salsa_ci: Optional[CommentedMap] = None 

103 

104 def state_for( 

105 self, 

106 path: str, 

107 content: str, 

108 lines: List[str], 

109 lint_implementation: Optional[LinterImpl], 

110 ) -> LintStateImpl: 

111 return LintStateImpl( 

112 self.plugin_feature_set, 

113 self.maint_preference_table, 

114 self.source_root, 

115 self.debian_dir, 

116 path, 

117 content, 

118 lines, 

119 self.source_package, 

120 self.binary_packages, 

121 self.effective_preference, 

122 lint_implementation, 

123 ) 

124 

125 

126def gather_lint_info(context: CommandContext) -> LintContext: 

127 source_root = FSROOverlay.create_root_dir(".", ".") 

128 debian_dir = source_root.get("debian") 

129 if debian_dir is not None and not debian_dir.is_dir: 

130 debian_dir = None 

131 lint_context = LintContext( 

132 context.load_plugins(), 

133 MaintainerPreferenceTable.load_preferences(), 

134 source_root, 

135 debian_dir, 

136 ) 

137 try: 

138 with open("debian/control") as fd: 

139 deb822_file, source_package, binary_packages = ( 

140 context.dctrl_parser.parse_source_debian_control(fd, ignore_errors=True) 

141 ) 

142 except FileNotFoundError: 

143 source_package = None 

144 else: 

145 lint_context.parsed_deb822_file_content = deb822_file 

146 lint_context.source_package = source_package 

147 lint_context.binary_packages = binary_packages 

148 salsa_ci_map: Optional[CommentedMap] = None 

149 for ci_file in ("debian/salsa-ci.yml", ".gitlab-ci.yml"): 

150 try: 

151 with open(ci_file) as fd: 

152 salsa_ci_map = MANIFEST_YAML.load(fd) 

153 if not isinstance(salsa_ci_map, CommentedMap): 

154 salsa_ci_map = None 

155 break 

156 except FileNotFoundError: 

157 pass 

158 except YAMLError: 

159 break 

160 if source_package is not None or salsa_ci_map is not None: 

161 pref, tool, pref_reason = determine_effective_preference( 

162 lint_context.maint_preference_table, 

163 source_package, 

164 salsa_ci_map, 

165 ) 

166 lint_context.effective_preference = pref 

167 lint_context.style_tool = tool 

168 lint_context.unsupported_preference_reason = pref_reason 

169 

170 return lint_context 

171 

172 

173def initialize_lint_report(context: CommandContext) -> LintReport: 

174 lint_report_format = context.parsed_args.lint_report_format 

175 report_output = context.parsed_args.report_output 

176 

177 if lint_report_format == "term": 

178 fo = _output_styling(context.parsed_args, sys.stdout) 

179 if report_output is not None: 

180 _warn("--report-output is redundant for the `term` report") 

181 return TermLintReport(fo) 

182 if lint_report_format == "junit4-xml": 

183 try: 

184 import junit_xml 

185 except ImportError: 

186 _error( 

187 "The `junit4-xml` report format requires `python3-junit.xml` to be installed" 

188 ) 

189 

190 from debputy.linting.lint_report_junit import JunitLintReport 

191 

192 if report_output is None: 

193 report_output = "debputy-lint-junit.xml" 

194 

195 return JunitLintReport(report_output) 

196 

197 raise AssertionError(f"Missing case for lint_report_format: {lint_report_format}") 

198 

199 

200def perform_linting(context: CommandContext) -> None: 

201 parsed_args = context.parsed_args 

202 if not parsed_args.spellcheck: 

203 disable_spellchecking() 

204 linter_exit_code = parsed_args.linter_exit_code 

205 lint_report = initialize_lint_report(context) 

206 lint_context = gather_lint_info(context) 

207 

208 for name_stem in LINTER_FORMATS: 

209 filename = f"./{name_stem}" 

210 if not os.path.isfile(filename): 

211 continue 

212 perform_linting_of_file( 

213 lint_context, 

214 filename, 

215 name_stem, 

216 context.parsed_args.auto_fix, 

217 lint_report, 

218 ) 

219 if lint_report.number_of_invalid_diagnostics: 

220 _warn( 

221 "Some diagnostics did not explicitly set severity. Please report the bug and include the output" 

222 ) 

223 if lint_report.number_of_broken_diagnostics: 

224 _error( 

225 "Some sub-linters reported issues. Please report the bug and include the output" 

226 ) 

227 

228 if parsed_args.warn_about_check_manifest and os.path.isfile( 

229 "debian/debputy.manifest" 

230 ): 

231 _info("Note: Due to a limitation in the linter, debian/debputy.manifest is") 

232 _info("only **partially** checked by this command at the time of writing.") 

233 _info("Please use `debputy check-manifest` to fully check the manifest.") 

234 

235 lint_report.finish_report() 

236 

237 if linter_exit_code: 

238 _exit_with_lint_code(lint_report) 

239 

240 

241def perform_reformat( 

242 context: CommandContext, 

243 *, 

244 named_style: Optional[str] = None, 

245) -> None: 

246 parsed_args = context.parsed_args 

247 fo = _output_styling(context.parsed_args, sys.stdout) 

248 lint_context = gather_lint_info(context) 

249 if named_style is not None: 

250 style = lint_context.maint_preference_table.named_styles.get(named_style) 

251 if style is None: 

252 styles = ", ".join(lint_context.maint_preference_table.named_styles) 

253 _error(f'There is no style named "{style}". Options include: {styles}') 

254 if ( 

255 lint_context.effective_preference is not None 

256 and lint_context.effective_preference != style 

257 ): 

258 _info( 

259 f'Note that the style "{named_style}" does not match the style that `debputy` was configured to use.' 

260 ) 

261 _info("This may be a non-issue (if the configuration is out of date).") 

262 lint_context.effective_preference = style 

263 

264 if lint_context.effective_preference is None: 

265 if lint_context.unsupported_preference_reason is not None: 

266 _warn( 

267 "While `debputy` could identify a formatting for this package, it does not support it." 

268 ) 

269 _warn(f"{lint_context.unsupported_preference_reason}") 

270 if lint_context.style_tool is not None: 

271 _info( 

272 f"The following tool might be able to apply the style: {lint_context.style_tool}" 

273 ) 

274 if parsed_args.supported_style_required: 

275 _error( 

276 "Sorry; `debputy` does not support the style. Use --unknown-or-unsupported-style-is-ok to make" 

277 " this a non-error (note that `debputy` will not reformat the packaging in this case; just not" 

278 " exit with an error code)." 

279 ) 

280 else: 

281 print( 

282 textwrap.dedent( 

283 """\ 

284 You can enable set a style by doing either of: 

285 

286 * You can set `X-Style: black` in the source stanza of `debian/control` to pick 

287 `black` as the preferred style for this package. 

288 - Note: `black` is an opinionated style that follows the spirit of the `black` code formatter 

289 for Python. 

290 - If you use `pre-commit`, then there is a formatting hook at 

291 https://salsa.debian.org/debian/debputy-pre-commit-hooks 

292 

293 * If you use the Debian Salsa CI pipeline, then you can set SALSA_CI_DISABLE_WRAP_AND_SORT (`no`) 

294 or SALSA_CI_ENABLE_WRAP_AND_SORT (`yes`) to the relevant value and `debputy` will pick up the 

295 configuration from there. 

296 - Note: The option must be in `.gitlab-ci.yml` or `debian/salsa-ci.yml` to work. The Salsa CI 

297 pipeline will use `wrap-and-sort` while `debputy` uses its own emulation of `wrap-and-sort` 

298 (`debputy` also needs to apply the style via `debputy lsp server`). 

299 

300 * The `debputy` code also comes with a built-in style database. This may be interesting for 

301 packaging teams, so set a default team style that applies to all packages maintained by 

302 that packaging team. 

303 - Individuals can also add their style, which can useful for ad-hoc packaging teams, where 

304 `debputy` will automatically apply a style if *all* co-maintainers agree to it. 

305 

306 Note the above list is an ordered list of how `debputy` determines which style to use in case 

307 multiple options are available. 

308 """ 

309 ) 

310 ) 

311 if parsed_args.supported_style_required: 

312 if lint_context.style_tool is not None: 

313 _error( 

314 "Sorry, `debputy reformat` does not support the packaging style. However, the" 

315 f" formatting is supposedly handled by: {lint_context.style_tool}" 

316 ) 

317 _error( 

318 "Sorry; `debputy` does not know which style to use for this package. Please either set a" 

319 "style or use --unknown-or-unsupported-style-is-ok to make this a non-error" 

320 ) 

321 _info("") 

322 _info( 

323 "Doing nothing since no supported style could be identified as requested." 

324 " See above how to set a style." 

325 ) 

326 _info("Use --supported-style-is-required if this should be an error instead.") 

327 sys.exit(0) 

328 

329 changes = False 

330 auto_fix = context.parsed_args.auto_fix 

331 for name_stem in REFORMAT_FORMATS: 

332 formatter = REFORMAT_FORMATS.get(name_stem) 

333 filename = f"./{name_stem}" 

334 if formatter is None or not os.path.isfile(filename): 

335 continue 

336 

337 reformatted = perform_reformat_of_file( 

338 fo, 

339 lint_context, 

340 filename, 

341 formatter, 

342 auto_fix, 

343 ) 

344 if reformatted: 

345 changes = True 

346 

347 if changes and parsed_args.linter_exit_code: 

348 sys.exit(2) 

349 

350 

351def perform_reformat_of_file( 

352 fo: OutputStylingBase, 

353 lint_context: LintContext, 

354 filename: str, 

355 formatter: FormatterImpl, 

356 auto_fix: bool, 

357) -> bool: 

358 with open(filename, "rt", encoding="utf-8") as fd: 

359 text = fd.read() 

360 

361 lines = text.splitlines(keepends=True) 

362 lint_state = lint_context.state_for( 

363 filename, 

364 text, 

365 lines, 

366 None, 

367 ) 

368 edits = formatter(lint_state) 

369 if not edits: 

370 return False 

371 

372 try: 

373 replacement = apply_text_edits(text, lines, edits) 

374 except OverLappingTextEditException: 

375 _error( 

376 f"The reformater for {filename} produced overlapping edits (which is broken and will not work)" 

377 ) 

378 

379 output_filename = f"{filename}.tmp" 

380 with open(output_filename, "wt", encoding="utf-8") as fd: 

381 fd.write(replacement) 

382 

383 r = subprocess.run(["diff", "-u", filename, output_filename]).returncode 

384 if r != 0 and r != 1: 

385 _warn(f"diff -u {filename} {output_filename} failed!?") 

386 if auto_fix: 

387 orig_mode = stat.S_IMODE(os.stat(filename).st_mode) 

388 os.chmod(output_filename, orig_mode) 

389 os.rename(output_filename, filename) 

390 print( 

391 fo.colored( 

392 f"Reformatted {filename}.", 

393 fg="green", 

394 style="bold", 

395 ) 

396 ) 

397 else: 

398 os.unlink(output_filename) 

399 

400 return True 

401 

402 

403def _exit_with_lint_code(lint_report: LintReport) -> NoReturn: 

404 diagnostics_count = lint_report.diagnostics_count 

405 if ( 

406 diagnostics_count[DiagnosticSeverity.Error] 

407 or diagnostics_count[DiagnosticSeverity.Warning] 

408 ): 

409 sys.exit(2) 

410 sys.exit(0) 

411 

412 

413def perform_linting_of_file( 

414 lint_context: LintContext, 

415 filename: str, 

416 file_format: str, 

417 auto_fixing_enabled: bool, 

418 lint_report: LintReport, 

419) -> None: 

420 handler = LINTER_FORMATS.get(file_format) 

421 if handler is None: 

422 return 

423 with open(filename, "rt", encoding="utf-8") as fd: 

424 text = fd.read() 

425 

426 if auto_fixing_enabled: 

427 _auto_fix_run( 

428 lint_context, 

429 filename, 

430 text, 

431 handler, 

432 lint_report, 

433 ) 

434 else: 

435 _diagnostics_run( 

436 lint_context, 

437 filename, 

438 text, 

439 handler, 

440 lint_report, 

441 ) 

442 

443 

444def _overlapping_edit( 

445 last_edit_range: Range, 

446 last_fix_range: Range, 

447) -> bool: 

448 last_edit_start_pos = last_edit_range.start 

449 last_fix_start_pos = last_fix_range.start 

450 if last_edit_start_pos.line < last_fix_start_pos.line: 

451 return True 

452 if ( 

453 last_edit_start_pos.line == last_fix_start_pos.character 

454 and last_edit_start_pos.character < last_fix_start_pos.character 

455 ): 

456 return True 

457 

458 if last_fix_range.end == last_fix_start_pos: 

459 return False 

460 

461 if last_fix_range.end < last_edit_start_pos: 

462 return False 

463 return True 

464 

465 

466def _max_range( 

467 r1: Range, 

468 r2: Range, 

469) -> Range: 

470 if r2.end > r1.end: 

471 return r2 

472 if r2.end < r1.end: 

473 return r1 

474 if r2.start > r1.start: 

475 return r2 

476 return r1 

477 

478 

479_INITIAL_FIX_RANGE = Range( 

480 Position(0, 0), 

481 Position(0, 0), 

482) 

483 

484 

485def _is_non_interactive_auto_fix_allowed(diagnostic: Diagnostic) -> bool: 

486 diag_data: Optional[DiagnosticData] = diagnostic.data 

487 return diag_data is None or diag_data.get("enable_non_interactive_auto_fix", True) 

488 

489 

490def _auto_fix_run( 

491 lint_context: LintContext, 

492 filename: str, 

493 text: str, 

494 linter: LinterImpl, 

495 lint_report: LintReport, 

496) -> None: 

497 another_round = True 

498 unfixed_diagnostics: List[Diagnostic] = [] 

499 remaining_rounds = 10 

500 fixed_count = 0 

501 too_many_rounds = False 

502 lines = text.splitlines(keepends=True) 

503 lint_state = lint_context.state_for( 

504 filename, 

505 text, 

506 lines, 

507 linter, 

508 ) 

509 current_issues = lint_state.gather_diagnostics() 

510 issue_count_start = len(current_issues) if current_issues else 0 

511 while another_round and current_issues: 

512 another_round = False 

513 last_fix_range = _INITIAL_FIX_RANGE 

514 unfixed_diagnostics.clear() 

515 edits = [] 

516 fixed_diagnostics = [] 

517 for diagnostic in current_issues: 

518 if not _is_non_interactive_auto_fix_allowed(diagnostic): 

519 unfixed_diagnostics.append(diagnostic) 

520 continue 

521 actions = provide_standard_quickfixes_from_diagnostics( 

522 CodeActionParams( 

523 TextDocumentIdentifier(filename), 

524 diagnostic.range, 

525 CodeActionContext( 

526 [diagnostic], 

527 ), 

528 ), 

529 ) 

530 auto_fixing_edits = resolve_auto_fixer(filename, actions) 

531 

532 if not auto_fixing_edits: 

533 unfixed_diagnostics.append(diagnostic) 

534 continue 

535 

536 sorted_edits = merge_sort_text_edits( 

537 [get_well_formatted_edit(e) for e in auto_fixing_edits], 

538 ) 

539 last_edit = sorted_edits[-1] 

540 last_edit_range = last_edit.range 

541 if _overlapping_edit(last_edit_range, last_fix_range): 

542 if not another_round: 

543 if remaining_rounds > 0: 

544 remaining_rounds -= 1 

545 print( 

546 "Detected overlapping edit; scheduling another edit round." 

547 ) 

548 another_round = True 

549 else: 

550 _warn( 

551 "Too many overlapping edits; stopping after this round (circuit breaker)." 

552 ) 

553 too_many_rounds = True 

554 continue 

555 edits.extend(sorted_edits) 

556 fixed_diagnostics.append(diagnostic) 

557 last_fix_range = _max_range(last_fix_range, sorted_edits[-1].range) 

558 

559 if another_round and not edits: 

560 _error( 

561 "Internal error: Detected an overlapping edit and yet had no edits to perform..." 

562 ) 

563 

564 fixed_count += len(fixed_diagnostics) 

565 

566 try: 

567 text = apply_text_edits( 

568 text, 

569 lines, 

570 edits, 

571 ) 

572 except OverLappingTextEditException: 

573 _error( 

574 f"Failed to apply edits for f{filename} due to over lapping edits. Please file a bug" 

575 f" against `debputy` with a contents of the `debian` directory or a minimal reproducer" 

576 ) 

577 lines = text.splitlines(keepends=True) 

578 

579 with lint_report.line_state(lint_state): 

580 for diagnostic in fixed_diagnostics: 

581 lint_report.report_diagnostic( 

582 diagnostic, 

583 result_state=LintDiagnosticResultState.FIXED, 

584 ) 

585 lint_state.content = text 

586 lint_state.lines = lines 

587 lint_state.clear_cache() 

588 current_issues = lint_state.gather_diagnostics() 

589 

590 if fixed_count: 

591 output_filename = f"{filename}.tmp" 

592 with open(output_filename, "wt", encoding="utf-8") as fd: 

593 fd.write(text) 

594 orig_mode = stat.S_IMODE(os.stat(filename).st_mode) 

595 os.chmod(output_filename, orig_mode) 

596 os.rename(output_filename, filename) 

597 lines = text.splitlines(keepends=True) 

598 lint_state.content = text 

599 lint_state.lines = lines 

600 remaining_issues = lint_state.gather_diagnostics() or [] 

601 else: 

602 remaining_issues = current_issues or [] 

603 

604 with lint_report.line_state(lint_state): 

605 for diagnostic in remaining_issues: 

606 lint_report.report_diagnostic(diagnostic) 

607 

608 if isinstance(lint_report, TermLintReport): 

609 # TODO: Not optimal, but will do for now. 

610 fo = lint_report.fo 

611 print() 

612 if fixed_count: 

613 remaining_issues_count = len(remaining_issues) 

614 print( 

615 fo.colored( 

616 f"Fixes applied to {filename}: {fixed_count}." 

617 f" Number of issues went from {issue_count_start} to {remaining_issues_count}", 

618 fg="green", 

619 style="bold", 

620 ) 

621 ) 

622 elif remaining_issues: 

623 print( 

624 fo.colored( 

625 f"None of the issues in {filename} could be fixed automatically. Sorry!", 

626 fg="yellow", 

627 bg="black", 

628 style="bold", 

629 ) 

630 ) 

631 else: 

632 assert not current_issues 

633 print( 

634 fo.colored( 

635 f"No issues detected in {filename}", 

636 fg="green", 

637 style="bold", 

638 ) 

639 ) 

640 if too_many_rounds: 

641 print( 

642 fo.colored( 

643 f"Not all fixes for issues in {filename} could be applied due to overlapping edits.", 

644 fg="yellow", 

645 bg="black", 

646 style="bold", 

647 ) 

648 ) 

649 print( 

650 "Running once more may cause more fixes to be applied. However, you may be facing" 

651 " pathological performance." 

652 ) 

653 

654 

655def _diagnostics_run( 

656 lint_context: LintContext, 

657 filename: str, 

658 text: str, 

659 linter: LinterImpl, 

660 lint_report: LintReport, 

661) -> None: 

662 lines = text.splitlines(keepends=True) 

663 lint_state = lint_context.state_for(filename, text, lines, linter) 

664 with lint_report.line_state(lint_state): 

665 issues = lint_state.gather_diagnostics() 

666 for diagnostic in issues: 

667 actions = provide_standard_quickfixes_from_diagnostics( 

668 CodeActionParams( 

669 TextDocumentIdentifier(filename), 

670 diagnostic.range, 

671 CodeActionContext( 

672 [diagnostic], 

673 ), 

674 ), 

675 ) 

676 auto_fixer = resolve_auto_fixer(filename, actions) 

677 has_auto_fixer = bool(auto_fixer) and _is_non_interactive_auto_fix_allowed( 

678 diagnostic 

679 ) 

680 

681 result_state = LintDiagnosticResultState.REPORTED 

682 if has_auto_fixer: 

683 result_state = LintDiagnosticResultState.AUTO_FIXABLE 

684 elif has_at_least_lsp_quickfix(actions): 

685 result_state = LintDiagnosticResultState.MANUAL_FIXABLE 

686 

687 lint_report.report_diagnostic(diagnostic, result_state=result_state) 

688 

689 

690def has_at_least_lsp_quickfix( 

691 actions: Optional[List[Union[Command, CodeAction]]], 

692) -> bool: 

693 if actions is None: 

694 return False 

695 for action in actions: 

696 if not isinstance(action, CodeAction): 

697 continue 

698 if action.edit is None and action.command is None: 

699 continue 

700 return True 

701 return False 

702 

703 

704def resolve_auto_fixer( 

705 document_ref: str, 

706 actions: Optional[List[Union[Command, CodeAction]]], 

707) -> Optional[List[TextEdit]]: 

708 if actions is None or len(actions) != 1: 

709 return None 

710 action = actions[0] 

711 if not isinstance(action, CodeAction): 

712 return None 

713 workspace_edit = action.edit 

714 if workspace_edit is None or action.command is not None: 

715 return None 

716 if ( 

717 not workspace_edit.changes 

718 or len(workspace_edit.changes) != 1 

719 or document_ref not in workspace_edit.changes 

720 ): 

721 return None 

722 return workspace_edit.changes[document_ref]