diff --git a/README.md b/README.md index 03aed26..8f2a89c 100644 --- a/README.md +++ b/README.md @@ -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::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::String getTaggedString() { + result.make(TTagA(), "string value") +} + +from Tagged::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 diff --git a/src/qtil/strings/TaggedString.qll b/src/qtil/strings/TaggedString.qll new file mode 100644 index 0000000..68f0e23 --- /dev/null +++ b/src/qtil/strings/TaggedString.qll @@ -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::String`. + * + * See `Tagged::String` for more details. + */ +module Tagged { + /** + * 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::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::String getTaggedString() { + * result.make(TOptionA(), "string value") + * } + * ``` + */ + bindingset[this] + class String extends Final::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 } + } +} diff --git a/src/qtil/testing/impl/Qnit.qll b/src/qtil/testing/impl/Qnit.qll index c06bd7e..67ddcda 100644 --- a/src/qtil/testing/impl/Qnit.qll +++ b/src/qtil/testing/impl/Qnit.qll @@ -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. @@ -10,7 +12,7 @@ private import qtil.inheritance.UnderlyingString * - `isFailing()`/`isPassing()`: Primarily for internal use. */ bindingset[this] -class Qnit extends UnderlyingString { +class Qnit extends InfInstance::String>::Type { /** * Call this method inside of `Test.run(Qnit test)` to report a failing test case. * @@ -18,7 +20,7 @@ class Qnit extends UnderlyingString { * 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. @@ -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() } } diff --git a/src/qtil/testing/impl/TestResult.qll b/src/qtil/testing/impl/TestResult.qll new file mode 100644 index 0000000..be1bd47 --- /dev/null +++ b/src/qtil/testing/impl/TestResult.qll @@ -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() } +} diff --git a/test/qtil/strings/TaggedStringTest.actual b/test/qtil/strings/TaggedStringTest.actual new file mode 100644 index 0000000..6d15d97 --- /dev/null +++ b/test/qtil/strings/TaggedStringTest.actual @@ -0,0 +1 @@ +| All 2 tests passed. | diff --git a/test/qtil/strings/TaggedStringTest.expected b/test/qtil/strings/TaggedStringTest.expected new file mode 100644 index 0000000..6d15d97 --- /dev/null +++ b/test/qtil/strings/TaggedStringTest.expected @@ -0,0 +1 @@ +| All 2 tests passed. | diff --git a/test/qtil/strings/TaggedStringTest.ql b/test/qtil/strings/TaggedStringTest.ql new file mode 100644 index 0000000..5c9b1fc --- /dev/null +++ b/test/qtil/strings/TaggedStringTest.ql @@ -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::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::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") + } +}