Rust: generate test code from schema docstrings

This generates test source files from code blocks in class docstrings.

By default the test code is generated as is, but it can optionally:
* be wrapped in a function providing an adequate context using
  `@rust.doc_test_function(name, *, lifetimes=(), return_type="()", **kwargs)`,
  with `kwargs` providing both generic and normal params depending on
  capitalization
* be skipped altogether using `@rust.skip_doc_test`

So for example an annotation like
```python
@rust.doc_test_function("foo",
                        lifetimes=("a",),
                        T="Eq",
                        x="&'a T",
                        y="&'a T",
                        return_type="&'a T")
```
will result in the following wrapper:
```rust
fn foo<'a, T: Eq>(x: &'a T, y: &'a T) -> &'a T {
    // example code here
}
```
This commit is contained in:
Paolo Tranquilli
2024-09-06 13:58:49 +02:00
parent 122e5a7598
commit 8c5cc2efdc
19 changed files with 199 additions and 35 deletions

View File

@@ -1,4 +1,4 @@
from . import dbschemegen, qlgen, trapgen, cppgen, rustgen
from . import dbschemegen, trapgen, cppgen, rustgen, rusttestgen, qlgen
def generate(target, opts, renderer):

View File

@@ -287,7 +287,7 @@ def _is_under_qltest_collapsed_hierarchy(cls: schema.Class, lookup: typing.Dict[
_is_in_qltest_collapsed_hierarchy(lookup[b], lookup) for b in cls.bases)
def _should_skip_qltest(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
def should_skip_qltest(cls: schema.Class, lookup: typing.Dict[str, schema.Class]):
return "qltest_skip" in cls.pragmas or not (
cls.final or "qltest_collapse_hierarchy" in cls.pragmas) or _is_under_qltest_collapsed_hierarchy(
cls, lookup)
@@ -413,7 +413,7 @@ def generate(opts, renderer):
if test_out:
for c in data.classes.values():
if _should_skip_qltest(c, data.classes):
if should_skip_qltest(c, data.classes):
continue
test_with = data.classes[c.test_with] if c.test_with else c
test_dir = test_out / test_with.group / test_with.name

View File

@@ -86,20 +86,24 @@ def generate(opts, renderer):
processor = Processor(schemaloader.load_file(opts.schema))
out = opts.rust_output
groups = set()
for group, classes in processor.get_classes().items():
group = group or "top"
groups.add(group)
with renderer.manage(generated=out.rglob("*.rs"),
stubs=(),
registry=opts.generated_registry,
force=opts.force) as renderer:
for group, classes in processor.get_classes().items():
group = group or "top"
groups.add(group)
renderer.render(
rust.ClassList(
classes,
opts.schema,
),
out / f"{group}.rs",
)
renderer.render(
rust.ClassList(
classes,
rust.ModuleList(
groups,
opts.schema,
),
out / f"{group}.rs",
out / f"mod.rs",
)
renderer.render(
rust.ModuleList(
groups,
opts.schema,
),
out / f"mod.rs",
)

View File

@@ -0,0 +1,70 @@
import dataclasses
import typing
from misc.codegen.loaders import schemaloader
from . import qlgen
@dataclasses.dataclass
class Param:
name: str
type: str
first: bool = False
@dataclasses.dataclass
class Function:
name: str
generic_params: list[Param]
params: list[Param]
return_type: str
def __post_init__(self):
if self.generic_params:
self.generic_params[0].first = True
if self.params:
self.params[0].first = True
@dataclasses.dataclass
class TestCode:
template: typing.ClassVar[str] = "rust_test_code"
code: str
function: Function | None = None
def generate(opts, renderer):
assert opts.ql_test_output
schema = schemaloader.load_file(opts.schema)
with renderer.manage(generated=opts.ql_test_output.rglob("gen_*.rs"),
stubs=(),
registry=opts.generated_registry,
force=opts.force) as renderer:
for cls in schema.classes.values():
if (qlgen.should_skip_qltest(cls, schema.classes) or
"rust_skip_test_from_doc" in cls.pragmas or
not cls.doc
):
continue
fn = cls.rust_doc_test_function
if fn:
generic_params = [Param(k, v) for k, v in fn.params.items() if k[0].isupper() or k[0] == "'"]
params = [Param(k, v) for k, v in fn.params.items() if k[0].islower()]
fn = Function(fn.name, generic_params, params, fn.return_type)
code = []
adding_code = False
for line in cls.doc:
match line, adding_code:
case "```", _:
adding_code = not adding_code
case _, False:
code.append(f"// {line}")
case _, True:
code.append(line)
if fn:
indent = 4 * " "
code = [indent + l for l in code]
test_with = schema.classes[cls.test_with] if cls.test_with else cls
test = opts.ql_test_output / test_with.group / test_with.name / f"gen_{cls.name.lower()}.rs"
renderer.render(TestCode(code="\n".join(code), function=fn), test)

View File

@@ -94,6 +94,7 @@ class Class:
default_doc_name: Optional[str] = None
hideable: bool = False
test_with: Optional[str] = None
rust_doc_test_function: Optional["FunctionInfo"] = None # TODO: parametrized pragmas
@property
def final(self):
@@ -202,3 +203,10 @@ def split_doc(doc):
while trimmed and not trimmed[0]:
trimmed.pop(0)
return trimmed
@dataclass
class FunctionInfo:
name: str
params: dict[str, str]
return_type: str

View File

@@ -52,6 +52,7 @@ class _SynthModifier(_schema.PropertyModifier, _Namespace):
qltest = _Namespace()
ql = _Namespace()
cpp = _Namespace()
rust = _Namespace()
synth = _SynthModifier()
@@ -156,6 +157,14 @@ _Pragma("ql_internal")
_Pragma("cpp_skip")
_Pragma("rust_skip_doc_test")
rust.doc_test_function = lambda name, *, lifetimes=(), return_type="()", **kwargs: _annotate(
rust_doc_test_function=_schema.FunctionInfo(name,
params={f"'{lifetime}": "" for lifetime in lifetimes} | kwargs,
return_type=return_type)
)
def group(name: str = "") -> _ClassDecorator:
return _annotate(group=name)

View File

@@ -56,6 +56,7 @@ def _get_class(cls: type) -> schema.Class:
],
doc=schema.split_doc(cls.__doc__),
default_doc_name=cls.__dict__.get("_doc_name"),
rust_doc_test_function=cls.__dict__.get("_rust_doc_test_function")
)

View File

@@ -0,0 +1,9 @@
// generated by {{generator}}
{{#function}}
fn {{name}}<{{#generic_params}}{{^first}}, {{/first}}{{name}}{{#type}}: {{.}}{{/type}}{{/generic_params}}>({{#params}}{{^first}}, {{/first}}{{name}}: {{type}}{{/params}}) -> {{return_type}} {
{{/function}}
{{code}}
{{#function}}
}
{{/function}}

12
rust/.generated.list generated
View File

@@ -1,33 +1,31 @@
extractor/src/generated/mod.rs 7cdfedcd68cf8e41134daf810c1af78624082b0c3e8be6570339b1a69a5d457e 7cdfedcd68cf8e41134daf810c1af78624082b0c3e8be6570339b1a69a5d457e
extractor/src/generated/top.rs 6a38e468c94037d0c00a1a463753de206b6c8a3ca16f11ee86b4165cd7c91645 6a38e468c94037d0c00a1a463753de206b6c8a3ca16f11ee86b4165cd7c91645
ql/lib/codeql/rust/elements/DbFile.qll 056d363e1ba5ec08cacb2e90b8a7a3b47f52ded5dc2289edd4e85921fc50f37e 18e6926f77265a3a6319ca2f3bf3d529d17d46cebdd2ef6753357fdc53c22c70
ql/lib/codeql/rust/elements/DbFileConstructor.qll ea93dc49b23b1c6d800ab9d0b9cacfa9dc661bfdb04b9e6efcad2bdb050fb0ab f7a891b1786604eee57a784733555b677e2580770d51d18073b59e7ca65df1d4
ql/lib/codeql/rust/elements/DbLocation.qll 1f694594e8e4ab65a8781cd443ad4f864447ca88e2cb65504aee5a779393c84d 003ec72275406eb8f5ddd6ccc2b258fb7c906d4bb2c0ef1ba235f291624321ca
ql/lib/codeql/rust/elements/DbLocationConstructor.qll 8848abace985818a5d3a6eddfc4cb200795970146d282b037b4f22ae6230b894 44dba880e17bb1072fa12451ccaae4830fd04dcc61f7403d35510309fde6906e
ql/lib/codeql/rust/elements/Declaration.qll d4ec5c83728f1837243caf2f27d06fd05ecdd2ca440112accff99bfd37b45e5f c1cd9b297be8b69207e75d24b29949b9f71c78406ee0ffd38d0b0810288d6140
ql/lib/codeql/rust/elements/Function.qll 220eb1d0e6b49e83b7674a9f505d3f529cf7f4d022ddca44fa6367e0d75daa0f d156cff59ecdcffc0d62041b801025b1aaedbc92d44efd107f93f6fd06c34a6d
ql/lib/codeql/rust/elements/FunctionConstructor.qll a9269b37182c0bf432f9b2b015691da5dbd64819b9bd25445af229d873014a91 69107a7503af14a51e091e6918094a4e9fc316a72de2e1514f001872ce0f2c0c
ql/lib/codeql/rust/elements/Locatable.qll dfc0235cf5aff0ec91d85375ac03998b0e6a69f3722b099330ab0a2161af988a bb1055f59ef7e95e89748765c79f1b5832aa18226f227e689a6801adfc6247ad
ql/lib/codeql/rust/elements/Location.qll 17a222ffe30ecfa252bfd2a5d6ef668f9579824b78d241d576c8c0112a9f93f0 9ae822cd9ce3c70824cccae500651f0cba70ad9c9195b611e7542cb928093b0b
ql/lib/codeql/rust/elements/Module.qll d8995b361cc672f86a314bd53bd3e4d1ddb26b6afde62eb7c380923810785af0 3c10180c812d89a8116ac6e32cbd4d7ac2f549c8a76d327ed75c764b09251d52
ql/lib/codeql/rust/elements/ModuleConstructor.qll 109ed8c1b5c61cc1d3e8613aa8bb8c168dc1943c93b5b622fa79665751b78318 601526c7f56578883d261d14653fdad08329f80fea71de14a5ac5ce671a8d436
ql/lib/codeql/rust/elements/UnknownFile.qll 638ac1c5baef4ab8eb98ef4a37202334290001ca183dab769f729b61f9fc0aa9 fae24f8b29c3e5a124e6c71c9bcb0e2bf4bb033eaf3348fd2db3644ce2803b43
ql/lib/codeql/rust/elements/UnknownLocation.qll eaadf1702e2f5df0c0917dd9e452a3ceb81e5c2d182675c32e988d826ac3fc71 9bf63fbd74c6114678189a62f63795156c0e5b9ad467c020f16842038e84a226
ql/lib/codeql/rust/elements.qll 658d3d26ac2685a077714d41d83ac47f87b0df1c4f0cc7d099c96e06bd1f9d0e 658d3d26ac2685a077714d41d83ac47f87b0df1c4f0cc7d099c96e06bd1f9d0e
ql/lib/codeql/rust/generated/DbFile.qll 4dbf1931124291e0d6a958ae882f8aeef006642f72adc7ff86cffd3a4e9a970a 4dbf1931124291e0d6a958ae882f8aeef006642f72adc7ff86cffd3a4e9a970a
ql/lib/codeql/rust/generated/DbLocation.qll 735d9351b5eb46a3231b528600dddec3a4122c18c210d4d632a8d4ceaf7f02e9 735d9351b5eb46a3231b528600dddec3a4122c18c210d4d632a8d4ceaf7f02e9
ql/lib/codeql/rust/generated/Declaration.qll 4487ac3f5ffa5b92e8628bc04b51e818d4ea1c9a333375cf1b729428d36a4ee7 6481d5e2d99a548f857213a283da75d45db8b3adac949e90fd5d17ceb5e22b54
ql/lib/codeql/rust/generated/Element.qll 21567fa7348dccdf69dd34e73cb6de7cc9c7e0f3f7bb419a1abd787f7dc851a1 21567fa7348dccdf69dd34e73cb6de7cc9c7e0f3f7bb419a1abd787f7dc851a1
ql/lib/codeql/rust/generated/File.qll 2eff5c882d044d2e360fe6382fb55e5a45f6ccb9df323cfa1fae41eae9d2a47f 87e7a906b3a1b4de056952e288ddd7f69fa7cb1bd9dc7dd3972868a9002ac1e4
ql/lib/codeql/rust/generated/Function.qll 6429619a284cadaf64d74ab408f57a399c7e5df2ad182bd38f77b580879d9e33 8c4cfa4f23b5ed461f0f8aa40c5833a953fc9ba3f99584f0f05cf7d7d6a9b617
ql/lib/codeql/rust/generated/Function.qll e41ca13aadd457a4fd853ac4e2f643de199e2b4d0a320add0cab890a9b72844a fc27a68775957e537eb95951cb7109e174b8dc5b0b5e0d8cc24901d431faad2f
ql/lib/codeql/rust/generated/Locatable.qll 9e9685bba50ad2220701f3465e63af9331f7f9dc548ad906ff954fc2ce0a4400 78c89b2cc78a357d682ab65310dd474603071f07c1eaaab07711714ce17549f2
ql/lib/codeql/rust/generated/Location.qll bce4c72988ec6fedd1439c60a37c45aa6147c962904709ef9f12206937174be4 d57000571771a2d997c50d9a43ef1c2f075960f847effa0e80ea91fd4c6b4d6c
ql/lib/codeql/rust/generated/Module.qll 2a931a4f2cdb2fee00ed83af045ea63d36b7dbd708e58c30445b5610feaae333 cd62add5c31a509f965aa294f44a1607ec7c62e3a9e3fe9ee063b3c814f4eb62
ql/lib/codeql/rust/generated/ParentChild.qll b44f149bff05a96ee16c88883f06d5d2c159a89423ec32b00765da6964af029e 1d13e54fbc27d9cc29405a21c2f4a63043cbb7aade317184f440a17d3f5645c4
ql/lib/codeql/rust/generated/PureSynthConstructors.qll 5eb1fc4f6a04172c34ae31e4931e4bf1f8b72fbe414c5f644731a45372d13573 5eb1fc4f6a04172c34ae31e4931e4bf1f8b72fbe414c5f644731a45372d13573
ql/lib/codeql/rust/generated/Raw.qll 921cb1afb5c1c3177acb557151755d4f97e7c65f656c5069d6f095b0e078074f a25fdad01e70bbab2d2663b152a4a5844677edcf0a0af2ec93c42dc3248ac9b2
ql/lib/codeql/rust/generated/Raw.qll 8d6d9e0cf2c9a8a82f9238e63904e9d19e4ae040d614fbfcab8c01c814576f2a 54c3f382587317b336fb76f496e566fd654008f3394271c695ae9a5605675c38
ql/lib/codeql/rust/generated/Synth.qll d278de9c2d06cb7549cd8f2e10ed186827a2ceab6ff46725ca76e78e7fecac72 acacd9afc5ca4a288e037a43375d933c3ba3cd8d08ef122b31695e74be260eb2
ql/lib/codeql/rust/generated/SynthConstructors.qll 35b36df0c4fff05bcbd4ed10b1e6fa2e58fe8d8c818e7805111044825788fc01 35b36df0c4fff05bcbd4ed10b1e6fa2e58fe8d8c818e7805111044825788fc01
ql/lib/codeql/rust/generated/UnknownFile.qll ec9d1a3f15ecbf1743d4e39cb3b2f217aa9b54951c93302c2c4c238c3f0ce595 ec9d1a3f15ecbf1743d4e39cb3b2f217aa9b54951c93302c2c4c238c3f0ce595
ql/lib/codeql/rust/generated/UnknownLocation.qll a19e2838c52d702d268ae530f3dbd6fcd8bb28a237a52636a960f225454103cf a19e2838c52d702d268ae530f3dbd6fcd8bb28a237a52636a960f225454103cf
ql/test/extractor-tests/generated/File/File.ql dec43be882fad904fab0c6447ca93633d801cb08ff8bec309befde7d2b9e5dda 74e1f1d698558c35fa03935cc34f4c8145d376b56d7657b18aeb338f5ca752cf
ql/test/extractor-tests/generated/Function/Function.ql ae5d44a85047d50d8fbd3b62290c6935f061f07076b0070998173957e54eb43f 3e7fb6fb82463b96577394213915d8deae5332acdec2fcc07aa3eb8560420edd
ql/test/extractor-tests/generated/Function/gen_function.rs d5c3cced4c377b05a4aa7e7b82deeb0f4c0bdf5dbca19490c589040ef79c9314 d5c3cced4c377b05a4aa7e7b82deeb0f4c0bdf5dbca19490c589040ef79c9314
ql/test/extractor-tests/generated/Module/MISSING_SOURCE.txt cc7c395e7c651d62596826b1a0bedf10f35d01b8afeef47600b4ddaf804f406e cc7c395e7c651d62596826b1a0bedf10f35d01b8afeef47600b4ddaf804f406e

8
rust/.gitattributes generated vendored
View File

@@ -1,18 +1,15 @@
/.generated.list linguist-generated
/.gitattributes linguist-generated
/extractor/src/generated/mod.rs linguist-generated
/extractor/src/generated/top.rs linguist-generated
/ql/lib/codeql/rust/elements/DbFile.qll linguist-generated
/ql/lib/codeql/rust/elements/DbFileConstructor.qll linguist-generated
/ql/lib/codeql/rust/elements/DbLocation.qll linguist-generated
/ql/lib/codeql/rust/elements/DbLocationConstructor.qll linguist-generated
/ql/lib/codeql/rust/elements/Declaration.qll linguist-generated
/ql/lib/codeql/rust/elements/Function.qll linguist-generated
/ql/lib/codeql/rust/elements/FunctionConstructor.qll linguist-generated
/ql/lib/codeql/rust/elements/Locatable.qll linguist-generated
/ql/lib/codeql/rust/elements/Location.qll linguist-generated
/ql/lib/codeql/rust/elements/Module.qll linguist-generated
/ql/lib/codeql/rust/elements/ModuleConstructor.qll linguist-generated
/ql/lib/codeql/rust/elements/UnknownFile.qll linguist-generated
/ql/lib/codeql/rust/elements/UnknownLocation.qll linguist-generated
/ql/lib/codeql/rust/elements.qll linguist-generated
/ql/lib/codeql/rust/generated/DbFile.qll linguist-generated
/ql/lib/codeql/rust/generated/DbLocation.qll linguist-generated
@@ -32,4 +29,5 @@
/ql/lib/codeql/rust/generated/UnknownLocation.qll linguist-generated
/ql/test/extractor-tests/generated/File/File.ql linguist-generated
/ql/test/extractor-tests/generated/Function/Function.ql linguist-generated
/ql/test/extractor-tests/generated/Function/gen_function.rs linguist-generated
/ql/test/extractor-tests/generated/Module/MISSING_SOURCE.txt linguist-generated

View File

@@ -1,5 +1,5 @@
# configuration file for Swift code generation default options
--generate=dbscheme,ql,rust
--generate=dbscheme,rusttest,ql,rust
--dbscheme=ql/lib/rust.dbscheme
--ql-output=ql/lib/codeql/rust/generated
--ql-stub-output=ql/lib/codeql/rust/elements

View File

@@ -4,6 +4,20 @@
private import codeql.rust.generated.Function
// the following QLdoc is generated: if you need to edit it, do it in the schema file
/**
* A function declaration. For example
* ```
* fn foo(x: u32) -> u64 { (x + 1).into()
* }
* ```
* A function declaration within a trait might not have a body:
* ```
* trait Trait {
* fn bar();
* }
* ```
*/
class Function extends Generated::Function {
override string toString() { result = this.getName() }
}

View File

@@ -14,6 +14,17 @@ import codeql.rust.elements.Declaration
*/
module Generated {
/**
* A function declaration. For example
* ```
* fn foo(x: u32) -> u64 { (x + 1).into()
* }
* ```
* A function declaration within a trait might not have a body:
* ```
* trait Trait {
* fn bar();
* }
* ```
* INTERNAL: Do not reference the `Generated::Function` class directly.
* Use the subclass `Function`, where the following predicates are available.
*/

View File

@@ -81,6 +81,17 @@ module Raw {
/**
* INTERNAL: Do not use.
* A function declaration. For example
* ```
* fn foo(x: u32) -> u64 { (x + 1).into()
* }
* ```
* A function declaration within a trait might not have a body:
* ```
* trait Trait {
* fn bar();
* }
* ```
*/
class Function extends @function, Declaration {
override string toString() { result = "Function" }

View File

@@ -1,3 +1 @@
| test.rs:0:0:0:11 | foo | getName: | foo |
| test.rs:1:0:1:11 | bar | getName: | bar |
| test.rs:2:0:2:11 | baz | getName: | baz |
| gen_function.rs:2:0:3:40 | foo | getName: | foo |

View File

@@ -0,0 +1,11 @@
// generated by codegen
fn my_test<'a, 'b, T: Eq>(x: &'a [T], y: &'b [T]) -> &'a [T] {
// A function declaration. For example
fn foo(x: u32) -> u64 { (x + 1).into()
}
// A function declaration within a trait might not have a body:
trait Trait {
fn bar();
}
}

View File

@@ -0,0 +1,11 @@
// generated by codegen
fn my_test<'a, 'b, T: Eq>(x: &'a [T], y: &'b [T]) -> &'a [T] {
// A function declaration. For example
fn foo(x: u32) -> u64 { (x + 1).into()
}
// A function declaration within a trait might not have a body:
trait Trait {
fn bar();
}
}

View File

@@ -1,3 +0,0 @@
fn foo() {}
fn bar() {}
fn baz() {}

View File

@@ -56,5 +56,19 @@ class Module(Declaration):
# TODO name
declarations: list[Declaration] | child
@rust.doc_test_function("my_test", lifetimes="ab", T="Eq", x="&'a [T]", y="&'b [T]", return_type="&'a [T]")
class Function(Declaration):
"""
A function declaration. For example
```
fn foo(x: u32) -> u64 { (x + 1).into()
}
```
A function declaration within a trait might not have a body:
```
trait Trait {
fn bar();
}
```
"""
name: string