diff --git a/justfile b/justfile index 009656e..24291fc 100644 --- a/justfile +++ b/justfile @@ -9,8 +9,8 @@ install: pip install --no-deps -r requirements-dev.txt fmt: - ruff --fix ./ruff_lsp ./tests black ./ruff_lsp ./tests + ruff --fix ./ruff_lsp ./tests check: ruff ./ruff_lsp ./tests diff --git a/ruff_lsp/server.py b/ruff_lsp/server.py index 9bc34ea..2f321bb 100755 --- a/ruff_lsp/server.py +++ b/ruff_lsp/server.py @@ -132,11 +132,14 @@ def _parse_output_using_regex(content: str) -> list[Diagnostic]: # { # "code": "F841", # "message": "Local variable `x` is assigned to but never used", - # "fixed": false, # "location": { # "row": 2, # "column": 5 # }, + # "end_location": { + # "row": 2, + # "column": 6 + # }, # "fix": { # "content: "", # "location": { @@ -148,7 +151,8 @@ def _parse_output_using_regex(content: str) -> list[Diagnostic]: # "column: 0 # } # }, - # "filename": "/path/to/test.py" + # "filename": "/path/to/test.py", + # "noqa_row": 2 # }, # ... # ] @@ -167,7 +171,11 @@ def _parse_output_using_regex(content: str) -> list[Diagnostic]: severity=_get_severity(check["code"]), code=check["code"], source=TOOL_DISPLAY, - data=check.get("fix"), + data=DiagnosticData( + fix=check.get("fix"), + # Available since Ruff v0.0.253. + noqa_row=check.get("noqa_row"), + ), tags=_get_tags(check["code"]), ) diagnostics.append(diagnostic) @@ -195,7 +203,8 @@ def _get_severity(code: str) -> DiagnosticSeverity: NOQA_REGEX = re.compile( - r"(?i:# (?:(?:ruff|flake8): )?noqa)(?::\s?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?" + r"(?i:# (?:(?:ruff|flake8): )?(?Pnoqa))" + r"(?::\s?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?" ) CODE_REGEX = re.compile(r"[A-Z]{1,3}[0-9]{3}") @@ -253,6 +262,11 @@ class Fix(TypedDict): end_location: Location +class DiagnosticData(TypedDict): + fix: Fix | None + noqa_row: int | None + + @LSP_SERVER.feature( TEXT_DOCUMENT_CODE_ACTION, CodeActionOptions( @@ -326,7 +340,8 @@ def code_action(params: CodeActionParams) -> list[CodeAction] | None: diagnostic for diagnostic in params.context.diagnostics if diagnostic.source == "Ruff" - and diagnostic.data is not None + and cast(DiagnosticData, diagnostic.data)["fix"] + is not None ], ), ] @@ -391,7 +406,8 @@ def code_action(params: CodeActionParams) -> list[CodeAction] | None: diagnostic for diagnostic in params.context.diagnostics if diagnostic.source == "Ruff" - and diagnostic.data is not None + and cast(DiagnosticData, diagnostic.data)["fix"] + is not None ], ), ) @@ -400,9 +416,8 @@ def code_action(params: CodeActionParams) -> list[CodeAction] | None: if not params.context.only or CodeActionKind.QuickFix in params.context.only: for diagnostic in params.context.diagnostics: if diagnostic.source == "Ruff": - if diagnostic.data is not None: - fix = cast(Fix, diagnostic.data) - + fix = cast(DiagnosticData, diagnostic.data)["fix"] + if fix is not None: title: str if fix.get("message"): title = f"Ruff ({diagnostic.code}): {fix['message']}" @@ -416,9 +431,50 @@ def code_action(params: CodeActionParams) -> list[CodeAction] | None: title=title, kind=CodeActionKind.QuickFix, data=params.text_document.uri, - edit=_create_workspace_edit( - document, cast(Fix, diagnostic.data) - ), + edit=_create_workspace_edit(document, fix), + diagnostics=[diagnostic], + ), + ) + + # Add "Disable for this line" for every diagnostic. + if not params.context.only or CodeActionKind.QuickFix in params.context.only: + for diagnostic in params.context.diagnostics: + if diagnostic.source == "Ruff": + noqa_row = cast(DiagnosticData, diagnostic.data)["noqa_row"] + if noqa_row is not None: + line = document.lines[noqa_row - 1].rstrip("\r\n") + match = NOQA_REGEX.search(line) + # `foo # noqa: OLD` -> `foo # noqa: OLD,NEW` + if match and match.group("codes") is not None: + codes = match.group("codes") + f", {diagnostic.code}" + start, end = match.start("codes"), match.end("codes") + new_line = line[:start] + codes + line[end:] + # `foo # noqa` -> `foo # noqa: NEW` + elif match: + end = match.end("noqa") + new_line = line[:end] + f": {diagnostic.code}" + line[end:] + # `foo` -> `foo # noqa: NEW` + else: + new_line = f"{line} # noqa: {diagnostic.code}" + fix = Fix( + content=new_line, + message=None, + location=Location( + row=noqa_row, + column=0, + ), + end_location=Location( + row=noqa_row, + column=len(line), + ), + ) + + actions.append( + CodeAction( + title=f"Ruff: Disable {diagnostic.code} for this line", + kind=CodeActionKind.QuickFix, + data=params.text_document.uri, + edit=_create_workspace_edit(document, fix), diagnostics=[diagnostic], ), ) diff --git a/tests/test_server.py b/tests/test_server.py index ee1d4d8..46d7742 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -14,6 +14,8 @@ class TestServer(unittest.TestCase): + maxDiff = None # noqa: N815 + def test_linting_example(self): logging.info(TEST_FILE_PATH) contents = TEST_FILE_PATH.read_text() @@ -54,10 +56,13 @@ def _handler(params): "end": {"line": 0, "character": 10}, }, "data": { - "content": "", - "message": "Remove unused import: `sys`", - "location": {"row": 1, "column": 0}, - "end_location": {"row": 2, "column": 0}, + "fix": { + "content": "", + "message": "Remove unused import: `sys`", + "location": {"row": 1, "column": 0}, + "end_location": {"row": 2, "column": 0}, + }, + "noqa_row": None, }, "message": "`sys` imported but unused", "severity": 2, @@ -70,6 +75,7 @@ def _handler(params): "start": {"line": 2, "character": 6}, "end": {"line": 2, "character": 7}, }, + "data": {"fix": None, "noqa_row": None}, "message": "Undefined name `x`", "severity": 2, "code": "F821", @@ -122,10 +128,13 @@ def _handler(params): "end": {"line": 0, "character": 10}, }, "data": { - "content": "", - "message": "Remove unused import: `sys`", - "location": {"row": 1, "column": 0}, - "end_location": {"row": 2, "column": 0}, + "fix": { + "content": "", + "message": "Remove unused import: `sys`", + "location": {"row": 1, "column": 0}, + "end_location": {"row": 2, "column": 0}, + }, + "noqa_row": None, }, "message": "`sys` imported but unused", "severity": 2, @@ -138,6 +147,7 @@ def _handler(params): "start": {"line": 2, "character": 6}, "end": {"line": 2, "character": 7}, }, + "data": {"fix": None, "noqa_row": None}, "message": "Undefined name `x`", "severity": 2, "code": "F821",