1944 if isinstance(dim, ast.Name): |
1950 if isinstance(dim, ast.Name): |
1945 yield dim.id |
1951 yield dim.id |
1946 elif isinstance(dim, ast.Tuple): |
1952 elif isinstance(dim, ast.Tuple): |
1947 yield from self.__getNamesFromTuple(dim) |
1953 yield from self.__getNamesFromTuple(dim) |
1948 |
1954 |
1949 def __getDictCompLoopVarNames(self, node): |
1955 def __getDictCompLoopAndNamedExprVarNames(self, node): |
1950 """ |
1956 """ |
1951 Private method to get the names of comprehension loop variables. |
1957 Private method to get the names of comprehension loop variables. |
1952 |
1958 |
1953 @param node ast node to be processed |
1959 @param node ast node to be processed |
1954 @type ast.DictComp |
1960 @type ast.DictComp |
1955 @yield loop variable names |
1961 @yield loop variable names |
1956 @ytype str |
1962 @ytype str |
1957 """ |
1963 """ |
|
1964 finder = NamedExprFinder() |
1958 for gen in node.generators: |
1965 for gen in node.generators: |
1959 if isinstance(gen.target, ast.Name): |
1966 if isinstance(gen.target, ast.Name): |
1960 yield gen.target.id |
1967 yield gen.target.id |
1961 elif isinstance(gen.target, ast.Tuple): |
1968 elif isinstance(gen.target, ast.Tuple): |
1962 yield from self.__getNamesFromTuple(gen.target) |
1969 yield from self.__getNamesFromTuple(gen.target) |
1963 |
1970 |
|
1971 finder.visit(gen.ifs) |
|
1972 |
|
1973 yield from finder.getNames().keys() |
|
1974 |
|
1975 def __inClassInit(self): |
|
1976 """ |
|
1977 Private method to check, if we are inside an '__init__' method. |
|
1978 |
|
1979 @return flag indicating being within the '__init__' method |
|
1980 @rtype bool |
|
1981 """ |
|
1982 return ( |
|
1983 len(self.contexts) >= 2 |
|
1984 and isinstance(self.contexts[-2].node, ast.ClassDef) |
|
1985 and isinstance(self.contexts[-1].node, ast.FunctionDef) |
|
1986 and self.contexts[-1].node.name == "__init__" |
|
1987 ) |
|
1988 |
|
1989 def visit_Return(self, node): |
|
1990 """ |
|
1991 Public method to handle 'Return' nodes. |
|
1992 |
|
1993 @param node reference to the node to be processed |
|
1994 @type ast.Return |
|
1995 """ |
|
1996 if self.__inClassInit() and node.value is not None: |
|
1997 self.violations.append((node, "M537")) |
|
1998 |
|
1999 self.generic_visit(node) |
|
2000 |
|
2001 def visit_Yield(self, node): |
|
2002 """ |
|
2003 Public method to handle 'Yield' nodes. |
|
2004 |
|
2005 @param node reference to the node to be processed |
|
2006 @type ast.Yield |
|
2007 """ |
|
2008 if self.__inClassInit(): |
|
2009 self.violations.append((node, "M537")) |
|
2010 |
|
2011 self.generic_visit(node) |
|
2012 |
|
2013 def visit_YieldFrom(self, node) -> None: |
|
2014 """ |
|
2015 Public method to handle 'YieldFrom' nodes. |
|
2016 |
|
2017 @param node reference to the node to be processed |
|
2018 @type ast.YieldFrom |
|
2019 """ |
|
2020 if self.__inClassInit(): |
|
2021 self.violations.append((node, "M537")) |
|
2022 |
|
2023 self.generic_visit(node) |
|
2024 |
1964 def visit(self, node): |
2025 def visit(self, node): |
1965 """ |
2026 """ |
1966 Public method to traverse a given AST node. |
2027 Public method to traverse a given AST node. |
1967 |
2028 |
1968 @param node AST node to be traversed |
2029 @param node AST node to be traversed |
2470 itemContext.func.attr == "assertRaises" |
2535 itemContext.func.attr == "assertRaises" |
2471 or ( |
2536 or ( |
2472 itemContext.func.attr == "raises" |
2537 itemContext.func.attr == "raises" |
2473 and isinstance(itemContext.func.value, ast.Name) |
2538 and isinstance(itemContext.func.value, ast.Name) |
2474 and itemContext.func.value.id == "pytest" |
2539 and itemContext.func.value.id == "pytest" |
2475 and "match" not in [kwd.arg for kwd in itemContext.keywords] |
2540 and "match" not in (kwd.arg for kwd in itemContext.keywords) |
2476 ) |
2541 ) |
2477 ) |
2542 ) |
2478 ) |
2543 ) |
2479 or ( |
2544 or ( |
2480 isinstance(itemContext.func, ast.Name) |
2545 isinstance(itemContext.func, ast.Name) |
2481 and itemContext.func.id == "raises" |
2546 and itemContext.func.id == "raises" |
2482 and isinstance(itemContext.func.ctx, ast.Load) |
2547 and isinstance(itemContext.func.ctx, ast.Load) |
2483 and "pytest.raises" in self.__M505Imports |
2548 and "pytest.raises" in self.__M505Imports |
2484 and "match" not in [kwd.arg for kwd in itemContext.keywords] |
2549 and "match" not in (kwd.arg for kwd in itemContext.keywords) |
2485 ) |
2550 ) |
2486 ) |
2551 ) |
2487 and len(itemContext.args) == 1 |
2552 and len(itemContext.args) == 1 |
2488 and isinstance(itemContext.args[0], ast.Name) |
2553 and isinstance(itemContext.args[0], ast.Name) |
2489 and itemContext.args[0].id == "Exception" |
2554 and itemContext.args[0].id in ("Exception", "BaseException") |
2490 and not item.optional_vars |
2555 and not item.optional_vars |
2491 ): |
2556 ): |
2492 self.violations.append((node, "M517")) |
2557 self.violations.append((node, "M517")) |
2493 |
2558 |
2494 def __checkForM518(self, node): |
2559 def __checkForM518(self, node): |
2496 Private method to check for useless expressions. |
2561 Private method to check for useless expressions. |
2497 |
2562 |
2498 @param node reference to the node to be processed |
2563 @param node reference to the node to be processed |
2499 @type ast.FunctionDef |
2564 @type ast.FunctionDef |
2500 """ |
2565 """ |
2501 for subnode in node.body: |
2566 if not isinstance(node, ast.Expr): |
2502 if not isinstance(subnode, ast.Expr): |
2567 return |
2503 continue |
2568 |
2504 |
2569 if isinstance( |
2505 if isinstance( |
2570 node.value, |
2506 subnode.value, |
2571 (ast.List, ast.Set, ast.Dict, ast.Tuple), |
2507 (ast.List, ast.Set, ast.Dict), |
2572 ) or ( |
2508 ) or ( |
2573 isinstance(node.value, ast.Constant) |
2509 isinstance(subnode.value, ast.Constant) |
2574 and ( |
2510 and ( |
2575 isinstance( |
2511 isinstance( |
2576 node.value.value, |
2512 subnode.value.value, |
2577 (int, float, complex, bytes, bool), |
2513 (int, float, complex, bytes, bool), |
|
2514 ) |
|
2515 or subnode.value.value is None |
|
2516 ) |
2578 ) |
2517 ): |
2579 or node.value.value is None |
2518 self.violations.append((subnode, "M518")) |
2580 ) |
|
2581 ): |
|
2582 self.violations.append((node, "M518", node.value.__class__.__name__)) |
2519 |
2583 |
2520 def __checkForM519(self, node): |
2584 def __checkForM519(self, node): |
2521 """ |
2585 """ |
2522 Private method to check for use of 'functools.lru_cache' or 'functools.cache'. |
2586 Private method to check for use of 'functools.lru_cache' or 'functools.cache'. |
2523 |
2587 |
2932 if node.func.attr in ("sub", "subn"): |
2996 if node.func.attr in ("sub", "subn"): |
2933 check(3, "count") |
2997 check(3, "count") |
2934 elif node.func.attr == "split": |
2998 elif node.func.attr == "split": |
2935 check(2, "maxsplit") |
2999 check(2, "maxsplit") |
2936 |
3000 |
2937 def __checkForM535(self, node: ast.DictComp): |
3001 def __checkForM535(self, node): |
2938 """ |
3002 """ |
2939 Private method to check that a static key isn't used in a dict comprehension. |
3003 Private method to check that a static key isn't used in a dict comprehension. |
2940 |
3004 |
2941 Record a warning if a likely unchanging key is used - either a constant, |
3005 Record a warning if a likely unchanging key is used - either a constant, |
2942 or a variable that isn't coming from the generator expression. |
3006 or a variable that isn't coming from the generator expression. |
2943 |
3007 |
2944 @param node reference to the node to be processed |
3008 @param node reference to the node to be processed |
2945 @type ast.DictComp |
3009 @type ast.DictComp |
2946 """ |
|
2947 """Check that a static key isn't used in a dict comprehension. |
|
2948 |
|
2949 Emit a warning if a likely unchanging key is used - either a constant, |
|
2950 or a variable that isn't coming from the generator expression. |
|
2951 """ |
3010 """ |
2952 if isinstance(node.key, ast.Constant): |
3011 if isinstance(node.key, ast.Constant): |
2953 self.violations.append((node, "M535", node.key.value)) |
3012 self.violations.append((node, "M535", node.key.value)) |
2954 elif isinstance( |
3013 elif isinstance( |
2955 node.key, ast.Name |
3014 node.key, ast.Name |
2956 ) and node.key.id not in self.__getDictCompLoopVarNames(node): |
3015 ) and node.key.id not in self.__getDictCompLoopAndNamedExprVarNames(node): |
2957 self.violations.append((node, "M535", node.key.id)) |
3016 self.violations.append((node, "M535", node.key.id)) |
|
3017 |
|
3018 def __checkForM538(self, node): |
|
3019 """ |
|
3020 Private method to check for changes to a loop's mutable iterable. |
|
3021 |
|
3022 @param node loop node to be checked |
|
3023 @type ast.For |
|
3024 """ |
|
3025 if isinstance(node.iter, ast.Name): |
|
3026 name = self.toNameStr(node.iter) |
|
3027 elif isinstance(node.iter, ast.Attribute): |
|
3028 name = self.toNameStr(node.iter) |
|
3029 else: |
|
3030 return |
|
3031 checker = M538Checker(name, self) |
|
3032 checker.visit(node.body) |
|
3033 for mutation in checker.mutations: |
|
3034 self.violations.append((mutation, "M538")) |
|
3035 |
|
3036 |
|
3037 class M538Checker(ast.NodeVisitor): |
|
3038 """ |
|
3039 Class traversing a 'for' loop body to check for modifications to a loop's |
|
3040 mutable iterable. |
|
3041 """ |
|
3042 |
|
3043 # https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types |
|
3044 MUTATING_FUNCTIONS = ( |
|
3045 "append", |
|
3046 "sort", |
|
3047 "reverse", |
|
3048 "remove", |
|
3049 "clear", |
|
3050 "extend", |
|
3051 "insert", |
|
3052 "pop", |
|
3053 "popitem", |
|
3054 ) |
|
3055 |
|
3056 def __init__(self, name, bugbear): |
|
3057 """ |
|
3058 Constructor |
|
3059 |
|
3060 @param name name of the iterator |
|
3061 @type str |
|
3062 @param bugbear reference to the bugbear visitor |
|
3063 @type BugBearVisitor |
|
3064 """ |
|
3065 self.__name = name |
|
3066 self.__bb = bugbear |
|
3067 self.mutations = [] |
|
3068 |
|
3069 def visit_Delete(self, node): |
|
3070 """ |
|
3071 Public method handling 'Delete' nodes. |
|
3072 |
|
3073 @param node reference to the node to be processed |
|
3074 @type ast.Delete |
|
3075 """ |
|
3076 for target in node.targets: |
|
3077 if isinstance(target, ast.Subscript): |
|
3078 name = self.__bb.toNameStr(target.value) |
|
3079 elif isinstance(target, (ast.Attribute, ast.Name)): |
|
3080 name = self.__bb.toNameStr(target) |
|
3081 else: |
|
3082 name = "" # fallback |
|
3083 self.generic_visit(target) |
|
3084 |
|
3085 if name == self.__name: |
|
3086 self.mutations.append(node) |
|
3087 |
|
3088 def visit_Call(self, node): |
|
3089 """ |
|
3090 Public method handling 'Call' nodes. |
|
3091 |
|
3092 @param node reference to the node to be processed |
|
3093 @type ast.Call |
|
3094 """ |
|
3095 if isinstance(node.func, ast.Attribute): |
|
3096 name = self.__bb.toNameStr(node.func.value) |
|
3097 functionObject = name |
|
3098 functionName = node.func.attr |
|
3099 |
|
3100 if ( |
|
3101 functionObject == self.__name |
|
3102 and functionName in self.MUTATING_FUNCTIONS |
|
3103 ): |
|
3104 self.mutations.append(node) |
|
3105 |
|
3106 self.generic_visit(node) |
|
3107 |
|
3108 def visit(self, node): |
|
3109 """ |
|
3110 Public method to inspect an ast node. |
|
3111 |
|
3112 Like super-visit but supports iteration over lists. |
|
3113 |
|
3114 @param node AST node to be traversed |
|
3115 @type TYPE |
|
3116 @return reference to the last processed node |
|
3117 @rtype ast.Node |
|
3118 """ |
|
3119 if not isinstance(node, list): |
|
3120 return super().visit(node) |
|
3121 |
|
3122 for elem in node: |
|
3123 super().visit(elem) |
|
3124 return node |
|
3125 |
|
3126 |
|
3127 class ExceptBaseExceptionVisitor(ast.NodeVisitor): |
|
3128 """ |
|
3129 Class to determine, if a 'BaseException' is re-raised. |
|
3130 """ |
|
3131 |
|
3132 def __init__(self, exceptNode): |
|
3133 """ |
|
3134 Constructor |
|
3135 |
|
3136 @param exceptNode exception node to be inspected |
|
3137 @type ast.ExceptHandler |
|
3138 """ |
|
3139 super().__init__() |
|
3140 self.__root = exceptNode |
|
3141 self.__reRaised = False |
|
3142 |
|
3143 def reRaised(self) -> bool: |
|
3144 """ |
|
3145 Public method to check, if the exception is re-raised. |
|
3146 |
|
3147 @return flag indicating a re-raised exception |
|
3148 @rtype bool |
|
3149 """ |
|
3150 self.visit(self.__root) |
|
3151 return self.__reRaised |
|
3152 |
|
3153 def visit_Raise(self, node): |
|
3154 """ |
|
3155 Public method to handle 'Raise' nodes. |
|
3156 |
|
3157 If we find a corresponding `raise` or `raise e` where e was from |
|
3158 `except BaseException as e:` then we mark re_raised as True and can |
|
3159 stop scanning. |
|
3160 |
|
3161 @param node reference to the node to be processed |
|
3162 @type ast.Raise |
|
3163 """ |
|
3164 if node.exc is None or ( |
|
3165 isinstance(node.exc, ast.Name) and node.exc.id == self.__root.name |
|
3166 ): |
|
3167 self.__reRaised = True |
|
3168 return |
|
3169 |
|
3170 super().generic_visit(node) |
|
3171 |
|
3172 def visit_ExceptHandler(self, node: ast.ExceptHandler): |
|
3173 """ |
|
3174 Public method to handle 'ExceptHandler' nodes. |
|
3175 |
|
3176 @param node reference to the node to be processed |
|
3177 @type ast.ExceptHandler |
|
3178 """ |
|
3179 if node is not self.__root: |
|
3180 return # entered a nested except - stop searching |
|
3181 |
|
3182 super().generic_visit(node) |
2958 |
3183 |
2959 |
3184 |
2960 class NameFinder(ast.NodeVisitor): |
3185 class NameFinder(ast.NodeVisitor): |
2961 """ |
3186 """ |
2962 Class to extract a name out of a tree of nodes. |
3187 Class to extract a name out of a tree of nodes. |