def walk(self, astroid: nodes.NodeNG) -> None: """Call visit events of astroid checkers for the given node, recurse on its children, then leave events. """ cid = astroid.__class__.__name__.lower() # Detect if the node is a new name for a deprecated alias. # In this case, favour the methods for the deprecated # alias if any, in order to maintain backwards # compatibility. visit_events: Sequence[AstCallback] = self.visit_events.get(cid, ()) leave_events: Sequence[AstCallback] = self.leave_events.get(cid, ()) try: if astroid.is_statement: self.nbstatements += 1 # generate events for this node on each checker for callback in visit_events: callback(astroid) # recurse on children for child in astroid.get_children(): self.walk(child) for callback in leave_events: callback(astroid) except Exception: if self.exception_msg is False: file = getattr(astroid.root(), "file", None) print( f"Exception on node {repr(astroid)} in file '{file}'", file=sys.stderr, ) traceback.print_exc() self.exception_msg = True raise
def check_deprecated_method(self, node: nodes.Call, inferred: nodes.NodeNG) -> None: """Executes the checker for the given node. This method should be called from the checker implementing this mixin. """ # Reject nodes which aren't of interest to us. if not isinstance(inferred, ACCEPTABLE_NODES): return if isinstance(node.func, nodes.Attribute): func_name = node.func.attrname elif isinstance(node.func, nodes.Name): func_name = node.func.name else: # Not interested in other nodes. return if hasattr(inferred.parent, "qname") and inferred.parent.qname(): # Handling the situation when deprecated function is # alias to existing function. qnames = { inferred.qname(), f"{inferred.parent.qname()}.{func_name}", func_name, } else: qnames = {inferred.qname(), func_name} if any(name in self.deprecated_methods() for name in qnames): self.add_message("deprecated-method", node=node, args=(func_name, )) return num_of_args = len(node.args) kwargs = {kw.arg for kw in node.keywords} if node.keywords else {} deprecated_arguments = (self.deprecated_arguments(qn) for qn in qnames) for position, arg_name in chain(*deprecated_arguments): if arg_name in kwargs: # function was called with deprecated argument as keyword argument self.add_message("deprecated-argument", node=node, args=(arg_name, func_name)) elif position is not None and position < num_of_args: # function was called with deprecated argument as positional argument self.add_message("deprecated-argument", node=node, args=(arg_name, func_name))
def _modified_iterating_check_on_node_and_children( self, body_node: nodes.NodeNG, iter_obj: nodes.NodeNG ) -> None: """See if node or any of its children raises modified iterating messages.""" self._modified_iterating_check(body_node, iter_obj) for child in body_node.get_children(): self._modified_iterating_check_on_node_and_children(child, iter_obj)
def get_nodes( self, statement: nodes.NodeNG) -> Generator[nodes.NodeNG, None, None]: multiple_nodes = lambda nodes_: chain.from_iterable( self.get_nodes(node) for node in nodes_) if isinstance(statement, nodes.Assign): # RHS is evaluated before assigned in an assignment statement yield from self.get_nodes(statement.value) yield from multiple_nodes( statement.targets) # statement.targets is a list of nodes elif isinstance(statement, (nodes.ListComp, nodes.SetComp, nodes.DictComp, nodes.GeneratorExp)): # Comprehension targets are assigned before expression is evaluated. yield from multiple_nodes( statement.generators ) # statement.generators is a list of nodes if not hasattr(statement, "elt"): yield from self.get_nodes( statement.key) # keys evaluated first yield from self.get_nodes(statement.value) else: yield from self.get_nodes(statement.elt) else: yield from statement.nodes_of_class( (nodes.AssignName, nodes.DelName, nodes.Name), nodes.FunctionDef)
def ignore_type_check_filter(node: nc.NodeNG) -> nc.NodeNG: """Ignore stuff under 'if TYPE_CHECKING:' block at module level.""" # Look for a non-nested 'if TYPE_CHECKING:' if (isinstance(node.test, astroid.Name) and node.test.name == 'TYPE_CHECKING' and isinstance(node.parent, astroid.Module)): # Find the module node. mnode = node while mnode.parent is not None: mnode = mnode.parent # First off, remove any names that are getting defined # in this block from the module locals. for cnode in node.body: _strip_import(cnode, mnode) # Now replace the body with a simple 'pass'. This will # keep pylint from complaining about grouped imports/etc. passnode = astroid.Pass(parent=node, lineno=node.lineno + 1, col_offset=node.col_offset + 1) node.body = [passnode] return node
def ignore_type_check_filter(if_node: nc.NodeNG) -> nc.NodeNG: """Ignore stuff under 'if TYPE_CHECKING:' block at module level.""" # Look for a non-nested 'if TYPE_CHECKING:' if (isinstance(if_node.test, astroid.Name) and if_node.test.name == 'TYPE_CHECKING' and isinstance(if_node.parent, astroid.Module)): module_node = if_node.parent # Remove any locals getting defined under this if statement. # (ideally should recurse in case we have nested if statements/etc # but keeping it simple for now). for name, locations in list(module_node.locals.items()): # Calc which remaining name locations are outside of the if # block. Update or delete the list as needed. new_locs = [l for l in locations if not _under_if(l, if_node)] if len(new_locs) == len(locations): continue if new_locs: module_node.locals[name] = new_locs continue del module_node.locals[name] # Now replace its children with a simple pass statement. passnode = astroid.Pass(parent=if_node, lineno=if_node.lineno + 1, col_offset=if_node.col_offset + 1) if_node.body = [passnode] return if_node
def is_requests_func(node: NodeNG) -> bool: """ Checks if the node represents a requests HTTP call function. Examples are: requests.get/post/..., requests.Session.get/post/... """ # simple case if isinstance(node, nodes.Attribute) and node.attrname in SESSION_METHODS: if ( is_requests_api_module(node.expr) and node.attrname not in SESSION_ONLY_METHODS ) or is_requests_session(node.expr): return True if isinstance(node, nodes.Name): _, assigns = node.lookup(node.name) for assign in assigns: if ( isinstance(assign, nodes.ImportFrom) and assign.modname in REQUESTS_API_MODULES and assign.real_name(node.name) in HTTP_METHOD_FUNCTIONS ): return True if hasattr(assign, 'assign_type'): assign = assign.assign_type() if isinstance(assign, nodes.Assign) and is_requests_func(assign.value): return True return False
def _set_state_on_block_lines( self, msgs_store: MessageDefinitionStore, node: nodes.NodeNG, msg: MessageDefinition, msg_state: dict[int, bool], ) -> None: """Recursively walk (depth first) AST to collect block level options line numbers and set the state correctly. """ for child in node.get_children(): self._set_state_on_block_lines(msgs_store, child, msg, msg_state) # first child line number used to distinguish between disable # which are the first child of scoped node with those defined later. # For instance in the code below: # # 1. def meth8(self): # 2. """test late disabling""" # 3. pylint: disable=not-callable, useless-suppression # 4. print(self.blip) # 5. pylint: disable=no-member, useless-suppression # 6. print(self.bla) # # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6 # # this is necessary to disable locally messages applying to class / # function using their fromlineno if (isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef)) and node.body): firstchildlineno = node.body[0].fromlineno else: firstchildlineno = node.tolineno self._set_message_state_in_block(msg, msg_state, node, firstchildlineno)
def _strip_import(cnode: nc.NodeNG, mnode: nc.NodeNG) -> None: if isinstance(cnode, (astroid.Import, astroid.ImportFrom)): for name, val in list(mnode.locals.items()): if cnode in val: # Pull us out of the list. valnew = [v for v in val if v is not cnode] if valnew: mnode.locals[name] = valnew else: del mnode.locals[name]
def _is_float_nan(node: nodes.NodeNG) -> bool: try: if isinstance(node, nodes.Call) and len(node.args) == 1: if ( node.args[0].value.lower() == "nan" and node.inferred()[0].pytype() == "builtins.float" ): return True return False except AttributeError: return False
def is_requests_api_module(node: NodeNG) -> bool: """True if node represents the requests or requests.api module.""" if isinstance(node, (nodes.Name, nodes.Expr)): _, assigns = node.lookup(node.name) for assign in assigns: if ( isinstance(assign, (nodes.Import, nodes.ImportFrom)) and assign.real_name(node.name) in REQUESTS_API_MODULES ): return True return False
def visit_default(self, node: nodes.NodeNG) -> None: """Check the node line number and check it if not yet done.""" if not node.is_statement: return if not node.root().pure_python: return prev_sibl = node.previous_sibling() if prev_sibl is not None: prev_line = prev_sibl.fromlineno # The line on which a 'finally': occurs in a 'try/finally' # is not directly represented in the AST. We infer it # by taking the last line of the body and adding 1, which # should be the line of finally: elif (isinstance(node.parent, nodes.TryFinally) and node in node.parent.finalbody): prev_line = node.parent.body[0].tolineno + 1 elif isinstance(node.parent, nodes.Module): prev_line = 0 else: prev_line = node.parent.statement(future=True).fromlineno line = node.fromlineno assert line, node if prev_line == line and self._visited_lines.get(line) != 2: self._check_multi_statement_line(node, line) return if line in self._visited_lines: return try: tolineno = node.blockstart_tolineno except AttributeError: tolineno = node.tolineno assert tolineno, node lines = [] for line in range(line, tolineno + 1): self._visited_lines[line] = 1 try: lines.append(self._lines[line].rstrip()) except KeyError: lines.append("")
def transform(node: NodeNG, infer_function: InferFn = infer_function) -> NodeNG: if (raise_on_overwrite and node._explicit_inference is not None and node._explicit_inference is not infer_function): raise InferenceOverwriteError( "Inference already set to {existing_inference}. " "Trying to overwrite with {new_inference} for {node}".format( existing_inference=infer_function, new_inference=node._explicit_inference, node=node, )) # pylint: disable=no-value-for-parameter node._explicit_inference = _inference_tip_cached(infer_function) return node
def get_module_and_frameid(node: nodes.NodeNG) -> Tuple[str, str]: """Return the module name and the frame id in the module.""" frame = node.frame(future=True) module, obj = "", [] while frame: if isinstance(frame, Module): module = frame.name else: obj.append(getattr(frame, "name", "<lambda>")) try: frame = frame.parent.frame(future=True) except AttributeError: break obj.reverse() return module, ".".join(obj)
def visit(self, node: nodes.NodeNG) -> Any: """Launch the visit starting from the given node.""" if node in self._visited: return None self._visited.add(node) methods = self.get_callbacks(node) if methods[0] is not None: methods[0](node) if hasattr(node, "locals"): # skip Instance and other proxy for local_node in node.values(): self.visit(local_node) if methods[1] is not None: return methods[1](node) return None
def is_session_class(node: NodeNG) -> bool: """True if node represents the requests.Session class.""" try: for inferred in node.infer(): if ( isinstance(inferred, nodes.ClassDef) and inferred.name == SESSION_CLASS_NAME and isinstance(inferred.parent, nodes.Module) and inferred.parent.name == SESSION_MODULE ): return True except InferenceError: pass return False
def _check_key_in_kwargs(unpacked: NodeNG, key: str) -> bool: """Given a **kwargs node in a function call, tries to check presence of a key.""" try: for inferred in unpacked.infer(): if ( isinstance(inferred, nodes.Dict) and inferred.items and not _check_key_in_dict(inferred, key) ): # only if we know that it is a dict and astroid inferred some values return False except InferenceError: pass return True
def _lookup(node: NodeNG) -> List[NodeNG]: """Given a name or attribute node, returns the assignments to that name/attr.""" if isinstance(node, nodes.Name): return node.lookup(node.name)[1] if isinstance(node, nodes.Attribute): try: obj = next(node.expr.infer()) except InferenceError: return [] if isinstance(obj, bases.Instance): try: return obj.getattr(node.attrname) except AttributeInferenceError: return [] return []
def func_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: """Filter annotated function args/retvals. This accounts for deferred evaluation available in in Python 3.7+ via 'from __future__ import annotations'. In this case we don't want Pylint to complain about missing symbols in annotations when they aren't actually needed at runtime. """ # Only do this if deferred annotations are on. if not using_future_annotations(node): return node # Wipe out argument annotations. # Special-case: certain function decorators *do* # evaluate annotations at runtime so we want to leave theirs intact. # This includes functools.singledispatch, ba.dispatchmethod, and # efro.MessageReceiver. # Lets just look for a @XXX.register or @XXX.handler decorators for # now; can get more specific if we get false positives. if node.decorators is not None: for dnode in node.decorators.nodes: if (isinstance(dnode, astroid.nodes.Name) and dnode.name in {'dispatchmethod', 'singledispatch'}): return node # Leave annotations intact. if (isinstance(dnode, astroid.nodes.Attribute) and dnode.attrname in {'register', 'handler'}): return node # Leave annotations intact. node.args.annotations = [None for _ in node.args.args] node.args.varargannotation = None node.args.kwargannotation = None node.args.kwonlyargs_annotations = [None for _ in node.args.kwonlyargs] node.args.posonlyargs_annotations = [None for _ in node.args.kwonlyargs] # Wipe out return-value annotation. if node.returns is not None: node.returns = None return node
def func_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: """Filter annotated function args/retvals. This accounts for deferred evaluation available in in Python 3.7+ via 'from __future__ import annotations'. In this case we don't want Pylint to complain about missing symbols in annotations when they aren't actually needed at runtime. """ # Only do this if deferred annotations are on. if not using_future_annotations(node): return node # Wipe out argument annotations. # Special-case: functools.singledispatch and ba.dispatchmethod *do* # evaluate annotations at runtime so we want to leave theirs intact. # Lets just look for a @XXX.register decorator used by both I guess. if node.decorators is not None: for dnode in node.decorators.nodes: if (isinstance(dnode, astroid.nodes.Name) and dnode.name in ('dispatchmethod', 'singledispatch')): return node # Leave annotations intact. if (isinstance(dnode, astroid.nodes.Attribute) and dnode.attrname == 'register'): return node # Leave annotations intact. node.args.annotations = [None for _ in node.args.args] node.args.varargannotation = None node.args.kwargannotation = None node.args.kwonlyargs_annotations = [None for _ in node.args.kwonlyargs] node.args.posonlyargs_annotations = [None for _ in node.args.kwonlyargs] # Wipe out return-value annotation. if node.returns is not None: node.returns = None return node
def class_generics_filter(node: nc.NodeNG) -> nc.NodeNG: """Filter generics subscripts out of class declarations.""" # First, quick-out if nothing here should be filtered. found = False for base in node.bases: if _is_strippable_subscript(base): found = True if not found: return node # Now strip subscripts from base classes. new_bases: list[nc.NodeNG] = [] for base in node.bases: if _is_strippable_subscript(base): new_bases.append(base.value) base.value.parent = node else: new_bases.append(base) node.bases = new_bases return node
def _to_literal(node: nodes.NodeNG) -> Any: # Can raise SyntaxError or ValueError from ast.literal_eval # Can raise AttributeError from node.as_string() as not all nodes have a visitor # Is this the stupidest idea or the simplest idea? return ast.literal_eval(node.as_string())
def _set_message_state_in_block( self, msg: MessageDefinition, lines: dict[int, bool], node: nodes.NodeNG, firstchildlineno: int, ) -> None: """Set the state of a message in a block of lines.""" first = node.fromlineno last = node.tolineno for lineno, state in list(lines.items()): original_lineno = lineno if first > lineno or last < lineno: continue # Set state for all lines for this block, if the # warning is applied to nodes. if msg.scope == WarningScope.NODE: if lineno > firstchildlineno: state = True first_, last_ = node.block_range(lineno) # pylint: disable=useless-suppression # For block nodes first_ is their definition line. For example, we # set the state of line zero for a module to allow disabling # invalid-name for the module. For example: # 1. # pylint: disable=invalid-name # 2. ... # OR # 1. """Module docstring""" # 2. # pylint: disable=invalid-name # 3. ... # # But if we already visited line 0 we don't need to set its state again # 1. # pylint: disable=invalid-name # 2. # pylint: enable=invalid-name # 3. ... # The state should come from line 1, not from line 2 # Therefore, if the 'fromlineno' is already in the states we just start # with the lineno we were originally visiting. # pylint: enable=useless-suppression if (first_ == node.fromlineno and first_ >= firstchildlineno and node.fromlineno in self._module_msgs_state.get( msg.msgid, ())): first_ = lineno else: first_ = lineno last_ = last for line in range(first_, last_ + 1): # Do not override existing entries. This is especially important # when parsing the states for a scoped node where some line-disables # have already been parsed. if ((isinstance(node, nodes.Module) and node.fromlineno <= line < lineno) or (not isinstance(node, nodes.Module) and node.fromlineno < line < lineno) ) and line in self._module_msgs_state.get(msg.msgid, ()): continue if line in lines: # state change in the same block state = lines[line] original_lineno = line # Update suppression mapping if not state: self._suppression_mapping[(msg.msgid, line)] = original_lineno else: self._suppression_mapping.pop((msg.msgid, line), None) # Update message state for respective line try: self._module_msgs_state[msg.msgid][line] = state except KeyError: self._module_msgs_state[msg.msgid] = {line: state} del lines[lineno]
def _filter_stmts(base_node: nodes.NodeNG, stmts, frame, offset): """Filter the given list of statements to remove ignorable statements. If base_node is not a frame itself and the name is found in the inner frame locals, statements will be filtered to remove ignorable statements according to base_node's location. :param stmts: The statements to filter. :type stmts: list(nodes.NodeNG) :param frame: The frame that all of the given statements belong to. :type frame: nodes.NodeNG :param offset: The line offset to filter statements up to. :type offset: int :returns: The filtered statements. :rtype: list(nodes.NodeNG) """ # if offset == -1, my actual frame is not the inner frame but its parent # # class A(B): pass # # we need this to resolve B correctly if offset == -1: myframe = base_node.frame().parent.frame() else: myframe = base_node.frame() # If the frame of this node is the same as the statement # of this node, then the node is part of a class or # a function definition and the frame of this node should be the # the upper frame, not the frame of the definition. # For more information why this is important, # see Pylint issue #295. # For example, for 'b', the statement is the same # as the frame / scope: # # def test(b=1): # ... if (base_node.parent and base_node.statement(future=True) is myframe and myframe.parent): myframe = myframe.parent.frame() mystmt: Optional[nodes.Statement] = None if base_node.parent: mystmt = base_node.statement(future=True) # line filtering if we are in the same frame # # take care node may be missing lineno information (this is the case for # nodes inserted for living objects) if myframe is frame and mystmt and mystmt.fromlineno is not None: assert mystmt.fromlineno is not None, mystmt mylineno = mystmt.fromlineno + offset else: # disabling lineno filtering mylineno = 0 _stmts = [] _stmt_parents = [] statements = _get_filtered_node_statements(base_node, stmts) for node, stmt in statements: # line filtering is on and we have reached our location, break if stmt.fromlineno and stmt.fromlineno > mylineno > 0: break # Ignore decorators with the same name as the # decorated function # Fixes issue #375 if mystmt is stmt and _is_from_decorator(base_node): continue assert hasattr(node, "assign_type"), ( node, node.scope(), node.scope().locals, ) assign_type = node.assign_type() if node.has_base(base_node): break _stmts, done = assign_type._get_filtered_stmts(base_node, node, _stmts, mystmt) if done: break optional_assign = assign_type.optional_assign if optional_assign and assign_type.parent_of(base_node): # we are inside a loop, loop var assignment is hiding previous # assignment _stmts = [node] _stmt_parents = [stmt.parent] continue if isinstance(assign_type, nodes.NamedExpr): # If the NamedExpr is in an if statement we do some basic control flow inference if_parent = _get_if_statement_ancestor(assign_type) if if_parent: # If the if statement is within another if statement we append the node # to possible statements if _get_if_statement_ancestor(if_parent): optional_assign = False _stmts.append(node) _stmt_parents.append(stmt.parent) # If the if statement is first-level and not within an orelse block # we know that it will be evaluated elif not if_parent.is_orelse: _stmts = [node] _stmt_parents = [stmt.parent] # Else we do not known enough about the control flow to be 100% certain # and we append to possible statements else: _stmts.append(node) _stmt_parents.append(stmt.parent) else: _stmts = [node] _stmt_parents = [stmt.parent] # XXX comment various branches below!!! try: pindex = _stmt_parents.index(stmt.parent) except ValueError: pass else: # we got a parent index, this means the currently visited node # is at the same block level as a previously visited node if _stmts[pindex].assign_type().parent_of(assign_type): # both statements are not at the same block level continue # if currently visited node is following previously considered # assignment and both are not exclusive, we can drop the # previous one. For instance in the following code :: # # if a: # x = 1 # else: # x = 2 # print x # # we can't remove neither x = 1 nor x = 2 when looking for 'x' # of 'print x'; while in the following :: # # x = 1 # x = 2 # print x # # we can remove x = 1 when we see x = 2 # # moreover, on loop assignment types, assignment won't # necessarily be done if the loop has no iteration, so we don't # want to clear previous assignments if any (hence the test on # optional_assign) if not (optional_assign or nodes.are_exclusive(_stmts[pindex], node)): del _stmt_parents[pindex] del _stmts[pindex] # If base_node and node are exclusive, then we can ignore node if nodes.are_exclusive(base_node, node): continue # An AssignName node overrides previous assignments if: # 1. node's statement always assigns # 2. node and base_node are in the same block (i.e., has the same parent as base_node) if isinstance(node, (nodes.NamedExpr, nodes.AssignName)): if isinstance(stmt, nodes.ExceptHandler): # If node's statement is an ExceptHandler, then it is the variable # bound to the caught exception. If base_node is not contained within # the exception handler block, node should override previous assignments; # otherwise, node should be ignored, as an exception variable # is local to the handler block. if stmt.parent_of(base_node): _stmts = [] _stmt_parents = [] else: continue elif not optional_assign and mystmt and stmt.parent is mystmt.parent: _stmts = [] _stmt_parents = [] elif isinstance(node, nodes.DelName): # Remove all previously stored assignments _stmts = [] _stmt_parents = [] continue # Add the new assignment _stmts.append(node) if isinstance(node, nodes.Arguments) or isinstance( node.parent, nodes.Arguments): # Special case for _stmt_parents when node is a function parameter; # in this case, stmt is the enclosing FunctionDef, which is what we # want to add to _stmt_parents, not stmt.parent. This case occurs when # node is an Arguments node (representing varargs or kwargs parameter), # and when node.parent is an Arguments node (other parameters). # See issue #180. _stmt_parents.append(stmt) else: _stmt_parents.append(stmt.parent) return _stmts
def _get_if_statement_ancestor(node: nodes.NodeNG) -> Optional[nodes.If]: """Return the first parent node that is an If node (or None)""" for parent in node.node_ancestors(): if isinstance(parent, nodes.If): return parent return None
def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: """Filter annotated function variable assigns. This accounts for deferred evaluation. """ # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks if using_future_annotations(node): # Future behavior: # Annotated assigns under functions are not evaluated. # Class and module vars are normally not either. However we # do evaluate if we come across an 'ioprepped' dataclass # decorator. (the 'ioprepped' decorator explicitly evaluates # dataclass annotations). fnode = node willeval = False while fnode is not None: if isinstance(fnode, astroid.ClassDef): if fnode.decorators is not None: found_ioprepped = False for dec in fnode.decorators.nodes: # Look for dataclassio.ioprepped. if (isinstance(dec, astroid.nodes.Attribute) and dec.attrname in {'ioprepped', 'will_ioprep'} and isinstance(dec.expr, astroid.nodes.Name) and dec.expr.name == 'dataclassio'): found_ioprepped = True break # Look for simply 'ioprepped'. if (isinstance(dec, astroid.nodes.Name) and dec.name in {'ioprepped', 'will_ioprep'}): found_ioprepped = True break if found_ioprepped: willeval = True break fnode = fnode.parent else: # Legacy behavior: # Annotated assigns under functions are not evaluated, # but class or module vars are. fnode = node willeval = True while fnode is not None: if isinstance(fnode, (astroid.FunctionDef, astroid.AsyncFunctionDef)): willeval = False break if isinstance(fnode, astroid.ClassDef): willeval = True break fnode = fnode.parent # If this annotation won't be eval'ed, replace it with a dummy string. if not willeval: dummyval = astroid.Const(parent=node, value='dummyval') node.annotation = dummyval return node
def default(self, node: nodes.NodeNG, *args: Any) -> None: for child in node.get_children(): self.dispatch(child, *args)
def _collect_block_lines( self, msgs_store: "MessageDefinitionStore", node: nodes.NodeNG, msg_state: MessageStateDict, ) -> None: """Recursively walk (depth first) AST to collect block level options line numbers. """ for child in node.get_children(): self._collect_block_lines(msgs_store, child, msg_state) first = node.fromlineno last = node.tolineno # first child line number used to distinguish between disable # which are the first child of scoped node with those defined later. # For instance in the code below: # # 1. def meth8(self): # 2. """test late disabling""" # 3. pylint: disable=not-callable, useless-suppression # 4. print(self.blip) # 5. pylint: disable=no-member, useless-suppression # 6. print(self.bla) # # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6 # # this is necessary to disable locally messages applying to class / # function using their fromlineno if ( isinstance(node, (nodes.Module, nodes.ClassDef, nodes.FunctionDef)) and node.body ): firstchildlineno = node.body[0].fromlineno else: firstchildlineno = last for msgid, lines in msg_state.items(): for lineno, state in list(lines.items()): original_lineno = lineno if first > lineno or last < lineno: continue # Set state for all lines for this block, if the # warning is applied to nodes. message_definitions = msgs_store.get_message_definitions(msgid) for message_definition in message_definitions: if message_definition.scope == WarningScope.NODE: if lineno > firstchildlineno: state = True first_, last_ = node.block_range(lineno) else: first_ = lineno last_ = last for line in range(first_, last_ + 1): # do not override existing entries if line in self._module_msgs_state.get(msgid, ()): continue if line in lines: # state change in the same block state = lines[line] original_lineno = line if not state: self._suppression_mapping[(msgid, line)] = original_lineno try: self._module_msgs_state[msgid][line] = state except KeyError: self._module_msgs_state[msgid] = {line: state} del lines[lineno]
def _msg_postponed_eval_hint(self, node: nodes.NodeNG) -> str: """Message hint if postponed evaluation isn't enabled.""" if self._py310_plus or "annotations" in node.root().future_imports: return "" return ". Add 'from __future__ import annotations' as well"
def _check_singleton_comparison( self, left_value: nodes.NodeNG, right_value: nodes.NodeNG, root_node: nodes.Compare, checking_for_absence: bool = False, ) -> None: """Check if == or != is being used to compare a singleton value.""" singleton_values = (True, False, None) def _is_singleton_const(node: nodes.NodeNG) -> bool: return isinstance(node, nodes.Const) and any( node.value is value for value in singleton_values ) if _is_singleton_const(left_value): singleton, other_value = left_value.value, right_value elif _is_singleton_const(right_value): singleton, other_value = right_value.value, left_value else: return singleton_comparison_example = {False: "'{} is {}'", True: "'{} is not {}'"} # True/False singletons have a special-cased message in case the user is # mistakenly using == or != to check for truthiness if singleton in {True, False}: suggestion_template = ( "{} if checking for the singleton value {}, or {} if testing for {}" ) truthiness_example = {False: "not {}", True: "{}"} truthiness_phrase = {True: "truthiness", False: "falsiness"} # Looks for comparisons like x == True or x != False checking_truthiness = singleton is not checking_for_absence suggestion = suggestion_template.format( singleton_comparison_example[checking_for_absence].format( left_value.as_string(), right_value.as_string() ), singleton, ( "'bool({})'" if not utils.is_test_condition(root_node) and checking_truthiness else "'{}'" ).format( truthiness_example[checking_truthiness].format( other_value.as_string() ) ), truthiness_phrase[checking_truthiness], ) else: suggestion = singleton_comparison_example[checking_for_absence].format( left_value.as_string(), right_value.as_string() ) self.add_message( "singleton-comparison", node=root_node, args=(f"'{root_node.as_string()}'", suggestion), )