|
| 1 | +# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html |
| 2 | +# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE |
| 3 | + |
| 4 | +"""_filter_stmts and helper functions. This method gets used in LocalsDictnodes.NodeNG._scope_lookup. |
| 5 | +It is not considered public. |
| 6 | +""" |
| 7 | + |
| 8 | +from typing import List, Optional, Tuple |
| 9 | + |
| 10 | +from astroid import nodes |
| 11 | + |
| 12 | + |
| 13 | +def _get_filtered_node_statements( |
| 14 | + base_node: nodes.NodeNG, stmt_nodes: List[nodes.NodeNG] |
| 15 | +) -> List[Tuple[nodes.NodeNG, nodes.Statement]]: |
| 16 | + statements = [(node, node.statement(future=True)) for node in stmt_nodes] |
| 17 | + # Next we check if we have ExceptHandlers that are parent |
| 18 | + # of the underlying variable, in which case the last one survives |
| 19 | + if len(statements) > 1 and all( |
| 20 | + isinstance(stmt, nodes.ExceptHandler) for _, stmt in statements |
| 21 | + ): |
| 22 | + statements = [ |
| 23 | + (node, stmt) for node, stmt in statements if stmt.parent_of(base_node) |
| 24 | + ] |
| 25 | + return statements |
| 26 | + |
| 27 | + |
| 28 | +def _is_from_decorator(node): |
| 29 | + """Return True if the given node is the child of a decorator""" |
| 30 | + return any(isinstance(parent, nodes.Decorators) for parent in node.node_ancestors()) |
| 31 | + |
| 32 | + |
| 33 | +def _get_if_statement_ancestor(node: nodes.NodeNG) -> Optional[nodes.If]: |
| 34 | + """Return the first parent node that is an If node (or None)""" |
| 35 | + for parent in node.node_ancestors(): |
| 36 | + if isinstance(parent, nodes.If): |
| 37 | + return parent |
| 38 | + return None |
| 39 | + |
| 40 | + |
| 41 | +def _filter_stmts(base_node: nodes.NodeNG, stmts, frame, offset): |
| 42 | + """Filter the given list of statements to remove ignorable statements. |
| 43 | +
|
| 44 | + If base_node is not a frame itself and the name is found in the inner |
| 45 | + frame locals, statements will be filtered to remove ignorable |
| 46 | + statements according to base_node's location. |
| 47 | +
|
| 48 | + :param stmts: The statements to filter. |
| 49 | + :type stmts: list(nodes.NodeNG) |
| 50 | +
|
| 51 | + :param frame: The frame that all of the given statements belong to. |
| 52 | + :type frame: nodes.NodeNG |
| 53 | +
|
| 54 | + :param offset: The line offset to filter statements up to. |
| 55 | + :type offset: int |
| 56 | +
|
| 57 | + :returns: The filtered statements. |
| 58 | + :rtype: list(nodes.NodeNG) |
| 59 | + """ |
| 60 | + # if offset == -1, my actual frame is not the inner frame but its parent |
| 61 | + # |
| 62 | + # class A(B): pass |
| 63 | + # |
| 64 | + # we need this to resolve B correctly |
| 65 | + if offset == -1: |
| 66 | + myframe = base_node.frame().parent.frame() |
| 67 | + else: |
| 68 | + myframe = base_node.frame() |
| 69 | + # If the frame of this node is the same as the statement |
| 70 | + # of this node, then the node is part of a class or |
| 71 | + # a function definition and the frame of this node should be the |
| 72 | + # the upper frame, not the frame of the definition. |
| 73 | + # For more information why this is important, |
| 74 | + # see Pylint issue #295. |
| 75 | + # For example, for 'b', the statement is the same |
| 76 | + # as the frame / scope: |
| 77 | + # |
| 78 | + # def test(b=1): |
| 79 | + # ... |
| 80 | + if ( |
| 81 | + base_node.parent |
| 82 | + and base_node.statement(future=True) is myframe |
| 83 | + and myframe.parent |
| 84 | + ): |
| 85 | + myframe = myframe.parent.frame() |
| 86 | + |
| 87 | + mystmt: Optional[nodes.Statement] = None |
| 88 | + if base_node.parent: |
| 89 | + mystmt = base_node.statement(future=True) |
| 90 | + |
| 91 | + # line filtering if we are in the same frame |
| 92 | + # |
| 93 | + # take care node may be missing lineno information (this is the case for |
| 94 | + # nodes inserted for living objects) |
| 95 | + if myframe is frame and mystmt and mystmt.fromlineno is not None: |
| 96 | + assert mystmt.fromlineno is not None, mystmt |
| 97 | + mylineno = mystmt.fromlineno + offset |
| 98 | + else: |
| 99 | + # disabling lineno filtering |
| 100 | + mylineno = 0 |
| 101 | + |
| 102 | + _stmts = [] |
| 103 | + _stmt_parents = [] |
| 104 | + statements = _get_filtered_node_statements(base_node, stmts) |
| 105 | + for node, stmt in statements: |
| 106 | + # line filtering is on and we have reached our location, break |
| 107 | + if stmt.fromlineno and stmt.fromlineno > mylineno > 0: |
| 108 | + break |
| 109 | + # Ignore decorators with the same name as the |
| 110 | + # decorated function |
| 111 | + # Fixes issue #375 |
| 112 | + if mystmt is stmt and _is_from_decorator(base_node): |
| 113 | + continue |
| 114 | + assert hasattr(node, "assign_type"), ( |
| 115 | + node, |
| 116 | + node.scope(), |
| 117 | + node.scope().locals, |
| 118 | + ) |
| 119 | + assign_type = node.assign_type() |
| 120 | + if node.has_base(base_node): |
| 121 | + break |
| 122 | + |
| 123 | + _stmts, done = assign_type._get_filtered_stmts(base_node, node, _stmts, mystmt) |
| 124 | + if done: |
| 125 | + break |
| 126 | + |
| 127 | + optional_assign = assign_type.optional_assign |
| 128 | + if optional_assign and assign_type.parent_of(base_node): |
| 129 | + # we are inside a loop, loop var assignment is hiding previous |
| 130 | + # assignment |
| 131 | + _stmts = [node] |
| 132 | + _stmt_parents = [stmt.parent] |
| 133 | + continue |
| 134 | + |
| 135 | + if isinstance(assign_type, nodes.NamedExpr): |
| 136 | + # If the NamedExpr is in an if statement we do some basic control flow inference |
| 137 | + if_parent = _get_if_statement_ancestor(assign_type) |
| 138 | + if if_parent: |
| 139 | + # If the if statement is within another if statement we append the node |
| 140 | + # to possible statements |
| 141 | + if _get_if_statement_ancestor(if_parent): |
| 142 | + optional_assign = False |
| 143 | + _stmts.append(node) |
| 144 | + _stmt_parents.append(stmt.parent) |
| 145 | + # If the if statement is first-level and not within an orelse block |
| 146 | + # we know that it will be evaluated |
| 147 | + elif not if_parent.is_orelse: |
| 148 | + _stmts = [node] |
| 149 | + _stmt_parents = [stmt.parent] |
| 150 | + # Else we do not known enough about the control flow to be 100% certain |
| 151 | + # and we append to possible statements |
| 152 | + else: |
| 153 | + _stmts.append(node) |
| 154 | + _stmt_parents.append(stmt.parent) |
| 155 | + else: |
| 156 | + _stmts = [node] |
| 157 | + _stmt_parents = [stmt.parent] |
| 158 | + |
| 159 | + # XXX comment various branches below!!! |
| 160 | + try: |
| 161 | + pindex = _stmt_parents.index(stmt.parent) |
| 162 | + except ValueError: |
| 163 | + pass |
| 164 | + else: |
| 165 | + # we got a parent index, this means the currently visited node |
| 166 | + # is at the same block level as a previously visited node |
| 167 | + if _stmts[pindex].assign_type().parent_of(assign_type): |
| 168 | + # both statements are not at the same block level |
| 169 | + continue |
| 170 | + # if currently visited node is following previously considered |
| 171 | + # assignment and both are not exclusive, we can drop the |
| 172 | + # previous one. For instance in the following code :: |
| 173 | + # |
| 174 | + # if a: |
| 175 | + # x = 1 |
| 176 | + # else: |
| 177 | + # x = 2 |
| 178 | + # print x |
| 179 | + # |
| 180 | + # we can't remove neither x = 1 nor x = 2 when looking for 'x' |
| 181 | + # of 'print x'; while in the following :: |
| 182 | + # |
| 183 | + # x = 1 |
| 184 | + # x = 2 |
| 185 | + # print x |
| 186 | + # |
| 187 | + # we can remove x = 1 when we see x = 2 |
| 188 | + # |
| 189 | + # moreover, on loop assignment types, assignment won't |
| 190 | + # necessarily be done if the loop has no iteration, so we don't |
| 191 | + # want to clear previous assignments if any (hence the test on |
| 192 | + # optional_assign) |
| 193 | + if not (optional_assign or nodes.are_exclusive(_stmts[pindex], node)): |
| 194 | + del _stmt_parents[pindex] |
| 195 | + del _stmts[pindex] |
| 196 | + |
| 197 | + # If base_node and node are exclusive, then we can ignore node |
| 198 | + if nodes.are_exclusive(base_node, node): |
| 199 | + continue |
| 200 | + |
| 201 | + # An AssignName node overrides previous assignments if: |
| 202 | + # 1. node's statement always assigns |
| 203 | + # 2. node and base_node are in the same block (i.e., has the same parent as base_node) |
| 204 | + if isinstance(node, (nodes.NamedExpr, nodes.AssignName)): |
| 205 | + if isinstance(stmt, nodes.ExceptHandler): |
| 206 | + # If node's statement is an ExceptHandler, then it is the variable |
| 207 | + # bound to the caught exception. If base_node is not contained within |
| 208 | + # the exception handler block, node should override previous assignments; |
| 209 | + # otherwise, node should be ignored, as an exception variable |
| 210 | + # is local to the handler block. |
| 211 | + if stmt.parent_of(base_node): |
| 212 | + _stmts = [] |
| 213 | + _stmt_parents = [] |
| 214 | + else: |
| 215 | + continue |
| 216 | + elif not optional_assign and mystmt and stmt.parent is mystmt.parent: |
| 217 | + _stmts = [] |
| 218 | + _stmt_parents = [] |
| 219 | + elif isinstance(node, nodes.DelName): |
| 220 | + # Remove all previously stored assignments |
| 221 | + _stmts = [] |
| 222 | + _stmt_parents = [] |
| 223 | + continue |
| 224 | + # Add the new assignment |
| 225 | + _stmts.append(node) |
| 226 | + if isinstance(node, nodes.Arguments) or isinstance( |
| 227 | + node.parent, nodes.Arguments |
| 228 | + ): |
| 229 | + # Special case for _stmt_parents when node is a function parameter; |
| 230 | + # in this case, stmt is the enclosing FunctionDef, which is what we |
| 231 | + # want to add to _stmt_parents, not stmt.parent. This case occurs when |
| 232 | + # node is an Arguments node (representing varargs or kwargs parameter), |
| 233 | + # and when node.parent is an Arguments node (other parameters). |
| 234 | + # See issue #180. |
| 235 | + _stmt_parents.append(stmt) |
| 236 | + else: |
| 237 | + _stmt_parents.append(stmt.parent) |
| 238 | + return _stmts |
0 commit comments