mirror of
https://github.com/github/codeql.git
synced 2025-12-17 01:03:14 +01:00
Codegen: allow full annotation of classes
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
""" schema format representation """
|
""" schema format representation """
|
||||||
|
import abc
|
||||||
import typing
|
import typing
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Set, Union, Dict, Optional
|
from typing import List, Set, Union, Dict, Optional
|
||||||
@@ -136,40 +137,29 @@ predicate_marker = object()
|
|||||||
TypeRef = Union[type, str]
|
TypeRef = Union[type, str]
|
||||||
|
|
||||||
|
|
||||||
@functools.singledispatch
|
|
||||||
def get_type_name(arg: TypeRef) -> str:
|
def get_type_name(arg: TypeRef) -> str:
|
||||||
raise Error(f"Not a schema type or string ({arg})")
|
match arg:
|
||||||
|
case type():
|
||||||
|
return arg.__name__
|
||||||
|
case str():
|
||||||
|
return arg
|
||||||
|
case _:
|
||||||
|
raise Error(f"Not a schema type or string ({arg})")
|
||||||
|
|
||||||
|
|
||||||
@get_type_name.register
|
|
||||||
def _(arg: type):
|
|
||||||
return arg.__name__
|
|
||||||
|
|
||||||
|
|
||||||
@get_type_name.register
|
|
||||||
def _(arg: str):
|
|
||||||
return arg
|
|
||||||
|
|
||||||
|
|
||||||
@functools.singledispatch
|
|
||||||
def _make_property(arg: object) -> Property:
|
def _make_property(arg: object) -> Property:
|
||||||
if arg is predicate_marker:
|
match arg:
|
||||||
return PredicateProperty()
|
case _ if arg is predicate_marker:
|
||||||
raise Error(f"Illegal property specifier {arg}")
|
return PredicateProperty()
|
||||||
|
case str() | type():
|
||||||
|
return SingleProperty(type=get_type_name(arg))
|
||||||
|
case Property():
|
||||||
|
return arg
|
||||||
|
case _:
|
||||||
|
raise Error(f"Illegal property specifier {arg}")
|
||||||
|
|
||||||
|
|
||||||
@_make_property.register(str)
|
class PropertyModifier(abc.ABC):
|
||||||
@_make_property.register(type)
|
|
||||||
def _(arg: TypeRef):
|
|
||||||
return SingleProperty(type=get_type_name(arg))
|
|
||||||
|
|
||||||
|
|
||||||
@_make_property.register
|
|
||||||
def _(arg: Property):
|
|
||||||
return arg
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyModifier:
|
|
||||||
""" Modifier of `Property` objects.
|
""" Modifier of `Property` objects.
|
||||||
Being on the right of `|` it will trigger construction of a `Property` from
|
Being on the right of `|` it will trigger construction of a `Property` from
|
||||||
the left operand.
|
the left operand.
|
||||||
@@ -180,8 +170,14 @@ class PropertyModifier:
|
|||||||
self.modify(ret)
|
self.modify(ret)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def __invert__(self) -> "PropertyModifier":
|
||||||
|
return self.negate()
|
||||||
|
|
||||||
def modify(self, prop: Property):
|
def modify(self, prop: Property):
|
||||||
raise NotImplementedError
|
...
|
||||||
|
|
||||||
|
def negate(self) -> "PropertyModifier":
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
def split_doc(doc):
|
def split_doc(doc):
|
||||||
|
|||||||
@@ -1,40 +1,66 @@
|
|||||||
from typing import Callable as _Callable
|
from typing import Callable as _Callable, List as _List
|
||||||
from misc.codegen.lib import schema as _schema
|
from misc.codegen.lib import schema as _schema
|
||||||
import inspect as _inspect
|
import inspect as _inspect
|
||||||
from dataclasses import dataclass as _dataclass
|
from dataclasses import dataclass as _dataclass
|
||||||
|
|
||||||
|
from misc.codegen.lib.schema import Property
|
||||||
|
|
||||||
|
|
||||||
|
@_dataclass
|
||||||
class _ChildModifier(_schema.PropertyModifier):
|
class _ChildModifier(_schema.PropertyModifier):
|
||||||
|
child: bool = True
|
||||||
|
|
||||||
def modify(self, prop: _schema.Property):
|
def modify(self, prop: _schema.Property):
|
||||||
if prop.type is None or prop.type[0].islower():
|
if prop.type is None or prop.type[0].islower():
|
||||||
raise _schema.Error("Non-class properties cannot be children")
|
raise _schema.Error("Non-class properties cannot be children")
|
||||||
if prop.is_unordered:
|
if prop.is_unordered:
|
||||||
raise _schema.Error("Set properties cannot be children")
|
raise _schema.Error("Set properties cannot be children")
|
||||||
prop.is_child = True
|
prop.is_child = self.child
|
||||||
|
|
||||||
|
def negate(self) -> _schema.PropertyModifier:
|
||||||
|
return _ChildModifier(False)
|
||||||
|
|
||||||
|
# make ~doc same as doc(None)
|
||||||
|
|
||||||
|
|
||||||
|
class _DocModifierMetaclass(type(_schema.PropertyModifier)):
|
||||||
|
def __invert__(self) -> _schema.PropertyModifier:
|
||||||
|
return _DocModifier(None)
|
||||||
|
|
||||||
|
|
||||||
@_dataclass
|
@_dataclass
|
||||||
class _DocModifier(_schema.PropertyModifier):
|
class _DocModifier(_schema.PropertyModifier, metaclass=_DocModifierMetaclass):
|
||||||
doc: str
|
doc: str | None
|
||||||
|
|
||||||
def modify(self, prop: _schema.Property):
|
def modify(self, prop: _schema.Property):
|
||||||
if "\n" in self.doc or self.doc[-1] == ".":
|
if self.doc and ("\n" in self.doc or self.doc[-1] == "."):
|
||||||
raise _schema.Error("No newlines or trailing dots are allowed in doc, did you intend to use desc?")
|
raise _schema.Error("No newlines or trailing dots are allowed in doc, did you intend to use desc?")
|
||||||
prop.doc = self.doc
|
prop.doc = self.doc
|
||||||
|
|
||||||
|
def negate(self) -> _schema.PropertyModifier:
|
||||||
|
return _DocModifier(None)
|
||||||
|
|
||||||
|
|
||||||
|
# make ~desc same as desc(None)
|
||||||
|
class _DescModifierMetaclass(type(_schema.PropertyModifier)):
|
||||||
|
def __invert__(self) -> _schema.PropertyModifier:
|
||||||
|
return _DescModifier(None)
|
||||||
|
|
||||||
|
|
||||||
@_dataclass
|
@_dataclass
|
||||||
class _DescModifier(_schema.PropertyModifier):
|
class _DescModifier(_schema.PropertyModifier, metaclass=_DescModifierMetaclass):
|
||||||
description: str
|
description: str | None
|
||||||
|
|
||||||
def modify(self, prop: _schema.Property):
|
def modify(self, prop: _schema.Property):
|
||||||
prop.description = _schema.split_doc(self.description)
|
prop.description = _schema.split_doc(self.description)
|
||||||
|
|
||||||
|
def negate(self) -> _schema.PropertyModifier:
|
||||||
|
return _DescModifier(None)
|
||||||
|
|
||||||
|
|
||||||
def include(source: str):
|
def include(source: str):
|
||||||
# add to `includes` variable in calling context
|
# add to `includes` variable in calling context
|
||||||
_inspect.currentframe().f_back.f_locals.setdefault(
|
_inspect.currentframe().f_back.f_locals.setdefault("includes", []).append(source)
|
||||||
"__includes", []).append(source)
|
|
||||||
|
|
||||||
|
|
||||||
class _Namespace:
|
class _Namespace:
|
||||||
@@ -44,9 +70,15 @@ class _Namespace:
|
|||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@_dataclass
|
||||||
class _SynthModifier(_schema.PropertyModifier, _Namespace):
|
class _SynthModifier(_schema.PropertyModifier, _Namespace):
|
||||||
|
synth: bool = True
|
||||||
|
|
||||||
def modify(self, prop: _schema.Property):
|
def modify(self, prop: _schema.Property):
|
||||||
prop.synth = True
|
prop.synth = self.synth
|
||||||
|
|
||||||
|
def negate(self) -> "PropertyModifier":
|
||||||
|
return _SynthModifier(False)
|
||||||
|
|
||||||
|
|
||||||
qltest = _Namespace()
|
qltest = _Namespace()
|
||||||
@@ -63,22 +95,35 @@ class _Pragma(_schema.PropertyModifier):
|
|||||||
For schema classes it acts as a python decorator with `@`.
|
For schema classes it acts as a python decorator with `@`.
|
||||||
"""
|
"""
|
||||||
pragma: str
|
pragma: str
|
||||||
|
remove: bool = False
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
namespace, _, name = self.pragma.partition('_')
|
namespace, _, name = self.pragma.partition('_')
|
||||||
setattr(globals()[namespace], name, self)
|
setattr(globals()[namespace], name, self)
|
||||||
|
|
||||||
def modify(self, prop: _schema.Property):
|
def modify(self, prop: _schema.Property):
|
||||||
prop.pragmas.append(self.pragma)
|
self._apply(prop.pragmas)
|
||||||
|
|
||||||
|
def negate(self) -> "PropertyModifier":
|
||||||
|
return _Pragma(self.pragma, remove=True)
|
||||||
|
|
||||||
def __call__(self, cls: type) -> type:
|
def __call__(self, cls: type) -> type:
|
||||||
""" use this pragma as a decorator on classes """
|
""" use this pragma as a decorator on classes """
|
||||||
if "_pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas
|
if "_pragmas" in cls.__dict__: # not using hasattr as we don't want to land on inherited pragmas
|
||||||
cls._pragmas.append(self.pragma)
|
self._apply(cls._pragmas)
|
||||||
else:
|
elif not self.remove:
|
||||||
cls._pragmas = [self.pragma]
|
cls._pragmas = [self.pragma]
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
def _apply(self, pragmas: _List[str]) -> None:
|
||||||
|
if self.remove:
|
||||||
|
try:
|
||||||
|
pragmas.remove(self.pragma)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
pragmas.append(self.pragma)
|
||||||
|
|
||||||
|
|
||||||
class _Optionalizer(_schema.PropertyModifier):
|
class _Optionalizer(_schema.PropertyModifier):
|
||||||
def modify(self, prop: _schema.Property):
|
def modify(self, prop: _schema.Property):
|
||||||
@@ -172,7 +217,28 @@ synth.on_arguments = lambda **kwargs: _annotate(
|
|||||||
synth=_schema.SynthInfo(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()}))
|
||||||
|
|
||||||
|
|
||||||
def annotate(annotated_cls: type) -> _Callable[[type], None]:
|
class _PropertyModifierList(_schema.PropertyModifier):
|
||||||
|
def __init__(self):
|
||||||
|
self._mods = []
|
||||||
|
|
||||||
|
def __or__(self, other: _schema.PropertyModifier):
|
||||||
|
self._mods.append(other)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def modify(self, prop: Property):
|
||||||
|
for m in self._mods:
|
||||||
|
m.modify(prop)
|
||||||
|
|
||||||
|
|
||||||
|
class _PropertyAnnotation:
|
||||||
|
def __or__(self, other: _schema.PropertyModifier):
|
||||||
|
return _PropertyModifierList() | other
|
||||||
|
|
||||||
|
|
||||||
|
_ = _PropertyAnnotation()
|
||||||
|
|
||||||
|
|
||||||
|
def annotate(annotated_cls: type) -> _Callable[[type], _PropertyAnnotation]:
|
||||||
"""
|
"""
|
||||||
Add or modify schema annotations after a class has been defined
|
Add or modify schema annotations after a class has been defined
|
||||||
For the moment, only docstring annotation is supported. In the future, any kind of
|
For the moment, only docstring annotation is supported. In the future, any kind of
|
||||||
@@ -180,9 +246,17 @@ def annotate(annotated_cls: type) -> _Callable[[type], None]:
|
|||||||
|
|
||||||
The name of the class used for annotation must be `_`
|
The name of the class used for annotation must be `_`
|
||||||
"""
|
"""
|
||||||
def decorator(cls: type) -> None:
|
def decorator(cls: type) -> _PropertyAnnotation:
|
||||||
if cls.__name__ != "_":
|
if cls.__name__ != "_":
|
||||||
raise _schema.Error("Annotation classes must be named _")
|
raise _schema.Error("Annotation classes must be named _")
|
||||||
annotated_cls.__doc__ = cls.__doc__
|
annotated_cls.__doc__ = cls.__doc__
|
||||||
return None
|
for p, a in cls.__annotations__.items():
|
||||||
|
if p in annotated_cls.__annotations__:
|
||||||
|
annotated_cls.__annotations__[p] |= a
|
||||||
|
elif isinstance(a, (_PropertyAnnotation, _PropertyModifierList)):
|
||||||
|
raise _schema.Error(f"annotated property {p} not present in annotated class "
|
||||||
|
f"{annotated_cls.__name__}")
|
||||||
|
else:
|
||||||
|
annotated_cls.__annotations__[p] = a
|
||||||
|
return _
|
||||||
return decorator
|
return decorator
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def load(m: types.ModuleType) -> schema.Schema:
|
|||||||
for name, data in m.__dict__.items():
|
for name, data in m.__dict__.items():
|
||||||
if hasattr(defs, name):
|
if hasattr(defs, name):
|
||||||
continue
|
continue
|
||||||
if name == "__includes":
|
if name == "includes":
|
||||||
includes = data
|
includes = data
|
||||||
continue
|
continue
|
||||||
if name.startswith("__") or name == "_":
|
if name.startswith("__") or name == "_":
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from misc.codegen.lib.schemadefs import optional
|
||||||
|
|
||||||
from misc.codegen.test.utils import *
|
from misc.codegen.test.utils import *
|
||||||
from misc.codegen.lib import schemadefs as defs
|
from misc.codegen.lib import schemadefs as defs
|
||||||
@@ -774,6 +775,63 @@ def test_annotate_docstring():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotate_fields():
|
||||||
|
@load
|
||||||
|
class data:
|
||||||
|
class Root:
|
||||||
|
x: defs.int
|
||||||
|
y: defs.optional["Root"] | defs.child
|
||||||
|
|
||||||
|
@defs.annotate(Root)
|
||||||
|
class _:
|
||||||
|
x: defs._ | defs.doc("foo")
|
||||||
|
y: defs._ | defs.ql.internal
|
||||||
|
z: defs.string
|
||||||
|
|
||||||
|
assert data.classes == {
|
||||||
|
"Root": schema.Class("Root", properties=[
|
||||||
|
schema.SingleProperty("x", "int", doc="foo"),
|
||||||
|
schema.OptionalProperty("y", "Root", pragmas=["ql_internal"], is_child=True),
|
||||||
|
schema.SingleProperty("z", "string"),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotate_fields_negations():
|
||||||
|
@load
|
||||||
|
class data:
|
||||||
|
class Root:
|
||||||
|
x: defs.int | defs.ql.internal | defs.qltest.skip
|
||||||
|
y: defs.optional["Root"] | defs.child | defs.desc("foo\nbar\n")
|
||||||
|
z: defs.string | defs.synth | defs.doc("foo")
|
||||||
|
|
||||||
|
@defs.annotate(Root)
|
||||||
|
class _:
|
||||||
|
x: defs._ | ~defs.ql.internal
|
||||||
|
y: defs._ | ~defs.child | ~defs.ql.internal | ~defs.desc
|
||||||
|
z: defs._ | ~defs.synth | ~defs.doc
|
||||||
|
|
||||||
|
assert data.classes == {
|
||||||
|
"Root": schema.Class("Root", properties=[
|
||||||
|
schema.SingleProperty("x", "int", pragmas=["qltest_skip"]),
|
||||||
|
schema.OptionalProperty("y", "Root"),
|
||||||
|
schema.SingleProperty("z", "string"),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotate_non_existing_field():
|
||||||
|
with pytest.raises(schema.Error):
|
||||||
|
@load
|
||||||
|
class data:
|
||||||
|
class Root:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@defs.annotate(Root)
|
||||||
|
class _:
|
||||||
|
x: defs._ | defs.doc("foo")
|
||||||
|
|
||||||
|
|
||||||
def test_annotate_not_underscore():
|
def test_annotate_not_underscore():
|
||||||
with pytest.raises(schema.Error):
|
with pytest.raises(schema.Error):
|
||||||
@load
|
@load
|
||||||
|
|||||||
@@ -11,6 +11,3 @@ For how documentation of generated QL code works, please read `misc/codegen/sche
|
|||||||
|
|
||||||
from .prelude import *
|
from .prelude import *
|
||||||
from .ast import *
|
from .ast import *
|
||||||
|
|
||||||
include("../shared/tree-sitter-extractor/src/generator/prefix.dbscheme")
|
|
||||||
include("prefix.dbscheme")
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from misc.codegen.lib.schemadefs import *
|
from misc.codegen.lib.schemadefs import *
|
||||||
|
|
||||||
|
include("../shared/tree-sitter-extractor/src/generator/prefix.dbscheme")
|
||||||
|
include("prefix.dbscheme")
|
||||||
|
|
||||||
@qltest.skip
|
@qltest.skip
|
||||||
class Element:
|
class Element:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user