-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathregistry.py
More file actions
164 lines (134 loc) · 5.41 KB
/
registry.py
File metadata and controls
164 lines (134 loc) · 5.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
from __future__ import annotations
import os
import re
from collections import defaultdict
from dataclasses import dataclass
from importlib.metadata import EntryPoint, entry_points
from itertools import chain
from typing import TYPE_CHECKING, Callable, Optional
from codemodder.logging import logger
if TYPE_CHECKING:
from codemodder.codemods.base_codemod import BaseCodemod
# These are generally not intended to be applied directly so they are excluded by default.
DEFAULT_EXCLUDED_CODEMODS = [
"pixee:python/order-imports",
"pixee:python/unused-imports",
# See https://github.com/pixee/codemodder-python/pull/212 for concerns regarding this codemod.
"pixee:python/fix-empty-sequence-comparison",
]
@dataclass
class CodemodCollection:
"""A collection of codemods that all share the same origin and documentation."""
origin: str
codemods: list
class CodemodRegistry:
_codemods_by_id: dict[str, BaseCodemod]
_default_include_paths: set[str]
def __init__(self):
self._codemods_by_id = {}
self._codemods_by_tool = defaultdict(list)
self._default_include_paths = set()
@property
def ids(self):
return list(self._codemods_by_id.keys())
@property
def codemods(self):
return list(self._codemods_by_id.values())
@property
def default_include_paths(self) -> list[str]:
return list(self._default_include_paths)
@property
def all_tool_rules(self) -> list[str]:
return [
rule
for key, values in self._codemods_by_tool.items()
if key != "pixee"
for codemod in values
for rule in codemod.requested_rules
]
def codemods_by_tool(self, tool_name: str) -> list[BaseCodemod]:
return self._codemods_by_tool.get(tool_name, [])
def add_codemod_collection(self, collection: CodemodCollection):
for codemod in collection.codemods:
wrapper = codemod() if isinstance(codemod, type) else codemod
if wrapper.id in self._codemods_by_id:
raise KeyError(
f"Codemod with id {wrapper.id} is already registered. Consider changing the codemod name or origin."
)
self._codemods_by_id[wrapper.id] = wrapper
self._codemods_by_tool[collection.origin].append(wrapper)
self._default_include_paths.update(
chain(
*[
(f"*{ext}", os.path.join("**", f"*{ext}"))
for ext in wrapper.default_extensions
]
)
)
def match_codemods(
self,
codemod_include: Optional[list] = None,
codemod_exclude: Optional[list] = None,
sast_only=False,
) -> list[BaseCodemod]:
codemod_include = codemod_include or []
codemod_exclude = codemod_exclude or DEFAULT_EXCLUDED_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 codemod.id in names or any(
pat.match(codemod.id) for pat in patterns
):
continue
if bool(sast_only) != bool(codemod.origin == "pixee"):
base_codemods[codemod.id] = codemod
# Remove duplicates and preserve order
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_id[name])
except KeyError:
logger.warning(f"Requested codemod to include '{name}' does not exist.")
return matched_codemods
def describe_codemods(
self,
codemod_include: Optional[list] = None,
codemod_exclude: Optional[list] = None,
) -> list[dict]:
codemods = self.match_codemods(codemod_include, codemod_exclude)
return [codemod.describe() for codemod in codemods]
def load_registered_codemods(ep_filter: Optional[Callable[[EntryPoint], bool]] = None):
registry = CodemodRegistry()
logger.debug("loading registered codemod collections")
for entry_point in set(entry_points().select(group="codemods")):
if ep_filter and not ep_filter(entry_point):
logger.debug(
'- skipping codemod collection "%s" from "%s as requested"',
entry_point.name,
entry_point.module,
)
continue
logger.debug(
'- loading codemod collection "%s" from "%s"',
entry_point.name,
entry_point.module,
)
collection = entry_point.load()
registry.add_codemod_collection(collection)
return registry