Skip to content

Add custom severity codes #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,17 @@ 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.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

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
`"E"`, `"W"`, `"I"` and `"H"` to be displayed as errors, warnings, information and hints, respectively.
For more information on the diagnostic severities please refer to
[the official LSP reference](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity).

Note that `python-lsp-ruff` does *not* accept regex, and it will *not* check whether the error code exists. If the custom severity level is not displayed,
please check first that the error code is correct and that the given value is one of the possible keys from above.
83 changes: 56 additions & 27 deletions pylsp_ruff/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@
"F841", # local variable `name` is assigned to but never used
}

DIAGNOSTIC_SEVERITIES = {
"E": DiagnosticSeverity.Error,
"W": DiagnosticSeverity.Warning,
"I": DiagnosticSeverity.Information,
"H": DiagnosticSeverity.Hint,
}


@hookimpl
def pylsp_settings():
Expand Down Expand Up @@ -91,7 +98,10 @@ def pylsp_format_document(workspace: Workspace, document: Document) -> Generator
else:
source = document.source

new_text = run_ruff_format(workspace, document.path, document_source=source)
settings = load_settings(workspace=workspace, document_path=document.path)
new_text = run_ruff_format(
settings=settings, document_path=document.path, document_source=source
)

# Avoid applying empty text edit
if new_text == source:
Expand Down Expand Up @@ -122,12 +132,13 @@ def pylsp_lint(workspace: Workspace, document: Document) -> List[Dict]:
-------
List of dicts containing the diagnostics.
"""
checks = run_ruff_check(workspace, document)
diagnostics = [create_diagnostic(c) for c in checks]
settings = load_settings(workspace, document.path)
checks = run_ruff_check(document=document, settings=settings)
diagnostics = [create_diagnostic(check=c, settings=settings) for c in checks]
return converter.unstructure(diagnostics)


def create_diagnostic(check: RuffCheck) -> Diagnostic:
def create_diagnostic(check: RuffCheck, settings: PluginSettings) -> Diagnostic:
# Adapt range to LSP specification (zero-based)
range = Range(
start=Position(
Expand All @@ -146,6 +157,12 @@ def create_diagnostic(check: RuffCheck) -> Diagnostic:
if check.code == "E999" or check.code[0] == "F":
severity = DiagnosticSeverity.Error

# Override severity with custom severity if possible, use default otherwise
if settings.severities is not None:
custom_sev = settings.severities.get(check.code, None)
if custom_sev is not None:
severity = DIAGNOSTIC_SEVERITIES.get(custom_sev, severity)

tags = []
if check.code in UNNECESSITY_CODES:
tags = [DiagnosticTag.Unnecessary]
Expand Down Expand Up @@ -198,39 +215,48 @@ def pylsp_code_actions(
has_organize_imports = False

for diagnostic in diagnostics:
code_actions.append(create_disable_code_action(document, diagnostic))
code_actions.append(
create_disable_code_action(document=document, diagnostic=diagnostic)
)

if diagnostic.data: # Has fix
fix = converter.structure(diagnostic.data, RuffFix)

if diagnostic.code == "I001":
code_actions.append(
create_organize_imports_code_action(document, diagnostic, fix)
create_organize_imports_code_action(
document=document, diagnostic=diagnostic, fix=fix
)
)
has_organize_imports = True
else:
code_actions.append(
create_fix_code_action(document, diagnostic, fix),
create_fix_code_action(
document=document, diagnostic=diagnostic, fix=fix
),
)

checks = run_ruff_check(workspace, document)
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"]

if not has_organize_imports and checks_organize_imports:
check = checks_organize_imports[0]
fix = check.fix # type: ignore
diagnostic = create_diagnostic(check)
diagnostic = create_diagnostic(check=check, settings=settings)
code_actions.extend(
[
create_organize_imports_code_action(document, diagnostic, fix),
create_disable_code_action(document, diagnostic),
create_organize_imports_code_action(
document=document, diagnostic=diagnostic, fix=fix
),
create_disable_code_action(document=document, diagnostic=diagnostic),
]
)

if checks_with_fixes:
code_actions.append(
create_fix_all_code_action(workspace, document),
create_fix_all_code_action(document=document, settings=settings),
)

return converter.unstructure(code_actions)
Expand Down Expand Up @@ -308,13 +334,13 @@ def create_organize_imports_code_action(


def create_fix_all_code_action(
workspace: Workspace,
document: Document,
settings: PluginSettings,
) -> CodeAction:
title = "Ruff: Fix All"
kind = CodeActionKind.SourceFixAll

new_text = run_ruff_fix(workspace, document)
new_text = run_ruff_fix(document=document, settings=settings)
range = Range(
start=Position(line=0, character=0),
end=Position(line=len(document.lines), character=0),
Expand Down Expand Up @@ -345,9 +371,11 @@ def create_text_edits(fix: RuffFix) -> List[TextEdit]:
return edits


def run_ruff_check(workspace: Workspace, document: Document) -> List[RuffCheck]:
def run_ruff_check(document: Document, settings: PluginSettings) -> List[RuffCheck]:
result = run_ruff(
workspace, document_path=document.path, document_source=document.source
document_path=document.path,
document_source=document.source,
settings=settings,
)
try:
result = json.loads(result)
Expand All @@ -356,38 +384,39 @@ def run_ruff_check(workspace: Workspace, document: Document) -> List[RuffCheck]:
return converter.structure(result, List[RuffCheck])


def run_ruff_fix(workspace: Workspace, document: Document) -> str:
def run_ruff_fix(document: Document, settings: PluginSettings) -> str:
result = run_ruff(
workspace,
document_path=document.path,
document_source=document.source,
fix=True,
settings=settings,
)
return result


def run_ruff_format(
workspace: Workspace, document_path: str, document_source: str
settings: PluginSettings,
document_path: str,
document_source: str,
) -> str:
settings = load_settings(workspace, document_path)
fixable_codes = ["I"]
if settings.format:
fixable_codes.extend(settings.format)
extra_arguments = [
f"--fixable={','.join(fixable_codes)}",
]
result = run_ruff(
workspace,
document_path,
document_source,
settings=settings,
document_path=document_path,
document_source=document_source,
fix=True,
extra_arguments=extra_arguments,
)
return result


def run_ruff(
workspace: Workspace,
settings: PluginSettings,
document_path: str,
document_source: str,
fix: bool = False,
Expand All @@ -398,8 +427,8 @@ def run_ruff(

Parameters
----------
workspace : pyls.workspace.Workspace
Workspace to run ruff in.
settings : PluginSettings
Settings to use.
document_path : str
Path to file to run ruff on.
document_source : str
Expand All @@ -414,7 +443,6 @@ def run_ruff(
-------
String containing the result in json format.
"""
settings = load_settings(workspace, document_path)
executable = settings.executable
arguments = build_arguments(document_path, settings, fix, extra_arguments)

