def _ast_pattern(tree, variables): """Shared logic to compile an AST matcher recursively. Args: tree: the ast.expr/ast.stmt to match, or a list of ast nodes. variables: names to replace with submatchers instead of literally. Returns: A raw_aw matcher with any Name nodes from the variables map swapped out, as in ExprPattern. """ # recursion isn't good because we can blow the stack for a ~1000-deep ++++foo, # but does that even happen IRL? # TODO: use a stack. if isinstance(tree, list): return base_matchers.ItemsAre( [_ast_pattern(e, variables) for e in tree]) if not isinstance(tree, ast.AST): # e.g. the identifier for an ast.Name. return base_matchers.Equals(tree) if isinstance(tree, ast.Name): if tree.id in variables: return variables[tree.id] return getattr(ast_matchers, type(tree).__name__)( **{ field: _ast_pattern(getattr(tree, field), variables) for field in type(tree)._fields # Filter out variable ctx. if field != 'ctx' or not isinstance(tree, ast.Name) })
def test_match(self): container = [1] self.assertEqual( base_matchers.ItemsAre([base_matchers.Bind('a') ]).match(_FAKE_CONTEXT, container), matcher.MatchInfo(match.ObjectMatch(container), {'a': matcher.BoundValue(match.ObjectMatch(1))}))
def test_key(self): shared = 'shared' once_any = base_matchers.Once(base_matchers.Anything(), key=shared) once_never = base_matchers.Once(base_matchers.Unless( base_matchers.Anything()), key=shared) # once_never reuses the cached result of once_any, because they share a key. m = base_matchers.ItemsAre([once_any, once_never]) self.assertIsNotNone(m.match(_FAKE_CONTEXT.new(), [1, 2]))
def test_cached_same_match(self): """Once caches results even within the same match attempt. This isn't a very realistic test case, but just in case the matcher gets used in unexpectedly complex ways, it should be sound. """ once_equals_one = base_matchers.Once(base_matchers.Equals(1)) m = base_matchers.ItemsAre([once_equals_one] * 3) # Fails if it sees non-one entries: self.assertIsNone(m.match(_FAKE_CONTEXT.new(), [2, 2, 3])) # But if it sees a 1 first, then all subsequent calls will also match, even # if they are not one, because the result is cached. self.assertIsNotNone(m.match(_FAKE_CONTEXT.new(), [1, 2, 3]))
# matched function call. This isn't perfect, since it # will still match `foo(func_that_can_raise())` and it # won't match cases where a try block has more than one # statement but only one can obviously raise an error # (for instance, a function call followed by continue, # break, or return). body=base_matchers.AllOf( base_matchers.ItemsAre([ base_matchers.AllOf( base_matchers.AnyOf( ast_matchers.Return( value=ast_matchers.Call( func=base_matchers.Bind('func'))), ast_matchers.Expr(value=ast_matchers.Call( func=base_matchers.Bind('func'))), ast_matchers.Assign( value=ast_matchers.Call( func=base_matchers.Bind('func'))), ), # TODO: Escape double quotes in the # replacement base_matchers.Unless( base_matchers.MatchesRegex(r'.+".+'))), ]), ), ), )), replacement=dict( # TODO: Wrapping $func in quotes prevents the linter from # complaining if the outer quotes don't match the rest of the file, # since different quote style is allowed everywhere if it avoids # escaping. Is there a better option to keep the linter happy? # Also, in the case that all the arguments to $func were variables,
def test_submatcher_wrong(self): self.assertIsNone( base_matchers.ItemsAre([ base_matchers.Unless(base_matchers.Anything()) ]).match(_FAKE_CONTEXT, [1]))
def test_too_long(self): self.assertIsNone(base_matchers.ItemsAre([]).match(_FAKE_CONTEXT, [1]))
def test_too_short(self): self.assertIsNone( base_matchers.ItemsAre([base_matchers.Anything() ]).match(_FAKE_CONTEXT, []))
def _pattern_factory(pattern): return ast_matchers.Module(body=base_matchers.ItemsAre( [syntax_matchers.StmtPattern(pattern)]))
def _pattern_factory(pattern): return ast_matchers.Module(body=base_matchers.ItemsAre( [ast_matchers.Expr(value=syntax_matchers.ExprPattern(pattern))]))