#!/usr/bin/env python3 import logging import subprocess import inflection from swift.codegen.lib import schema, paths, generator, ql log = logging.getLogger(__name__) def get_ql_property(cls: schema.Class, prop: schema.Property): if prop.is_single: return ql.QlProperty( singular=inflection.camelize(prop.name), type=prop.type, tablename=inflection.tableize(cls.name), tableparams=["this"] + ["result" if p is prop else "_" for p in cls.properties if p.is_single], ) elif prop.is_optional: return ql.QlProperty( singular=inflection.camelize(prop.name), type=prop.type, tablename=inflection.tableize(f"{cls.name}_{prop.name}"), tableparams=["this", "result"], ) elif prop.is_repeated: return ql.QlProperty( singular=inflection.singularize(inflection.camelize(prop.name)), plural=inflection.pluralize(inflection.camelize(prop.name)), type=prop.type, tablename=inflection.tableize(f"{cls.name}_{prop.name}"), tableparams=["this", "index", "result"], params=[ql.QlParam("index", type="int")], ) def get_ql_class(cls: schema.Class): return ql.QlClass( name=cls.name, bases=cls.bases, final=not cls.derived, properties=[get_ql_property(cls, p) for p in cls.properties], dir=cls.dir, ) def get_import(file): stem = file.relative_to(paths.swift_dir / "ql/lib").with_suffix("") return str(stem).replace("/", ".") def get_types_used_by(cls: ql.QlClass): for b in cls.bases: yield b for p in cls.properties: yield p.type for param in p.params: yield param.type def get_classes_used_by(cls: ql.QlClass): return sorted(set(t for t in get_types_used_by(cls) if t[0].isupper())) def is_generated(file): with open(file) as contents: return next(contents).startswith("// generated") def format(codeql, files): format_cmd = [codeql, "query", "format", "--in-place", "--"] format_cmd.extend(str(f) for f in files) res = subprocess.run(format_cmd, check=True, stderr=subprocess.PIPE, text=True) for line in res.stderr.splitlines(): log.debug(line.strip()) def generate(opts, renderer): input = opts.schema out = opts.ql_output stub_out = opts.ql_stub_output existing = {q for q in out.rglob("*.qll")} existing |= {q for q in stub_out.rglob("*.qll") if is_generated(q)} data = schema.load(input) classes = [get_ql_class(cls) for cls in data.classes] imports = {} for c in classes: imports[c.name] = get_import(stub_out / c.path) for c in classes: qll = (out / c.path).with_suffix(".qll") c.imports = [imports[t] for t in get_classes_used_by(c)] renderer.render(c, qll) stub_file = (stub_out / c.path).with_suffix(".qll") if not stub_file.is_file() or is_generated(stub_file): stub = ql.QlStub(name=c.name, base_import=get_import(qll)) renderer.render(stub, stub_file) # for example path/to/syntax/generated -> path/to/syntax.qll include_file = stub_out.with_suffix(".qll") all_imports = ql.QlImportList([v for _, v in sorted(imports.items())]) renderer.render(all_imports, include_file) renderer.cleanup(existing) format(opts.codeql_binary, renderer.written) if __name__ == "__main__": generator.run(generate, tags=["schema", "ql"])