Skip to content

Commit e1a62b6

Browse files
authored
Merge pull request #135 from robotpy/parse-typename
Add parse_typename function to simple API
2 parents 4b1daa0 + 88f831d commit e1a62b6

4 files changed

Lines changed: 245 additions & 40 deletions

File tree

cxxheaderparser/gentest.py

Lines changed: 92 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
import dataclasses
33
import inspect
44
import subprocess
5+
import textwrap
56
import typing
67

78
from .errors import CxxParseError
89
from .preprocessor import make_pcpp_preprocessor
910
from .options import ParserOptions
10-
from .simple import parse_string, ParsedData
11+
from .simple import parse_string, parse_typename, ParsedData
1112

1213

13-
def nondefault_repr(data: ParsedData) -> str:
14+
def nondefault_repr(data: typing.Any) -> str:
1415
"""
1516
Similar to the default dataclass repr, but exclude any
1617
default parameters or parameters with compare=False
@@ -50,7 +51,13 @@ def _inner_repr(o: typing.Any) -> str:
5051

5152

5253
def gentest(
53-
infile: str, name: str, outfile: str, verbose: bool, fail: bool, pcpp: bool
54+
infile: str,
55+
name: str,
56+
outfile: str,
57+
verbose: bool,
58+
fail: bool,
59+
pcpp: bool,
60+
typename_mode: bool,
5461
) -> None:
5562
# Goal is to allow making a unit test as easy as running this dumper
5663
# on a file and copy/pasting this into a test
@@ -67,46 +74,76 @@ def gentest(
6774
maybe_options = "options = ParserOptions(preprocessor=make_pcpp_preprocessor())"
6875
popt = ", options=options"
6976

70-
try:
71-
data = parse_string(content, options=options)
72-
if fail:
73-
raise ValueError("did not fail")
74-
except CxxParseError:
75-
if not fail:
76-
raise
77-
# do it again, but strip the content so the error message matches
77+
if typename_mode:
7878
try:
79-
parse_string(content.strip(), options=options)
80-
except CxxParseError as e2:
81-
err = str(e2)
82-
83-
if not fail:
84-
stmt = nondefault_repr(data)
85-
stmt = f"""
86-
{maybe_options}
87-
data = parse_string(content, cleandoc=True{popt})
88-
89-
assert data == {stmt}
90-
"""
79+
dtype = parse_typename(content.strip(), options=options)
80+
if fail:
81+
raise ValueError("did not fail")
82+
except CxxParseError:
83+
if not fail:
84+
raise
85+
try:
86+
parse_typename(content.strip(), options=options)
87+
except CxxParseError as e2:
88+
err = str(e2)
89+
90+
if not fail:
91+
stmt = nondefault_repr(dtype)
92+
stmt = f"""
93+
{maybe_options}
94+
dtype = parse_typename(content.strip(){popt})
95+
96+
assert dtype == {stmt}
97+
"""
98+
else:
99+
stmt = f"""
100+
{maybe_options}
101+
err = {repr(err)}
102+
with pytest.raises(CxxParseError, match=re.escape(err)):
103+
parse_typename(content.strip(){popt})
104+
"""
91105
else:
92-
stmt = f"""
93-
{maybe_options}
94-
err = {repr(err)}
95-
with pytest.raises(CxxParseError, match=re.escape(err)):
96-
parse_string(content, cleandoc=True{popt})
97-
"""
98-
99-
content = ("\n" + content.strip()).replace("\n", "\n ")
100-
content = "\n".join(l.rstrip() for l in content.splitlines())
106+
try:
107+
data = parse_string(content, options=options)
108+
if fail:
109+
raise ValueError("did not fail")
110+
except CxxParseError:
111+
if not fail:
112+
raise
113+
# do it again, but strip the content so the error message matches
114+
try:
115+
parse_string(content.strip(), options=options)
116+
except CxxParseError as e2:
117+
err = str(e2)
118+
119+
if not fail:
120+
stmt = nondefault_repr(data)
121+
stmt = f"""
122+
{maybe_options}
123+
data = parse_string(content, cleandoc=True{popt})
101124
102-
stmt = inspect.cleandoc(
103-
f'''
104-
def test_{name}() -> None:
105-
content = """{content}
125+
assert data == {stmt}
106126
"""
107-
{stmt.strip()}
108-
'''
109-
)
127+
else:
128+
stmt = f"""
129+
{maybe_options}
130+
err = {repr(err)}
131+
with pytest.raises(CxxParseError, match=re.escape(err)):
132+
parse_string(content, cleandoc=True{popt})
133+
"""
134+
135+
stmt = textwrap.dedent(stmt).strip()
136+
stmt = textwrap.indent(stmt, " " * 4)
137+
content = inspect.cleandoc(content)
138+
content = textwrap.indent("\n" + content, " " * 8)
139+
content = "\n".join(l.rstrip() for l in content.splitlines())
140+
141+
stmt = f"""def test_{name}() -> None:
142+
content = \"\"\"{content}
143+
\"\"\"
144+
145+
{stmt}
146+
"""
110147

