Skip to content

Commit 5bbce5e

Browse files
ssbrcopybara-github
authored andcommitted
Support for anonymous wildcards in pattern syntax: $_.
Once we have support for the concept, we can then support wildcards in places where we aren't yet comfortable implementing full support for named wildcards. In particular, repeated fields are easier to implement if they are unconstrained and unbound. PiperOrigin-RevId: 568282402
1 parent 9cc48a7 commit 5bbce5e

File tree

2 files changed

+46
-24
lines changed

2 files changed

+46
-24
lines changed

refex/python/matchers/syntax_matchers.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,18 @@
125125
from refex.python.matchers import base_matchers
126126

127127

128-
def _remap_macro_variables(pattern):
128+
def _remap_macro_variables(pattern: str) -> tuple[str, dict[str, str], set[str]]:
129129
"""Renames the variables from the source pattern to give valid Python.
130130
131131
Args:
132132
pattern: A source pattern containing metavariables like "$foo".
133133
134134
Returns:
135-
(remapped_source, variables)
135+
(remapped_source, variables, anonymous_variables)
136136
* remapped_source is the pattern, but with all dollar-prefixed variables
137137
replaced with unique non-dollar-prefixed versions.
138138
* variables is the mapping of the original name to the remapped name.
139+
* anonymous_variables is a set of remapped names that came from `_`.
139140
140141
Raises:
141142
SyntaxError: The pattern can't be parsed.
@@ -147,6 +148,7 @@ def _remap_macro_variables(pattern):
147148
if i not in metavar_indices
148149
}
149150
original_to_unique = {}
151+
anonymous_unique = set()
150152

151153
for metavar_index in metavar_indices:
152154
metavar_token = list(remapped_tokens[metavar_index])
@@ -168,12 +170,19 @@ def _remap_macro_variables(pattern):
168170
remapped_name = 'gensym%s_%s' % (suffix, variable)
169171
if remapped_name not in taken_tokens:
170172
taken_tokens.add(remapped_name)
171-
original_to_unique[variable] = remapped_name
173+
if variable == '_':
174+
anonymous_unique.add(remapped_name)
175+
else:
176+
original_to_unique[variable] = remapped_name
172177
break
173178
metavar_token[1] = remapped_name
174179
remapped_tokens[metavar_index] = tuple(metavar_token)
175180

176-
return tokenize.untokenize(remapped_tokens), original_to_unique
181+
return (
182+
tokenize.untokenize(remapped_tokens),
183+
original_to_unique,
184+
anonymous_unique,
185+
)
177186

178187

179188
def _rewrite_submatchers(pattern, restrictions):
@@ -190,20 +199,22 @@ def _rewrite_submatchers(pattern, restrictions):
190199
valid Python syntax.
191200
* variables is the mapping of the original name to the remapped name.
192201
* new_submatchers is a dict from remapped names to submatchers. Every
193-
variable is put in a Bind() node, which has a submatcher taken from
194-
`restrictions`.
202+
non-anonymous variable is put in a Bind() node, which has a submatcher
203+
taken from `restrictions`.
195204
196205
Raises:
197206
KeyError: if restrictions has a key that isn't a variable name.
198207
"""
199-
pattern, variables = _remap_macro_variables(pattern)
208+
pattern, variables, anonymous_remapped = _remap_macro_variables(pattern)
200209
incorrect_variables = set(restrictions) - set(variables)
201210
if incorrect_variables:
202211
raise KeyError('Some variables specified in restrictions were missing. '
203212
'Did you misplace a "$"? Missing variables: %r' %
204213
incorrect_variables)
205214

206-
submatchers = {}
215+
submatchers = {
216+
new_name: base_matchers.Anything() for new_name in anonymous_remapped
217+
}
207218
for old_name, new_name in variables.items():
208219
submatchers[new_name] = base_matchers.Bind(
209220
old_name,

refex/python/matchers/test_syntax_matchers.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@
1515
# python3 python2
1616
"""Tests for refex.python.matchers.syntax_matchers."""
1717

18-
from __future__ import absolute_import
19-
from __future__ import division
20-
from __future__ import print_function
21-
2218
import textwrap
2319
import unittest
2420
from unittest import mock
@@ -59,21 +55,21 @@ def test_error_spacing_line(self):
5955

6056
def test_identity(self):
6157
self.assertEqual(
62-
syntax_matchers._remap_macro_variables('a + b'), ('a + b', {}))
58+
syntax_matchers._remap_macro_variables('a + b'), ('a + b', {}, set())
59+
)
6360

6461
def test_remap(self):
6562
self.assertEqual(
66-
syntax_matchers._remap_macro_variables('a + $b'), ('a + gensym_b', {
67-
'b': 'gensym_b'
68-
}))
63+
syntax_matchers._remap_macro_variables('a + $b'),
64+
('a + gensym_b', {'b': 'gensym_b'}, set()),
65+
)
6966

7067
def test_remap_twice(self):
7168
# But why would you _do_ this?
7269
self.assertEqual(
7370
syntax_matchers._remap_macro_variables('gensym_b + $b'),
74-
('gensym_b + gensym0_b', {
75-
'b': 'gensym0_b'
76-
}))
71+
('gensym_b + gensym0_b', {'b': 'gensym0_b'}, set()),
72+
)
7773

7874
def test_remap_doesnt_eat_tokens(self):
7975
"""Expanding the size of a variable mustn't eat into neighboring tokens."""
@@ -83,15 +79,16 @@ def test_remap_doesnt_eat_tokens(self):
8379
# columns to regenerate where things should go:
8480
# 1) eating whitespace: 'gensym_ain b'
8581
# 2) leavint the $ empty and causing a pahton indent: ' gensym_a in b'
86-
('gensym_a in b', {
87-
'a': 'gensym_a'
88-
}))
82+
('gensym_a in b', {'a': 'gensym_a'}, set()),
83+
)
8984

9085
def test_remap_is_noninvasive(self):
9186
"""Remapping is lexical and doesn't invade comments or strings."""
9287
for s in ('# $cash', '"$money"'):
9388
with self.subTest(s=s):
94-
self.assertEqual(syntax_matchers._remap_macro_variables(s), (s, {}))
89+
self.assertEqual(
90+
syntax_matchers._remap_macro_variables(s), (s, {}, set())
91+
)
9592

9693

9794
class ExprPatternTest(matcher_test_util.MatcherTestCase):
@@ -153,7 +150,21 @@ def test_nonvariable_name_fails(self):
153150
expr = parsed.tree.body[0].value
154151
self.assertIsNone(
155152
syntax_matchers.ExprPattern('name').match(
156-
matcher.MatchContext(parsed), expr))
153+
matcher.MatchContext(parsed), expr
154+
)
155+
)
156+
157+
def test_anonymous_wildcard(self):
158+
parsed = matcher.parse_ast('3', '<string>')
159+
expr = parsed.tree.body[0].value
160+
m = syntax_matchers.ExprPattern('$_').match(
161+
matcher.MatchContext(parsed), expr
162+
)
163+
self.assertIsNotNone(m)
164+
self.assertEqual(
165+
m.bindings,
166+
{},
167+
)
157168

158169
def test_variable_name(self):
159170
parsed = matcher.parse_ast('3', '<string>')

0 commit comments

Comments
 (0)