Skip to content

Commit 157477d

Browse files
authored
Correct node position for FuncDef nodes and fix sonar codemods (#423)
* handle node position for function def * add exclude logic to fix-mutable-params * end is 0-idx length * json-ify issues * fix missing self or cls should have node selection logic * add unit test * dedupe code
1 parent d01665f commit 157477d

File tree

11 files changed

+2626
-58
lines changed

11 files changed

+2626
-58
lines changed

src/codemodder/codemods/base_visitor.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import ClassVar, Collection
22

3+
import libcst as cst
34
from libcst import MetadataDependent
5+
from libcst._position import CodePosition, CodeRange
46
from libcst.codemod import ContextAwareVisitor, VisitorBasedCodemodCommand
57
from libcst.metadata import PositionProvider, ProviderT
68

@@ -48,7 +50,18 @@ def node_is_selected(self, node) -> bool:
4850

4951
def node_position(self, node):
5052
# See https://github.com/Instagram/LibCST/blob/main/libcst/_metadata_dependent.py#L112
51-
return self.get_metadata(PositionProvider, node)
53+
match node:
54+
case cst.FunctionDef():
55+
# By default a function's position includes the entire
56+
# function definition. Instead, we will only use the first line
57+
# of the function definition.
58+
params_end = self.get_metadata(PositionProvider, node.params).end
59+
return CodeRange(
60+
start=self.get_metadata(PositionProvider, node).start,
61+
end=CodePosition(params_end.line, params_end.column + 1),
62+
)
63+
case _:
64+
return self.get_metadata(PositionProvider, node)
5265

5366
def lineno_for_node(self, node):
5467
return self.node_position(node).start.line

src/codemodder/codemods/imported_call_modifier.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,3 @@ def leave_Call(self, original_node: cst.Call, updated_node: cst.Call):
9292
)
9393

9494
return updated_node
95-
96-
def filter_by_path_includes_or_excludes(self, pos_to_match):
97-
"""
98-
Returns False if the node, whose position in the file is pos_to_match, matches any of the lines specified in the path-includes or path-excludes flags.
99-
"""
100-
# excludes takes precedence if defined
101-
if self.line_exclude:
102-
return not any(match_line(pos_to_match, line) for line in self.line_exclude)
103-
if self.line_include:
104-
return any(match_line(pos_to_match, line) for line in self.line_include)
105-
return True
106-
107-
def node_position(self, node):
108-
# See https://github.com/Instagram/LibCST/blob/main/libcst/_metadata_dependent.py#L112
109-
return self.get_metadata(PositionProvider, node)
110-
111-
112-
def match_line(pos, line):
113-
return pos.start.line == line and pos.end.line == line

src/codemodder/codemods/libcst_transformer.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from libcst._position import CodeRange
66
from libcst.codemod import CodemodContext
77
from libcst.codemod.visitors import AddImportsVisitor, RemoveImportsVisitor
8-
from libcst.metadata import PositionProvider
98

109
from codemodder.codemods.base_transformer import BaseTransformerPipeline
1110
from codemodder.codemods.base_visitor import BaseTransformer
@@ -98,10 +97,6 @@ def leave_ClassDef(
9897
) -> cst.ClassDef:
9998
return self._new_or_updated_node(original_node, updated_node)
10099

101-
def node_position(self, node):
102-
# See https://github.com/Instagram/LibCST/blob/main/libcst/_metadata_dependent.py#L112
103-
return self.get_metadata(PositionProvider, node)
104-
105100
def add_change(self, node, description: str, start: bool = True):
106101
position = self.node_position(node)
107102
self.add_change_from_position(position, description, start)

