Merge pull request #13258 from github/redsun82/swift-synth-properties

Codegen: allow `synth` properties of non-`synth` classes
This commit is contained in:
Paolo Tranquilli
2023-06-07 10:31:06 +02:00
committed by GitHub
17 changed files with 246 additions and 153 deletions

View File

@@ -76,7 +76,7 @@ class Processor:
bases=[self._get_class(b) for b in cls.bases],
fields=[
_get_field(cls, p, self._add_or_none_except)
for p in cls.properties if "cpp_skip" not in p.pragmas
for p in cls.properties if "cpp_skip" not in p.pragmas and not p.synth
],
final=not cls.derived,
trap_name=trap_name,
@@ -85,7 +85,7 @@ class Processor:
def get_classes(self):
ret = {'': []}
for k, cls in self._classmap.items():
if not cls.ipa:
if not cls.synth:
ret.setdefault(cls.group, []).append(self._get_class(cls.name))
return ret

View File

@@ -40,16 +40,16 @@ def dbtype(typename: str, add_or_none_except: typing.Optional[str] = None) -> st
def cls_to_dbscheme(cls: schema.Class, lookup: typing.Dict[str, schema.Class], add_or_none_except: typing.Optional[str] = None):
""" Yield all dbscheme entities needed to model class `cls` """
if cls.ipa:
if cls.synth:
return
if cls.derived:
yield Union(dbtype(cls.name), (dbtype(c) for c in cls.derived if not lookup[c].ipa))
yield Union(dbtype(cls.name), (dbtype(c) for c in cls.derived if not lookup[c].synth))
dir = pathlib.Path(cls.group) if cls.group else None
# output a table specific to a class only if it is a leaf class or it has 1-to-1 properties
# Leaf classes need a table to bind the `@` ids
# 1-to-1 properties are added to a class specific table
# in other cases, separate tables are used for the properties, and a class specific table is unneeded
if not cls.derived or any(f.is_single for f in cls.properties):
if not cls.derived or any(f.is_single and not f.synth for f in cls.properties):
binding = not cls.derived
keyset = KeySet(["id"]) if cls.derived else None
yield Table(
@@ -58,12 +58,15 @@ def cls_to_dbscheme(cls: schema.Class, lookup: typing.Dict[str, schema.Class], a
columns=[
Column("id", type=dbtype(cls.name), binding=binding),
] + [
Column(f.name, dbtype(f.type, add_or_none_except)) for f in cls.properties if f.is_single
Column(f.name, dbtype(f.type, add_or_none_except))
for f in cls.properties if f.is_single and not f.synth
],
dir=dir,
)
# use property-specific tables for 1-to-many and 1-to-at-most-1 properties
for f in cls.properties:
if f.synth:
continue
if f.is_unordered:
yield Table(
name=inflection.tableize(f"{cls.name}_{f.name}"),

View File

@@ -111,6 +111,7 @@ def get_ql_property(cls: schema.Class, prop: schema.Property, lookup: typing.Dic
is_predicate=prop.is_predicate,
is_unordered=prop.is_unordered,
description=prop.description,
synth=bool(cls.synth) or prop.synth,
type_is_hideable=lookup[prop.type].hideable if prop.type in lookup else False,
)
if prop.is_single:
@@ -163,7 +164,6 @@ def get_ql_class(cls: schema.Class, lookup: typing.Dict[str, schema.Class]) -> q
final=not cls.derived,
properties=properties,
dir=pathlib.Path(cls.group or ""),
ipa=bool(cls.ipa),
doc=cls.doc,
hideable=cls.hideable,
**pragmas,
@@ -179,26 +179,26 @@ def _to_db_type(x: str) -> str:
_final_db_class_lookup = {}
def get_ql_ipa_class_db(name: str) -> ql.Synth.FinalClassDb:
def get_ql_synth_class_db(name: str) -> ql.Synth.FinalClassDb:
return _final_db_class_lookup.setdefault(name, ql.Synth.FinalClassDb(name=name,
params=[
ql.Synth.Param("id", _to_db_type(name))]))
def get_ql_ipa_class(cls: schema.Class):
def get_ql_synth_class(cls: schema.Class):
if cls.derived:
return ql.Synth.NonFinalClass(name=cls.name, derived=sorted(cls.derived),
root=not cls.bases)
if cls.ipa and cls.ipa.from_class is not None:
source = cls.ipa.from_class
get_ql_ipa_class_db(source).subtract_type(cls.name)
return ql.Synth.FinalClassDerivedIpa(name=cls.name,
params=[ql.Synth.Param("id", _to_db_type(source))])
if cls.ipa and cls.ipa.on_arguments is not None:
return ql.Synth.FinalClassFreshIpa(name=cls.name,
params=[ql.Synth.Param(k, _to_db_type(v))
for k, v in cls.ipa.on_arguments.items()])
return get_ql_ipa_class_db(cls.name)
if cls.synth and cls.synth.from_class is not None:
source = cls.synth.from_class
get_ql_synth_class_db(source).subtract_type(cls.name)
return ql.Synth.FinalClassDerivedSynth(name=cls.name,
params=[ql.Synth.Param("id", _to_db_type(source))])
if cls.synth and cls.synth.on_arguments is not None:
return ql.Synth.FinalClassFreshSynth(name=cls.name,
params=[ql.Synth.Param(k, _to_db_type(v))
for k, v in cls.synth.on_arguments.items()])
return get_ql_synth_class_db(cls.name)
def get_import(file: pathlib.Path, root_dir: pathlib.Path):
@@ -291,26 +291,26 @@ def _should_skip_qltest(cls: schema.Class, lookup: typing.Dict[str, schema.Class
def _get_stub(cls: schema.Class, base_import: str, generated_import_prefix: str) -> ql.Stub:
if isinstance(cls.ipa, schema.IpaInfo):
if cls.ipa.from_class is not None:
if isinstance(cls.synth, schema.SynthInfo):
if cls.synth.from_class is not None:
accessors = [
ql.IpaUnderlyingAccessor(
ql.SynthUnderlyingAccessor(
argument="Entity",
type=_to_db_type(cls.ipa.from_class),
type=_to_db_type(cls.synth.from_class),
constructorparams=["result"]
)
]
elif cls.ipa.on_arguments is not None:
elif cls.synth.on_arguments is not None:
accessors = [
ql.IpaUnderlyingAccessor(
ql.SynthUnderlyingAccessor(
argument=inflection.camelize(arg),
type=_to_db_type(type),
constructorparams=["result" if a == arg else "_" for a in cls.ipa.on_arguments]
) for arg, type in cls.ipa.on_arguments.items()
constructorparams=["result" if a == arg else "_" for a in cls.synth.on_arguments]
) for arg, type in cls.synth.on_arguments.items()
]
else:
accessors = []
return ql.Stub(name=cls.name, base_import=base_import, import_prefix=generated_import_prefix, ipa_accessors=accessors)
return ql.Stub(name=cls.name, base_import=base_import, import_prefix=generated_import_prefix, synth_accessors=accessors)
def generate(opts, renderer):
@@ -344,7 +344,7 @@ def generate(opts, renderer):
with renderer.manage(generated=generated, stubs=stubs, registry=opts.generated_registry,
force=opts.force) as renderer:
db_classes = [cls for cls in classes.values() if not cls.ipa]
db_classes = [cls for name, cls in classes.items() if not data.classes[name].synth]
renderer.render(ql.DbClasses(db_classes), out / "Raw.qll")
classes_by_dir_and_name = sorted(classes.values(), key=lambda cls: (cls.dir, cls.name))
@@ -401,32 +401,32 @@ def generate(opts, renderer):
elements_module=elements_module,
property=p), test_dir / f"{c.name}_{p.getter}.ql")
final_ipa_types = []
non_final_ipa_types = []
final_synth_types = []
non_final_synth_types = []
constructor_imports = []
ipa_constructor_imports = []
synth_constructor_imports = []
stubs = {}
for cls in sorted(data.classes.values(), key=lambda cls: (cls.group, cls.name)):
ipa_type = get_ql_ipa_class(cls)
if ipa_type.is_final:
final_ipa_types.append(ipa_type)
if ipa_type.has_params:
synth_type = get_ql_synth_class(cls)
if synth_type.is_final:
final_synth_types.append(synth_type)
if synth_type.has_params:
stub_file = stub_out / cls.group / f"{cls.name}Constructor.qll"
if not renderer.is_customized_stub(stub_file):
# stub rendering must be postponed as we might not have yet all subtracted ipa types in `ipa_type`
stubs[stub_file] = ql.Synth.ConstructorStub(ipa_type, import_prefix=generated_import_prefix)
# stub rendering must be postponed as we might not have yet all subtracted synth types in `synth_type`
stubs[stub_file] = ql.Synth.ConstructorStub(synth_type, import_prefix=generated_import_prefix)
constructor_import = get_import(stub_file, opts.root_dir)
constructor_imports.append(constructor_import)
if ipa_type.is_ipa:
ipa_constructor_imports.append(constructor_import)
if synth_type.is_synth:
synth_constructor_imports.append(constructor_import)
else:
non_final_ipa_types.append(ipa_type)
non_final_synth_types.append(synth_type)
for stub_file, data in stubs.items():
renderer.render(data, stub_file)
renderer.render(ql.Synth.Types(root.name, generated_import_prefix,
final_ipa_types, non_final_ipa_types), out / "Synth.qll")
final_synth_types, non_final_synth_types), out / "Synth.qll")
renderer.render(ql.ImportList(constructor_imports), out / "SynthConstructors.qll")
renderer.render(ql.ImportList(ipa_constructor_imports), out / "PureSynthConstructors.qll")
renderer.render(ql.ImportList(synth_constructor_imports), out / "PureSynthConstructors.qll")
if opts.ql_format:
format(opts.codeql_binary, renderer.written)

View File

@@ -42,6 +42,7 @@ class Property:
description: List[str] = field(default_factory=list)
doc: Optional[str] = None
doc_plural: Optional[str] = None
synth: bool = False
type_is_hideable: bool = False
def __post_init__(self):
@@ -112,7 +113,6 @@ class Class:
qltest_collapse_hierarchy: bool = False
qltest_uncollapse_hierarchy: bool = False
ql_internal: bool = False
ipa: bool = False
doc: List[str] = field(default_factory=list)
hideable: bool = False
@@ -147,7 +147,7 @@ class Class:
@dataclass
class IpaUnderlyingAccessor:
class SynthUnderlyingAccessor:
argument: str
type: str
constructorparams: List[Param]
@@ -165,11 +165,11 @@ class Stub:
name: str
base_import: str
import_prefix: str
ipa_accessors: List[IpaUnderlyingAccessor] = field(default_factory=list)
synth_accessors: List[SynthUnderlyingAccessor] = field(default_factory=list)
@property
def has_ipa_accessors(self) -> bool:
return bool(self.ipa_accessors)
def has_synth_accessors(self) -> bool:
return bool(self.synth_accessors)
@dataclass
@@ -245,8 +245,8 @@ class Synth:
@dataclass
class FinalClass(Class):
is_final: ClassVar = True
is_derived_ipa: ClassVar = False
is_fresh_ipa: ClassVar = False
is_derived_synth: ClassVar = False
is_fresh_synth: ClassVar = False
is_db: ClassVar = False
params: List["Synth.Param"] = field(default_factory=list)
@@ -256,37 +256,37 @@ class Synth:
self.params[0].first = True
@property
def is_ipa(self):
return self.is_fresh_ipa or self.is_derived_ipa
def is_synth(self):
return self.is_fresh_synth or self.is_derived_synth
@property
def has_params(self) -> bool:
return bool(self.params)
@dataclass
class FinalClassIpa(FinalClass):
class FinalClassSynth(FinalClass):
pass
@dataclass
class FinalClassDerivedIpa(FinalClassIpa):
is_derived_ipa: ClassVar = True
class FinalClassDerivedSynth(FinalClassSynth):
is_derived_synth: ClassVar = True
@dataclass
class FinalClassFreshIpa(FinalClassIpa):
is_fresh_ipa: ClassVar = True
class FinalClassFreshSynth(FinalClassSynth):
is_fresh_synth: ClassVar = True
@dataclass
class FinalClassDb(FinalClass):
is_db: ClassVar = True
subtracted_ipa_types: List["Synth.Class"] = field(default_factory=list)
subtracted_synth_types: List["Synth.Class"] = field(default_factory=list)
def subtract_type(self, type: str):
self.subtracted_ipa_types.append(Synth.Class(type, first=not self.subtracted_ipa_types))
self.subtracted_synth_types.append(Synth.Class(type, first=not self.subtracted_synth_types))
@property
def has_subtracted_ipa_types(self) -> bool:
return bool(self.subtracted_ipa_types)
def has_subtracted_synth_types(self) -> bool:
return bool(self.subtracted_synth_types)
@property
def db_id(self) -> str:
@@ -304,7 +304,7 @@ class Synth:
@dataclass
class Types:
template: ClassVar = "ql_ipa_types"
template: ClassVar = "ql_synth_types"
root: str
import_prefix: str
@@ -317,7 +317,7 @@ class Synth:
@dataclass
class ConstructorStub:
template: ClassVar = "ql_ipa_constructor_stub"
template: ClassVar = "ql_synth_constructor_stub"
cls: "Synth.FinalClass"
import_prefix: str

View File

@@ -34,6 +34,7 @@ class Property:
pragmas: List[str] = field(default_factory=list)
doc: Optional[str] = None
description: List[str] = field(default_factory=list)
synth: bool = False
@property
def is_single(self) -> bool:
@@ -74,7 +75,7 @@ RepeatedUnorderedProperty = functools.partial(Property, Property.Kind.REPEATED_U
@dataclass
class IpaInfo:
class SynthInfo:
from_class: Optional[str] = None
on_arguments: Optional[Dict[str, str]] = None
@@ -87,7 +88,7 @@ class Class:
properties: List[Property] = field(default_factory=list)
group: str = ""
pragmas: List[str] = field(default_factory=list)
ipa: Optional[Union[IpaInfo, bool]] = None
synth: Optional[Union[SynthInfo, bool]] = None
"""^^^ filled with `True` for non-final classes with only synthesized final descendants """
doc: List[str] = field(default_factory=list)
default_doc_name: Optional[str] = None
@@ -104,10 +105,10 @@ class Class:
_check_type(d, known)
for p in self.properties:
_check_type(p.type, known)
if self.ipa is not None:
_check_type(self.ipa.from_class, known)
if self.ipa.on_arguments is not None:
for t in self.ipa.on_arguments.values():
if self.synth is not None:
_check_type(self.synth.from_class, known)
if self.synth.on_arguments is not None:
for t in self.synth.on_arguments.values():
_check_type(t, known)

View File

@@ -44,10 +44,15 @@ class _Namespace:
self.__dict__.update(kwargs)
class _SynthModifier(_schema.PropertyModifier, _Namespace):
def modify(self, prop: _schema.Property):
prop.synth = True
qltest = _Namespace()
ql = _Namespace()
cpp = _Namespace()
synth = _Namespace()
synth = _SynthModifier()
@_dataclass
@@ -155,7 +160,7 @@ def group(name: str = "") -> _ClassDecorator:
return _annotate(group=name)
synth.from_class = lambda ref: _annotate(ipa=_schema.IpaInfo(
synth.from_class = lambda ref: _annotate(synth=_schema.SynthInfo(
from_class=_schema.get_type_name(ref)))
synth.on_arguments = lambda **kwargs: _annotate(
ipa=_schema.IpaInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()}))
synth=_schema.SynthInfo(on_arguments={k: _schema.get_type_name(t) for k, t in kwargs.items()}))

View File

@@ -40,7 +40,7 @@ def _get_class(cls: type) -> schema.Class:
hideable=getattr(cls, "_hideable", False),
# in the following we don't use `getattr` to avoid inheriting
pragmas=cls.__dict__.get("_pragmas", []),
ipa=cls.__dict__.get("_ipa", None),
synth=cls.__dict__.get("_synth", None),
properties=[
a | _PropertyNamer(n)
for n, a in cls.__dict__.get("__annotations__", {}).items()
@@ -65,34 +65,34 @@ def _toposort_classes_by_group(classes: typing.Dict[str, schema.Class]) -> typin
return ret
def _fill_ipa_information(classes: typing.Dict[str, schema.Class]):
""" Take a dictionary where the `ipa` field is filled for all explicitly synthesized classes
def _fill_synth_information(classes: typing.Dict[str, schema.Class]):
""" Take a dictionary where the `synth` field is filled for all explicitly synthesized classes
and update it so that all non-final classes that have only synthesized final descendants
get `True` as` value for the `ipa` field
get `True` as` value for the `synth` field
"""
if not classes:
return
is_ipa: typing.Dict[str, bool] = {}
is_synth: typing.Dict[str, bool] = {}
def fill_is_ipa(name: str):
if name not in is_ipa:
def fill_is_synth(name: str):
if name not in is_synth:
cls = classes[name]
for d in cls.derived:
fill_is_ipa(d)
if cls.ipa is not None:
is_ipa[name] = True
fill_is_synth(d)
if cls.synth is not None:
is_synth[name] = True
elif not cls.derived:
is_ipa[name] = False
is_synth[name] = False
else:
is_ipa[name] = all(is_ipa[d] for d in cls.derived)
is_synth[name] = all(is_synth[d] for d in cls.derived)
root = next(iter(classes))
fill_is_ipa(root)
fill_is_synth(root)
for name, cls in classes.items():
if cls.ipa is None and is_ipa[name]:
cls.ipa = True
if cls.synth is None and is_synth[name]:
cls.synth = True
def _fill_hideable_information(classes: typing.Dict[str, schema.Class]):
@@ -134,7 +134,7 @@ def load(m: types.ModuleType) -> schema.Schema:
null = name
cls.is_null_class = True
_fill_ipa_information(classes)
_fill_synth_information(classes)
_fill_hideable_information(classes)
return schema.Schema(includes=includes, classes=_toposort_classes_by_group(classes), null=null)

View File

@@ -67,12 +67,12 @@ module Generated {
* behavior of both the `Immediate` and non-`Immediate` versions.
*/
{{type}} get{{#is_unordered}}An{{/is_unordered}}Immediate{{singular}}({{#is_indexed}}int index{{/is_indexed}}) {
{{^ipa}}
{{^synth}}
result = Synth::convert{{type}}FromRaw(Synth::convert{{name}}ToRaw(this){{^root}}.(Raw::{{name}}){{/root}}.{{getter}}({{#is_indexed}}index{{/is_indexed}}))
{{/ipa}}
{{#ipa}}
{{/synth}}
{{#synth}}
none()
{{/ipa}}
{{/synth}}
}
/**
@@ -99,12 +99,12 @@ module Generated {
{{/has_description}}
*/
{{type}} {{getter}}({{#is_indexed}}int index{{/is_indexed}}) {
{{^ipa}}
{{^synth}}
{{^is_predicate}}result = {{/is_predicate}}{{#type_is_class}}Synth::convert{{type}}FromRaw({{/type_is_class}}Synth::convert{{name}}ToRaw(this){{^root}}.(Raw::{{name}}){{/root}}.{{getter}}({{#is_indexed}}index{{/is_indexed}}){{#type_is_class}}){{/type_is_class}}
{{/ipa}}
{{#ipa}}
{{/synth}}
{{#synth}}
none()
{{/ipa}}
{{/synth}}
}
{{/type_is_hideable}}

View File

@@ -15,6 +15,7 @@ module Raw {
{{#final}}override string toString() { result = "{{name}}" }{{/final}}
{{#properties}}
{{^synth}}
/**
* {{>ql_property_doc}} *
{{#has_description}}
@@ -26,6 +27,7 @@ module Raw {
{{type}} {{getter}}({{#is_indexed}}int index{{/is_indexed}}) {
{{tablename}}({{#tableparams}}{{^first}}, {{/first}}{{param}}{{/tableparams}})
}
{{/synth}}
{{/properties}}
}

View File

@@ -1,9 +1,9 @@
// generated by {{generator}}, remove this comment if you wish to edit this file
private import {{base_import}}
{{#has_ipa_accessors}}
{{#has_synth_accessors}}
private import {{import_prefix}}.Raw
private import {{import_prefix}}.Synth
{{/has_ipa_accessors}}
{{/has_synth_accessors}}
{{#ql_internal}}
/**
@@ -11,8 +11,8 @@ private import {{import_prefix}}.Synth
*/
{{/ql_internal}}
class {{name}} extends Generated::{{name}} {
{{#ipa_accessors}}
{{#synth_accessors}}
private
cached {{type}} getUnderlying{{argument}}() { this = Synth::T{{name}}({{#constructorparams}}{{^first}},{{/first}}{{param}}{{/constructorparams}})}
{{/ipa_accessors}}
{{/synth_accessors}}
}

View File

@@ -2,15 +2,15 @@
private import {{import_prefix}}.Raw
{{#cls}}
{{#is_db}}
{{#has_subtracted_ipa_types}}
{{#has_subtracted_synth_types}}
private import {{import_prefix}}.PureSynthConstructors
{{/has_subtracted_ipa_types}}
{{/has_subtracted_synth_types}}
{{/is_db}}
predicate construct{{name}}({{#params}}{{^first}}, {{/first}}{{type}} {{param}}{{/params}}) {
{{#is_db}}
{{#subtracted_ipa_types}}{{^first}} and {{/first}}not construct{{name}}(id){{/subtracted_ipa_types}}
{{^subtracted_ipa_types}}any(){{/subtracted_ipa_types}}
{{#subtracted_synth_types}}{{^first}} and {{/first}}not construct{{name}}(id){{/subtracted_synth_types}}
{{^subtracted_synth_types}}any(){{/subtracted_synth_types}}
{{/is_db}}
{{^is_db}}
none()

View File

@@ -38,12 +38,12 @@ cached module Synth {
* Converts a raw element to a synthesized `T{{name}}`, if possible.
*/
cached T{{name}} convert{{name}}FromRaw(Raw::Element e) {
{{^is_fresh_ipa}}
{{^is_fresh_synth}}
result = T{{name}}(e)
{{/is_fresh_ipa}}
{{#is_fresh_ipa}}
{{/is_fresh_synth}}
{{#is_fresh_synth}}
none()
{{/is_fresh_ipa}}
{{/is_fresh_synth}}
}
{{/final_classes}}
@@ -68,12 +68,12 @@ cached module Synth {
* Converts a synthesized `T{{name}}` to a raw DB element, if possible.
*/
cached Raw::Element convert{{name}}ToRaw(T{{name}} e) {
{{^is_fresh_ipa}}
{{^is_fresh_synth}}
e = T{{name}}(result)
{{/is_fresh_ipa}}
{{#is_fresh_ipa}}
{{/is_fresh_synth}}
{{#is_fresh_synth}}
none()
{{/is_fresh_ipa}}
{{/is_fresh_synth}}
}
{{/final_classes}}

View File

@@ -181,19 +181,19 @@ def test_cpp_skip_pragma(generate):
]
def test_ipa_classes_ignored(generate):
def test_synth_classes_ignored(generate):
assert generate([
schema.Class(
name="W",
ipa=schema.IpaInfo(),
synth=schema.SynthInfo(),
),
schema.Class(
name="X",
ipa=schema.IpaInfo(from_class="A"),
synth=schema.SynthInfo(from_class="A"),
),
schema.Class(
name="Y",
ipa=schema.IpaInfo(on_arguments={"a": "A", "b": "int"}),
synth=schema.SynthInfo(on_arguments={"a": "A", "b": "int"}),
),
schema.Class(
name="Z",
@@ -203,5 +203,27 @@ def test_ipa_classes_ignored(generate):
]
def test_synth_properties_ignored(generate):
assert generate([
schema.Class(
name="X",
properties=[
schema.SingleProperty("x", "a"),
schema.SingleProperty("y", "b", synth=True),
schema.SingleProperty("z", "c"),
schema.OptionalProperty("foo", "bar", synth=True),
schema.RepeatedProperty("baz", "bazz", synth=True),
schema.RepeatedOptionalProperty("bazzz", "bazzzz", synth=True),
schema.RepeatedUnorderedProperty("bazzzzz", "bazzzzzz", synth=True),
],
),
]) == [
cpp.Class(name="X", final=True, trap_name="Xes", fields=[
cpp.Field("x", "a"),
cpp.Field("z", "c"),
]),
]
if __name__ == '__main__':
sys.exit(pytest.main([__file__] + sys.argv[1:]))

View File

@@ -534,11 +534,11 @@ def test_null_class(generate):
)
def test_ipa_classes_ignored(generate):
def test_synth_classes_ignored(generate):
assert generate([
schema.Class(name="A", ipa=schema.IpaInfo()),
schema.Class(name="B", ipa=schema.IpaInfo(from_class="A")),
schema.Class(name="C", ipa=schema.IpaInfo(on_arguments={"x": "A"})),
schema.Class(name="A", synth=schema.SynthInfo()),
schema.Class(name="B", synth=schema.SynthInfo(from_class="A")),
schema.Class(name="C", synth=schema.SynthInfo(on_arguments={"x": "A"})),
]) == dbscheme.Scheme(
src=schema_file.name,
includes=[],
@@ -546,10 +546,10 @@ def test_ipa_classes_ignored(generate):
)
def test_ipa_derived_classes_ignored(generate):
def test_synth_derived_classes_ignored(generate):
assert generate([
schema.Class(name="A", derived={"B", "C"}),
schema.Class(name="B", bases=["A"], ipa=schema.IpaInfo()),
schema.Class(name="B", bases=["A"], synth=schema.SynthInfo()),
schema.Class(name="C", bases=["A"]),
]) == dbscheme.Scheme(
src=schema_file.name,
@@ -566,5 +566,32 @@ def test_ipa_derived_classes_ignored(generate):
)
def test_synth_properties_ignored(generate):
assert generate([
schema.Class(name="A", properties=[
schema.SingleProperty("x", "a"),
schema.SingleProperty("y", "b", synth=True),
schema.SingleProperty("z", "c"),
schema.OptionalProperty("foo", "bar", synth=True),
schema.RepeatedProperty("baz", "bazz", synth=True),
schema.RepeatedOptionalProperty("bazzz", "bazzzz", synth=True),
schema.RepeatedUnorderedProperty("bazzzzz", "bazzzzzz", synth=True),
]),
]) == dbscheme.Scheme(
src=schema_file.name,
includes=[],
declarations=[
dbscheme.Table(
name="as",
columns=[
dbscheme.Column("id", "@a", binding=True),
dbscheme.Column("x", "a"),
dbscheme.Column("z", "c"),
],
)
],
)
if __name__ == '__main__':
sys.exit(pytest.main([__file__] + sys.argv[1:]))

View File

@@ -169,9 +169,9 @@ def test_class_without_description():
assert prop.has_description is False
def test_ipa_accessor_has_first_constructor_param_marked():
def test_synth_accessor_has_first_constructor_param_marked():
params = ["a", "b", "c"]
x = ql.IpaUnderlyingAccessor("foo", "bar", params)
x = ql.SynthUnderlyingAccessor("foo", "bar", params)
assert x.constructorparams[0].first
assert [p.param for p in x.constructorparams] == params

View File

@@ -848,27 +848,49 @@ def test_property_on_class_with_default_doc_name(generate_classes):
}
def test_stub_on_class_with_ipa_from_class(generate_classes):
def test_stub_on_class_with_synth_from_class(generate_classes):
assert generate_classes([
schema.Class("MyObject", ipa=schema.IpaInfo(from_class="A")),
schema.Class("MyObject", synth=schema.SynthInfo(from_class="A"),
properties=[schema.SingleProperty("foo", "bar")]),
]) == {
"MyObject.qll": (a_ql_stub(name="MyObject", ipa_accessors=[
ql.IpaUnderlyingAccessor(argument="Entity", type="Raw::A", constructorparams=["result"]),
"MyObject.qll": (a_ql_stub(name="MyObject", synth_accessors=[
ql.SynthUnderlyingAccessor(argument="Entity", type="Raw::A", constructorparams=["result"]),
]),
a_ql_class(name="MyObject", final=True, ipa=True)),
a_ql_class(name="MyObject", final=True, properties=[
ql.Property(singular="Foo", type="bar", tablename="my_objects", synth=True,
tableparams=["this", "result"], doc="foo of this my object"),
])),
}
def test_stub_on_class_with_ipa_on_arguments(generate_classes):
def test_stub_on_class_with_synth_on_arguments(generate_classes):
assert generate_classes([
schema.Class("MyObject", ipa=schema.IpaInfo(on_arguments={"base": "A", "index": "int", "label": "string"})),
schema.Class("MyObject", synth=schema.SynthInfo(on_arguments={"base": "A", "index": "int", "label": "string"}),
properties=[schema.SingleProperty("foo", "bar")]),
]) == {
"MyObject.qll": (a_ql_stub(name="MyObject", ipa_accessors=[
ql.IpaUnderlyingAccessor(argument="Base", type="Raw::A", constructorparams=["result", "_", "_"]),
ql.IpaUnderlyingAccessor(argument="Index", type="int", constructorparams=["_", "result", "_"]),
ql.IpaUnderlyingAccessor(argument="Label", type="string", constructorparams=["_", "_", "result"]),
"MyObject.qll": (a_ql_stub(name="MyObject", synth_accessors=[
ql.SynthUnderlyingAccessor(argument="Base", type="Raw::A", constructorparams=["result", "_", "_"]),
ql.SynthUnderlyingAccessor(argument="Index", type="int", constructorparams=["_", "result", "_"]),
ql.SynthUnderlyingAccessor(argument="Label", type="string", constructorparams=["_", "_", "result"]),
]),
a_ql_class(name="MyObject", final=True, ipa=True)),
a_ql_class(name="MyObject", final=True, properties=[
ql.Property(singular="Foo", type="bar", tablename="my_objects", synth=True,
tableparams=["this", "result"], doc="foo of this my object"),
])),
}
def test_synth_property(generate_classes):
assert generate_classes([
schema.Class("MyObject", properties=[
schema.SingleProperty("foo", "bar", synth=True)]),
]) == {
"MyObject.qll": (a_ql_stub(name="MyObject"),
a_ql_class(name="MyObject", final=True,
properties=[
ql.Property(singular="Foo", type="bar", tablename="my_objects", synth=True,
tableparams=["this", "result"], doc="foo of this my object"),
])),
}

View File

@@ -337,7 +337,7 @@ def test_class_with_pragmas():
}
def test_ipa_from_class():
def test_synth_from_class():
@load
class data:
class A:
@@ -348,12 +348,12 @@ def test_ipa_from_class():
pass
assert data.classes == {
'A': schema.Class('A', derived={'B'}, ipa=True),
'B': schema.Class('B', bases=['A'], ipa=schema.IpaInfo(from_class="A")),
'A': schema.Class('A', derived={'B'}, synth=True),
'B': schema.Class('B', bases=['A'], synth=schema.SynthInfo(from_class="A")),
}
def test_ipa_from_class_ref():
def test_synth_from_class_ref():
@load
class data:
@defs.synth.from_class("B")
@@ -364,12 +364,12 @@ def test_ipa_from_class_ref():
pass
assert data.classes == {
'A': schema.Class('A', derived={'B'}, ipa=schema.IpaInfo(from_class="B")),
'A': schema.Class('A', derived={'B'}, synth=schema.SynthInfo(from_class="B")),
'B': schema.Class('B', bases=['A']),
}
def test_ipa_from_class_dangling():
def test_synth_from_class_dangling():
with pytest.raises(schema.Error):
@load
class data:
@@ -378,7 +378,7 @@ def test_ipa_from_class_dangling():
pass
def test_ipa_class_on():
def test_synth_class_on():
@load
class data:
class A:
@@ -389,12 +389,12 @@ def test_ipa_class_on():
pass
assert data.classes == {
'A': schema.Class('A', derived={'B'}, ipa=True),
'B': schema.Class('B', bases=['A'], ipa=schema.IpaInfo(on_arguments={'a': 'A', 'i': 'int'})),
'A': schema.Class('A', derived={'B'}, synth=True),
'B': schema.Class('B', bases=['A'], synth=schema.SynthInfo(on_arguments={'a': 'A', 'i': 'int'})),
}
def test_ipa_class_on_ref():
def test_synth_class_on_ref():
class A:
pass
@@ -408,12 +408,12 @@ def test_ipa_class_on_ref():
pass
assert data.classes == {
'A': schema.Class('A', derived={'B'}, ipa=schema.IpaInfo(on_arguments={'b': 'B', 'i': 'int'})),
'A': schema.Class('A', derived={'B'}, synth=schema.SynthInfo(on_arguments={'b': 'B', 'i': 'int'})),
'B': schema.Class('B', bases=['A']),
}
def test_ipa_class_on_dangling():
def test_synth_class_on_dangling():
with pytest.raises(schema.Error):
@load
class data:
@@ -422,7 +422,7 @@ def test_ipa_class_on_dangling():
pass
def test_ipa_class_hierarchy():
def test_synth_class_hierarchy():
@load
class data:
class Root:
@@ -447,14 +447,25 @@ def test_ipa_class_hierarchy():
assert data.classes == {
'Root': schema.Class('Root', derived={'Base', 'C'}),
'Base': schema.Class('Base', bases=['Root'], derived={'Intermediate', 'B'}, ipa=True),
'Intermediate': schema.Class('Intermediate', bases=['Base'], derived={'A'}, ipa=True),
'A': schema.Class('A', bases=['Intermediate'], ipa=schema.IpaInfo(on_arguments={'a': 'Base', 'i': 'int'})),
'B': schema.Class('B', bases=['Base'], ipa=schema.IpaInfo(from_class='Base')),
'Base': schema.Class('Base', bases=['Root'], derived={'Intermediate', 'B'}, synth=True),
'Intermediate': schema.Class('Intermediate', bases=['Base'], derived={'A'}, synth=True),
'A': schema.Class('A', bases=['Intermediate'], synth=schema.SynthInfo(on_arguments={'a': 'Base', 'i': 'int'})),
'B': schema.Class('B', bases=['Base'], synth=schema.SynthInfo(from_class='Base')),
'C': schema.Class('C', bases=['Root']),
}
def test_synthesized_property():
@load
class data:
class A:
x: defs.int | defs.synth
assert data.classes["A"].properties == [
schema.SingleProperty("x", "int", synth=True)
]
def test_class_docstring():
@load
class data: