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 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_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 test_variable_conflict(self): """Variables use the default conflict resolution outside of the pattern. Inside of the pattern, they use MERGE_EQUIVALENT_AST, but this is opaque to callers. """ # The AllOf shouldn't make a difference, because the $x variable is just # a regular Bind() variable outside of the pattern, and merges via KEEP_LAST # per normal. self.assertEqual( self.get_all_match_strings( base_matchers.AllOf(syntax_matchers.StmtPattern('$x'), base_matchers.Bind('x')), '1'), ['1'])
def test_discards_unparseable_stmt(self): """Searching discards unparseable substitutions for statements. (Note: this only happens during fixedpoint computation. """ fx = fixer.CombiningPythonFixer([ fixer.SimplePythonFixer( message='', matcher=syntax_matchers.StmtPattern('raise e'), replacement=formatting.ShTemplate('x x x'), url='') ]) self.assertEqual( list(search.find_iter(fx, 'raise e', 'foo.py', max_iterations=10)), [])
def test_identical_patterns(self): """Tests that patterns match themselves when not parameterized. Many cases (e.g. None) are interesting for 2/3 compatibility, because the AST changes in Python 3. syntax_matchers gives an easy way to get cross-version compatibility. """ for code in [ 'None', '{}', '[]', '{1:2, 3:4}', 'lambda a: a', '""', 'x=1' ]: parsed = matcher.parse_ast(code, '<string>') stmt = parsed.tree.body[0] for extra_comment in ['', "# comment doesn't matter"]: with self.subTest(code=code, extra_comment=extra_comment): self.assertEqual( syntax_matchers.StmtPattern( code + extra_comment).match( matcher.MatchContext(parsed), stmt), matcher.MatchInfo( matcher.LexicalASTMatch(stmt, parsed.text, stmt.first_token, stmt.last_token)))
def from_pattern( cls, pattern: str, templates: Optional[Dict[str, formatting.Template]] ) -> "PyStmtRewritingSearcher": """Creates a searcher from a ``--mode=py.stmt`` template.""" return cls.from_matcher(syntax_matchers.StmtPattern(pattern), templates=templates)
class NoCommentsTest(matcher_test_util.MatcherTestCase): _comments_source = '(a # comment\n + b)' _nocomments_source = '(a + b)' _including_comments_matcher = syntax_matchers.StmtPattern('a + b') _requiring_comments_matcher = lexical_matchers.HasComments( _including_comments_matcher) _banning_comments_matcher = lexical_matchers.NoComments( _including_comments_matcher) def test_outside_comment_irrelevant(self): for prefix in ['', '# earlier comment\n']: for suffix in ['', ' # trailing comment']: source_code = prefix + self._nocomments_source + suffix for m in [ self._including_comments_matcher, self._requiring_comments_matcher, self._banning_comments_matcher ]: with self.subTest(source_code=source_code, matcher=m): self.assertEqual( self.get_all_match_strings(m, source_code), self.get_all_match_strings( m, self._nocomments_source)) def test_interior_comments(self): for m in [ self._including_comments_matcher, self._requiring_comments_matcher ]: with self.subTest(matcher=m): self.assertEqual( self.get_all_match_strings(m, self._comments_source), [self._comments_source]) for m in [self._banning_comments_matcher]: with self.subTest(matcher=m): self.assertEqual( self.get_all_match_strings(m, self._comments_source), []) def test_no_interior_comments(self): for m in [self._requiring_comments_matcher]: with self.subTest(matcher=m): self.assertEqual( self.get_all_match_strings(m, self._nocomments_source), []) for m in [ self._including_comments_matcher, self._banning_comments_matcher ]: with self.subTest(matcher=m): self.assertEqual( self.get_all_match_strings(m, self._nocomments_source), [self._nocomments_source]) def test_incorrect_match_type(self): nonlexical_matcher = ast_matchers.Add() for m in [ lexical_matchers.NoComments(nonlexical_matcher), lexical_matchers.HasComments(nonlexical_matcher) ]: with self.subTest(matcher=m): with self.assertRaises(TypeError): self.get_all_match_strings(m, 'a + b')
_NON_NONE_RETURN = matcher_.DebugLabeledMatcher( 'Non-none return', ast_matchers.Return(value=base_matchers.Unless( base_matchers.AnyOf(base_matchers.Equals(None), syntax_matchers.ExprPattern('None'))))) _NONE_RETURNS_FIXERS = [ fixer.SimplePythonFixer( message= 'If a function ever returns a value, all the code paths should have a return statement with a return value.', url= 'https://refex.readthedocs.io/en/latest/guide/fixers/return_none.html', significant=False, category=_NONE_RETURNS_CATEGORY, matcher=base_matchers.AllOf( syntax_matchers.StmtPattern('return'), syntax_matchers.InNamedFunction( _function_containing(_NON_NONE_RETURN)), # Nested functions are too weird to consider right now. # TODO: Add matchers to match only the first ancestor # function and a way to use IsOrHasDescendant that doesn't recurse # into nested functions. base_matchers.Unless( syntax_matchers.InNamedFunction( _function_containing( syntax_matchers.NamedFunctionDefinition())))), replacement=syntactic_template.PythonStmtTemplate('return None'), example_fragment=textwrap.dedent(""" def f(x): if x: return
def test_repeated_variable(self): self.assertEqual( self.get_all_match_strings(syntax_matchers.StmtPattern('$x + $x'), '1 + 1\n1 + 2\na + a\na + b'), ['1 + 1', 'a + a'])
def test_dict_wrong_order(self): parsed = matcher.parse_ast('{1:2, 3:4}', '<string>') stmt = parsed.tree.body[0] self.assertIsNone( syntax_matchers.StmtPattern('{3:4, 1:2}').match( matcher.MatchContext(parsed), stmt))
def test_multiple_statements(self): with self.assertRaises(ValueError): syntax_matchers.StmtPattern('{}; {}')
def test_no_statement(self): with self.assertRaises(ValueError): syntax_matchers.StmtPattern('')
def test_syntaxerror(self): with self.assertRaises(ValueError): syntax_matchers.StmtPattern('{')
def test_no_such_variable(self): with self.assertRaises(KeyError): syntax_matchers.StmtPattern('a', {'x': base_matchers.Anything()})
def _pattern_factory(pattern): return ast_matchers.Module(body=base_matchers.ItemsAre( [syntax_matchers.StmtPattern(pattern)]))