Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 92 additions & 40 deletions cxxheaderparser/gentest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
import dataclasses
import inspect
import subprocess
import textwrap
import typing

from .errors import CxxParseError
from .preprocessor import make_pcpp_preprocessor
from .options import ParserOptions
from .simple import parse_string, ParsedData
from .simple import parse_string, parse_typename, ParsedData


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


def gentest(
infile: str, name: str, outfile: str, verbose: bool, fail: bool, pcpp: bool
infile: str,
name: str,
outfile: str,
verbose: bool,
fail: bool,
pcpp: bool,
typename_mode: bool,
) -> None:
# Goal is to allow making a unit test as easy as running this dumper
# on a file and copy/pasting this into a test
Expand All @@ -67,46 +74,76 @@ def gentest(
maybe_options = "options = ParserOptions(preprocessor=make_pcpp_preprocessor())"
popt = ", options=options"

try:
data = parse_string(content, options=options)
if fail:
raise ValueError("did not fail")
except CxxParseError:
if not fail:
raise
# do it again, but strip the content so the error message matches
if typename_mode:
try:
parse_string(content.strip(), options=options)
except CxxParseError as e2:
err = str(e2)

if not fail:
stmt = nondefault_repr(data)
stmt = f"""
{maybe_options}
data = parse_string(content, cleandoc=True{popt})

assert data == {stmt}
"""
dtype = parse_typename(content.strip(), options=options)
if fail:
raise ValueError("did not fail")
except CxxParseError:
if not fail:
raise
try:
parse_typename(content.strip(), options=options)
except CxxParseError as e2:
err = str(e2)

if not fail:
stmt = nondefault_repr(dtype)
stmt = f"""
{maybe_options}
dtype = parse_typename(content.strip(){popt})

assert dtype == {stmt}
"""
else:
stmt = f"""
{maybe_options}
err = {repr(err)}
with pytest.raises(CxxParseError, match=re.escape(err)):
parse_typename(content.strip(){popt})
"""
else:
stmt = f"""
{maybe_options}
err = {repr(err)}
with pytest.raises(CxxParseError, match=re.escape(err)):
parse_string(content, cleandoc=True{popt})
"""

content = ("\n" + content.strip()).replace("\n", "\n ")
content = "\n".join(l.rstrip() for l in content.splitlines())
try:
data = parse_string(content, options=options)
if fail:
raise ValueError("did not fail")
except CxxParseError:
if not fail:
raise
# do it again, but strip the content so the error message matches
try:
parse_string(content.strip(), options=options)
except CxxParseError as e2:
err = str(e2)

if not fail:
stmt = nondefault_repr(data)
stmt = f"""
{maybe_options}
data = parse_string(content, cleandoc=True{popt})

stmt = inspect.cleandoc(
f'''
def test_{name}() -> None:
content = """{content}
assert data == {stmt}
"""
{stmt.strip()}
'''
)
else:
stmt = f"""
{maybe_options}
err = {repr(err)}
with pytest.raises(CxxParseError, match=re.escape(err)):
parse_string(content, cleandoc=True{popt})
"""

stmt = textwrap.dedent(stmt).strip()
stmt = textwrap.indent(stmt, " " * 4)
content = inspect.cleandoc(content)
content = textwrap.indent("\n" + content, " " * 8)
content = "\n".join(l.rstrip() for l in content.splitlines())

stmt = f"""def test_{name}() -> None:
content = \"\"\"{content}
\"\"\"

{stmt}
"""

# format it with black
stmt = subprocess.check_output(
Expand All @@ -125,11 +162,26 @@ def test_{name}() -> None:
parser.add_argument("header")
parser.add_argument("name", nargs="?", default="TODO")
parser.add_argument("--pcpp", default=False, action="store_true")
parser.add_argument(
"--typename",
dest="typename_mode",
default=False,
action="store_true",
help="Generate tests for parse_typename",
)
parser.add_argument("-v", "--verbose", default=False, action="store_true")
parser.add_argument("-o", "--output", default="-")
parser.add_argument(
"-x", "--fail", default=False, action="store_true", help="Expect failure"
)
args = parser.parse_args()

gentest(args.header, args.name, args.output, args.verbose, args.fail, args.pcpp)
gentest(
args.header,
args.name,
args.output,
args.verbose,
args.fail,
args.pcpp,
args.typename_mode,
)
26 changes: 26 additions & 0 deletions cxxheaderparser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,32 @@ def _parse_trailing_return_type(

return dtype

def parse_typename(self) -> DecoratedType:
"""
Parse a single C++ type name from the current token stream.
"""
parsed_type, mods = self._parse_type(None)
if parsed_type is None:
raise CxxParseError("missing type name")

mods.validate(var_ok=False, meth_ok=False, msg="parsing type name")

dtype = self._parse_cv_ptr_or_fn(parsed_type)
if isinstance(dtype, FunctionType):
raise CxxParseError("function types are not supported")

tok = self.lex.token_if("[")
while tok:
dtype = self._parse_array_type(tok, dtype)
tok = self.lex.token_if("[")

self.lex.token_if(";")
extra = self.lex.token_eof_ok()
if extra is not None:
raise self._parse_error(extra)

return dtype

def _parse_fn_end(self, fn: Function) -> None:
"""
Consumes the various keywords after the parameters in a function
Expand Down
15 changes: 15 additions & 0 deletions cxxheaderparser/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
ClassDecl,
Concept,
DeductionGuide,
DecoratedType,
EnumDecl,
Field,
ForwardDecl,
Expand All @@ -58,6 +59,7 @@
)
from .parser import CxxParser
from .options import ParserOptions
from .visitor import null_visitor

