Skip to content
Open
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
18 changes: 16 additions & 2 deletions misc/codegen/generators/qlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ def _humanize(s: str) -> str:


_format_re = re.compile(r"\{(\w+)\}")
# Regular expression to find manual `toStringImpl`s
_to_string_impl_re = re.compile(r"\bstring\s+toStringImpl\s*\(")


def _get_doc(cls: schema.Class, prop: schema.Property, plural=None):
Expand Down Expand Up @@ -117,6 +119,7 @@ def _get_doc(cls: schema.Class, prop: schema.Property, plural=None):
@dataclasses.dataclass
class Resolver:
lookup: typing.Dict[str, schema.ClassBase]
custom_to_string_impls: typing.Set[str] = dataclasses.field(default_factory=set)
_property_cache: typing.Dict[tuple[int, int], ql.Property] = dataclasses.field(
default_factory=dict, init=False
)
Expand Down Expand Up @@ -237,6 +240,10 @@ def get_ql_class(self, cls: schema.Class) -> ql.Class:
hideable="ql_hideable" in cls.pragmas,
internal="ql_internal" in cls.pragmas,
cfg=cls.cfg,
to_string_impl_from_primary_class=bool(
cls.pragmas.get("ql_to_string_impl_from_primary_class")
)
and cls.name not in self.custom_to_string_impls,
)
return self._class_cache[cache_key]

Expand Down Expand Up @@ -496,7 +503,15 @@ def generate(opts, renderer):

data = schemaloader.load_file(input)

resolver = Resolver(data.classes)
# Schema class names whose wrapper define a manual `toStringImpl`
custom_to_string_impls = {
cls.name
for cls in data.classes.values()
if not cls.imported
and (stub_out / _get_path_impl(cls)).is_file()
and _to_string_impl_re.search((stub_out / _get_path_impl(cls)).read_text())
}
resolver = Resolver(data.classes, custom_to_string_impls)

classes = {
name: resolver.get_ql_class(cls)
Expand All @@ -522,7 +537,6 @@ def generate(opts, renderer):
with renderer.manage(
generated=generated, stubs=stubs, registry=registry, force=opts.force
) as renderer:

db_classes = [
cls for name, cls in classes.items() if not data.classes[name].synth
]
Expand Down
1 change: 1 addition & 0 deletions misc/codegen/lib/ql.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class Class:
doc: List[str] = field(default_factory=list)
hideable: bool = False
cfg: bool = False
to_string_impl_from_primary_class: bool = False

def __post_init__(self):
def get_bases(bases):
Expand Down
6 changes: 6 additions & 0 deletions misc/codegen/lib/schemadefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ def __getitem__(self, item):

ql.add(_Parametrized(_ClassPragma("default_doc_name"), factory=lambda doc: doc))
ql.add(_ClassPragma("hideable", inherited=True))
ql.add(
_Parametrized(
_ClassPragma("to_string_impl_from_primary_class", inherited=True),
factory=lambda enabled: enabled,
)
)
ql.add(_Pragma("internal"))
ql.add(_Parametrized(_Pragma("name"), factory=lambda name: name))
ql.add(_Parametrized(_PropertyPragma("db_table_name"), factory=lambda name: name))
Expand Down
4 changes: 4 additions & 0 deletions misc/codegen/templates/ql_class.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ module Generated {
{{/root}}
{{#final}}
override string getAPrimaryQlClass() { result = "{{name}}" }
{{#to_string_impl_from_primary_class}}

override string toStringImpl() { result = this.getAPrimaryQlClass() }
{{/to_string_impl_from_primary_class}}
{{/final}}
{{#properties}}

Expand Down
53 changes: 53 additions & 0 deletions misc/codegen/test/test_qlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,59 @@ def test_one_empty_class(generate_classes):
}


def test_to_string_impl_from_primary_class(generate_classes):
assert generate_classes(
[
schema.Class(
"A",
derived={"B"},
pragmas={"ql_to_string_impl_from_primary_class": True},
),
schema.Class(
"B",
bases=["A"],
pragmas={"ql_to_string_impl_from_primary_class": True},
),
]
) == {
"A.qll": (
a_ql_class_public(name="A"),
a_ql_stub(name="A"),
a_ql_class(
name="A",
imports=[stub_import_prefix + "A"],
to_string_impl_from_primary_class=True,
),
),
"B.qll": (
a_ql_class_public(name="B", imports=[stub_import_prefix + "A"]),
a_ql_stub(name="B"),
a_ql_class(
name="B",
final=True,
bases=["A"],
bases_impl=["AImpl::A"],
imports=[
stub_import_prefix_internal + "AImpl::Impl as AImpl",
],
to_string_impl_from_primary_class=True,
),
),
}


def test_to_string_impl_from_primary_class_is_not_generated_when_custom():
cls = schema.Class(
"A",
pragmas={"ql_to_string_impl_from_primary_class": True},
)
# Pass `A` as a class with a custom `toStringImpl`
resolver = qlgen.Resolver({"A": cls}, {"A"})

# Resolving `A` gives a class where `to_string_impl_from_primary_class` does not hold.
assert not resolver.get_ql_class(cls).to_string_impl_from_primary_class


def test_one_empty_internal_class(generate_classes):
assert generate_classes([schema.Class("A", pragmas=["ql_internal"])]) == {
"A.qll": (
Expand Down
23 changes: 23 additions & 0 deletions misc/codegen/test/test_schemaloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,29 @@ class A:
}


def test_inherited_to_string_impl_from_primary_class_pragma():
@load
class data:
@defs.ql.to_string_impl_from_primary_class(True)
class A:
pass

class B(A):
pass

@defs.ql.to_string_impl_from_primary_class(False)
class C(B):
pass

class D(C):
pass

assert data.classes["A"].pragmas["ql_to_string_impl_from_primary_class"] is True
assert data.classes["B"].pragmas["ql_to_string_impl_from_primary_class"] is True
assert data.classes["C"].pragmas["ql_to_string_impl_from_primary_class"] is False
assert data.classes["D"].pragmas["ql_to_string_impl_from_primary_class"] is False


def test_synth_from_class():
@load
class data:
Expand Down
Loading
Loading