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

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

eric ide

mercurial