eric7/Plugins/CheckerPlugins/CodeStyleChecker/PathLib/PathlibChecker.py

branch
eric7
changeset 8312
800c432b34c8
parent 8243
cc717c2ae956
child 8764
18c69de2292f
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the checker for functions that can be replaced by use of
8 the pathlib module.
9 """
10
11 import ast
12 import copy
13 import contextlib
14
15
16 class PathlibChecker:
17 """
18 Class implementing a checker for functions that can be replaced by use of
19 the pathlib module.
20 """
21 Codes = [
22 ## Replacements for the os module functions
23 "P101", "P102", "P103", "P104", "P105", "P106", "P107",
24 "P108", "P109", "P110", "P111",
25
26 ## Replacements for the os.path module functions
27 "P201", "P202", "P203", "P204", "P205", "P206", "P207",
28 "P208", "P209", "P210", "P211", "P212",
29
30 ## Replacements for some Python standrd library functions
31 "P301",
32
33 ## Replacements for py.path.local
34 "P401",
35 ]
36
37 # map functions to be replaced to error codes
38 Function2Code = {
39 "os.chmod": "P101",
40 "os.mkdir": "P102",
41 "os.makedirs": "P103",
42 "os.rename": "P104",
43 "os.replace": "P105",
44 "os.rmdir": "P106",
45 "os.remove": "P107",
46 "os.unlink": "P108",
47 "os.getcwd": "P109",
48 "os.readlink": "P110",
49 "os.stat": "P111",
50
51 "os.path.abspath": "P201",
52 "os.path.exists": "P202",
53 "os.path.expanduser": "P203",
54 "os.path.isdir": "P204",
55 "os.path.isfile": "P205",
56 "os.path.islink": "P206",
57 "os.path.isabs": "P207",
58 "os.path.join": "P208",
59 "os.path.basename": "P209",
60 "os.path.dirname": "P210",
61 "os.path.samefile": "P211",
62 "os.path.splitext": "P212",
63
64 "open": "P301",
65
66 "py.path.local": "P401",
67 }
68
69 def __init__(self, source, filename, tree, selected, ignored, expected,
70 repeat):
71 """
72 Constructor
73
74 @param source source code to be checked
75 @type list of str
76 @param filename name of the source file
77 @type str
78 @param tree AST tree of the source code
79 @type ast.Module
80 @param selected list of selected codes
81 @type list of str
82 @param ignored list of codes to be ignored
83 @type list of str
84 @param expected list of expected codes
85 @type list of str
86 @param repeat flag indicating to report each occurrence of a code
87 @type bool
88 """
89 self.__select = tuple(selected)
90 self.__ignore = ('',) if selected else tuple(ignored)
91 self.__expected = expected[:]
92 self.__repeat = repeat
93 self.__filename = filename
94 self.__source = source[:]
95 self.__tree = copy.deepcopy(tree)
96
97 # statistics counters
98 self.counters = {}
99
100 # collection of detected errors
101 self.errors = []
102
103 self.__checkCodes = (code for code in self.Codes
104 if not self.__ignoreCode(code))
105
106 def __ignoreCode(self, code):
107 """
108 Private method to check if the message code should be ignored.
109
110 @param code message code to check for
111 @type str
112 @return flag indicating to ignore the given code
113 @rtype bool
114 """
115 return (code.startswith(self.__ignore) and
116 not code.startswith(self.__select))
117
118 def __error(self, lineNumber, offset, code, *args):
119 """
120 Private method to record an issue.
121
122 @param lineNumber line number of the issue
123 @type int
124 @param offset position within line of the issue
125 @type int
126 @param code message code
127 @type str
128 @param args arguments for the message
129 @type list
130 """
131 if self.__ignoreCode(code):
132 return
133
134 if code in self.counters:
135 self.counters[code] += 1
136 else:
137 self.counters[code] = 1
138
139 # Don't care about expected codes
140 if code in self.__expected:
141 return
142
143 if code and (self.counters[code] == 1 or self.__repeat):
144 # record the issue with one based line number
145 self.errors.append(
146 {
147 "file": self.__filename,
148 "line": lineNumber + 1,
149 "offset": offset,
150 "code": code,
151 "args": args,
152 }
153 )
154
155 def run(self):
156 """
157 Public method to check the given source against functions
158 to be replaced by 'pathlib' equivalents.
159 """
160 if not self.__filename:
161 # don't do anything, if essential data is missing
162 return
163
164 if not self.__checkCodes:
165 # don't do anything, if no codes were selected
166 return
167
168 visitor = PathlibVisitor(self.__checkForReplacement)
169 visitor.visit(self.__tree)
170
171 def __checkForReplacement(self, node, name):
172 """
173 Private method to check the given node for the need for a
174 replacement.
175
176 @param node reference to the AST node to check
177 @type ast.AST
178 @param name resolved name of the node
179 @type str
180 """
181 with contextlib.suppress(KeyError):
182 errorCode = self.Function2Code[name]
183 self.__error(node.lineno - 1, node.col_offset, errorCode)
184
185
186 class PathlibVisitor(ast.NodeVisitor):
187 """
188 Class to traverse the AST node tree and check for potential issues.
189 """
190 def __init__(self, checkCallback):
191 """
192 Constructor
193
194 @param checkCallback callback function taking a reference to the
195 AST node and the resolved name
196 @type func
197 """
198 super().__init__()
199
200 self.__checkCallback = checkCallback
201 self.__importAlias = {}
202
203 def visit_ImportFrom(self, node):
204 """
205 Public method handle the ImportFrom AST node.
206
207 @param node reference to the ImportFrom AST node
208 @type ast.ImportFrom
209 """
210 for imp in node.names:
211 if imp.asname:
212 self.__importAlias[imp.asname] = f"{node.module}.{imp.name}"
213 else:
214 self.__importAlias[imp.name] = f"{node.module}.{imp.name}"
215
216 def visit_Import(self, node):
217 """
218 Public method to handle the Import AST node.
219
220 @param node reference to the Import AST node
221 @type ast.Import
222 """
223 for imp in node.names:
224 if imp.asname:
225 self.__importAlias[imp.asname] = imp.name
226
227 def visit_Call(self, node):
228 """
229 Public method to handle the Call AST node.
230
231 @param node reference to the Call AST node
232 @type ast.Call
233 """
234 nameResolver = NameResolver(self.__importAlias)
235 nameResolver.visit(node.func)
236
237 self.__checkCallback(node, nameResolver.name())
238
239
240 class NameResolver(ast.NodeVisitor):
241 """
242 Class to resolve a Name or Attribute node.
243 """
244 def __init__(self, importAlias):
245 """
246 Constructor
247
248 @param importAlias reference to the import aliases dictionary
249 @type dict
250 """
251 self.__importAlias = importAlias
252 self.__names = []
253
254 def name(self):
255 """
256 Public method to resolve the name.
257
258 @return resolved name
259 @rtype str
260 """
261 with contextlib.suppress(KeyError, IndexError):
262 attr = self.__importAlias[self.__names[-1]]
263 self.__names[-1] = attr
264 # do nothing if there is no such name or the names list is empty
265
266 return ".".join(reversed(self.__names))
267
268 def visit_Name(self, node):
269 """
270 Public method to handle the Name AST node.
271
272 @param node reference to the Name AST node
273 @type ast.Name
274 """
275 self.__names.append(node.id)
276
277 def visit_Attribute(self, node):
278 """
279 Public method to handle the Attribute AST node.
280
281 @param node reference to the Attribute AST node
282 @type ast.Attribute
283 """
284 try:
285 self.__names.append(node.attr)
286 self.__names.append(node.value.id)
287 except AttributeError:
288 self.generic_visit(node)

eric ide

mercurial