From 2d563c9ffbb3a352db90015d2be89b7f90122a66 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Fri, 26 Jan 2024 15:49:15 +0100 Subject: [PATCH] Implement real fixers Using the quick action API expose the fixes eslint reports on JSON. --- .flake8 | 1 + README.md | 13 +++++++++ linter.py | 87 +++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/.flake8 b/.flake8 index c6bba1e..28fd309 100644 --- a/.flake8 +++ b/.flake8 @@ -2,5 +2,6 @@ max-line-length = 100 ignore = + E731, D1, W503 diff --git a/README.md b/README.md index 06e561c..c4bd0e8 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,19 @@ npm install -g eslint npm install -D eslint ``` +## Quick Fixes + +`eslint` provides fixes for some errors. These fixes are available in SublimeLinter as quick actions. See the Command Palette: `SublimeLinter: Quick Action`. (Also: https://github.com/SublimeLinter/SublimeLinter#quick-actionsfixers) + +You may want to define a key binding: + +``` + // To trigger a quick action + { "keys": ["ctrl+k", "ctrl+f"], + "command": "sublime_linter_quick_actions" + }, +``` + ## Using eslint with plugins (e.g. vue) SublimeLinter will detect _some_ installed **local** plugins, and thus it should work automatically for e.g. `.vue` or `.ts` files. If it works on the command line, there is a chance it works in Sublime without further ado. diff --git a/linter.py b/linter.py index f921c9f..51e5741 100644 --- a/linter.py +++ b/linter.py @@ -10,19 +10,27 @@ """This module exports the ESLint plugin class.""" +from functools import partial import json import logging import os import re import shutil -from SublimeLinter.lint import LintMatch, NodeLinter, PermanentError +import sublime + +from SublimeLinter.lint import LintMatch, NodeLinter, PermanentError from SublimeLinter.lint.base_linter.node_linter import read_json_file +from SublimeLinter.lint.quick_fix import ( + TextRange, QuickAction, merge_actions_by_code_and_line, quick_actions_for) MYPY = False if MYPY: - from typing import List, Optional, Union + from typing import Iterator, List, Optional, Union + from SublimeLinter.lint import util + from SublimeLinter.lint.linter import VirtualView + from SublimeLinter.lint.persist import LintError logger = logging.getLogger('SublimeLinter.plugin.eslint') @@ -177,21 +185,27 @@ def on_stderr(self, stderr): logger.error(stderr) self.notify_failure() - def find_errors(self, output): + def parse_output(self, proc, virtual_view): # type: ignore[override] + # type: (util.popen_output, VirtualView) -> Iterator[LintError] """Parse errors from linter's output.""" + assert proc.stdout is not None + assert proc.stderr is not None + if proc.stderr.strip(): + self.on_stderr(proc.stderr) + try: # It is possible that users output debug messages to stdout, so we # only parse the last line, which is hopefully the actual eslint # output. # https://github.com/SublimeLinter/SublimeLinter-eslint/issues/251 - last_line = output.rstrip().split('\n')[-1] + last_line = proc.stdout.rstrip().split('\n')[-1] content = json.loads(last_line) except ValueError: logger.error( "JSON Decode error: We expected JSON from 'eslint', " "but instead got this:\n{}\n\n" "Be aware that we only parse the last line of above " - "output.".format(output)) + "output.".format(proc.stdout)) self.notify_failure() return @@ -207,26 +221,63 @@ def find_errors(self, output): elif filename and os.path.basename(filename).startswith(BUFFER_FILE_STEM + '.'): filename = 'stdin' - for match in entry['messages']: - if match['message'].startswith('File ignored'): + for item in entry['messages']: + if item['message'].startswith('File ignored'): continue - if 'line' not in match: - logger.error(match['message']) + if 'line' not in item: + logger.error(item['message']) self.notify_failure() continue - yield LintMatch( - match=match, + match = LintMatch( + match=item, filename=filename, - line=match['line'] - 1, # apply line_col_base manually - col=_try(lambda: match['column'] - 1), - end_line=_try(lambda: match['endLine'] - 1), - end_col=_try(lambda: match['endColumn'] - 1), - error_type='error' if match['severity'] == 2 else 'warning', - code=match.get('ruleId', ''), - message=match['message'], + line=item['line'] - 1, # apply line_col_base manually + col=_try(lambda: item['column'] - 1), + end_line=_try(lambda: item['endLine'] - 1), + end_col=_try(lambda: item['endColumn'] - 1), + error_type='error' if item['severity'] == 2 else 'warning', + code=item.get('ruleId', ''), + message=item['message'], ) + error = self.process_match(match, virtual_view) + if error: + try: + fix_description = item["fix"] + except KeyError: + pass + else: + if fix_description: + error["fix"] = fix_description # type: ignore[typeddict-unknown-key] + yield error + + +@quick_actions_for("eslint") +def eslint_fixes_provider(errors, _view): + # type: (List[LintError], Optional[sublime.View]) -> Iterator[QuickAction] + def make_action(error): + # type: (LintError) -> QuickAction + return QuickAction( + "eslint: Fix {code}".format(**error), + partial(eslint_fix_error, error), + "{msg}".format(**error), + solves=[error] + ) + + except_ = lambda error: "fix" not in error + yield from merge_actions_by_code_and_line(make_action, except_, errors, _view) + + +def eslint_fix_error(error, view) -> "Iterator[TextRange]": + """ + 'fix': {'text': '; ', 'range': [40, 44]} + """ + fix_description = error["fix"] + yield TextRange( + fix_description["text"], + sublime.Region(*fix_description["range"]) + ) def _try(getter, otherwise=None, catch=Exception):