111148
# format it with black
112149
stmt = subprocess.check_output(
@@ -125,11 +162,26 @@ def test_{name}() -> None:
125162
parser.add_argument("header")
126163
parser.add_argument("name", nargs="?", default="TODO")
127164
parser.add_argument("--pcpp", default=False, action="store_true")
165+
parser.add_argument(
166+
"--typename",
167+
dest="typename_mode",
168+
default=False,
169+
action="store_true",
170+
help="Generate tests for parse_typename",
171+
)
128172
parser.add_argument("-v", "--verbose", default=False, action="store_true")
129173
parser.add_argument("-o", "--output", default="-")
130174
parser.add_argument(
131175
"-x", "--fail", default=False, action="store_true", help="Expect failure"
132176
)
133177
args = parser.parse_args()
134178

135-
gentest(args.header, args.name, args.output, args.verbose, args.fail, args.pcpp)
179+
gentest(
180+
args.header,
181+
args.name,
182+
args.output,
183+
args.verbose,
184+
args.fail,
185+
args.pcpp,
186+
args.typename_mode,
187+
)

cxxheaderparser/parser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1945,6 +1945,32 @@ def _parse_trailing_return_type(
19451945

19461946
return dtype
19471947

1948+
def parse_typename(self) -> DecoratedType:
1949+
"""
1950+
Parse a single C++ type name from the current token stream.
1951+
"""
1952+
parsed_type, mods = self._parse_type(None)
1953+
if parsed_type is None:
1954+
raise CxxParseError("missing type name")
1955+
1956+
mods.validate(var_ok=False, meth_ok=False, msg="parsing type name")
1957+
1958+
dtype = self._parse_cv_ptr_or_fn(parsed_type)
1959+
if isinstance(dtype, FunctionType):
1960+
raise CxxParseError("function types are not supported")
1961+
1962+
tok = self.lex.token_if("[")
1963+
while tok:
1964+
dtype = self._parse_array_type(tok, dtype)
1965+
tok = self.lex.token_if("[")
1966+
1967+
self.lex.token_if(";")
1968+
extra = self.lex.token_eof_ok()
1969+
if extra is not None:
1970+
raise self._parse_error(extra)
1971+
1972+
return dtype
1973+
19481974
def _parse_fn_end(self, fn: Function) -> None:
19491975
"""
19501976
Consumes the various keywords after the parameters in a function

cxxheaderparser/simple.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
ClassDecl,
3737
Concept,
3838
DeductionGuide,
39+
DecoratedType,
3940
EnumDecl,
4041
Field,
4142
ForwardDecl,
@@ -58,6 +59,7 @@
5859
)
5960
from .parser import CxxParser
6061
from .options import ParserOptions
62+
from .visitor import null_visitor
6163

6264
#
6365
# Data structure
@@ -347,6 +349,19 @@ def parse_string(
347349
return visitor.data
348350

349351

352+
def parse_typename(
353+
typename: str,
354+
*,
355+
filename: str = "<str>",
356+
options: typing.Optional[ParserOptions] = None,
357+
) -> DecoratedType:
358+
"""
359+
Parse a C++ type name and return a DecoratedType.
360+
"""
361+
parser = CxxParser(filename, f"{typename};", null_visitor, options)
362+
return parser.parse_typename()
363+
364+
350365
def parse_file(
351366
filename: typing.Union[str, os.PathLike],
352367
encoding: typing.Optional[str] = None,

tests/test_parse_typename.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import re
2+
3+
import pytest
4+
5+
from cxxheaderparser.errors import CxxParseError
6+
from cxxheaderparser.simple import parse_typename
7+
from cxxheaderparser.types import (
8+
Array,
9+
FundamentalSpecifier,
10+
FunctionType,
11+
NameSpecifier,
12+
Parameter,
13+
Pointer,
14+
PQName,
15+
Reference,
16+
TemplateArgument,
17+
TemplateSpecialization,
18+
Token,
19+
Type,
20+
Value,
21+
)
22+
23+
24+
def test_parse_typename_basic() -> None:
25+
content = """
26+
const int
27+
"""
28+
29+
dtype = parse_typename(content.strip())
30+
31+
assert dtype == Type(
32+
typename=PQName(segments=[FundamentalSpecifier(name="int")]), const=True
33+
)
34+
35+
36+
def test_parse_typename_template_ref() -> None:
37+
content = """
38+
const std::vector<int>&
39+
"""
40+
41+
dtype = parse_typename(content.strip())
42+
43+
assert dtype == Reference(
44+
ref_to=Type(
45+
typename=PQName(
46+
segments=[
47+
NameSpecifier(name="std"),
48+
NameSpecifier(
49+
name="vector",
50+
specialization=TemplateSpecialization(
51+
args=[
52+
TemplateArgument(
53+
arg=Type(
54+
typename=PQName(
55+
segments=[FundamentalSpecifier(name="int")]
56+
)
57+
)
58+
)
59+
]
60+
),
61+
),
62+
]
63+
),
64+
const=True,
65+
)
66+
)
67+
68+
69+
def test_parse_typename_function_pointer() -> None:
70+
content = """
71+
int (*)(int)
72+
"""
73+
74+
dtype = parse_typename(content.strip())
75+
76+
assert dtype == Pointer(
77+
ptr_to=FunctionType(
78+
return_type=Type(
79+
typename=PQName(segments=[FundamentalSpecifier(name="int")])
80+
),
81+
parameters=[
82+
Parameter(
83+
type=Type(
84+
typename=PQName(segments=[FundamentalSpecifier(name="int")])
85+
)
86+
)
87+
],
88+
)
89+
)
90+
91+
92+
def test_parse_typename_array() -> None:
93+
content = """
94+
int[3]
95+
"""
96+
97+
dtype = parse_typename(content.strip())
98+
99+
assert dtype == Array(
100+
array_of=Type(typename=PQName(segments=[FundamentalSpecifier(name="int")])),
101+
size=Value(tokens=[Token(value="3")]),
102+
)
103+
104+
105+
def test_parse_typename_rejects_modifiers() -> None:
106+
content = """
107+
static int
108+
"""
109+
110+
err = "parsing type name: unexpected 'static'"
111+
with pytest.raises(CxxParseError, match=re.escape(err)):
112+
parse_typename(content.strip())

0 commit comments

Comments
 (0)