|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a checker for unused arguments, variables, ... . |
|
8 """ |
|
9 |
|
10 import ast |
|
11 import copy |
|
12 |
|
13 import AstUtilities |
|
14 |
|
15 |
|
16 class UnusedChecker: |
|
17 """ |
|
18 Class implementing a checker for unused arguments, variables, ... . |
|
19 """ |
|
20 |
|
21 Codes = [ |
|
22 ## Unused Arguments |
|
23 "U100", |
|
24 "U101", |
|
25 ] |
|
26 |
|
27 def __init__(self, source, filename, tree, select, ignore, expected, repeat, args): |
|
28 """ |
|
29 Constructor |
|
30 |
|
31 @param source source code to be checked |
|
32 @type list of str |
|
33 @param filename name of the source file |
|
34 @type str |
|
35 @param tree AST tree of the source code |
|
36 @type ast.Module |
|
37 @param select list of selected codes |
|
38 @type list of str |
|
39 @param ignore list of codes to be ignored |
|
40 @type list of str |
|
41 @param expected list of expected codes |
|
42 @type list of str |
|
43 @param repeat flag indicating to report each occurrence of a code |
|
44 @type bool |
|
45 @param args dictionary of arguments for the various checks |
|
46 @type dict |
|
47 """ |
|
48 self.__select = tuple(select) |
|
49 self.__ignore = ("",) if select else tuple(ignore) |
|
50 self.__expected = expected[:] |
|
51 self.__repeat = repeat |
|
52 self.__filename = filename |
|
53 self.__source = source[:] |
|
54 self.__tree = copy.deepcopy(tree) |
|
55 self.__args = args |
|
56 |
|
57 ### parameters for unused arguments checks |
|
58 ##self.__ignoreAbstract "IgnoreAbstract": False, |
|
59 ##self.__ignoreOverload "IgnoreOverload": False, |
|
60 ##self.__ignoreOverride "IgnoreOverride": False, |
|
61 ##self.__ignoreStubs "IgnoreStubs": False, |
|
62 ##self.__ignoreVariadicNames "IgnoreVariadicNames": False, |
|
63 ##self.__ignoreLambdas "IgnoreLambdas": False, |
|
64 ##self.__ignoreNestedFunctions "IgnoreNestedFunctions": False, |
|
65 ##self.__ignoreDunderMethods "IgnoreDunderMethods": False, |
|
66 |
|
67 # statistics counters |
|
68 self.counters = {} |
|
69 |
|
70 # collection of detected errors |
|
71 self.errors = [] |
|
72 |
|
73 checkersWithCodes = [ |
|
74 (self.__checkUnusedArguments, ("U100", "U101")), |
|
75 ] |
|
76 |
|
77 self.__checkers = [] |
|
78 for checker, codes in checkersWithCodes: |
|
79 if any(not (code and self.__ignoreCode(code)) for code in codes): |
|
80 self.__checkers.append(checker) |
|
81 |
|
82 def __ignoreCode(self, code): |
|
83 """ |
|
84 Private method to check if the message code should be ignored. |
|
85 |
|
86 @param code message code to check for |
|
87 @type str |
|
88 @return flag indicating to ignore the given code |
|
89 @rtype bool |
|
90 """ |
|
91 return code.startswith(self.__ignore) and not code.startswith(self.__select) |
|
92 |
|
93 def __error(self, lineNumber, offset, code, *args): |
|
94 """ |
|
95 Private method to record an issue. |
|
96 |
|
97 @param lineNumber line number of the issue |
|
98 @type int |
|
99 @param offset position within line of the issue |
|
100 @type int |
|
101 @param code message code |
|
102 @type str |
|
103 @param args arguments for the message |
|
104 @type list |
|
105 """ |
|
106 if self.__ignoreCode(code): |
|
107 return |
|
108 |
|
109 if code in self.counters: |
|
110 self.counters[code] += 1 |
|
111 else: |
|
112 self.counters[code] = 1 |
|
113 |
|
114 # Don't care about expected codes |
|
115 if code in self.__expected: |
|
116 return |
|
117 |
|
118 if code and (self.counters[code] == 1 or self.__repeat): |
|
119 # record the issue with one based line number |
|
120 self.errors.append( |
|
121 { |
|
122 "file": self.__filename, |
|
123 "line": lineNumber + 1, |
|
124 "offset": offset, |
|
125 "code": code, |
|
126 "args": args, |
|
127 } |
|
128 ) |
|
129 |
|
130 def run(self): |
|
131 """ |
|
132 Public method to check the given source against miscellaneous |
|
133 conditions. |
|
134 """ |
|
135 if not self.__filename: |
|
136 # don't do anything, if essential data is missing |
|
137 return |
|
138 |
|
139 if not self.__checkers: |
|
140 # don't do anything, if no codes were selected |
|
141 return |
|
142 |
|
143 for check in self.__checkers: |
|
144 check() |
|
145 |
|
146 ####################################################################### |
|
147 ## Unused Arguments |
|
148 ## |
|
149 ## adapted from: flake8-unused-arguments v0.0.13 |
|
150 ####################################################################### |
|
151 |
|
152 def __checkUnusedArguments(self): |
|
153 """ |
|
154 Private method to check function and method definitions for unused arguments. |
|
155 """ |
|
156 finder = FunctionFinder(self.__args["IgnoreNestedFunctions"]) |
|
157 finder.visit(self.__tree) |
|
158 |
|
159 for functionNode in finder.functionNodes(): |
|
160 decoratorNames = set(self.__getDecoratorNames(functionNode)) |
|
161 |
|
162 # ignore overload functions, it's not a surprise when they're empty |
|
163 if self.__args["IgnoreOverload"] and "overload" in decoratorNames: |
|
164 continue |
|
165 |
|
166 # ignore overridden functions |
|
167 if self.__args["IgnoreOverride"] and "override" in decoratorNames: |
|
168 continue |
|
169 |
|
170 # ignore abstractmethods, it's not a surprise when they're empty |
|
171 if self.__args["IgnoreAbstract"] and "abstractmethod" in decoratorNames: |
|
172 continue |
|
173 |
|
174 # ignore Qt slot methods |
|
175 if self.__args["IgnoreSlotMethods"] and ( |
|
176 "pyqtSlot" in decoratorNames or "Slot" in decoratorNames |
|
177 ): |
|
178 continue |
|
179 |
|
180 # ignore stub functions |
|
181 if self.__args["IgnoreStubs"] and self.__isStubFunction(functionNode): |
|
182 continue |
|
183 |
|
184 # ignore lambdas |
|
185 if self.__args["IgnoreLambdas"] and isinstance(functionNode, ast.Lambda): |
|
186 continue |
|
187 |
|
188 # ignore __double_underscore_methods__() |
|
189 if self.__args["IgnoreDunderMethods"] and self.__isDunderMethod( |
|
190 functionNode |
|
191 ): |
|
192 continue |
|
193 |
|
194 for i, argument in self.__getUnusedArguments(functionNode): |
|
195 name = argument.arg |
|
196 if self.__args["IgnoreVariadicNames"]: |
|
197 if ( |
|
198 functionNode.args.vararg |
|
199 and functionNode.args.vararg.arg == name |
|
200 ): |
|
201 continue |
|
202 if functionNode.args.kwarg and functionNode.args.kwarg.arg == name: |
|
203 continue |
|
204 |
|
205 # ignore self or whatever the first argument is for a classmethod |
|
206 if i == 0 and ( |
|
207 name in ("self", "cls") or "classmethod" in decoratorNames |
|
208 ): |
|
209 continue |
|
210 |
|
211 lineNumber = argument.lineno |
|
212 offset = argument.col_offset |
|
213 |
|
214 errorCode = "U101" if name.startswith("_") else "U100" |
|
215 self.__error(lineNumber - 1, offset, errorCode, name) |
|
216 |
|
217 def __getDecoratorNames(self, functionNode): |
|
218 """ |
|
219 Private method to yield the decorator names of the function. |
|
220 |
|
221 @param functionNode reference to the node defining the function or lambda |
|
222 @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda |
|
223 @yield decorator name |
|
224 @ytype str |
|
225 """ |
|
226 if isinstance(functionNode, ast.Lambda): |
|
227 return |
|
228 |
|
229 for decorator in functionNode.decorator_list: |
|
230 if isinstance(decorator, ast.Name): |
|
231 yield decorator.id |
|
232 elif isinstance(decorator, ast.Attribute): |
|
233 yield decorator.attr |
|
234 elif isinstance(decorator, ast.Call): |
|
235 if isinstance(decorator.func, ast.Name): |
|
236 yield decorator.func.id |
|
237 else: |
|
238 yield decorator.func.attr |
|
239 |
|
240 def __isStubFunction(self, functionNode): |
|
241 """ |
|
242 Private method to check, if the given function node defines a stub function. |
|
243 |
|
244 @param functionNode reference to the node defining the function or lambda |
|
245 @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda |
|
246 @return flag indicating a stub function |
|
247 @rtype bool |
|
248 """ |
|
249 if isinstance(functionNode, ast.Lambda): |
|
250 return AstUtilities.isEllipsis(functionNode.body) |
|
251 |
|
252 statement = functionNode.body[0] |
|
253 if isinstance(statement, ast.Expr) and AstUtilities.isString(statement.value): |
|
254 if len(functionNode.body) > 1: |
|
255 # first statement is a docstring, let's skip it |
|
256 statement = functionNode.body[1] |
|
257 else: |
|
258 # it's a function with only a docstring, that's a stub |
|
259 return True |
|
260 |
|
261 if isinstance(statement, ast.Pass): |
|
262 return True |
|
263 if isinstance(statement, ast.Expr) and AstUtilities.isEllipsis(statement.value): |
|
264 return True |
|
265 |
|
266 if isinstance(statement, ast.Raise): |
|
267 # like 'raise NotImplementedError()' |
|
268 if ( |
|
269 isinstance(statement.exc, ast.Call) |
|
270 and hasattr(statement.exc.func, "id") |
|
271 and statement.exc.func.id == "NotImplementedError" |
|
272 ): |
|
273 return True |
|
274 |
|
275 # like 'raise NotImplementedError' |
|
276 elif ( |
|
277 isinstance(statement.exc, ast.Name) |
|
278 and hasattr(statement.exc, "id") |
|
279 and statement.exc.id == "NotImplementedError" |
|
280 ): |
|
281 return True |
|
282 |
|
283 return False |
|
284 |
|
285 def __isDunderMethod(self, functionNode): |
|
286 """ |
|
287 Private method to check, if the function node defines a special function. |
|
288 |
|
289 @param functionNode reference to the node defining the function or lambda |
|
290 @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda |
|
291 @return flag indicating a special function |
|
292 @rtype bool |
|
293 """ |
|
294 if isinstance(functionNode, ast.Lambda): |
|
295 return False |
|
296 |
|
297 if not hasattr(functionNode, "name"): |
|
298 return False |
|
299 |
|
300 name = functionNode.name |
|
301 return len(name) > 4 and name.startswith("__") and name.endswith("__") |
|
302 |
|
303 def __getUnusedArguments(self, functionNode): |
|
304 """ |
|
305 Private method to get a list of unused arguments of the given function. |
|
306 |
|
307 @param functionNode reference to the node defining the function or lambda |
|
308 @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda |
|
309 @return list of tuples of the argument position and the argument |
|
310 @rtype list of tuples of (int, ast.arg) |
|
311 """ |
|
312 arguments = list(enumerate(self.__getArguments(functionNode))) |
|
313 |
|
314 class NameFinder(ast.NodeVisitor): |
|
315 """ |
|
316 Class to find the used argument names. |
|
317 """ |
|
318 |
|
319 def visit_Name(self, name): |
|
320 """ |
|
321 Public method to check a Name node. |
|
322 |
|
323 @param name reference to the name node to be checked |
|
324 @type ast.Name |
|
325 """ |
|
326 nonlocal arguments |
|
327 |
|
328 if isinstance(name.ctx, ast.Store): |
|
329 return |
|
330 |
|
331 arguments = [ |
|
332 (argIndex, arg) for argIndex, arg in arguments if arg.arg != name.id |
|
333 ] |
|
334 |
|
335 NameFinder().visit(functionNode) |
|
336 return arguments |
|
337 |
|
338 def __getArguments(self, functionNode): |
|
339 """ |
|
340 Private method to get all argument names of the given function. |
|
341 |
|
342 @param functionNode reference to the node defining the function or lambda |
|
343 @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda |
|
344 @return list of argument names |
|
345 @rtype list of ast.arg |
|
346 """ |
|
347 args = functionNode.args |
|
348 |
|
349 orderedArguments = [] |
|
350 |
|
351 # plain old args |
|
352 orderedArguments.extend(args.args) |
|
353 |
|
354 # *arg name |
|
355 if args.vararg is not None: |
|
356 orderedArguments.append(args.vararg) |
|
357 |
|
358 # *, key, word, only, args |
|
359 orderedArguments.extend(args.kwonlyargs) |
|
360 |
|
361 # **kwarg name |
|
362 if args.kwarg is not None: |
|
363 orderedArguments.append(args.kwarg) |
|
364 |
|
365 return orderedArguments |
|
366 |
|
367 |
|
368 class FunctionFinder(ast.NodeVisitor): |
|
369 """ |
|
370 Class to find all defined functions and methods. |
|
371 """ |
|
372 |
|
373 def __init__(self, onlyTopLevel=False): |
|
374 """ |
|
375 Constructor |
|
376 |
|
377 @param onlyTopLevel flag indicating to search for top level functions only |
|
378 (defaults to False) |
|
379 @type bool (optional) |
|
380 """ |
|
381 super().__init__() |
|
382 |
|
383 self.__functions = [] |
|
384 self.__onlyTopLevel = onlyTopLevel |
|
385 |
|
386 def functionNodes(self): |
|
387 """ |
|
388 Public method to get the list of detected functions and lambdas. |
|
389 |
|
390 @return list of detected functions and lambdas |
|
391 @rtype list of ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda |
|
392 """ |
|
393 return self.__functions |
|
394 |
|
395 def __visitFunctionTypes(self, functionNode): |
|
396 """ |
|
397 Private method to handle an AST node defining a function or lambda. |
|
398 |
|
399 @param functionNode reference to the node defining a function or lambda |
|
400 @type ast.AsyncFunctionDef, ast.FunctionDef or ast.Lambda |
|
401 """ |
|
402 self.__functions.append(functionNode) |
|
403 if not self.__onlyTopLevel: |
|
404 if isinstance(functionNode, ast.Lambda): |
|
405 self.visit(functionNode.body) |
|
406 else: |
|
407 for obj in functionNode.body: |
|
408 self.visit(obj) |
|
409 |
|
410 visit_AsyncFunctionDef = visit_FunctionDef = visit_Lambda = __visitFunctionTypes |