src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Unused/UnusedChecker.py

branch
eric7
changeset 10052
041d0785dd42
child 10053
9914b7b4b11c
equal deleted inserted replaced
10051:1128cb7bbb42 10052:041d0785dd42
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

eric ide

mercurial