Skip to content

Commit 1ccb8cf

Browse files
authored
Support simple patterns for codemod include/exclude (#458)
* Support simple patterns for codemod include/exclude * Add a warning when pattern doesn't match any codemods
1 parent 66b796d commit 1ccb8cf

File tree

3 files changed

+75
-27
lines changed

3 files changed

+75
-27
lines changed

src/codemodder/registry.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import re
34
from dataclasses import dataclass
45
from importlib.metadata import entry_points
56
from typing import TYPE_CHECKING, Optional
@@ -64,36 +65,44 @@ def match_codemods(
6465

6566
if codemod_exclude and not codemod_include:
6667
base_codemods = {}
68+
patterns = [
69+
re.compile(exclude.replace("*", ".*"))
70+
for exclude in codemod_exclude
71+
if "*" in exclude
72+
]
73+
names = set(name for name in codemod_exclude if "*" not in name)
6774
for codemod in self.codemods:
68-
if (sast_only and codemod.origin != "pixee") or (
69-
not sast_only and codemod.origin == "pixee"
75+
if (
76+
codemod.id in names
77+
or (codemod.origin == "pixee" and codemod.name in names)
78+
or any(pat.match(codemod.id) for pat in patterns)
7079
):
71-
base_codemods[codemod.id] = codemod
72-
base_codemods[codemod.name] = codemod
73-
74-
for name_or_id in codemod_exclude:
75-
try:
76-
codemod = base_codemods[name_or_id]
77-
except KeyError:
78-
logger.warning(
79-
f"Requested codemod to exclude'{name_or_id}' does not exist."
80-
)
8180
continue
8281

83-
# remove both by name and id since we don't know which `name_or_id` represented
84-
base_codemods.pop(codemod.name, None)
85-
base_codemods.pop(codemod.id, None)
82+
if bool(sast_only) != bool(codemod.origin == "pixee"):
83+
base_codemods[codemod.id] = codemod
84+
8685
# Remove duplicates and preserve order
87-
return list(dict.fromkeys(base_codemods.values()))
86+
return list(base_codemods.values())
8887

8988
matched_codemods = []
9089
for name in codemod_include:
90+
if "*" in name:
91+
pat = re.compile(name.replace("*", ".*"))
92+
pattern_matches = [code for code in self.codemods if pat.match(code.id)]
93+
matched_codemods.extend(pattern_matches)
94+
if not pattern_matches:
95+
logger.warning(
96+
"Given codemod pattern '%s' does not match any codemods.", name
97+
)
98+
continue
99+
91100
try:
92101
matched_codemods.append(
93102
self._codemods_by_name.get(name) or self._codemods_by_id[name]
94103
)
95104
except KeyError:
96-
logger.warning(f"Requested codemod to include'{name}' does not exist.")
105+
logger.warning(f"Requested codemod to include '{name}' does not exist.")
97106
return matched_codemods
98107

99108
def describe_codemods(

tests/codemods/test_include_exclude.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,50 @@ def test_exclude_some_match(self):
8989
for c in self.registry.codemods
9090
if c.name not in "secure-random" and c.id in self.all_ids
9191
]
92+
93+
def test_include_with_pattern(self):
94+
assert self.registry.match_codemods(["*django*"], None) == [
95+
c for c in self.registry.codemods if "django" in c.id
96+
]
97+
98+
def test_include_with_pattern_and_another(self):
99+
assert self.registry.match_codemods(["*django*", "use-defusedxml"], None) == [
100+
c for c in self.registry.codemods if "django" in c.id
101+
] + [self.codemod_map["use-defusedxml"]]
102+
103+
def test_include_sast_with_prefix(self):
104+
assert self.registry.match_codemods(["sonar*"], None, sast_only=False) == [
105+
c for c in self.registry.codemods if c.origin == "sonar"
106+
]
107+
108+
def test_warn_pattern_no_match(self, caplog):
109+
assert self.registry.match_codemods(["*doesntexist*"], None) == []
110+
assert (
111+
"Given codemod pattern '*doesntexist*' does not match any codemods"
112+
in caplog.text
113+
)
114+
115+
def test_exclude_with_pattern(self):
116+
assert self.registry.match_codemods(None, ["*django*"], sast_only=False) == [
117+
c
118+
for c in self.registry.codemods
119+
if "django" not in c.id and c.id in self.all_ids
120+
]
121+
122+
def test_exclude_with_pattern_and_another(self):
123+
assert self.registry.match_codemods(
124+
None, ["*django*", "use-defusedxml"], sast_only=False
125+
) == [
126+
c
127+
for c in self.registry.codemods
128+
if "django" not in c.id
129+
and c.id in self.all_ids
130+
and c.name != "use-defusedxml"
131+
]
132+
133+
def test_exclude_pixee_with_prefix(self):
134+
assert self.registry.match_codemods(None, ["pixee*"], sast_only=False) == [
135+
c
136+
for c in self.registry.codemods
137+
if not c.origin == "pixee" and c.id in self.all_ids
138+
]

tests/test_codemodder.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def test_codemod_include_no_match(
210210
assert any(x[0] == ("scanned: %s files", 0) for x in info_logger.call_args_list)
211211

212212
assert any(
213-
f"Requested codemod to include'{bad_codemod}' does not exist." in x[0][0]
213+
f"Requested codemod to include '{bad_codemod}' does not exist." in x[0][0]
214214
for x in warning_logger.call_args_list
215215
)
216216

@@ -233,7 +233,7 @@ def test_codemod_include_some_match(
233233
write_report.assert_called_once()
234234
assert any("running codemod %s" in x[0][0] for x in info_logger.call_args_list)
235235
assert any(
236-
f"Requested codemod to include'{bad_codemod}' does not exist." in x[0][0]
236+
f"Requested codemod to include '{bad_codemod}' does not exist." in x[0][0]
237237
for x in warning_logger.call_args_list
238238
)
239239

@@ -262,10 +262,6 @@ def test_codemod_exclude_some_match(
262262

263263
assert f"pixee:python/{good_codemod}" not in codemods_that_ran
264264
assert any("running codemod %s" in x[0][0] for x in info_logger.call_args_list)
265-
assert any(
266-
f"Requested codemod to exclude'{bad_codemod}' does not exist." in x[0][0]
267-
for x in warning_logger.call_args_list
268-
)
269265

270266
@mock.patch("codemodder.registry.logger.warning")
271267
@mock.patch("codemodder.codemodder.logger.info")
@@ -286,10 +282,6 @@ def test_codemod_exclude_no_match(
286282
run(args)
287283
write_report.assert_called_once()
288284
assert any("running codemod %s" in x[0][0] for x in info_logger.call_args_list)
289-
assert any(
290-
f"Requested codemod to exclude'{bad_codemod}' does not exist." in x[0][0]
291-
for x in warning_logger.call_args_list
292-
)
293285

294286
@mock.patch("codemodder.codemods.semgrep.semgrep_run")
295287
def test_exclude_all_registered_codemods(self, mock_semgrep_run, dir_structure):

0 commit comments

Comments
 (0)