def _check_new_format(self, node, func): """ Check the new string formatting """ if (isinstance(node.func, astroid.Attribute) and not isinstance(node.func.expr, astroid.Const)): return try: strnode = next(func.bound.infer()) except astroid.InferenceError: return if not isinstance(strnode, astroid.Const): return if isinstance(strnode.value, bytes): self.add_message('ansible-no-format-on-bytestring', node=node) return if not isinstance(strnode.value, str): return if node.starargs or node.kwargs: return try: num_args = parse_format_method_string(strnode.value)[1] except utils.IncompleteFormatString: return if num_args: self.add_message('ansible-format-automatic-specification', node=node) return
def _check_format_string(self, node, format_arg): """Checks that format string tokens match the supplied arguments. Args: node (astroid.node_classes.NodeNG): AST node to be checked. format_arg (int): Index of the format string in the node arguments. """ num_args = _count_supplied_tokens(node.args[format_arg + 1:]) if not num_args: # If no args were supplied the string is not interpolated and can contain # formatting characters - it's used verbatim. Don't check any further. return format_string = node.args[format_arg].value required_num_args = 0 if isinstance(format_string, bytes): format_string = format_string.decode() if isinstance(format_string, str): try: if self._format_style == "old": keyword_args, required_num_args, _, _ = utils.parse_format_string( format_string) if keyword_args: # Keyword checking on logging strings is complicated by # special keywords - out of scope. return elif self._format_style == "new": ( keyword_arguments, implicit_pos_args, explicit_pos_args, ) = utils.parse_format_method_string(format_string) keyword_args_cnt = len( set(k for k, l in keyword_arguments if not isinstance(k, int))) required_num_args = (keyword_args_cnt + implicit_pos_args + explicit_pos_args) else: self.add_message( "logging-format-interpolation", node=node, args=self._format_style_args, ) return except utils.UnsupportedFormatCharacter as ex: char = format_string[ex.index] self.add_message( "logging-unsupported-format", node=node, args=(char, ord(char), ex.index), ) return except utils.IncompleteFormatString: self.add_message("logging-format-truncated", node=node) return if num_args > required_num_args: self.add_message("logging-too-many-args", node=node) elif num_args < required_num_args: self.add_message("logging-too-few-args", node=node)
def visit_call(self, node): """Make sure "print_function" is imported if necessary""" if (isinstance(node.func, astroid.nodes.Name) and node.func.name == "print"): if "print_function" in node.root().future_imports: def previous(node): if node is not None: parent = node.parent previous = node.previous_sibling() if previous is None: return parent return previous prev = previous(node) while prev is not None: if (isinstance(prev, astroid.nodes.ImportFrom) and prev.modname == "__future__" and "print_function" in (name_alias[0] for name_alias in prev.names)): return prev = previous(prev) self.add_message("print-without-import", node=node, confidence=HIGH) else: self.add_message("print-without-import", node=node, confidence=HIGH) func = utils.safe_infer(node.func) if (isinstance(func, astroid.BoundMethod) and isinstance(func.bound, astroid.Instance) and func.bound.name in ("str", "unicode", "bytes")): if func.name == "format": if isinstance(node.func, astroid.Attribute) and not isinstance( node.func.expr, astroid.Const): return if node.starargs or node.kwargs: return try: strnode = next(func.bound.infer()) except astroid.InferenceError: return if not (isinstance(strnode, astroid.Const) and isinstance(strnode.value, str)): return try: fields, num_args, manual_pos = ( utils.parse_format_method_string(strnode.value)) except utils.IncompleteFormatString: self.add_message("bad-format-string", node=node) if num_args != 0: self.add_message("implicit-format-spec", node=node, confidence=HIGH)
def _check_format_string(self, node, format_arg): """Checks that format string tokens match the supplied arguments. Args: node (astroid.node_classes.NodeNG): AST node to be checked. format_arg (int): Index of the format string in the node arguments. """ num_args = _count_supplied_tokens(node.args[format_arg + 1 :]) if not num_args: # If no args were supplied the string is not interpolated and can contain # formatting characters - it's used verbatim. Don't check any further. return format_string = node.args[format_arg].value if not isinstance(format_string, str): # If the log format is constant non-string (e.g. logging.debug(5)), # ensure there are no arguments. required_num_args = 0 else: try: if self._format_style == "old": keyword_args, required_num_args, _, _ = utils.parse_format_string( format_string ) if keyword_args: # Keyword checking on logging strings is complicated by # special keywords - out of scope. return elif self._format_style == "new": keyword_arguments, implicit_pos_args, explicit_pos_args = utils.parse_format_method_string( format_string ) keyword_args_cnt = len( set(k for k, l in keyword_arguments if not isinstance(k, int)) ) required_num_args = ( keyword_args_cnt + implicit_pos_args + explicit_pos_args ) except utils.UnsupportedFormatCharacter as ex: char = format_string[ex.index] self.add_message( "logging-unsupported-format", node=node, args=(char, ord(char), ex.index), ) return except utils.IncompleteFormatString: self.add_message("logging-format-truncated", node=node) return if num_args > required_num_args: self.add_message("logging-too-many-args", node=node) elif num_args < required_num_args: self.add_message("logging-too-few-args", node=node)
def _check_format_string(self, node, format_arg): """Checks that format string tokens match the supplied arguments. Args: node (astroid.node_classes.NodeNG): AST node to be checked. format_arg (int): Index of the format string in the node arguments. """ num_args = _count_supplied_tokens(node.args[format_arg + 1:]) if not num_args: # If no args were supplied, then all format strings are valid - # don't check any further. return format_string = node.args[format_arg].value if not isinstance(format_string, str): # If the log format is constant non-string (e.g. logging.debug(5)), # ensure there are no arguments. required_num_args = 0 else: try: if self._format_style == "%": keyword_args, required_num_args, _, _ = utils.parse_format_string( format_string) if keyword_args: # Keyword checking on logging strings is complicated by # special keywords - out of scope. return elif self._format_style == "{": keys, num_args, manual_pos_arg = utils.parse_format_method_string( format_string) kargs = len( set(k for k, l in keys if not isinstance(k, int))) required_num_args = kargs + num_args + manual_pos_arg except utils.UnsupportedFormatCharacter as ex: char = format_string[ex.index] self.add_message( "logging-unsupported-format", node=node, args=(char, ord(char), ex.index), ) return except utils.IncompleteFormatString: self.add_message("logging-format-truncated", node=node) return if num_args > required_num_args: self.add_message("logging-too-many-args", node=node) elif num_args < required_num_args: self.add_message("logging-too-few-args", node=node)
def test_parse_format_method_string(): samples = [ ("{}", 1), ("{}:{}", 2), ("{field}", 1), ("{:5}", 1), ("{:10}", 1), ("{field:10}", 1), ("{field:10}{{}}", 1), ("{:5}{!r:10}", 2), ("{:5}{}{{}}{}", 3), ("{0}{1}{0}", 2), ("Coordinates: {latitude}, {longitude}", 2), ("X: {0[0]}; Y: {0[1]}", 1), ("{:*^30}", 1), ("{!r:}", 1), ] for fmt, count in samples: keys, num_args, pos_args = utils.parse_format_method_string(fmt) keyword_args = len(set(k for k, l in keys if not isinstance(k, int))) assert keyword_args + num_args + pos_args == count
def _check_new_format(self, node, func): """Check the new string formatting. """ # Skip format nodes which don't have an explicit string on the # left side of the format operation. # We do this because our inference engine can't properly handle # redefinitions of the original string. # Note that there may not be any left side at all, if the format method # has been assigned to another variable. See issue 351. For example: # # fmt = 'some string {}'.format # fmt('arg') if isinstance(node.func, astroid.Attribute) and not isinstance( node.func.expr, astroid.Const ): return if node.starargs or node.kwargs: return try: strnode = next(func.bound.infer()) except astroid.InferenceError: return if not (isinstance(strnode, astroid.Const) and isinstance(strnode.value, str)): return try: call_site = astroid.arguments.CallSite.from_call(node) except astroid.InferenceError: return try: fields, num_args, manual_pos = utils.parse_format_method_string( strnode.value ) except utils.IncompleteFormatString: self.add_message("bad-format-string", node=node) return positional_arguments = call_site.positional_arguments named_arguments = call_site.keyword_arguments named_fields = {field[0] for field in fields if isinstance(field[0], str)} if num_args and manual_pos: self.add_message("format-combined-specification", node=node) return check_args = False # Consider "{[0]} {[1]}" as num_args. num_args += sum(1 for field in named_fields if field == "") if named_fields: for field in named_fields: if field and field not in named_arguments: self.add_message( "missing-format-argument-key", node=node, args=(field,) ) for field in named_arguments: if field not in named_fields: self.add_message( "unused-format-string-argument", node=node, args=(field,) ) # num_args can be 0 if manual_pos is not. num_args = num_args or manual_pos if positional_arguments or num_args: empty = any(True for field in named_fields if field == "") if named_arguments or empty: # Verify the required number of positional arguments # only if the .format got at least one keyword argument. # This means that the format strings accepts both # positional and named fields and we should warn # when one of the them is missing or is extra. check_args = True else: check_args = True if check_args: # num_args can be 0 if manual_pos is not. num_args = num_args or manual_pos if len(positional_arguments) > num_args: self.add_message("too-many-format-args", node=node) elif len(positional_arguments) < num_args: self.add_message("too-few-format-args", node=node) self._detect_vacuous_formatting(node, positional_arguments) self._check_new_format_specifiers(node, fields, named_arguments)
def _check_new_format(self, node, func): """ Check the new string formatting. """ # TODO: skip (for now) format nodes which don't have # an explicit string on the left side of the format operation. # We do this because our inference engine can't properly handle # redefinitions of the original string. # For more details, see issue 287. # # Note that there may not be any left side at all, if the format method # has been assigned to another variable. See issue 351. For example: # # fmt = 'some string {}'.format # fmt('arg') if isinstance(node.func, astroid.Attribute) and not isinstance( node.func.expr, astroid.Const ): return if node.starargs or node.kwargs: return try: strnode = next(func.bound.infer()) except astroid.InferenceError: return if not (isinstance(strnode, astroid.Const) and isinstance(strnode.value, str)): return try: call_site = CallSite.from_call(node) except astroid.InferenceError: return try: fields, num_args, manual_pos = utils.parse_format_method_string( strnode.value ) except utils.IncompleteFormatString: self.add_message("bad-format-string", node=node) return positional_arguments = call_site.positional_arguments named_arguments = call_site.keyword_arguments named_fields = {field[0] for field in fields if isinstance(field[0], str)} if num_args and manual_pos: self.add_message("format-combined-specification", node=node) return check_args = False # Consider "{[0]} {[1]}" as num_args. num_args += sum(1 for field in named_fields if field == "") if named_fields: for field in named_fields: if field and field not in named_arguments: self.add_message( "missing-format-argument-key", node=node, args=(field,) ) for field in named_arguments: if field not in named_fields: self.add_message( "unused-format-string-argument", node=node, args=(field,) ) # num_args can be 0 if manual_pos is not. num_args = num_args or manual_pos if positional_arguments or num_args: empty = any(True for field in named_fields if field == "") if named_arguments or empty: # Verify the required number of positional arguments # only if the .format got at least one keyword argument. # This means that the format strings accepts both # positional and named fields and we should warn # when one of the them is missing or is extra. check_args = True else: check_args = True if check_args: # num_args can be 0 if manual_pos is not. num_args = num_args or manual_pos if len(positional_arguments) > num_args: self.add_message("too-many-format-args", node=node) elif len(positional_arguments) < num_args: self.add_message("too-few-format-args", node=node) self._detect_vacuous_formatting(node, positional_arguments) self._check_new_format_specifiers(node, fields, named_arguments)
def _detect_replacable_format_call(self, node: nodes.Const) -> None: """Check whether a string is used in a call to format() or '%' and whether it can be replaced by an f-string """ if (isinstance(node.parent, nodes.Attribute) and node.parent.attrname == "format"): # Don't warn on referencing / assigning .format without calling it if not isinstance(node.parent.parent, nodes.Call): return if node.parent.parent.args: for arg in node.parent.parent.args: # If star expressions with more than 1 element are being used if isinstance(arg, nodes.Starred): inferred = utils.safe_infer(arg.value) if (isinstance(inferred, astroid.List) and len(inferred.elts) > 1): return # Backslashes can't be in f-string expressions if "\\" in arg.as_string(): return elif node.parent.parent.keywords: keyword_args = [ i[0] for i in utils.parse_format_method_string(node.value)[0] ] for keyword in node.parent.parent.keywords: # If keyword is used multiple times if keyword_args.count(keyword.arg) > 1: return keyword = utils.safe_infer(keyword.value) # If lists of more than one element are being unpacked if isinstance(keyword, nodes.Dict): if len(keyword.items) > 1 and len(keyword_args) > 1: return # If all tests pass, then raise message self.add_message( "consider-using-f-string", node=node, line=node.lineno, col_offset=node.col_offset, ) elif isinstance(node.parent, nodes.BinOp) and node.parent.op == "%": # Backslashes can't be in f-string expressions if "\\" in node.parent.right.as_string(): return inferred_right = utils.safe_infer(node.parent.right) # If dicts or lists of length > 1 are used if isinstance(inferred_right, nodes.Dict): if len(inferred_right.items) > 1: return elif isinstance(inferred_right, nodes.List): if len(inferred_right.elts) > 1: return # If all tests pass, then raise message self.add_message( "consider-using-f-string", node=node, line=node.lineno, col_offset=node.col_offset, )