Add pretty formatting for types and template specializations

- Fixes #3
This commit is contained in:
Dustin Spicuzza 2023-09-30 18:28:42 -04:00
parent 1f5ba1b3ca
commit a5ce9a24e6
3 changed files with 461 additions and 21 deletions

View File

@ -1,7 +1,7 @@
from dataclasses import dataclass, field
import typing import typing
from .lexer import LexToken, PlyLexer, LexerTokenStream from .lexer import LexToken, PlyLexer, LexerTokenStream
from .types import Token
# key: token type, value: (left spacing, right spacing) # key: token type, value: (left spacing, right spacing)
_want_spacing = { _want_spacing = {
@ -35,6 +35,27 @@ _want_spacing = {
_want_spacing.update(dict.fromkeys(PlyLexer.keywords, (2, 2))) _want_spacing.update(dict.fromkeys(PlyLexer.keywords, (2, 2)))
@dataclass
class Token:
"""
In an ideal world, this Token class would not be exposed via the user
visible API. Unfortunately, getting to that point would take a significant
amount of effort.
It is not expected that these will change, but they might.
At the moment, the only supported use of Token objects are in conjunction
with the ``tokfmt`` function. As this library matures, we'll try to clarify
the expectations around these. File an issue on github if you have ideas!
"""
#: Raw value of the token
value: str
#: Lex type of the token
type: str = field(repr=False, compare=False, default="")
def tokfmt(toks: typing.List[Token]) -> str: def tokfmt(toks: typing.List[Token]) -> str:
""" """
Helper function that takes a list of tokens and converts them to a string Helper function that takes a list of tokens and converts them to a string

View File

@ -1,26 +1,7 @@
import typing import typing
from dataclasses import dataclass, field from dataclasses import dataclass, field
from .tokfmt import tokfmt, Token
@dataclass
class Token:
"""
In an ideal world, this Token class would not be exposed via the user
visible API. Unfortunately, getting to that point would take a significant
amount of effort.
It is not expected that these will change, but they might.
At the moment, the only supported use of Token objects are in conjunction
with the ``tokfmt`` function. As this library matures, we'll try to clarify
the expectations around these. File an issue on github if you have ideas!
"""
#: Raw value of the token
value: str
#: Lex type of the token
type: str = field(repr=False, compare=False, default="")
@dataclass @dataclass
@ -37,6 +18,9 @@ class Value:
#: Tokens corresponding to the value #: Tokens corresponding to the value
tokens: typing.List[Token] tokens: typing.List[Token]
def format(self) -> str:
return tokfmt(self.tokens)
@dataclass @dataclass
class NamespaceAlias: class NamespaceAlias:
@ -94,6 +78,9 @@ class DecltypeSpecifier:
#: Unparsed tokens within the decltype #: Unparsed tokens within the decltype
tokens: typing.List[Token] tokens: typing.List[Token]
def format(self) -> str:
return f"decltype({tokfmt(self.tokens)})"
@dataclass @dataclass
class FundamentalSpecifier: class FundamentalSpecifier:
@ -107,6 +94,9 @@ class FundamentalSpecifier:
name: str name: str
def format(self) -> str:
return self.name
@dataclass @dataclass
class NameSpecifier: class NameSpecifier:
@ -124,6 +114,12 @@ class NameSpecifier:
specialization: typing.Optional["TemplateSpecialization"] = None specialization: typing.Optional["TemplateSpecialization"] = None
def format(self) -> str:
if self.specialization:
return f"{self.name}{self.specialization.format()}"
else:
return self.name
@dataclass @dataclass
class AutoSpecifier: class AutoSpecifier:
@ -133,6 +129,9 @@ class AutoSpecifier:
name: str = "auto" name: str = "auto"
def format(self) -> str:
return self.name
@dataclass @dataclass
class AnonymousName: class AnonymousName:
@ -145,6 +144,10 @@ class AnonymousName:
#: Unique id associated with this name (only unique per parser instance!) #: Unique id associated with this name (only unique per parser instance!)
id: int id: int
def format(self) -> str:
# TODO: not sure what makes sense here, subject to change
return f"<<id={self.id}>>"
PQNameSegment = typing.Union[ PQNameSegment = typing.Union[
AnonymousName, FundamentalSpecifier, NameSpecifier, DecltypeSpecifier, AutoSpecifier AnonymousName, FundamentalSpecifier, NameSpecifier, DecltypeSpecifier, AutoSpecifier
@ -170,6 +173,13 @@ class PQName:
#: Set to true if the type was preceded with 'typename' #: Set to true if the type was preceded with 'typename'
has_typename: bool = False has_typename: bool = False
def format(self) -> str:
tn = "typename " if self.has_typename else ""
if self.classkey:
return f"{tn}{self.classkey} {'::'.join(seg.format() for seg in self.segments)}"
else:
return tn + "::".join(seg.format() for seg in self.segments)
@dataclass @dataclass
class TemplateArgument: class TemplateArgument:
@ -189,6 +199,12 @@ class TemplateArgument:
param_pack: bool = False param_pack: bool = False
def format(self) -> str:
if self.param_pack:
return f"{self.arg.format()}..."
else:
return self.arg.format()
@dataclass @dataclass
class TemplateSpecialization: class TemplateSpecialization:
@ -204,6 +220,9 @@ class TemplateSpecialization:
args: typing.List[TemplateArgument] args: typing.List[TemplateArgument]
def format(self) -> str:
return f"<{', '.join(arg.format() for arg in self.args)}>"
@dataclass @dataclass
class FunctionType: class FunctionType:
@ -238,6 +257,23 @@ class FunctionType:
#: calling convention #: calling convention
msvc_convention: typing.Optional[str] = None msvc_convention: typing.Optional[str] = None
def format(self) -> str:
vararg = "..." if self.vararg else ""
params = ", ".join(p.format() for p in self.parameters)
if self.has_trailing_return:
return f"auto ({params}{vararg}) -> {self.return_type.format()}"
else:
return f"{self.return_type.format()} ({params}{vararg})"
def format_decl(self, name: str) -> str:
"""Format as a named declaration"""
vararg = "..." if self.vararg else ""
params = ", ".join(p.format() for p in self.parameters)
if self.has_trailing_return:
return f"auto {name}({params}{vararg}) -> {self.return_type.format()}"
else:
return f"{self.return_type.format()} {name}({params}{vararg})"
@dataclass @dataclass
class Type: class Type:
@ -250,6 +286,17 @@ class Type:
const: bool = False const: bool = False
volatile: bool = False volatile: bool = False
def format(self) -> str:
c = "const " if self.const else ""
v = "volatile " if self.volatile else ""
return f"{c}{v}{self.typename.format()}"
def format_decl(self, name: str):
"""Format as a named declaration"""
c = "const " if self.const else ""
v = "volatile " if self.volatile else ""
return f"{c}{v}{self.typename.format()} {name}"
@dataclass @dataclass
class Array: class Array:
@ -269,6 +316,14 @@ class Array:
#: ~~ #: ~~
size: typing.Optional[Value] size: typing.Optional[Value]
def format(self) -> str:
s = self.size.format() if self.size else ""
return f"{self.array_of.format()}[{s}]"
def format_decl(self, name: str) -> str:
s = self.size.format() if self.size else ""
return f"{self.array_of.format()} {name}[{s}]"
@dataclass @dataclass
class Pointer: class Pointer:
@ -282,6 +337,25 @@ class Pointer:
const: bool = False const: bool = False
volatile: bool = False volatile: bool = False
def format(self) -> str:
c = " const" if self.const else ""
v = " volatile" if self.volatile else ""
ptr_to = self.ptr_to
if isinstance(ptr_to, (Array, FunctionType)):
return ptr_to.format_decl(f"(*{c}{v})")
else:
return f"{ptr_to.format()}*{c}{v}"
def format_decl(self, name: str):
"""Format as a named declaration"""
c = " const" if self.const else ""
v = " volatile" if self.volatile else ""
ptr_to = self.ptr_to
if isinstance(ptr_to, (Array, FunctionType)):
return ptr_to.format_decl(f"(*{c}{v} {name})")
else:
return f"{ptr_to.format()}*{c}{v} {name}"
@dataclass @dataclass
class Reference: class Reference:
@ -291,6 +365,22 @@ class Reference:
ref_to: typing.Union[Array, FunctionType, Pointer, Type] ref_to: typing.Union[Array, FunctionType, Pointer, Type]
def format(self) -> str:
ref_to = self.ref_to
if isinstance(ref_to, Array):
return ref_to.format_decl("(&)")
else:
return f"{ref_to.format()}&"
def format_decl(self, name: str):
"""Format as a named declaration"""
ref_to = self.ref_to
if isinstance(ref_to, Array):
return ref_to.format_decl(f"(& {name})")
else:
return f"{ref_to.format()}& {name}"
@dataclass @dataclass
class MoveReference: class MoveReference:
@ -300,6 +390,13 @@ class MoveReference:
moveref_to: typing.Union[Array, FunctionType, Pointer, Type] moveref_to: typing.Union[Array, FunctionType, Pointer, Type]
def format(self) -> str:
return f"{self.moveref_to.format()}&&"
def format_decl(self, name: str):
"""Format as a named declaration"""
return f"{self.moveref_to.format()}&& {name}"
#: A type or function type that is decorated with various things #: A type or function type that is decorated with various things
#: #:
@ -505,6 +602,15 @@ class Parameter:
default: typing.Optional[Value] = None default: typing.Optional[Value] = None
param_pack: bool = False param_pack: bool = False
def format(self) -> str:
default = f" = {self.default.format()}" if self.default else ""
pp = "... " if self.param_pack else ""
name = self.name
if name:
return f"{self.type.format_decl(f'{pp}{name}')}{default}"
else:
return f"{self.type.format()}{pp}{default}"
@dataclass @dataclass
class Function: class Function:

313
tests/test_typefmt.py Normal file
View File

@ -0,0 +1,313 @@
import typing
import pytest
from cxxheaderparser.tokfmt import Token
from cxxheaderparser.types import (
Array,
DecoratedType,
FunctionType,
FundamentalSpecifier,
Method,
MoveReference,
NameSpecifier,
PQName,
Parameter,
Pointer,
Reference,
TemplateArgument,
TemplateSpecialization,
TemplateDecl,
Type,
Value,
)
@pytest.mark.parametrize(
"pytype,typestr,declstr",
[
(
Type(typename=PQName(segments=[FundamentalSpecifier(name="int")])),
"int",
"int name",
),
(
Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")]), const=True
),
"const int",
"const int name",
),
(
Type(
typename=PQName(segments=[NameSpecifier(name="S")], classkey="struct")
),
"struct S",
"struct S name",
),
(
Pointer(
ptr_to=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
)
),
"int*",
"int* name",
),
(
Pointer(
ptr_to=Pointer(
ptr_to=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
)
)
),
"int**",
"int** name",
),
(
Reference(
ref_to=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
)
),
"int&",
"int& name",
),
(
Reference(
ref_to=Array(
array_of=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
),
size=Value(tokens=[Token(value="3")]),
)
),
"int (&)[3]",
"int (& name)[3]",
),
(
MoveReference(
moveref_to=Type(
typename=PQName(
segments=[NameSpecifier(name="T"), NameSpecifier(name="T")]
)
)
),
"T::T&&",
"T::T&& name",
),
(
Pointer(
ptr_to=Array(
array_of=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
),
size=Value(tokens=[Token(value="3")]),
)
),
"int (*)[3]",
"int (* name)[3]",
),
(
Pointer(
ptr_to=Array(
array_of=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
),
size=Value(tokens=[Token(value="3")]),
),
const=True,
),
"int (* const)[3]",
"int (* const name)[3]",
),
(
FunctionType(
return_type=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
),
parameters=[
Parameter(
type=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
)
)
],
),
"int (int)",
"int name(int)",
),
(
FunctionType(
return_type=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
),
parameters=[
Parameter(
type=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
)
)
],
has_trailing_return=True,
),
"auto (int) -> int",
"auto name(int) -> int",
),
(
FunctionType(
return_type=Type(
typename=PQName(
segments=[FundamentalSpecifier(name="void")],
),
),
parameters=[
Parameter(
type=Type(
typename=PQName(
segments=[FundamentalSpecifier(name="int")],
),
),
name="a",
),
Parameter(
type=Type(
typename=PQName(
segments=[FundamentalSpecifier(name="int")],
),
),
name="b",
),
],
),
"void (int a, int b)",
"void name(int a, int b)",
),
(
Pointer(
ptr_to=FunctionType(
return_type=Type(
typename=PQName(segments=[FundamentalSpecifier(name="int")])
),
parameters=[
Parameter(
type=Type(
typename=PQName(
segments=[FundamentalSpecifier(name="int")]
)
)
)
],
)
),
"int (*)(int)",
"int (* name)(int)",
),
(
Type(
typename=PQName(
segments=[
NameSpecifier(name="std"),
NameSpecifier(
name="function",
specialization=TemplateSpecialization(
args=[
TemplateArgument(
arg=FunctionType(
return_type=Type(
typename=PQName(
segments=[
FundamentalSpecifier(name="int")
]
)
),
parameters=[
Parameter(
type=Type(
typename=PQName(
segments=[
FundamentalSpecifier(
name="int"
)
]
)
)
)
],
)
)
]
),
),
]
)
),
"std::function<int (int)>",
"std::function<int (int)> name",
),
(
Type(
typename=PQName(
segments=[
NameSpecifier(
name="foo",
specialization=TemplateSpecialization(
args=[
TemplateArgument(
arg=Type(
typename=PQName(
segments=[
NameSpecifier(name=""),
NameSpecifier(name="T"),
],
)
),
)
]
),
)
]
),
),
"foo<::T>",
"foo<::T> name",
),
(
Type(
typename=PQName(
segments=[
NameSpecifier(
name="foo",
specialization=TemplateSpecialization(
args=[
TemplateArgument(
arg=Type(
typename=PQName(
segments=[
NameSpecifier(name=""),
NameSpecifier(name="T"),
],
has_typename=True,
)
),
)
]
),
)
]
),
),
"foo<typename ::T>",
"foo<typename ::T> name",
),
],
)
def test_typefmt(
pytype: typing.Union[DecoratedType, FunctionType], typestr: str, declstr: str
):
# basic formatting
assert pytype.format() == typestr
# as a type declaration
assert pytype.format_decl("name") == declstr