Skip to content

Commit 6a8240a

Browse files
Add code actions for simple Pylint items (#300)
Co-authored-by: Karthik Nadig <[email protected]>
1 parent 547ee7e commit 6a8240a

File tree

2 files changed

+166
-1
lines changed

2 files changed

+166
-1
lines changed

bundled/tool/lsp_server.py

+56-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import os
99
import pathlib
10+
import re
1011
import sys
1112
import traceback
1213
from typing import Any, Callable, Dict, List, Optional, Sequence, Union
@@ -311,6 +312,60 @@ def organize_imports(
311312
]
312313

313314

315+
REPLACEMENTS = {
316+
"R0205:useless-object-inheritance": {
317+
"pattern": r"class (\w+)\(object\):",
318+
"repl": r"class \1:",
319+
},
320+
"R1721:unnecessary-comprehension": {
321+
"pattern": r"\{([\w\s,]+) for [\w\s,]+ in ([\w\s,]+)\}",
322+
"repl": r"set(\2)",
323+
},
324+
"E1141:dict-iter-missing-items": {
325+
"pattern": r"for\s+(\w+),\s+(\w+)\s+in\s+(\w+)\s*:",
326+
"repl": r"for \1, \2 in \3.items():",
327+
},
328+
}
329+
330+
331+
def _get_replacement_edit(diagnostic: lsp.Diagnostic, lines: List[str]) -> lsp.TextEdit:
332+
return lsp.TextEdit(
333+
lsp.Range(
334+
start=lsp.Position(line=diagnostic.range.start.line, character=0),
335+
end=lsp.Position(line=diagnostic.range.start.line + 1, character=0),
336+
),
337+
re.sub(
338+
REPLACEMENTS[diagnostic.code]["pattern"],
339+
REPLACEMENTS[diagnostic.code]["repl"],
340+
lines[diagnostic.range.start.line],
341+
),
342+
)
343+
344+
345+
@QUICK_FIXES.quick_fix(
346+
codes=list(REPLACEMENTS.keys()),
347+
)
348+
def fix_with_replacement(
349+
document: workspace.Document, diagnostics: List[lsp.Diagnostic]
350+
) -> List[lsp.CodeAction]:
351+
"""Provides quick fixes which basic string replacements."""
352+
return [
353+
lsp.CodeAction(
354+
title=f"{TOOL_DISPLAY}: Run autofix code action",
355+
kind=lsp.CodeActionKind.QuickFix,
356+
diagnostics=diagnostics,
357+
edit=_create_workspace_edits(
358+
document,
359+
[
360+
_get_replacement_edit(diagnostic, document.lines)
361+
for diagnostic in diagnostics
362+
if diagnostic.code in REPLACEMENTS
363+
],
364+
),
365+
)
366+
]
367+
368+
314369
def _command_quick_fix(
315370
diagnostics: List[lsp.Diagnostic],
316371
title: str,
@@ -333,7 +388,7 @@ def _create_workspace_edits(
333388
lsp.TextDocumentEdit(
334389
text_document=lsp.OptionalVersionedTextDocumentIdentifier(
335390
uri=document.uri,
336-
version=document.version,
391+
version=document.version if document.version else 0,
337392
),
338393
edits=results,
339394
)

src/test/python_tests/test_code_actions.py

+110
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,113 @@ def _handler(params):
137137
]
138138

139139
assert_that(actual_code_actions, is_(expected))
140+
141+
142+
@pytest.mark.parametrize(
143+
("code", "contents", "new_text"),
144+
[
145+
(
146+
"R0205:useless-object-inheritance",
147+
"""
148+
class Banana(object):
149+
pass""",
150+
"""class Banana:
151+
""",
152+
),
153+
(
154+
"R1721:unnecessary-comprehension",
155+
"""
156+
NUMBERS = [1, 1, 2, 2, 3, 3]
157+
158+
UNIQUE_NUMBERS = {number for number in NUMBERS}
159+
""",
160+
"""UNIQUE_NUMBERS = set(NUMBERS)
161+
""",
162+
),
163+
(
164+
"E1141:dict-iter-missing-items",
165+
"""
166+
data = {'Paris': 2_165_423, 'New York City': 8_804_190, 'Tokyo': 13_988_129}
167+
for city, population in data:
168+
print(f"{city} has population {population}.")
169+
""",
170+
"""for city, population in data.items():
171+
""",
172+
),
173+
],
174+
)
175+
def test_edit_code_action(code, contents, new_text):
176+
"""Tests for code actions which run a command."""
177+
with utils.python_file(contents, TEST_FILE_PATH.parent) as temp_file:
178+
uri = utils.as_uri(os.fspath(temp_file))
179+
180+
actual = {}
181+
with session.LspSession() as ls_session:
182+
ls_session.initialize()
183+
184+
done = Event()
185+
186+
def _handler(params):
187+
nonlocal actual
188+
actual = params
189+
done.set()
190+
191+
ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler)
192+
193+
ls_session.notify_did_open(
194+
{
195+
"textDocument": {
196+
"uri": uri,
197+
"languageId": "python",
198+
"version": 1,
199+
"text": contents,
200+
}
201+
}
202+
)
203+
204+
# wait for some time to receive all notifications
205+
done.wait(TIMEOUT)
206+
207+
diagnostics = [d for d in actual["diagnostics"] if d["code"] == code]
208+
209+
assert_that(len(diagnostics), is_(greater_than(0)))
210+
211+
actual_code_actions = ls_session.text_document_code_action(
212+
{
213+
"textDocument": {"uri": uri},
214+
"range": {
215+
"start": {"line": 0, "character": 0},
216+
"end": {"line": 1, "character": 0},
217+
},
218+
"context": {"diagnostics": diagnostics},
219+
}
220+
)
221+
text_document = actual_code_actions[0]["edit"]["documentChanges"][0][
222+
"textDocument"
223+
]
224+
text_range = actual_code_actions[0]["edit"]["documentChanges"][0]["edits"][
225+
0
226+
]["range"]
227+
expected = [
228+
{
229+
"title": f"{LINTER}: Run autofix code action",
230+
"kind": "quickfix",
231+
"diagnostics": [d],
232+
"edit": {
233+
"documentChanges": [
234+
{
235+
"textDocument": text_document,
236+
"edits": [
237+
{
238+
"range": text_range,
239+
"newText": new_text,
240+
}
241+
],
242+
}
243+
]
244+
},
245+
}
246+
for d in diagnostics
247+
]
248+
249+
assert_that(actual_code_actions, is_(expected))

0 commit comments

Comments
 (0)