Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,27 @@ select b.toUppercase().toString()
See also the module `Chars` which defines standard nullary predicates that common characters, for
instance, `Qtil::Chars::dollar()` holds for the result `"$"`,`Qtil::Chars::a()` holds for `"a"`, and `Qtil::Chars::upperA()` holds for `"A"`.

**Tagged strings**: Attach a tag class to string values with the `Tagged<YourTag>::String` type:

```
newtype TMyTag = TTagA() or TTagB();
class MyTag extends TMyTag {
string toString() {
this = TTagA() and result = "tag_a"
or
this = TTagB() and result = "tag_b"
}
}

Tagged<MyTag>::String getTaggedString() {
result.make(TTagA(), "string value")
}

from Tagged<MyTag>::String str
where str = getTaggedString()
select str.getTag(), str.getStr()
```

### ASTs:

The following modules are usable by importing `qtil.lang`, for instance, `qtil.cpp`. However, the
Expand Down
91 changes: 91 additions & 0 deletions src/qtil/strings/TaggedString.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
private import qtil.parameterization.SignatureTypes
private import qtil.inheritance.UnderlyingString
private import qtil.parameterization.Finalize

/**
* A module for creating strings with an enum-like tag value.
*
* To use this module, create a type which is uniquely identified by its `toString()` member
* predicate. Then you may refer to a tagged string by type `Tagged<YourTag>::String`.
*
* See `Tagged::String` for more details.
*/
module Tagged<FiniteStringableType Tag> {
/**
* A class for representing strings with an enum-like tag value.
*
* To use this class, define a type which is uniquely identified by its `toString()` member
* predicate. Then refer to this class as `Tagged<YourTag>::String`.
*
* REQUIREMENTS: The toString() method of a tag must return non-overlapping results; no toString()
* may begin with or equal the toString() result from another instance (for instance, `"foo"` and
* `"foobar"` would be considered overlapping). The Tag type must be finite (not have
* `bindingset[this]`) and the toString() member predicate must be reversable (not have
* `bindingset[this]`).
*
* Tagged strings may be "constructed" by calling `s.make(tag, str)`, or with
* `s.make(str).getTag() = ...`.
*
* Example usage:
* ```ql
* newtype TMyEnum = TOptionA() or TOptionB();
* class MyEnum extends TMyEnum {
* string toString() {
* this = TOptionA() and result = "option_a"
* or
* this = TOptionB() and result = "option_b"
* }
* }
*
* Tagged<MyEnum>::String getTaggedString() {
* result.make(TOptionA(), "string value")
* }
* ```
*/
bindingset[this]
class String extends Final<UnderlyingString>::Type {
Tag tag;
string strVal;

String() { this = tag.toString() + strVal }

/**
* Holds if this tagged string has the given tag and string, which uniquely identifies and
* therefore may "make" this instance.
*/
bindingset[mstrVal]
bindingset[this]
predicate make(Tag mtag, string mstrVal) { this = mtag.toString() + mstrVal }

/**
* Holds if the tagged string has the given string contents, and any tag.
*
* To also set the tag of this string, either use `make(tag, str)`, or constrain the tag of
* the result of this predicate with `make(str).getTag() = ...`.
*/
bindingset[mstrVal]
bindingset[result]
String make(string mstrVal) {
make(_, mstrVal) and
result = this
}

/**
* Get the tag of this tagged string.
*/
bindingset[this]
Tag getTag() { result = tag }

/**
* Holds if this tagged string has the given tag.
*/
bindingset[this]
predicate isTagged(Tag mtag) { tag = mtag }

/**
* Get the string contents of this tagged string.
*/
bindingset[this]
string getStr() { result = strVal }
}
}
16 changes: 9 additions & 7 deletions src/qtil/testing/impl/Qnit.qll
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
private import qtil.inheritance.UnderlyingString
private import qtil.inheritance.Instance
private import qtil.strings.TaggedString
private import qtil.testing.impl.TestResult

/**
* A string that is re-typedef'd to Qnit for the sake of a pretty API.
Expand All @@ -10,15 +12,15 @@ private import qtil.inheritance.UnderlyingString
* - `isFailing()`/`isPassing()`: Primarily for internal use.
*/
bindingset[this]
class Qnit extends UnderlyingString {
class Qnit extends InfInstance<Tagged<TestResult>::String>::Type {
/**
* Call this method inside of `Test.run(Qnit test)` to report a failing test case.
*
* It is recommended to use unique strings for each test case, as this will allow you to
* uniquely identify which tests failed, due to the way QL works.
*/
bindingset[description]
predicate fail(string description) { this = "FAILURE: " + description }
predicate fail(string description) { inst().make(description).getTag().isFail() }

/**
* Call this method inside of `Test.run(Qnit test)` to report a passing test case.
Expand All @@ -27,20 +29,20 @@ class Qnit extends UnderlyingString {
* properly count the number of tests that passed, due to the way QL works.
*/
bindingset[name]
predicate pass(string name) { this = "PASS: " + name }
predicate pass(string name) { inst().make(name).getTag().isPass() }

/**
* Mostly intended for internal purposes, holds if the test fails.
*/
bindingset[this]
predicate isFailing() { str().matches("FAILURE: %") }
predicate isFailing() { inst().getTag().isFail() }

/**
* Mostly intended for internal purposes, holds if the test passes.
*/
bindingset[this]
predicate isPassing() { str().matches("PASS: %") }
predicate isPassing() { inst().getTag().isPass() }

bindingset[this]
string getDescription() { result = str().regexpReplaceAll("^(FAILURE|PASS): ", "") }
string getDescription() { result = inst().getStr() }
}
18 changes: 18 additions & 0 deletions src/qtil/testing/impl/TestResult.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
private import qtil.strings.TaggedString

private newtype TTestResult =
TPass() or
TFail()

class TestResult extends TTestResult {
string toString() {
// This resulting string will be printed and shown to users
this = TPass() and result = "PASS: "
or
this = TFail() and result = "FAILURE: "
}

predicate isPass() { this = TPass() }

predicate isFail() { this = TFail() }
}
1 change: 1 addition & 0 deletions test/qtil/strings/TaggedStringTest.actual
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| All 2 tests passed. |
1 change: 1 addition & 0 deletions test/qtil/strings/TaggedStringTest.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| All 2 tests passed. |
30 changes: 30 additions & 0 deletions test/qtil/strings/TaggedStringTest.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import qtil.strings.TaggedString
import qtil.testing.Qnit

class Tag extends string {
Tag() { this in ["tagA", "tagB"] }

string toString() { result = super.toString() }
}

class TestMakeTwoArguments extends Test, Case {
override predicate run(Qnit test) {
if
exists(Tagged<Tag>::String str | str.make("tagA", "string value") |
str.getTag() = "tagA" and str.getStr() = "string value"
)
then test.pass("make(tag, str) works correctly")
else test.fail("make(tag, str) didn't work correctly")
}
}

class TestMakeOneArgument extends Test, Case {
override predicate run(Qnit test) {
if
exists(Tagged<Tag>::String str | str.make("string value").isTagged("tagA") |
str.getTag() = "tagA" and str.getStr() = "string value"
)
then test.pass("make(str).isTagged(tag) works correctly")
else test.fail("make(str).isTagged(tag) didn't work correctly")
}
}