Skip to content

Commit 5811e8e

Browse files
authored
mkdir codemod fixes (#586)
* tmpfile codemod able to report unfixed * refactor * use original leading line and test in func scope * lower cov to allow for one py version * ensure class attrs are instance attrs * clear mktmp set
1 parent 435f635 commit 5811e8e

File tree

5 files changed

+134
-22
lines changed

5 files changed

+134
-22
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ extend-exclude = '''
114114
'''
115115

116116
[coverage-threshold]
117-
line_coverage_min = 93.5
117+
line_coverage_min = 93
118118
[coverage-threshold.modules."src/core_codemods/"]
119119
# Detect if a codemod is missing unit or integration tests
120120
file_line_coverage_min = 50

src/codemodder/context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535

3636

3737
class CodemodExecutionContext:
38-
_results_by_codemod: dict[str, list[ChangeSet]] = {}
3938
_failures_by_codemod: dict[str, list[Path]] = {}
4039
_dependency_update_by_codemod: dict[str, PackageStore | None] = {}
4140
_unfixed_findings_by_codemod: dict[str, list[UnfixedFinding]] = {}
@@ -69,6 +68,8 @@ def __init__(
6968
self.verbose = verbose
7069
self._changesets_by_codemod: dict[str, list[ChangeSet]] = {}
7170
self._failures_by_codemod = {}
71+
self._dependency_update_by_codemod = {}
72+
self._unfixed_findings_by_codemod = {}
7273
self.dependencies = {}
7374
self.registry = registry
7475
self.repo_manager = repo_manager

src/core_codemods/tempfile_mktemp.py

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33

44
import libcst as cst
55
from libcst import matchers
6+
from libcst.codemod import CodemodContext
67

78
from codemodder.codemods.libcst_transformer import (
89
LibcstResultTransformer,
910
LibcstTransformerPipeline,
1011
)
1112
from codemodder.codemods.utils_mixin import NameAndAncestorResolutionMixin
13+
from codemodder.file_context import FileContext
14+
from codemodder.result import Result, same_line
1215
from codemodder.utils.utils import clean_simplestring
1316
from core_codemods.api import CoreCodemod, Metadata, Reference, ReviewGuidance
1417

@@ -19,41 +22,83 @@ class TempfileMktempTransformer(
1922
change_description = "Replaces `tempfile.mktemp` with `tempfile.mkstemp`."
2023
_module_name = "tempfile"
2124

22-
def leave_SimpleStatementLine(self, original_node, updated_node):
25+
def __init__(
26+
self,
27+
context: CodemodContext,
28+
results: list[Result] | None,
29+
file_context: FileContext,
30+
_transformer: bool = False,
31+
):
32+
self.mktemp_calls: set[cst.Call] = set()
33+
super().__init__(context, results, file_context, _transformer)
34+
35+
def visit_Call(self, node: cst.Call) -> None:
36+
if self._is_mktemp_call(node):
37+
self.mktemp_calls.add(node)
38+
39+
def leave_SimpleStatementLine(
40+
self,
41+
original_node: cst.SimpleStatementLine,
42+
updated_node: cst.SimpleStatementLine,
43+
) -> cst.SimpleStatementLine | cst.FlattenSentinel:
44+
if not self.node_is_selected(original_node):
45+
return updated_node
46+
2347
match original_node:
2448
case cst.SimpleStatementLine(body=[bsstmt]):
25-
return self.check_mktemp(original_node, bsstmt)
49+
if maybe_tuple := self._is_assigned_to_mktemp(bsstmt):
50+
assign_name, call = maybe_tuple
51+
return self.report_and_change(
52+
call, assign_name, original_node.leading_lines
53+
)
54+
if maybe_tuple := self._mktemp_is_sink(bsstmt):
55+
wrapper_func_name, call = maybe_tuple
56+
return self.report_and_change(
57+
call,
58+
wrapper_func_name,
59+
original_node.leading_lines,
60+
assignment=False,
61+
)
62+
63+
# If we get here it's because there is a mktemp call but we haven't fixed it yet.
64+
for unfixed in self.mktemp_calls:
65+
self.report_unfixed(unfixed, reason="Pixee does not yet support this fix.")
66+
self.mktemp_calls.clear()
2667
return updated_node
2768

28-
def check_mktemp(
29-
self, original_node: cst.SimpleStatementLine, bsstmt: cst.BaseSmallStatement
30-
) -> cst.SimpleStatementLine | cst.FlattenSentinel:
31-
if maybe_tuple := self._is_assigned_to_mktemp(bsstmt): # type: ignore
32-
assign_name, call = maybe_tuple
33-
return self.report_and_change(call, assign_name)
34-
if maybe_tuple := self._mktemp_is_sink(bsstmt):
35-
wrapper_func_name, call = maybe_tuple
36-
return self.report_and_change(call, wrapper_func_name, assignment=False)
37-
return original_node
69+
def filter_by_result(self, node) -> bool:
70+
match node:
71+
case cst.SimpleStatementLine():
72+
pos_to_match = self.node_position(node)
73+
return self.results is None or any(
74+
self.match_location(pos_to_match, result)
75+
for result in self.results or []
76+
)
77+
return False
78+
79+
def match_location(self, pos, result):
80+
return any(same_line(pos, location) for location in result.locations)
3881

3982
def report_and_change(
40-
self, node: cst.Call, name: cst.Name, assignment=True
83+
self, node: cst.Call, name: cst.Name, leading_lines: tuple, assignment=True
4184
) -> cst.FlattenSentinel:
85+
self.mktemp_calls.clear()
4286
self.report_change(node)
4387
self.add_needed_import(self._module_name)
4488
self.remove_unused_import(node)
4589
with_block = (
4690
f"{name.value} = tf.name" if assignment else f"{name.value}(tf.name)"
4791
)
4892
new_stmt = dedent(
49-
f"""
93+
f"""\
5094
with tempfile.NamedTemporaryFile({self._make_args(node)}) as tf:
5195
{with_block}
5296
"""
5397
).rstrip()
98+
5499
return cst.FlattenSentinel(
55100
[
56-
cst.parse_statement(new_stmt),
101+
cst.parse_statement(new_stmt).with_changes(leading_lines=leading_lines),
57102
]
58103
)
59104

tests/codemods/sonar/test_sonar_tempfile_mktemp.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,43 @@ def test_simple(self, tmpdir):
3939
]
4040
}
4141
self.run_and_assert(tmpdir, input_code, expected, results=json.dumps(issues))
42+
unfixed = self.execution_context.get_unfixed_findings(self.codemod.id)
43+
assert len(unfixed) == 0
44+
45+
def test_unfixed(self, tmpdir):
46+
RULE_ID = "python:S5445"
47+
48+
input_code = (
49+
expected
50+
) = """
51+
import tempfile
52+
53+
tmp_file = open(tempfile.mktemp(), "w+")
54+
tmp_file.write("text")
55+
"""
56+
57+
issues = {
58+
"issues": [
59+
{
60+
"rule": RULE_ID,
61+
"status": "OPEN",
62+
"component": "code.py",
63+
"textRange": {
64+
"startLine": 4,
65+
"endLine": 4,
66+
"startOffset": 15,
67+
"endOffset": 33,
68+
},
69+
}
70+
]
71+
}
72+
73+
self.run_and_assert(tmpdir, input_code, expected, results=json.dumps(issues))
74+
75+
unfixed = self.execution_context.get_unfixed_findings(self.codemod.id)
76+
assert len(unfixed) == 1
77+
assert unfixed[0].id == RULE_ID
78+
assert unfixed[0].rule.id == RULE_ID
79+
assert unfixed[0].lineNumber == 4
80+
assert unfixed[0].reason == "Pixee does not yet support this fix."
81+
assert unfixed[0].path == "code.py"

tests/codemods/test_tempfile_mktemp.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ def test_as_sink(self, tmpdir):
4848
with tempfile.NamedTemporaryFile(delete=False) as tf:
4949
print(tf.name)
5050
var = "hello"
51-
5251
with tempfile.NamedTemporaryFile(delete=False) as tf:
5352
bool(tf.name)
5453
"""
@@ -70,16 +69,12 @@ def test_import_with_arg(self, tmpdir):
7069
7170
with tempfile.NamedTemporaryFile(suffix="suffix", delete=False) as tf:
7271
filename = tf.name
73-
7472
with tempfile.NamedTemporaryFile(suffix="suffix", prefix="prefix", delete=False) as tf:
7573
print(tf.name)
76-
7774
with tempfile.NamedTemporaryFile(suffix="suffix", prefix="prefix", dir="dir", delete=False) as tf:
7875
filename = tf.name
79-
8076
with tempfile.NamedTemporaryFile(suffix="suffix", prefix="prefix", delete=False) as tf:
8177
filename = tf.name
82-
8378
with tempfile.NamedTemporaryFile(suffix="suffix", prefix="prefix", dir="dir", delete=False) as tf:
8479
filename = tf.name
8580
var = "hello"
@@ -166,6 +161,37 @@ def test_open_and_write_no_change(self, tmpdir):
166161
"""
167162
self.run_and_assert(tmpdir, input_code, input_code)
168163

164+
def test_exclude_line(self, tmpdir):
165+
input_code = (
166+
expected
167+
) = """
168+
import tempfile
169+
name = tempfile.mktemp()
170+
"""
171+
lines_to_exclude = [3]
172+
self.run_and_assert(
173+
tmpdir,
174+
input_code,
175+
expected,
176+
lines_to_exclude=lines_to_exclude,
177+
)
178+
179+
def test_in_func_scope(self, tmpdir):
180+
input_code = """
181+
import tempfile
182+
183+
def make_file():
184+
filename = tempfile.mktemp()
185+
"""
186+
expected_output = """
187+
import tempfile
188+
189+
def make_file():
190+
with tempfile.NamedTemporaryFile(delete=False) as tf:
191+
filename = tf.name
192+
"""
193+
self.run_and_assert(tmpdir, input_code, expected_output)
194+
169195
@pytest.mark.xfail(reason="Not currently supported")
170196
def test_as_str_concat(self, tmpdir):
171197
input_code = """

0 commit comments

Comments
 (0)