def test_restrictions(self): parsed = matcher.parse_ast('a = 1\na = 2', '<string>') stmt_match, stmt_nomatch = parsed.tree.body m = syntax_matchers.StmtPattern( 'a = $name', {'name': syntax_matchers.ExprPattern('1')}) self.assertIsNotNone(m.match(matcher.MatchContext(parsed), stmt_match)) self.assertIsNone(m.match(matcher.MatchContext(parsed), stmt_nomatch))
def parse(self, data: Text, filename: str): """Returns a :class:`refex.python.matcher.PythonParsedFile`.""" try: return _matcher.parse_ast(data, filename) except _matcher.ParseError as e: # Probably Python 2. TODO: figure out how to handle this. raise SkipFileError(str(e))
def test_ancestor(self): """The matcher won't traverse into child nodes.""" parsed = matcher.parse_ast('~a', '<string>') self.assertIsNone( ast_matchers.UnaryOp( op=base_matchers.Unless(base_matchers.Anything())).match( matcher.MatchContext(parsed), parsed.tree.body[0]))
def test_nonexpr_in_expr_context(self, template): parsed = matcher.parse_ast('[x]') m = base_matchers.Bind('bound', ast_matchers.Name()) [matchinfo] = matcher.find_iter(m, parsed) with self.assertRaises(formatting.RewriteError): template.substitute_match(parsed, matchinfo.match, {'bound': matchinfo.match})
def test_nonvariable_name_fails(self): """Names are only treated as variables, or anything weird, on request.""" parsed = matcher.parse_ast('3', '<string>') stmt = parsed.tree.body[0] self.assertIsNone( syntax_matchers.StmtPattern('name').match( matcher.MatchContext(parsed), stmt))
def test_simple_node_is_expr(self): """In a non-simple statement, subexpressions are their own simple node.""" parsed = matcher.parse_ast('for x in y: pass') for_stmt = parsed.tree.body[0] self.assertIs(parsed.nav.get_simple_node(for_stmt.target), for_stmt.target) self.assertIs(parsed.nav.get_simple_node(for_stmt.iter), for_stmt.iter)
def test_named_constant(self, constant): parsed = matcher.parse_ast(str(constant), '<string>') for m in ast_matchers.NameConstant(), ast_matchers.NameConstant( value=constant): with self.subTest(matcher=m): self.assertIsNotNone( m.match(matcher.MatchContext(parsed), parsed.tree.body[0].value))
def test_num(self, num_matcher): for s in '0', '0.0', '0j': with self.subTest(s=s): parsed = matcher.parse_ast(s, '<string>') self.assertIsNotNone( num_matcher.match( matcher.MatchContext(parsed), parsed.tree.body[0].value))
def test_restrictions(self): parsed = matcher.parse_ast('1\n2', '<string>') expr_match = parsed.tree.body[0].value expr_nomatch = parsed.tree.body[1].value m = syntax_matchers.ExprPattern( '$name', {'name': syntax_matchers.ExprPattern('1')}) self.assertIsNotNone(m.match(matcher.MatchContext(parsed), expr_match)) self.assertIsNone(m.match(matcher.MatchContext(parsed), expr_nomatch))
def test_renamed_fromimport(self, import_stmt): any_m = base_matchers.Anything() m_success = syntax_matchers.WithTopLevelImport(any_m, 'os.path', 'renamed') m_fail = syntax_matchers.WithTopLevelImport(any_m, 'os.path') context = matcher.MatchContext(matcher.parse_ast(import_stmt)) self.assertIsNotNone(m_success.match(context, 1)) self.assertIsNone(m_fail.match(context, 1))
def test_invalid_syntax(self): parsed = matcher.parse_ast('x') m = base_matchers.Bind('x', ast_matchers.Name()) [matchinfo] = matcher.find_iter(m, parsed) template = syntactic_template.PythonTemplate('$x') with self.assertRaises(formatting.RewriteError): template.substitute_match(parsed, matchinfo.match, {'x': match.StringMatch('x y')}),
def test_nonpython_dollars_source(self, src): parsed = matcher.parse_ast(src) m = base_matchers.Bind('bound', ast_matchers.Call()) [matchinfo] = matcher.find_iter(m, parsed) self.assertEqual( src, syntactic_template.PythonExprTemplate('$bound').substitute_match( parsed, matchinfo.match, {'bound': matchinfo.match}))
def test_doesnt_match(self): parsed = matcher.parse_ast('hi = 42', '<string>') m = base_matchers.AllOf(base_matchers.FileMatchesRegex('hello'), ast_matchers.Num()) matches = list(matcher.find_iter(m, parsed)) self.assertEqual(matches, [])
def test_renamed_import(self): any_m = base_matchers.Anything() m_success = syntax_matchers.WithTopLevelImport(any_m, 'os', 'renamed') m_fail = syntax_matchers.WithTopLevelImport(any_m, 'os') context = matcher.MatchContext( matcher.parse_ast('import os as renamed')) self.assertIsNotNone(m_success.match(context, 1)) self.assertIsNone(m_fail.match(context, 1))
def test_matches(self): parsed = matcher.parse_ast('var_hello = 42', '<string>') m = base_matchers.AllOf(base_matchers.FileMatchesRegex('hello'), ast_matchers.Num()) matches = list(matcher.find_iter(m, parsed)) self.assertLen(matches, 1)
def test_inner_nomatches(self): parsed = matcher.parse_ast('xy = 2', '<string>') matches = list( matcher.find_iter( base_matchers.MatchesRegex( r'', base_matchers.Unless(base_matchers.Anything())), parsed)) self.assertEqual(matches, [])
def test_fullmatch_semantics(self): """The regexp only matches the full expression.""" for var in ['x_', '_x']: with self.subTest(var=var): parsed = matcher.parse_ast(var, '<string>') self.assertEqual( list( matcher.find_iter(base_matchers.MatchesRegex(r'x'), parsed)), [])
def test_variable_name(self): parsed = matcher.parse_ast('3', '<string>') expr = parsed.tree.body[0].value expr_match = matcher.LexicalASTMatch(expr, parsed.text, expr.first_token, expr.last_token) self.assertEqual( syntax_matchers.ExprPattern('$name').match( matcher.MatchContext(parsed), expr), matcher.MatchInfo(expr_match, {'name': matcher.BoundValue(expr_match)}))
def test_bindings(self): parsed = matcher.parse_ast('2', '<string>') matches = list( matcher.find_iter( base_matchers.MatchesRegex(r'(?P<var>.)', ast_matchers.Num()), parsed)) self.assertLen(matches, 1) [m] = matches self.assertIn('var', m.bindings) self.assertEqual(m.bindings['var'].value.span, (0, 1))
def test_autoparen_outer(self): parsed = matcher.parse_ast('x * 2') m = base_matchers.Bind('x', ast_matchers.Name()) [matchinfo] = matcher.find_iter(m, parsed) template = syntactic_template.PythonTemplate('$x') self.assertEqual( template.substitute_match(parsed, matchinfo.match, {'x': match.StringMatch('x + y')}), '(x + y)', )
def test_nonpython_dollars_dest(self): src = 'f' parsed = matcher.parse_ast(src) m = base_matchers.Bind('bound', ast_matchers.Name()) [matchinfo] = matcher.find_iter(m, parsed) self.assertEqual( 'f("$x")', syntactic_template.PythonExprTemplate( '$bound("$x")').substitute_match(parsed, matchinfo.match, {'bound': matchinfo.match}))
def test_complex_variable(self): parsed = matcher.parse_ast('foo + bar', '<string>') expr = parsed.tree.body[0].value self.assertEqual( syntax_matchers.ExprPattern('foo + $name').match( matcher.MatchContext(parsed), expr), matcher.MatchInfo(matcher.LexicalASTMatch(expr, parsed.text, expr.first_token, expr.last_token), bindings=mock.ANY))
def test_lvalue_variable(self): parsed = matcher.parse_ast('a = b', '<string>') stmt = parsed.tree.body[0] self.assertEqual( syntax_matchers.StmtPattern('$x = $y').match( matcher.MatchContext(parsed), stmt), matcher.MatchInfo(matcher.LexicalASTMatch(stmt, parsed.text, stmt.first_token, stmt.last_token), bindings=mock.ANY))
def empty_context(): """Returns a new match context for some empty file. The return value is suitable for use with matchers, e.g.: >>> from refex.python.matchers import base_matchers >>> base_matchers.Anything().match(empty_context(), object()) MatchInfo(...) """ return matcher.MatchContext(matcher.parse_ast(''))
def test_line_pragma(self): prefix = 'not_annotated()\n' suffix = 'a() # foo: bar=baz.quux' source = ''.join([prefix, suffix, '\nalso_not_annotated()']) self.assertEqual( matcher.parse_ast(source).pragmas, (parsed_file.Pragma(tag='foo', data={'bar': 'baz.quux'}, start=len(prefix), end=len(prefix) + len(suffix)), ))
def test_memoized_nodes(self): """Some nodes like the BinOp.op attribute are memoized, which we must undo. Otherwise, the parent would just be the last binop to be read, etc. """ parsed = matcher.parse_ast('[1+2, 3+4, 5+6]') binop1, binop2, binop3 = parsed.tree.body[0].value.elts self.assertIs(parsed.nav.get_parent(binop1.op), binop1) self.assertIs(parsed.nav.get_parent(binop2.op), binop2) self.assertIs(parsed.nav.get_parent(binop3.op), binop3)
def test_locally_scoped_pragma(self, first_line): prefix = 'def foo():\n' + first_line suffix = ' # foo: bar=baz.quux\n hello_world()\n\n' source = ''.join( [prefix, suffix, 'def bar():\n also_not_annotated()']) self.assertEqual( matcher.parse_ast(source).pragmas, (parsed_file.Pragma(tag='foo', data={'bar': 'baz.quux'}, start=len(prefix), end=len(prefix) + len(suffix)), ))
def test_has_nested_import(self, import_stmt): any_m = base_matchers.Anything() matchers = [ syntax_matchers.WithTopLevelImport(any_m, 'os.path'), syntax_matchers.WithTopLevelImport(any_m, 'os.path', 'path'), ] context = matcher.MatchContext(matcher.parse_ast(import_stmt)) for m in matchers: with self.subTest(m=m): self.assertIsNotNone(m.match(context, 1))
def test_assignment(self, template): template = syntactic_template.PythonStmtTemplate(template) # Test with different values of `ctx` for the variable being substituted. for variable_source in 'a = 1', 'a': with self.subTest(variable_souce=variable_source): [matchinfo] = matcher.find_iter( base_matchers.Bind('x', ast_matchers.Name()), matcher.parse_ast(variable_source)) substituted = template.substitute_match( None, None, {'x': matchinfo.match}) self.assertEqual(substituted, template.template.replace('$x', 'a'))
def test_multi_regex(self): """Tests that the lazy dictionary doesn't walk over itself or something.""" parsed = matcher.parse_ast('var_hello = 42', '<string>') m = base_matchers.AllOf( base_matchers.FileMatchesRegex('var'), base_matchers.FileMatchesRegex('hello'), base_matchers.FileMatchesRegex('42'), base_matchers.FileMatchesRegex(r'\Avar_hello = 42\Z'), ast_matchers.Num()) matches = list(matcher.find_iter(m, parsed)) self.assertLen(matches, 1)