Files
codeql/swift/codegen/lib/ql.py
2022-07-28 10:26:11 +02:00

256 lines
6.4 KiB
Python

"""
QL files generation
`generate(opts, renderer)` will generate QL classes and manage stub files out of a `yml` schema file.
Each class (for example, `Foo`) in the schema triggers:
* generation of a `FooBase` class implementation translating all properties into appropriate getters
* if not created or already customized, generation of a stub file which defines `Foo` as extending `FooBase`. This can
be used to add hand-written code to `Foo`, which requires removal of the `// generated` header comment in that file.
All generated base classes actually import these customizations when referencing other classes.
Generated files that do not correspond any more to any class in the schema are deleted. Customized stubs are however
left behind and must be dealt with by hand.
"""
import pathlib
from dataclasses import dataclass, field
from typing import List, ClassVar, Union, Optional
import inflection
@dataclass
class Param:
param: str
first: bool = False
@dataclass
class Property:
singular: str
type: Optional[str] = None
tablename: Optional[str] = None
tableparams: List[Param] = field(default_factory=list)
plural: Optional[str] = None
first: bool = False
is_optional: bool = False
is_predicate: bool = False
is_child: bool = False
qltest_skip: bool = False
def __post_init__(self):
if self.tableparams:
self.tableparams = [Param(x) for x in self.tableparams]
self.tableparams[0].first = True
@property
def getter(self):
return f"get{self.singular}" if not self.is_predicate else self.singular
@property
def indefinite_getter(self):
if self.plural:
article = "An" if self.singular[0] in "AEIO" else "A"
return f"get{article}{self.singular}"
@property
def type_is_class(self):
return bool(self.type) and self.type[0].isupper()
@property
def is_repeated(self):
return bool(self.plural)
@property
def is_single(self):
return not (self.is_optional or self.is_repeated or self.is_predicate)
@dataclass
class Class:
template: ClassVar = 'ql_class'
name: str
bases: List[str] = field(default_factory=list)
final: bool = False
properties: List[Property] = field(default_factory=list)
dir: pathlib.Path = pathlib.Path()
imports: List[str] = field(default_factory=list)
qltest_skip: bool = False
qltest_collapse_hierarchy: bool = False
qltest_uncollapse_hierarchy: bool = False
has_db_id: bool = True
ipa: bool = False
def __post_init__(self):
self.bases = sorted(self.bases)
if self.properties:
self.properties[0].first = True
@property
def root(self) -> bool:
return not self.bases
@property
def path(self) -> pathlib.Path:
return self.dir / self.name
@property
def db_id(self) -> Optional[str]:
if self.has_db_id:
return "@" + inflection.underscore(self.name)
@dataclass
class Stub:
template: ClassVar = 'ql_stub'
name: str
base_import: str
@dataclass
class DbClasses:
template: ClassVar = 'ql_db'
classes: List[Class] = field(default_factory=list)
@dataclass
class ImportList:
template: ClassVar = 'ql_imports'
imports: List[str] = field(default_factory=list)
@dataclass
class GetParentImplementation:
template: ClassVar = 'ql_parent'
classes: List[Class] = field(default_factory=list)
@dataclass
class PropertyForTest:
getter: str
type: Optional[str] = None
is_single: bool = False
is_predicate: bool = False
is_repeated: bool = False
@dataclass
class ClassTester:
template: ClassVar = 'ql_test_class'
class_name: str
properties: List[PropertyForTest] = field(default_factory=list)
@dataclass
class PropertyTester:
template: ClassVar = 'ql_test_property'
class_name: str
property: PropertyForTest
@dataclass
class MissingTestInstructions:
template: ClassVar = 'ql_test_missing'
class Ipa:
@dataclass
class Class:
is_final: ClassVar = False
name: str
first: bool = False
@dataclass
class FinalClass(Class):
is_final: ClassVar = True
is_ipa_from: ClassVar = False
is_ipa_on: ClassVar = False
is_db: ClassVar = False
@property
def is_ipa(self):
return self.is_ipa_on or self.is_ipa_from
@dataclass
class Param:
param: str
type: str
first: bool = False
@dataclass
class FinalClassIpaFrom(FinalClass):
is_ipa_from: ClassVar = True
type: str = None
@property
def params(self):
return [Ipa.Param("id", self.type, first=True)]
@dataclass
class FinalClassIpaOn(FinalClass):
is_ipa_on: ClassVar = True
params: List["Ipa.Param"] = field(default_factory=list)
def __post_init__(self):
if self.params:
self.params[0].first = True
@property
def has_params(self) -> bool:
return bool(self.params)
@dataclass
class FinalClassDb(FinalClass):
is_db: ClassVar = True
subtracted_ipa_types: List["Ipa.Class"] = field(default_factory=list)
def subtract_type(self, type: str):
self.subtracted_ipa_types.append(Ipa.Class(type, first=not self.subtracted_ipa_types))
@property
def has_subtracted_ipa_types(self):
return bool(self.subtracted_ipa_types)
@property
def db_id(self):
return "@" + inflection.underscore(self.name)
@dataclass
class NonFinalClass(Class):
derived: List["Ipa.Class"] = field(default_factory=list)
root: bool = False
def __post_init__(self):
self.derived = [Ipa.Class(c) for c in self.derived]
if self.derived:
self.derived[0].first = True
@dataclass
class Types:
template: ClassVar = "ql_ipa_types"
root: str
final_classes: List["Ipa.FinalClass"] = field(default_factory=list)
non_final_classes: List["Ipa.NonFinalClass"] = field(default_factory=list)
def __post_init__(self):
if self.final_classes:
self.final_classes[0].first = True
@dataclass
class ConstructorStub:
template: ClassVar = "ql_ipa_constructor_stub"
cls: Union["Ipa.FinalClassIpaFrom", "Ipa.FinalClassIpaOn"]