From c08e6fdc1ed904c8cabf809c1d581f2e490df4c4 Mon Sep 17 00:00:00 2001 From: Paolo Tranquilli Date: Mon, 9 May 2022 17:50:49 +0200 Subject: [PATCH] Swift codegen: add predicate properties Properties marked with `predicate` in the schema are now accepted. * in the dbscheme, they will translate to a table with a single `id` column (and the table name will not be pluralized) * in C++ classes, they will translate to `bool` fields * in QL classes, they will translate to predicates Closes https://github.com/github/codeql-c-team/issues/1016 --- swift/codegen/cppgen.py | 8 ++- swift/codegen/dbschemegen.py | 9 +++ swift/codegen/lib/cpp.py | 3 +- swift/codegen/lib/ql.py | 28 +++++---- swift/codegen/lib/schema.py | 22 ++++--- swift/codegen/qlgen.py | 8 +++ swift/codegen/schema.yml | 1 + swift/codegen/templates/cpp_classes.mustache | 3 + swift/codegen/templates/ql_class.mustache | 8 +-- swift/codegen/test/test_cpp.py | 15 ++--- swift/codegen/test/test_cppgen.py | 11 ++++ swift/codegen/test/test_dbschemegen.py | 35 +++++++++++ swift/codegen/test/test_ql.py | 63 +++++++++++-------- swift/codegen/test/test_qlgen.py | 16 ++++- swift/codegen/test/test_schema.py | 2 + .../swift/generated/type/AnyFunctionType.qll | 2 + swift/ql/lib/swift.dbscheme | 5 ++ 17 files changed, 179 insertions(+), 60 deletions(-) diff --git a/swift/codegen/cppgen.py b/swift/codegen/cppgen.py index f45512512aa..eee66a812f0 100644 --- a/swift/codegen/cppgen.py +++ b/swift/codegen/cppgen.py @@ -8,6 +8,9 @@ from swift.codegen.lib import cpp, generator, schema def _get_type(t: str, trap_affix: str) -> str: + if t is None: + # this is a predicate + return "bool" if t == "string": return "std::string" if t == "boolean": @@ -20,12 +23,15 @@ def _get_type(t: str, trap_affix: str) -> str: def _get_field(cls: schema.Class, p: schema.Property, trap_affix: str) -> cpp.Field: trap_name = None if not p.is_single: - trap_name = inflection.pluralize(inflection.camelize(f"{cls.name}_{p.name}")) + trap_name = inflection.camelize(f"{cls.name}_{p.name}") + if not p.is_predicate: + trap_name = inflection.pluralize(trap_name) args = dict( name=p.name + ("_" if p.name in cpp.cpp_keywords else ""), type=_get_type(p.type, trap_affix), is_optional=p.is_optional, is_repeated=p.is_repeated, + is_predicate=p.is_predicate, trap_name=trap_name, ) args.update(cpp.get_field_override(p.name)) diff --git a/swift/codegen/dbschemegen.py b/swift/codegen/dbschemegen.py index efe45c0b997..c95316e5499 100755 --- a/swift/codegen/dbschemegen.py +++ b/swift/codegen/dbschemegen.py @@ -57,6 +57,15 @@ def cls_to_dbscheme(cls: schema.Class): Column(f.name, dbtype(f.type)), ], ) + elif f.is_predicate: + yield Table( + keyset=KeySet(["id"]), + name=inflection.underscore(f"{cls.name}_{f.name}"), + columns=[ + Column("id", type=dbtype(cls.name)), + ], + ) + def get_declarations(data: schema.Schema): diff --git a/swift/codegen/lib/cpp.py b/swift/codegen/lib/cpp.py index 886ef8069e6..82dfbb44bcb 100644 --- a/swift/codegen/lib/cpp.py +++ b/swift/codegen/lib/cpp.py @@ -35,6 +35,7 @@ class Field: type: str is_optional: bool = False is_repeated: bool = False + is_predicate: bool = False trap_name: str = None first: bool = False @@ -61,7 +62,7 @@ class Field: @property def is_single(self): - return not (self.is_optional or self.is_repeated) + return not (self.is_optional or self.is_repeated or self.is_predicate) diff --git a/swift/codegen/lib/ql.py b/swift/codegen/lib/ql.py index ee9103fd990..5d4a7076414 100644 --- a/swift/codegen/lib/ql.py +++ b/swift/codegen/lib/ql.py @@ -14,29 +14,35 @@ class Param: @dataclass class Property: singular: str - type: str - tablename: str - tableparams: List[Param] + type: str = None + tablename: str = None + tableparams: List[Param] = field(default_factory=list) plural: str = None first: bool = False local_var: str = "x" is_optional: bool = False + is_predicate: bool = False def __post_init__(self): - assert self.tableparams - if self.type_is_class: - self.tableparams = [x if x != "result" else self.local_var for x in self.tableparams] - self.tableparams = [Param(x) for x in self.tableparams] - self.tableparams[0].first = True + if self.tableparams: + if self.type_is_class: + self.tableparams = [x if x != "result" else self.local_var for x in self.tableparams] + self.tableparams = [Param(x) for x in self.tableparams] + self.tableparams[0].first = True @property - def indefinite_article(self): + def getter(self): + return f"get{self.singular}" if not self.is_predicate else self.singular + + @property + def indefinite_getter(self): if self.plural: - return "An" if self.singular[0] in "AEIO" else "A" + article = "An" if self.singular[0] in "AEIO" else "A" + return f"get{article}{self.singular}" @property def type_is_class(self): - return self.type[0].isupper() + return bool(self.type) and self.type[0].isupper() @property def is_repeated(self): diff --git a/swift/codegen/lib/schema.py b/swift/codegen/lib/schema.py index 06ceb4b63bb..a65aaf24dca 100644 --- a/swift/codegen/lib/schema.py +++ b/swift/codegen/lib/schema.py @@ -15,9 +15,10 @@ class Property: is_single: ClassVar = False is_optional: ClassVar = False is_repeated: ClassVar = False + is_predicate: ClassVar = False name: str - type: str + type: str = None @dataclass @@ -41,6 +42,11 @@ class RepeatedOptionalProperty(Property): is_repeated: ClassVar = True +@dataclass +class PredicateProperty(Property): + is_predicate: ClassVar = True + + @dataclass class Class: name: str @@ -58,17 +64,15 @@ class Schema: def _parse_property(name, type): if type.endswith("?*"): - cls = RepeatedOptionalProperty - type = type[:-2] + return RepeatedOptionalProperty(name, type[:-2]) elif type.endswith("*"): - cls = RepeatedProperty - type = type[:-1] + return RepeatedProperty(name, type[:-1]) elif type.endswith("?"): - cls = OptionalProperty - type = type[:-1] + return OptionalProperty(name, type[:-1]) + elif type == "predicate": + return PredicateProperty(name) else: - cls = SingleProperty - return cls(name, type) + return SingleProperty(name, type) class _DirSelector: diff --git a/swift/codegen/qlgen.py b/swift/codegen/qlgen.py index 5d08f5a0410..4eea533904a 100755 --- a/swift/codegen/qlgen.py +++ b/swift/codegen/qlgen.py @@ -36,6 +36,14 @@ def get_ql_property(cls: schema.Class, prop: schema.Property): tableparams=["this", "result"], is_optional=True, ) + elif prop.is_predicate: + return ql.Property( + singular=inflection.camelize(prop.name, uppercase_first_letter=False), + type="predicate", + tablename=inflection.underscore(f"{cls.name}_{prop.name}"), + tableparams=["this"], + is_predicate=True, + ) def get_ql_class(cls: schema.Class): diff --git a/swift/codegen/schema.yml b/swift/codegen/schema.yml index 4ea4a8190a6..46e0b508ae5 100644 --- a/swift/codegen/schema.yml +++ b/swift/codegen/schema.yml @@ -59,6 +59,7 @@ AnyFunctionType: result: Type param_types: Type* param_labels: string* + is_throwing: predicate AnyGenericType: _extends: Type diff --git a/swift/codegen/templates/cpp_classes.mustache b/swift/codegen/templates/cpp_classes.mustache index 3e9d12084c9..15e33a378dc 100644 --- a/swift/codegen/templates/cpp_classes.mustache +++ b/swift/codegen/templates/cpp_classes.mustache @@ -33,6 +33,9 @@ struct {{name}}{{#final}} : Binding<{{name}}Tag>{{#bases}}, {{ref.name}}{{/bases {{ref.name}}::emit(id, out); {{/bases}} {{#fields}} + {{#is_predicate}} + if ({{name}}) out << {{trap_name}}{{trap_affix}}{id} << '\n'; + {{/is_predicate}} {{#is_optional}} {{^is_repeated}} if ({{name}}) out << {{trap_name}}{{trap_affix}}{id, *{{name}}} << '\n'; diff --git a/swift/codegen/templates/ql_class.mustache b/swift/codegen/templates/ql_class.mustache index 12ca76c9975..89da396be90 100644 --- a/swift/codegen/templates/ql_class.mustache +++ b/swift/codegen/templates/ql_class.mustache @@ -21,7 +21,7 @@ class {{name}}Base extends {{db_id}}{{#bases}}, {{.}}{{/bases}} { {{/final}} {{#properties}} - {{type}} get{{singular}}({{#is_repeated}}int index{{/is_repeated}}) { + {{type}} {{getter}}({{#is_repeated}}int index{{/is_repeated}}) { {{#type_is_class}} exists({{type}} {{local_var}} | {{tablename}}({{#tableparams}}{{^first}}, {{/first}}{{param}}{{/tableparams}}) @@ -34,13 +34,13 @@ class {{name}}Base extends {{db_id}}{{#bases}}, {{.}}{{/bases}} { } {{#is_repeated}} - {{type}} get{{indefinite_article}}{{singular}}() { - result = get{{singular}}(_) + {{type}} {{indefinite_getter}}() { + result = {{getter}}(_) } {{^is_optional}} int getNumberOf{{plural}}() { - result = count(get{{indefinite_article}}{{singular}}()) + result = count({{indefinite_getter}}()) } {{/is_optional}} {{/is_repeated}} diff --git a/swift/codegen/test/test_cpp.py b/swift/codegen/test/test_cpp.py index 74dbc2d2182..06ff50f8a51 100644 --- a/swift/codegen/test/test_cpp.py +++ b/swift/codegen/test/test_cpp.py @@ -27,14 +27,15 @@ def test_field_get_streamer(type, expected): assert f.get_streamer()("value") == expected -@pytest.mark.parametrize("is_optional,is_repeated,expected", [ - (False, False, True), - (True, False, False), - (False, True, False), - (True, True, False), +@pytest.mark.parametrize("is_optional,is_repeated,is_predicate,expected", [ + (False, False, False, True), + (True, False, False, False), + (False, True, False, False), + (True, True, False, False), + (False, False, True, False), ]) -def test_field_is_single(is_optional, is_repeated, expected): - f = cpp.Field("name", "type", is_optional=is_optional, is_repeated=is_repeated) +def test_field_is_single(is_optional, is_repeated, is_predicate, expected): + f = cpp.Field("name", "type", is_optional=is_optional, is_repeated=is_repeated, is_predicate=is_predicate) assert f.is_single is expected diff --git a/swift/codegen/test/test_cppgen.py b/swift/codegen/test/test_cppgen.py index ac37b7300c5..b4e766af9dc 100644 --- a/swift/codegen/test/test_cppgen.py +++ b/swift/codegen/test/test_cppgen.py @@ -92,6 +92,17 @@ def test_class_with_field(generate, type, expected, property_cls, optional, repe ] +def test_class_with_predicate(generate): + assert generate([ + schema.Class(name="MyClass", properties=[schema.PredicateProperty("prop")]), + ]) == [ + cpp.Class(name="MyClass", + fields=[cpp.Field("prop", "bool", trap_name="MyClassProp", is_predicate=True)], + trap_name="MyClasses", + final=True) + ] + + @pytest.mark.parametrize("name", ["start_line", "start_column", "end_line", "end_column", "index", "num_whatever", "width"]) def test_class_with_overridden_unsigned_field(generate, name): diff --git a/swift/codegen/test/test_dbschemegen.py b/swift/codegen/test/test_dbschemegen.py index 82521fb9892..54f1e1796f7 100644 --- a/swift/codegen/test/test_dbschemegen.py +++ b/swift/codegen/test/test_dbschemegen.py @@ -156,6 +156,33 @@ def test_final_class_with_repeated_field(opts, input, renderer, property_cls): ) +def test_final_class_with_predicate_field(opts, input, renderer): + input.classes = [ + schema.Class("Object", properties=[ + schema.PredicateProperty("foo"), + ]), + ] + assert generate(opts, renderer) == dbscheme.Scheme( + src=schema_file, + includes=[], + declarations=[ + dbscheme.Table( + name="objects", + columns=[ + dbscheme.Column('id', '@object', binding=True), + ] + ), + dbscheme.Table( + name="object_foo", + keyset=dbscheme.KeySet(["id"]), + columns=[ + dbscheme.Column('id', '@object'), + ] + ), + ], + ) + + def test_final_class_with_more_fields(opts, input, renderer): input.classes = [ schema.Class("Object", properties=[ @@ -164,6 +191,7 @@ def test_final_class_with_more_fields(opts, input, renderer): schema.OptionalProperty("three", "z"), schema.RepeatedProperty("four", "u"), schema.RepeatedOptionalProperty("five", "v"), + schema.PredicateProperty("six"), ]), ] assert generate(opts, renderer) == dbscheme.Scheme( @@ -204,6 +232,13 @@ def test_final_class_with_more_fields(opts, input, renderer): dbscheme.Column('five', 'v'), ] ), + dbscheme.Table( + name="object_six", + keyset=dbscheme.KeySet(["id"]), + columns=[ + dbscheme.Column('id', '@object'), + ] + ), ], ) diff --git a/swift/codegen/test/test_ql.py b/swift/codegen/test/test_ql.py index e87383695c6..caa513c1b15 100644 --- a/swift/codegen/test/test_ql.py +++ b/swift/codegen/test/test_ql.py @@ -12,31 +12,32 @@ def test_property_has_first_table_param_marked(): assert [p.param for p in prop.tableparams] == tableparams -def test_property_not_a_class(): - tableparams = ["x", "result", "y"] - prop = ql.Property("Prop", "foo", "props", tableparams) - assert not prop.type_is_class - assert [p.param for p in prop.tableparams] == tableparams - - -def test_property_is_a_class(): - tableparams = ["x", "result", "y"] - prop = ql.Property("Prop", "Foo", "props", tableparams) - assert prop.type_is_class - assert [p.param for p in prop.tableparams] == ["x", prop.local_var, "y"] - - -@pytest.mark.parametrize("name,expected_article", [ - ("Argument", "An"), - ("Element", "An"), - ("Integer", "An"), - ("Operator", "An"), - ("Unit", "A"), - ("Whatever", "A"), +@pytest.mark.parametrize("type,expected", [ + ("Foo", True), + ("Bar", True), + ("foo", False), + ("bar", False), + (None, False), ]) -def test_property_indefinite_article(name, expected_article): - prop = ql.Property(name, "Foo", "props", ["x"], plural="X") - assert prop.indefinite_article == expected_article +def test_property_is_a_class(type, expected): + tableparams = ["a", "result", "b"] + expected_tableparams = ["a", "x" if expected else "result", "b"] + prop = ql.Property("Prop", type, tableparams=tableparams) + assert prop.type_is_class is expected + assert [p.param for p in prop.tableparams] == expected_tableparams + + +@pytest.mark.parametrize("name,expected_getter", [ + ("Argument", "getAnArgument"), + ("Element", "getAnElement"), + ("Integer", "getAnInteger"), + ("Operator", "getAnOperator"), + ("Unit", "getAUnit"), + ("Whatever", "getAWhatever"), +]) +def test_property_indefinite_article(name, expected_getter): + prop = ql.Property(name, plural="X") + assert prop.indefinite_getter == expected_getter @pytest.mark.parametrize("plural,expected", [ @@ -49,9 +50,19 @@ def test_property_is_plural(plural, expected): assert prop.is_repeated is expected -def test_property_no_plural_no_indefinite_article(): +def test_property_no_plural_no_indefinite_getter(): prop = ql.Property("Prop", "Foo", "props", ["x"]) - assert prop.indefinite_article is None + assert prop.indefinite_getter is None + + +def test_property_getter(): + prop = ql.Property("Prop", "Foo") + assert prop.getter == "getProp" + + +def test_property_predicate_getter(): + prop = ql.Property("prop", is_predicate=True) + assert prop.getter == "prop" def test_class_sorts_bases(): diff --git a/swift/codegen/test/test_qlgen.py b/swift/codegen/test/test_qlgen.py index d273539e375..e5405406747 100644 --- a/swift/codegen/test/test_qlgen.py +++ b/swift/codegen/test/test_qlgen.py @@ -2,7 +2,7 @@ import subprocess import sys from swift.codegen import qlgen -from swift.codegen.lib import ql, paths +from swift.codegen.lib import ql from swift.codegen.test.utils import * @@ -141,6 +141,20 @@ def test_repeated_optional_property(opts, input, renderer): } +def test_predicate_property(opts, input, renderer): + input.classes = [ + schema.Class("MyObject", properties=[schema.PredicateProperty("is_foo")]), + ] + assert generate(opts, renderer) == { + import_file(): ql.ImportList([stub_import_prefix + "MyObject"]), + stub_path() / "MyObject.qll": ql.Stub(name="MyObject", base_import=gen_import_prefix + "MyObject"), + ql_output_path() / "MyObject.qll": ql.Class(name="MyObject", final=True, properties=[ + ql.Property(singular="isFoo", type="predicate", tablename="my_object_is_foo", tableparams=["this"], + is_predicate=True), + ]) + } + + def test_single_class_property(opts, input, renderer): input.classes = [ schema.Class("MyObject", properties=[schema.SingleProperty("foo", "Bar")]), diff --git a/swift/codegen/test/test_schema.py b/swift/codegen/test/test_schema.py index 36367a76b1f..c1b64648894 100644 --- a/swift/codegen/test/test_schema.py +++ b/swift/codegen/test/test_schema.py @@ -141,6 +141,7 @@ A: two: int? three: bool* four: x?* + five: predicate """) assert ret.classes == [ schema.Class(root_name, derived={'A'}), @@ -149,6 +150,7 @@ A: schema.OptionalProperty('two', 'int'), schema.RepeatedProperty('three', 'bool'), schema.RepeatedOptionalProperty('four', 'x'), + schema.PredicateProperty('five'), ]), ] diff --git a/swift/ql/lib/codeql/swift/generated/type/AnyFunctionType.qll b/swift/ql/lib/codeql/swift/generated/type/AnyFunctionType.qll index 847017e44f2..6add8032bc1 100644 --- a/swift/ql/lib/codeql/swift/generated/type/AnyFunctionType.qll +++ b/swift/ql/lib/codeql/swift/generated/type/AnyFunctionType.qll @@ -25,4 +25,6 @@ class AnyFunctionTypeBase extends @any_function_type, Type { string getAParamLabel() { result = getParamLabel(_) } int getNumberOfParamLabels() { result = count(getAParamLabel()) } + + predicate isThrowing() { any_function_type_is_throwing(this) } } diff --git a/swift/ql/lib/swift.dbscheme b/swift/ql/lib/swift.dbscheme index fbff22210bf..8d3ab41cf22 100644 --- a/swift/ql/lib/swift.dbscheme +++ b/swift/ql/lib/swift.dbscheme @@ -167,6 +167,11 @@ any_function_type_param_labels( string param_label: string ref ); +#keyset[id] +any_function_type_is_throwing( + int id: @any_function_type ref +); + @any_generic_type = @nominal_or_bound_generic_nominal_type | @unbound_generic_type