#
# Data structure
Expand Down Expand Up @@ -347,6 +349,19 @@ def parse_string(
return visitor.data


def parse_typename(
typename: str,
*,
filename: str = "<str>",
options: typing.Optional[ParserOptions] = None,
) -> DecoratedType:
"""
Parse a C++ type name and return a DecoratedType.
"""
parser = CxxParser(filename, f"{typename};", null_visitor, options)
return parser.parse_typename()


def parse_file(
filename: typing.Union[str, os.PathLike],
encoding: typing.Optional[str] = None,
Expand Down
112 changes: 112 additions & 0 deletions tests/test_parse_typename.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import re

import pytest

from cxxheaderparser.errors import CxxParseError
from cxxheaderparser.simple import parse_typename
from cxxheaderparser.types import (
Array,
FundamentalSpecifier,
FunctionType,
NameSpecifier,
Parameter,
Pointer,
PQName,
Reference,
TemplateArgument,
TemplateSpecialization,
Token,
Type,
Value,
)


def test_parse_typename_basic() -> None:
content = """
const int
"""

dtype = parse_typename(content.strip())

assert dtype == Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")]), const=True
)


def test_parse_typename_template_ref() -> None:
content = """
const std::vector<int>&
"""

dtype = parse_typename(content.strip())

assert dtype == Reference(
ref_to=Type(
typename=PQName(
segments=[
NameSpecifier(name="std"),
NameSpecifier(
name="vector",
specialization=TemplateSpecialization(
args=[
TemplateArgument(
arg=Type(
typename=PQName(
segments=[FundamentalSpecifier(name="int")]
)
)
)
]
),
),
]
),
const=True,
)
)


def test_parse_typename_function_pointer() -> None:
content = """
int (*)(int)
"""

dtype = parse_typename(content.strip())

assert dtype == Pointer(
ptr_to=FunctionType(
return_type=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
),
parameters=[
Parameter(
type=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
)
)
],
)
)


def test_parse_typename_array() -> None:
content = """
int[3]
"""

dtype = parse_typename(content.strip())

assert dtype == Array(
array_of=Type(typename=PQName(segments=[FundamentalSpecifier(name="int")])),
size=Value(tokens=[Token(value="3")]),
)


def test_parse_typename_rejects_modifiers() -> None:
content = """
static int
"""

err = "parsing type name: unexpected 'static'"
with pytest.raises(CxxParseError, match=re.escape(err)):
parse_typename(content.strip())