--- a/eric6/Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py Tue Feb 04 18:34:37 2020 +0100 +++ b/eric6/Plugins/CheckerPlugins/SyntaxChecker/pyflakes/checker.py Tue Feb 04 19:41:50 2020 +0100 @@ -19,6 +19,7 @@ import functools import os import re +import string import sys import tokenize @@ -36,6 +37,8 @@ builtin_vars = dir(__import__('__builtin__' if PY2 else 'builtins')) +parse_format_string = string.Formatter().parse + if PY2: tokenize_tokenize = tokenize.generate_tokens else: @@ -76,18 +79,103 @@ if PY35_PLUS: FOR_TYPES = (ast.For, ast.AsyncFor) LOOP_TYPES = (ast.While, ast.For, ast.AsyncFor) + FUNCTION_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) else: FOR_TYPES = (ast.For,) LOOP_TYPES = (ast.While, ast.For) + FUNCTION_TYPES = (ast.FunctionDef,) -# https://github.com/python/typed_ast/blob/55420396/ast27/Parser/tokenizer.c#L102-L104 +# https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L102-L104 TYPE_COMMENT_RE = re.compile(r'^#\s*type:\s*') -# https://github.com/python/typed_ast/blob/55420396/ast27/Parser/tokenizer.c#L1400 -TYPE_IGNORE_RE = re.compile(TYPE_COMMENT_RE.pattern + r'ignore\s*(#|$)') -# https://github.com/python/typed_ast/blob/55420396/ast27/Grammar/Grammar#L147 +# https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L1408-L1413 +ASCII_NON_ALNUM = ''.join([chr(i) for i in range(128) if not chr(i).isalnum()]) +TYPE_IGNORE_RE = re.compile( + TYPE_COMMENT_RE.pattern + r'ignore([{}]|$)'.format(ASCII_NON_ALNUM)) +# https://github.com/python/typed_ast/blob/1.4.0/ast27/Grammar/Grammar#L147 TYPE_FUNC_RE = re.compile(r'^(\(.*?\))\s*->\s*(.*)$') +MAPPING_KEY_RE = re.compile(r'\(([^()]*)\)') +CONVERSION_FLAG_RE = re.compile('[#0+ -]*') +WIDTH_RE = re.compile(r'(?:\*|\d*)') +PRECISION_RE = re.compile(r'(?:\.(?:\*|\d*))?') +LENGTH_RE = re.compile('[hlL]?') +# https://docs.python.org/3/library/stdtypes.html#old-string-formatting +VALID_CONVERSIONS = frozenset('diouxXeEfFgGcrsa%') + + +def _must_match(regex, string, pos): + # type: (Pattern[str], str, int) -> Match[str] + match = regex.match(string, pos) + assert match is not None + return match + + +def parse_percent_format(s): # type: (str) -> Tuple[PercentFormat, ...] + """Parses the string component of a `'...' % ...` format call + + Copied from https://github.com/asottile/pyupgrade at v1.20.1 + """ + + def _parse_inner(): + # type: () -> Generator[PercentFormat, None, None] + string_start = 0 + string_end = 0 + in_fmt = False + + i = 0 + while i < len(s): + if not in_fmt: + try: + i = s.index('%', i) + except ValueError: # no more % fields! + yield s[string_start:], None + return + else: + string_end = i + i += 1 + in_fmt = True + else: + key_match = MAPPING_KEY_RE.match(s, i) + if key_match: + key = key_match.group(1) # type: Optional[str] + i = key_match.end() + else: + key = None + + conversion_flag_match = _must_match(CONVERSION_FLAG_RE, s, i) + conversion_flag = conversion_flag_match.group() or None + i = conversion_flag_match.end() + + width_match = _must_match(WIDTH_RE, s, i) + width = width_match.group() or None + i = width_match.end() + + precision_match = _must_match(PRECISION_RE, s, i) + precision = precision_match.group() or None + i = precision_match.end() + + # length modifier is ignored + i = _must_match(LENGTH_RE, s, i).end() + + try: + conversion = s[i] + except IndexError: + raise ValueError('end-of-string while parsing format') + i += 1 + + fmt = (key, conversion_flag, width, precision, conversion) + yield s[string_start:string_end], fmt + + in_fmt = False + string_start = i + + if in_fmt: + raise ValueError('end-of-string while parsing format') + + return tuple(_parse_inner()) + + class _FieldsOrder(dict): """Fix order of AST node fields.""" @@ -543,11 +631,13 @@ if name in scope: return ( isinstance(scope[name], ImportationFrom) and - scope[name].fullName == 'typing.overload' + scope[name].fullName in ( + 'typing.overload', 'typing_extensions.overload', + ) ) - else: - return False - + + return False + def is_typing_overload_decorator(node): return ( ( @@ -561,7 +651,7 @@ ) return ( - isinstance(value.source, ast.FunctionDef) and + isinstance(value.source, FUNCTION_TYPES) and any( is_typing_overload_decorator(dec) for dec in value.source.decorator_list @@ -829,22 +919,31 @@ def getParent(self, node): # Lookup the first parent which is not Tuple, List or Starred while True: - node = node.parent + node = node._pyflakes_parent if not hasattr(node, 'elts') and not hasattr(node, 'ctx'): return node def getCommonAncestor(self, lnode, rnode, stop): - if stop in (lnode, rnode) or not (hasattr(lnode, 'parent') and - hasattr(rnode, 'parent')): + if ( + stop in (lnode, rnode) or + not ( + hasattr(lnode, '_pyflakes_parent') and + hasattr(rnode, '_pyflakes_parent') + ) + ): return None if lnode is rnode: return lnode - if (lnode.depth > rnode.depth): - return self.getCommonAncestor(lnode.parent, rnode, stop) - if (lnode.depth < rnode.depth): - return self.getCommonAncestor(lnode, rnode.parent, stop) - return self.getCommonAncestor(lnode.parent, rnode.parent, stop) + if (lnode._pyflakes_depth > rnode._pyflakes_depth): + return self.getCommonAncestor(lnode._pyflakes_parent, rnode, stop) + if (lnode._pyflakes_depth < rnode._pyflakes_depth): + return self.getCommonAncestor(lnode, rnode._pyflakes_parent, stop) + return self.getCommonAncestor( + lnode._pyflakes_parent, + rnode._pyflakes_parent, + stop, + ) def descendantOf(self, node, ancestors, stop): for a in ancestors: @@ -882,7 +981,7 @@ - `node` is the statement responsible for the change - `value` is the new value, a Binding instance """ - # assert value.source in (node, node.parent): + # assert value.source in (node, node._pyflakes_parent): for scope in self.scopeStack[::-1]: if value.name in scope: break @@ -1021,11 +1120,11 @@ parent_stmt = self.getParent(node) if isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or ( - parent_stmt != node.parent and + parent_stmt != node._pyflakes_parent and not self.isLiteralTupleUnpacking(parent_stmt)): binding = Binding(name, node) elif name == '__all__' and isinstance(self.scope, ModuleScope): - binding = ExportBinding(name, node.parent, self.scope) + binding = ExportBinding(name, node._pyflakes_parent, self.scope) elif isinstance(getattr(node, 'ctx', None), ast.Param): binding = Argument(name, self.getScopeNode(node)) else: @@ -1038,11 +1137,11 @@ """ Return `True` if node is part of a conditional body. """ - current = getattr(node, 'parent', None) + current = getattr(node, '_pyflakes_parent', None) while current: if isinstance(current, (ast.If, ast.While, ast.IfExp)): return True - current = getattr(current, 'parent', None) + current = getattr(current, '_pyflakes_parent', None) return False name = getNodeName(node) @@ -1129,8 +1228,8 @@ self.isDocstring(node)): self.futuresAllowed = False self.nodeDepth += 1 - node.depth = self.nodeDepth - node.parent = parent + node._pyflakes_depth = self.nodeDepth + node._pyflakes_parent = parent try: handler = self.getNodeHandler(node.__class__) handler(node) @@ -1239,9 +1338,249 @@ PASS = ignore # "expr" type nodes - BOOLOP = BINOP = UNARYOP = IFEXP = SET = \ - CALL = REPR = ATTRIBUTE = SUBSCRIPT = \ - STARRED = NAMECONSTANT = handleChildren + BOOLOP = UNARYOP = IFEXP = SET = \ + REPR = ATTRIBUTE = SUBSCRIPT = \ + STARRED = NAMECONSTANT = NAMEDEXPR = handleChildren + + def _handle_string_dot_format(self, node): + try: + placeholders = tuple(parse_format_string(node.func.value.s)) + except ValueError as e: + self.report(messages.StringDotFormatInvalidFormat, node, e) + return + + class state: # py2-compatible `nonlocal` + auto = None + next_auto = 0 + + placeholder_positional = set() + placeholder_named = set() + + def _add_key(fmtkey): + """Returns True if there is an error which should early-exit""" + if fmtkey is None: # end of string or `{` / `}` escapes + return False + + # attributes / indices are allowed in `.format(...)` + fmtkey, _, _ = fmtkey.partition('.') + fmtkey, _, _ = fmtkey.partition('[') + + try: + fmtkey = int(fmtkey) + except ValueError: + pass + else: # fmtkey was an integer + if state.auto is True: + self.report(messages.StringDotFormatMixingAutomatic, node) + return True + else: + state.auto = False + + if fmtkey == '': + if state.auto is False: + self.report(messages.StringDotFormatMixingAutomatic, node) + return True + else: + state.auto = True + + fmtkey = state.next_auto + state.next_auto += 1 + + if isinstance(fmtkey, int): + placeholder_positional.add(fmtkey) + else: + placeholder_named.add(fmtkey) + + return False + + for _, fmtkey, spec, _ in placeholders: + if _add_key(fmtkey): + return + + # spec can also contain format specifiers + if spec is not None: + try: + spec_placeholders = tuple(parse_format_string(spec)) + except ValueError as e: + self.report(messages.StringDotFormatInvalidFormat, node, e) + return + + for _, spec_fmtkey, spec_spec, _ in spec_placeholders: + # can't recurse again + if spec_spec is not None and '{' in spec_spec: + self.report( + messages.StringDotFormatInvalidFormat, + node, + 'Max string recursion exceeded', + ) + return + if _add_key(spec_fmtkey): + return + + # bail early if there is *args or **kwargs + if ( + # python 2.x *args / **kwargs + getattr(node, 'starargs', None) or + getattr(node, 'kwargs', None) or + # python 3.x *args + any( + isinstance(arg, getattr(ast, 'Starred', ())) + for arg in node.args + ) or + # python 3.x **kwargs + any(kwd.arg is None for kwd in node.keywords) + ): + return + + substitution_positional = set(range(len(node.args))) + substitution_named = {kwd.arg for kwd in node.keywords} + + extra_positional = substitution_positional - placeholder_positional + extra_named = substitution_named - placeholder_named + + missing_arguments = ( + (placeholder_positional | placeholder_named) - + (substitution_positional | substitution_named) + ) + + if extra_positional: + self.report( + messages.StringDotFormatExtraPositionalArguments, + node, + ', '.join(sorted(str(x) for x in extra_positional)), + ) + if extra_named: + self.report( + messages.StringDotFormatExtraNamedArguments, + node, + ', '.join(sorted(extra_named)), + ) + if missing_arguments: + self.report( + messages.StringDotFormatMissingArgument, + node, + ', '.join(sorted(str(x) for x in missing_arguments)), + ) + + def CALL(self, node): + if ( + isinstance(node.func, ast.Attribute) and + isinstance(node.func.value, ast.Str) and + node.func.attr == 'format' + ): + self._handle_string_dot_format(node) + self.handleChildren(node) + + def _handle_percent_format(self, node): + try: + placeholders = parse_percent_format(node.left.s) + except ValueError: + self.report( + messages.PercentFormatInvalidFormat, + node, + 'incomplete format', + ) + return + + named = set() + positional_count = 0 + positional = None + for _, placeholder in placeholders: + if placeholder is None: + continue + name, _, width, precision, conversion = placeholder + + if conversion == '%': + continue + + if conversion not in VALID_CONVERSIONS: + self.report( + messages.PercentFormatUnsupportedFormatCharacter, + node, + conversion, + ) + + if positional is None and conversion: + positional = name is None + + for part in (width, precision): + if part is not None and '*' in part: + if not positional: + self.report( + messages.PercentFormatStarRequiresSequence, + node, + ) + else: + positional_count += 1 + + if positional and name is not None: + self.report( + messages.PercentFormatMixedPositionalAndNamed, + node, + ) + return + elif not positional and name is None: + self.report( + messages.PercentFormatMixedPositionalAndNamed, + node, + ) + return + + if positional: + positional_count += 1 + else: + named.add(name) + + if ( + isinstance(node.right, (ast.List, ast.Tuple)) and + # does not have any *splats (py35+ feature) + not any( + isinstance(elt, getattr(ast, 'Starred', ())) + for elt in node.right.elts + ) + ): + substitution_count = len(node.right.elts) + if positional and positional_count != substitution_count: + self.report( + messages.PercentFormatPositionalCountMismatch, + node, + positional_count, + substitution_count, + ) + elif not positional: + self.report(messages.PercentFormatExpectedMapping, node) + + if ( + isinstance(node.right, ast.Dict) and + all(isinstance(k, ast.Str) for k in node.right.keys) + ): + if positional and positional_count > 1: + self.report(messages.PercentFormatExpectedSequence, node) + return + + substitution_keys = {k.s for k in node.right.keys} + extra_keys = substitution_keys - named + missing_keys = named - substitution_keys + if not positional and extra_keys: + self.report( + messages.PercentFormatExtraNamedArguments, + node, + ', '.join(sorted(extra_keys)), + ) + if not positional and missing_keys: + self.report( + messages.PercentFormatMissingArgument, + node, + ', '.join(sorted(missing_keys)), + ) + + def BINOP(self, node): + if ( + isinstance(node.op, ast.Mod) and + isinstance(node.left, ast.Str) + ): + self._handle_percent_format(node) + self.handleChildren(node) NUM = STR = BYTES = ELLIPSIS = CONSTANT = ignore @@ -1271,7 +1610,24 @@ self.report(messages.RaiseNotImplemented, node) # additional node types - COMPREHENSION = KEYWORD = FORMATTEDVALUE = JOINEDSTR = handleChildren + COMPREHENSION = KEYWORD = FORMATTEDVALUE = handleChildren + + _in_fstring = False + + def JOINEDSTR(self, node): + if ( + # the conversion / etc. flags are parsed as f-strings without + # placeholders + not self._in_fstring and + not any(isinstance(x, ast.FormattedValue) for x in node.values) + ): + self.report(messages.FStringMissingPlaceholders, node) + + self._in_fstring, orig = True, self._in_fstring + try: + self.handleChildren(node) + finally: + self._in_fstring = orig def DICT(self, node): # Complain if there are duplicate keys with different values @@ -1363,7 +1719,7 @@ if isinstance(node.ctx, (ast.Load, ast.AugLoad)): self.handleNodeLoad(node) if (node.id == 'locals' and isinstance(self.scope, FunctionScope) and - isinstance(node.parent, ast.Call)): + isinstance(node._pyflakes_parent, ast.Call)): # we are doing locals() call in current scope self.scope.usesLocals = True elif isinstance(node.ctx, (ast.Store, ast.AugStore, ast.Param)): @@ -1379,8 +1735,8 @@ # definition (not OK), for 'continue', a finally block (not OK), or # the top module scope (not OK) n = node - while hasattr(n, 'parent'): - n, n_child = n.parent, n + while hasattr(n, '_pyflakes_parent'): + n, n_child = n._pyflakes_parent, n if isinstance(n, LOOP_TYPES): # Doesn't apply unless it's in the loop itself if n_child not in n.orelse: @@ -1389,7 +1745,7 @@ break # Handle Try/TryFinally difference in Python < and >= 3.3 if hasattr(n, 'finalbody') and isinstance(node, ast.Continue): - if n_child in n.finalbody: + if n_child in n.finalbody and not PY38_PLUS: self.report(messages.ContinueInFinally, node) return if isinstance(node, ast.Continue): @@ -1450,6 +1806,10 @@ addArgs(node.args.args) defaults = node.args.defaults else: + if PY38_PLUS: + for arg in node.args.posonlyargs: + args.append(arg.arg) + annotations.append(arg.annotation) for arg in node.args.args + node.args.kwonlyargs: args.append(arg.arg) annotations.append(arg.annotation) @@ -1714,6 +2074,5 @@ self.handleChildren(node) - # # eflag: noqa = M702