Expand Down Expand Up @@ -558,6 +586,7 @@ def load_settings(workspace: Workspace, document_path: str) -> PluginSettings:
extend_ignore=plugin_settings.extend_ignore,
extend_select=plugin_settings.extend_select,
format=plugin_settings.format,
severities=plugin_settings.severities,
)

return plugin_settings
2 changes: 2 additions & 0 deletions pylsp_ruff/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class PluginSettings:

format: Optional[List[str]] = None

severities: Optional[Dict[str, str]] = None


def to_camel_case(snake_str: str) -> str:
components = snake_str.split("_")
Expand Down
9 changes: 6 additions & 3 deletions tests/test_code_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,16 @@ def f():
"""
)
_, doc = temp_document(codeaction_str, workspace)
fixed_str = ruff_lint.run_ruff_fix(workspace, doc)
settings = ruff_lint.load_settings(workspace, doc.path)
fixed_str = ruff_lint.run_ruff_fix(doc, settings)
assert fixed_str == expected_str


def test_format_document_default_settings(workspace):
_, doc = temp_document(import_str, workspace)
settings = ruff_lint.load_settings(workspace, doc.path)
formatted_str = ruff_lint.run_ruff_format(
workspace, document_path=doc.path, document_source=doc.source
settings, document_path=doc.path, document_source=doc.source
)
assert formatted_str == import_str

Expand All @@ -146,7 +148,8 @@ def test_format_document_settings(workspace):
}
)
_, doc = temp_document(import_str, workspace)
settings = ruff_lint.load_settings(workspace, doc.path)
formatted_str = ruff_lint.run_ruff_format(
workspace, document_path=doc.path, document_source=doc.source
settings, document_path=doc.path, document_source=doc.source
)
assert formatted_str == expected_str
8 changes: 8 additions & 0 deletions tests/test_ruff_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def f():
"plugins": {
"ruff": {
"extendIgnore": ["D104"],
"severities": {"E402": "E", "D103": "I"},
}
}
}
Expand All @@ -206,6 +207,13 @@ def f():
assert "D104" not in _list
assert "F841" not in _list

# Check custom severities
for diag in diags:
if diag["code"] == "E402":
assert diag["severity"] == 1
if diag["code"] == "D103":
assert diag["severity"] == 3

# Excludes
doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, "blah/__init__.py"))
workspace.put_document(doc_uri, doc_str)
Expand Down