diff --git a/README.md b/README.md index 2b393b5..a5e3cb2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,14 @@ pip install python-lsp-ruff There also exists an [AUR package](https://aur.archlinux.org/packages/python-lsp-ruff). -# Usage +### When using ruff before version 0.1.0 +Ruff version `0.1.0` introduced API changes that are fixed in Python LSP Ruff `v1.6.0`. To continue with `ruff<0.1.0` please use `v1.5.3`, e.g. using `pip`: + +```sh +pip install "ruff<0.1.0" "python-lsp-ruff==1.5.3" +``` + +## Usage This plugin will disable `pycodestyle`, `pyflakes`, `mccabe` and `pyls_isort` by default, unless they are explicitly enabled in the client configuration. When enabled, all linting diagnostics will be provided by `ruff`. @@ -43,7 +50,7 @@ lspconfig.pylsp.setup { } ``` -# Configuration +## Configuration Configuration options can be passed to the python-language-server. If a `pyproject.toml` file is present in the project, `python-lsp-ruff` will use these configuration options. @@ -66,11 +73,12 @@ the valid configuration keys: - `pylsp.plugins.ruff.select`: List of error codes to enable. - `pylsp.plugins.ruff.extendSelect`: Same as select, but append to existing error codes. - `pylsp.plugins.ruff.format`: List of error codes to fix during formatting. The default is `["I"]`, any additional codes are appended to this list. + - `pylsp.plugins.ruff.unsafeFixes`: boolean that enables/disables fixes that are marked "unsafe" by `ruff`. `false` by default. - `pylsp.plugins.ruff.severities`: Dictionary of custom severity levels for specific codes, see [below](#custom-severities). For more information on the configuration visit [Ruff's homepage](https://beta.ruff.rs/docs/configuration/). -## Custom severities +### Custom severities By default, all diagnostics are marked as warning, except for `"E999"` and all error codes starting with `"F"`, which are displayed as errors. This default can be changed through the `pylsp.plugins.ruff.severities` option, which takes the error code as a key and any of diff --git a/pylsp_ruff/plugin.py b/pylsp_ruff/plugin.py index 2342b99..e74b166 100644 --- a/pylsp_ruff/plugin.py +++ b/pylsp_ruff/plugin.py @@ -139,6 +139,20 @@ def pylsp_lint(workspace: Workspace, document: Document) -> List[Dict]: def create_diagnostic(check: RuffCheck, settings: PluginSettings) -> Diagnostic: + """ + Create a LSP diagnostic based on the given RuffCheck object. + + Parameters + ---------- + check : RuffCheck + RuffCheck object to convert. + settings : PluginSettings + Current settings. + + Returns + ------- + Diagnostic + """ # Adapt range to LSP specification (zero-based) range = Range( start=Position( @@ -214,6 +228,8 @@ def pylsp_code_actions( code_actions = [] has_organize_imports = False + settings = load_settings(workspace=workspace, document_path=document.path) + for diagnostic in diagnostics: code_actions.append( create_disable_code_action(document=document, diagnostic=diagnostic) @@ -222,6 +238,10 @@ def pylsp_code_actions( if diagnostic.data: # Has fix fix = converter.structure(diagnostic.data, RuffFix) + # Ignore fix if marked as unsafe and unsafe_fixes are disabled + if fix.applicability != "safe" and not settings.unsafe_fixes: + continue + if diagnostic.code == "I001": code_actions.append( create_organize_imports_code_action( @@ -236,7 +256,6 @@ def pylsp_code_actions( ), ) - settings = load_settings(workspace=workspace, document_path=document.path) checks = run_ruff_check(document=document, settings=settings) checks_with_fixes = [c for c in checks if c.fix] checks_organize_imports = [c for c in checks_with_fixes if c.code == "I001"] @@ -458,7 +477,7 @@ def run_ruff( p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) (stdout, stderr) = p.communicate(document_source.encode()) - if stderr: + if p.returncode != 0: log.error(f"Error running ruff: {stderr.decode()}") return stdout.decode() @@ -491,8 +510,10 @@ def build_arguments( args = [] # Suppress update announcements args.append("--quiet") + # Suppress exit 1 when violations were found + args.append("--exit-zero") # Use the json formatting for easier evaluation - args.append("--format=json") + args.append("--output-format=json") if fix: args.append("--fix") else: @@ -510,6 +531,9 @@ def build_arguments( if settings.line_length: args.append(f"--line-length={settings.line_length}") + if settings.unsafe_fixes: + args.append("--unsafe-fixes") + if settings.exclude: args.append(f"--exclude={','.join(settings.exclude)}") @@ -583,6 +607,7 @@ def load_settings(workspace: Workspace, document_path: str) -> PluginSettings: return PluginSettings( enabled=plugin_settings.enabled, executable=plugin_settings.executable, + unsafe_fixes=plugin_settings.unsafe_fixes, extend_ignore=plugin_settings.extend_ignore, extend_select=plugin_settings.extend_select, format=plugin_settings.format, diff --git a/pylsp_ruff/ruff.py b/pylsp_ruff/ruff.py index 2f83f9b..efda324 100644 --- a/pylsp_ruff/ruff.py +++ b/pylsp_ruff/ruff.py @@ -19,6 +19,7 @@ class Edit: class Fix: edits: List[Edit] message: str + applicability: str @dataclass diff --git a/pylsp_ruff/settings.py b/pylsp_ruff/settings.py index 7993183..5585e83 100644 --- a/pylsp_ruff/settings.py +++ b/pylsp_ruff/settings.py @@ -9,6 +9,7 @@ class PluginSettings: enabled: bool = True executable: str = "ruff" + unsafe_fixes: bool = False config: Optional[str] = None line_length: Optional[int] = None diff --git a/pyproject.toml b/pyproject.toml index f96f12e..1d374e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ readme = "README.md" requires-python = ">=3.7" license = {text = "MIT"} dependencies = [ - "ruff>=0.0.267,<0.1.0", + "ruff>=0.1.0, <0.2.0", "python-lsp-server", "lsprotocol>=2022.0.0a1", "tomli>=1.1.0; python_version < '3.11'", diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index 7e15de9..063b536 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -115,11 +115,40 @@ def f(): pass """ ) + expected_str_safe = dedent( + """ + def f(): + a = 2 + """ + ) + workspace._config.update( + { + "plugins": { + "ruff": { + "unsafeFixes": True, + } + } + } + ) _, doc = temp_document(codeaction_str, workspace) settings = ruff_lint.load_settings(workspace, doc.path) fixed_str = ruff_lint.run_ruff_fix(doc, settings) assert fixed_str == expected_str + workspace._config.update( + { + "plugins": { + "ruff": { + "unsafeFixes": False, + } + } + } + ) + _, doc = temp_document(codeaction_str, workspace) + settings = ruff_lint.load_settings(workspace, doc.path) + fixed_str = ruff_lint.run_ruff_fix(doc, settings) + assert fixed_str == expected_str_safe + def test_format_document_default_settings(workspace): _, doc = temp_document(import_str, workspace) diff --git a/tests/test_ruff_lint.py b/tests/test_ruff_lint.py index 9f6bac0..b1439ee 100644 --- a/tests/test_ruff_lint.py +++ b/tests/test_ruff_lint.py @@ -177,7 +177,8 @@ def f(): assert call_args == [ "ruff", "--quiet", - "--format=json", + "--exit-zero", + "--output-format=json", "--no-fix", "--force-exclude", f"--stdin-filename={os.path.join(workspace.root_path, '__init__.py')}",