Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support simple patterns for codemod include/exclude #458

Merged
merged 2 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 26 additions & 17 deletions src/codemodder/registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from dataclasses import dataclass
from importlib.metadata import entry_points
from typing import TYPE_CHECKING, Optional
Expand Down Expand Up @@ -64,36 +65,44 @@ def match_codemods(

if codemod_exclude and not codemod_include:
base_codemods = {}
patterns = [
re.compile(exclude.replace("*", ".*"))
for exclude in codemod_exclude
if "*" in exclude
]
names = set(name for name in codemod_exclude if "*" not in name)
for codemod in self.codemods:
if (sast_only and codemod.origin != "pixee") or (
not sast_only and codemod.origin == "pixee"
if (
codemod.id in names
or (codemod.origin == "pixee" and codemod.name in names)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this line accomplish this issue for excluded?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think it does for the exclude case but I think we'd want to add more tests so we can keep it as a separate issue.

or any(pat.match(codemod.id) for pat in patterns)
):
base_codemods[codemod.id] = codemod
base_codemods[codemod.name] = codemod

for name_or_id in codemod_exclude:
try:
codemod = base_codemods[name_or_id]
except KeyError:
logger.warning(
f"Requested codemod to exclude'{name_or_id}' does not exist."
)
continue

# remove both by name and id since we don't know which `name_or_id` represented
base_codemods.pop(codemod.name, None)
base_codemods.pop(codemod.id, None)
if bool(sast_only) != bool(codemod.origin == "pixee"):
base_codemods[codemod.id] = codemod

# Remove duplicates and preserve order
return list(dict.fromkeys(base_codemods.values()))
return list(base_codemods.values())

matched_codemods = []
for name in codemod_include:
if "*" in name:
pat = re.compile(name.replace("*", ".*"))
pattern_matches = [code for code in self.codemods if pat.match(code.id)]
matched_codemods.extend(pattern_matches)
if not pattern_matches:
logger.warning(
"Given codemod pattern '%s' does not match any codemods.", name
)
continue

try:
matched_codemods.append(
self._codemods_by_name.get(name) or self._codemods_by_id[name]
)
except KeyError:
logger.warning(f"Requested codemod to include'{name}' does not exist.")
logger.warning(f"Requested codemod to include '{name}' does not exist.")
return matched_codemods

def describe_codemods(
Expand Down
47 changes: 47 additions & 0 deletions tests/codemods/test_include_exclude.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,50 @@ def test_exclude_some_match(self):
for c in self.registry.codemods
if c.name not in "secure-random" and c.id in self.all_ids
]

def test_include_with_pattern(self):
assert self.registry.match_codemods(["*django*"], None) == [
c for c in self.registry.codemods if "django" in c.id
]

def test_include_with_pattern_and_another(self):
assert self.registry.match_codemods(["*django*", "use-defusedxml"], None) == [
c for c in self.registry.codemods if "django" in c.id
] + [self.codemod_map["use-defusedxml"]]

def test_include_sast_with_prefix(self):
assert self.registry.match_codemods(["sonar*"], None, sast_only=False) == [
c for c in self.registry.codemods if c.origin == "sonar"
]

def test_warn_pattern_no_match(self, caplog):
assert self.registry.match_codemods(["*doesntexist*"], None) == []
assert (
"Given codemod pattern '*doesntexist*' does not match any codemods"
in caplog.text
)

def test_exclude_with_pattern(self):
assert self.registry.match_codemods(None, ["*django*"], sast_only=False) == [
c
for c in self.registry.codemods
if "django" not in c.id and c.id in self.all_ids
]

def test_exclude_with_pattern_and_another(self):
assert self.registry.match_codemods(
None, ["*django*", "use-defusedxml"], sast_only=False
) == [
c
for c in self.registry.codemods
if "django" not in c.id
and c.id in self.all_ids
and c.name != "use-defusedxml"
]

def test_exclude_pixee_with_prefix(self):
assert self.registry.match_codemods(None, ["pixee*"], sast_only=False) == [
c
for c in self.registry.codemods
if not c.origin == "pixee" and c.id in self.all_ids
]
12 changes: 2 additions & 10 deletions tests/test_codemodder.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def test_codemod_include_no_match(
assert any(x[0] == ("scanned: %s files", 0) for x in info_logger.call_args_list)

assert any(
f"Requested codemod to include'{bad_codemod}' does not exist." in x[0][0]
f"Requested codemod to include '{bad_codemod}' does not exist." in x[0][0]
for x in warning_logger.call_args_list
)

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

Expand Down Expand Up @@ -262,10 +262,6 @@ def test_codemod_exclude_some_match(

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

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

@mock.patch("codemodder.codemods.semgrep.semgrep_run")
def test_exclude_all_registered_codemods(self, mock_semgrep_run, dir_structure):
Expand Down
Loading