src/core_codemods/fix_missing_self_or_cls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ class FixMissingSelfOrClsTransformer(
1717
def leave_FunctionDef(
1818
self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
1919
) -> cst.FunctionDef:
20-
# TODO: add filter by include or exclude that works for nodes
21-
# that that have different start/end numbers.
20+
if not self.node_is_selected(original_node):
21+
return original_node
2222

2323
if not self.find_immediate_class_def(original_node):
2424
# If `original_node` is not inside a class, nothing to do.

src/core_codemods/fix_mutable_params.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ def leave_FunctionDef(
167167
updated_node: cst.FunctionDef,
168168
):
169169
"""Transforms function definitions with mutable default parameters"""
170-
# TODO: add filter by include or exclude that works for nodes
171-
# that that have different start/end numbers.
170+
if not self.node_is_selected(original_node):
171+
return updated_node
172172

173173
(
174174
updated_params,

src/core_codemods/order_imports.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import libcst as cst
22
from libcst.metadata import PositionProvider
33

4+
from codemodder.codemods.base_visitor import UtilsMixin
45
from codemodder.codemods.transformations.clean_imports import (
56
GatherTopLevelImportBlocks,
67
OrderImportsBlocksTransform,
78
)
89
from core_codemods.api import Metadata, ReviewGuidance, SimpleCodemod
910

1011

11-
class OrderImports(SimpleCodemod):
12+
class OrderImports(SimpleCodemod, UtilsMixin):
1213
metadata = Metadata(
1314
name="order-imports",
1415
summary="Order Imports",
@@ -46,22 +47,3 @@ def transform_module_impl(self, tree: cst.Module) -> cst.Module:
4647
)
4748
return result_tree
4849
return tree
49-
50-
def filter_by_path_includes_or_excludes(self, pos_to_match):
51-
"""
52-
Returns False if the node, whose position in the file is pos_to_match, matches any of the lines specified in the path-includes or path-excludes flags.
53-
"""
54-
# excludes takes precedence if defined
55-
if self.line_exclude:
56-
return not any(match_line(pos_to_match, line) for line in self.line_exclude)
57-
if self.line_include:
58-
return any(match_line(pos_to_match, line) for line in self.line_include)
59-
return True
60-
61-
def node_position(self, node):
62-
# See https://github.com/Instagram/LibCST/blob/main/libcst/_metadata_dependent.py#L112
63-
return self.get_metadata(PositionProvider, node)
64-
65-
66-
def match_line(pos, line):
67-
return pos.start.line == line and pos.end.line == line

tests/codemods/test_base_visitor.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from collections import defaultdict
2+
from textwrap import dedent
23

34
import libcst as cst
5+
from libcst._position import CodePosition, CodeRange
46
from libcst.codemod import CodemodContext
57
from libcst.metadata import PositionProvider
68

@@ -29,6 +31,30 @@ def leave_SimpleStatementLine(
2931
return original_node
3032

3133

34+
class AssertPositionCodemod(BaseTransformer):
35+
METADATA_DEPENDENCIES = (PositionProvider,)
36+
37+
def __init__(
38+
self,
39+
context,
40+
results,
41+
expected_node_position,
42+
line_exclude=None,
43+
line_include=None,
44+
):
45+
BaseTransformer.__init__(
46+
self, context, results, line_include or [], line_exclude or []
47+
)
48+
self.expected_node_position = expected_node_position
49+
50+
def leave_FunctionDef(
51+
self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef
52+
) -> cst.FunctionDef:
53+
pos_to_match = self.node_position(original_node)
54+
assert pos_to_match == self.expected_node_position
55+
return updated_node
56+
57+
3258
class TestBaseVisitor:
3359
def run_and_assert(self, input_code, expected, line_exclude, line_include):
3460
input_tree = cst.parse_module(input_code)
@@ -59,3 +85,43 @@ def test_includes_excludes(self):
5985
line_exclude = [1]
6086
line_include = [1]
6187
self.run_and_assert(input_code, expected, line_exclude, line_include)
88+
89+
90+
class TestNodePosition:
91+
def run_and_assert(self, input_code, expected_pos):
92+
input_tree = cst.parse_module(dedent(input_code))
93+
command_instance = AssertPositionCodemod(
94+
CodemodContext(), defaultdict(list), expected_pos
95+
)
96+
command_instance.transform_module(input_tree)
97+
98+
def test_funcdef(self):
99+
input_code = """
100+
def hello():
101+
pass
102+
"""
103+
expected_pos = CodeRange(
104+
start=CodePosition(line=2, column=0), end=CodePosition(line=2, column=11)
105+
)
106+
self.run_and_assert(input_code, expected_pos)
107+
108+
def test_instance(self):
109+
input_code = """
110+
class MyClass:
111+
def instance_method():
112+
print("instance_method")
113+
"""
114+
expected_pos = CodeRange(
115+
start=CodePosition(line=3, column=4), end=CodePosition(line=3, column=25)
116+
)
117+
self.run_and_assert(input_code, expected_pos)
118+
119+
def test_funcdef_args(self):
120+
input_code = """
121+
def hello(one, *args, **kwargs):
122+
pass
123+
"""
124+
expected_pos = CodeRange(
125+
start=CodePosition(line=2, column=0), end=CodePosition(line=2, column=31)
126+
)
127+
self.run_and_assert(input_code, expected_pos)

tests/codemods/test_fix_missing_self_or_cls.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,19 @@ def kls(**kwargs):
135135
pass
136136
"""
137137
self.run_and_assert(tmpdir, input_code, input_code)
138+
139+
def test_exclude_line(self, tmpdir):
140+
input_code = (
141+
expected
142+
) = """
143+
class A:
144+
def method():
145+
pass
146+
"""
147+
lines_to_exclude = [3]
148+
self.run_and_assert(
149+
tmpdir,
150+
input_code,
151+
expected,
152+
lines_to_exclude=lines_to_exclude,
153+
)

tests/codemods/test_fix_mutable_params.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,18 @@ def foo(self, bar=None):
292292
pass
293293
"""
294294
self.run_and_assert(tmpdir, input_code, expected_output)
295+
296+
def test_exclude_line(self, tmpdir):
297+
input_code = (
298+
expected
299+
) = """
300+
def foo(one, *args, bar=[]):
301+
print(bar)
302+
"""
303+
lines_to_exclude = [2]
304+
self.run_and_assert(
305+
tmpdir,
306+
input_code,
307+
expected,
308+
lines_to_exclude=lines_to_exclude,
309+
)

tests/codemods/test_sonar_fix_missing_self_or_cls.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@ def test_name(self):
1414
def test_simple(self, tmpdir):
1515
input_code = """
1616
class A:
17-
def method():
17+
def instance_method():
1818
pass
1919
2020
@classmethod
21-
def clsmethod():
21+
def class_method():
2222
pass
2323
"""
2424
expected_output = """
2525
class A:
26-
def method(self):
26+
def instance_method(self):
2727
pass
2828
2929
@classmethod
30-
def clsmethod(cls):
30+
def class_method(cls):
3131
pass
3232
"""
3333
issues = {
@@ -37,8 +37,8 @@ def clsmethod(cls):
3737
"status": "OPEN",
3838
"component": "code.py",
3939
"textRange": {
40-
"startLine": 2,
41-
"endLine": 2,
40+
"startLine": 3,
41+
"endLine": 3,
4242
"startOffset": 4,
4343
"endOffset": 25,
4444
},
@@ -48,8 +48,8 @@ def clsmethod(cls):
4848
"status": "OPEN",
4949
"component": "code.py",
5050
"textRange": {
51-
"startLine": 6,
52-
"endLine": 6,
51+
"startLine": 7,
52+
"endLine": 7,
5353
"startOffset": 4,
5454
"endOffset": 22,
5555
},

0 commit comments

Comments
 (0)