From 39b2d5904581b2b07923c08b2304ef22ac6adac5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:51:23 +0000 Subject: [PATCH 1/3] Initial plan From 64e1d0f7120f1922f4a6a85cf9882347da40c64e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:44:24 +0000 Subject: [PATCH 2/3] Add PlaceholderList alert module Co-authored-by: MichaelRFairhurst <1627771+MichaelRFairhurst@users.noreply.github.com> Agent-Logs-Url: https://github.com/advanced-security/codeql-qtil/sessions/2e31ff0c-05bb-412a-9027-d6546a1fff77 --- .gitignore | 1 + src/qtil/Qtil.qll | 1 + src/qtil/list/PlaceholderList.qll | 246 ++++++++++++++++++++ test/qtil/list/PlaceholderListTest.expected | 1 + test/qtil/list/PlaceholderListTest.ql | 158 +++++++++++++ 5 files changed, 407 insertions(+) create mode 100644 .gitignore create mode 100644 src/qtil/list/PlaceholderList.qll create mode 100644 test/qtil/list/PlaceholderListTest.expected create mode 100644 test/qtil/list/PlaceholderListTest.ql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2886f99 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +codeql_home/ diff --git a/src/qtil/Qtil.qll b/src/qtil/Qtil.qll index 3595e30..bd2f497 100644 --- a/src/qtil/Qtil.qll +++ b/src/qtil/Qtil.qll @@ -1,6 +1,7 @@ module Qtil { import qtil.list.CondensedList import qtil.list.Ordered + import qtil.list.PlaceholderList import qtil.locations.NullLocation import qtil.locations.OptionalLocation import qtil.locations.StringLocation diff --git a/src/qtil/list/PlaceholderList.qll b/src/qtil/list/PlaceholderList.qll new file mode 100644 index 0000000..5d91231 --- /dev/null +++ b/src/qtil/list/PlaceholderList.qll @@ -0,0 +1,246 @@ +/** + * A module for combining multiple placeholder results into a single alert message, when + * there may be a variable number of placeholder values per element. + * + * When a query finds multiple related elements (placeholders) for a single alert element, + * this module combines them into a single alert with a formatted list message. + * + * For example, given a predicate: + * ```ql + * predicate problems(Element e, string msg, Placeholder p, string pStr) { + * e.meetsSomeCondition() and + * p.meetsSomeOtherCondition() and + * e.isSomehowRelatedTo(p) and + * msg = e.toString() + " is related to $@." and + * pStr = p.toString() + * } + * ``` + * + * If element `e` is related to a single placeholder, the message will be: + * "foo is related to $@." + * If element `e` is related to two placeholders, the message will be: + * "foo is related to $@ and $@." + * If element `e` is related to three placeholders, the message will be: + * "foo is related to $@, $@, and $@." + * If element `e` is related to more than `maxResults()` placeholders, the message will be: + * "foo is related to $@, $@, $@, and N more." + * + * To use this module, define a configuration module that implements `PlaceholderListSig`: + * ```ql + * module MyConfig implements PlaceholderListSig { + * predicate problems(MyElement e, string msg, MyPlaceholder p, string pStr) { ... } + * int maxResults() { result = 3 } + * string orderBy(MyPlaceholder p) { result = p.toString() } + * } + * + * import PlaceholderList + * ``` + */ + +private import qtil.parameterization.SignatureTypes + +/** + * A signature module for `PlaceholderList` configuration. + * + * Implement this module to provide the `problems` predicate and optional configuration. + * + * The `problems` predicate should associate each element with a message template (containing + * exactly one `$@` placeholder) and zero or more `(placeholder, string)` pairs. + * + * The `maxResults` predicate controls how many placeholders are shown per alert. + * If there are more placeholders than `maxResults()`, the remaining count is included in the + * message as plain text: "and N more". + * + * The `orderBy` predicate controls the order in which placeholders are presented. + * + * Example: + * ```ql + * module MyConfig implements PlaceholderListSig { + * predicate problems(Function f, string msg, Variable v, string vStr) { + * v = f.getAParameter() and + * msg = f.getName() + " has parameter $@." and + * vStr = v.getName() + * } + * int maxResults() { result = 3 } + * string orderBy(Variable v) { result = v.getName() } + * } + * ``` + */ +signature module PlaceholderListSig { + /** + * Defines problems as a set of (element, message, placeholder, placeholderString) tuples. + * + * For a given `(element, message)` pair, there may be multiple `(placeholder, placeholderString)` + * results — one for each related element. The message should contain exactly one `$@` placeholder, + * which will be expanded by the `PlaceholderList` module into the appropriate list format. + */ + predicate problems(Element e, string msg, Placeholder p, string pStr); + + /** + * The maximum number of placeholders to show per alert. When there are more placeholders than + * this limit, the message will include "and N more" as plain text. + * + * The effective maximum is capped at 3, which is the number of placeholder pairs in the output + * query predicate. + * + * Defaults to 3. + */ + default int maxResults() { result = 3 } + + /** + * An ordering key used to sort placeholders within each alert. Placeholders are shown in + * ascending order by this key. + * + * Defaults to the string representation of the placeholder. + */ + default string orderBy(Placeholder p) { result = p.toString() } +} + +/** + * A module for combining multiple placeholder results into a single alert message. + * + * This module takes a `problems` predicate that may have multiple `(placeholder, string)` pairs + * for a single `(element, message)` pair, and combines them into a single output row with an + * expanded message. Up to three placeholder pairs are shown; additional placeholders are + * represented as "and N more" in the message text. + * + * The output `query predicate problems` always has exactly 3 placeholder pairs. When there are + * fewer than 3 actual placeholders, the remaining slots are filled with the first placeholder + * and an empty string (so they are not highlighted in the UI). + * + * See `PlaceholderListSig` for configuration options. + */ +module PlaceholderList< + FiniteStringableType Element, FiniteStringableType Placeholder, + PlaceholderListSig Config> +{ + /** + * Count the number of distinct placeholder values for a given `(element, message)` pair. + */ + private int countPlaceholders(Element e, string msg) { + Config::problems(e, msg, _, _) and + result = count(Placeholder p | Config::problems(e, msg, p, _)) + } + + /** + * Get the `n`th (1-based) placeholder for a given `(element, message)` pair, ordered + * ascending by `Config::orderBy`, with the placeholder's `toString()` as a secondary sort key. + */ + private Placeholder getNthPlaceholder(Element e, string msg, int n) { + Config::problems(e, msg, _, _) and + result = + rank[n](Placeholder p | Config::problems(e, msg, p, _) | + p order by Config::orderBy(p), p.toString() + ) + } + + /** + * Get the string representation of the `n`th placeholder for a given `(element, message)` pair. + * + * If the same placeholder has multiple string representations in the input predicate, the + * lexicographically smallest one is used. + */ + private string getNthPlaceholderStr(Element e, string msg, int n) { + Config::problems(e, msg, _, _) and + result = + min(string pStr | + Config::problems(e, msg, getNthPlaceholder(e, msg, n), pStr) + | + pStr + ) + } + + /** + * Build the expansion string that replaces the single `$@` placeholder in the original message. + * + * `showCount` is the number of `$@` placeholders to include (1–3), and `moreCount` is the + * number of additional placeholders not shown (0 or more). + */ + bindingset[showCount, moreCount] + private string expansion(int showCount, int moreCount) { + showCount = 1 and moreCount = 0 and result = "$@" + or + showCount = 2 and moreCount = 0 and result = "$@ and $@" + or + showCount = 3 and moreCount = 0 and result = "$@, $@, and $@" + or + showCount = 1 and moreCount > 0 and result = "$@ and " + moreCount + " more" + or + showCount = 2 and moreCount > 0 and result = "$@, $@, and " + moreCount + " more" + or + showCount = 3 and moreCount > 0 and result = "$@, $@, $@, and " + moreCount + " more" + } + + /** + * Get the effective number of placeholders to show for a given `(element, message)` pair. + * + * This is the minimum of the total placeholder count and `Config::maxResults()`, capped at 3 + * (the number of placeholder pairs in the output query predicate). + */ + private int showCount(Element e, string msg) { + exists(int total, int maxR, int cappedMax | + total = countPlaceholders(e, msg) and + maxR = Config::maxResults() and + (maxR <= 3 and cappedMax = maxR or maxR > 3 and cappedMax = 3) and + (total <= cappedMax and result = total or total > cappedMax and result = cappedMax) + ) + } + + /** + * Get the expanded message for a given `(element, message)` pair, with the single `$@` + * in the original message replaced by the appropriate comma-separated list of `$@` placeholders + * and optional "and N more" suffix. + */ + private string expandedMsg(Element e, string origMsg) { + exists(int total, int maxR, int cappedMax, int sc, int mc | + total = countPlaceholders(e, origMsg) and + maxR = Config::maxResults() and + (maxR <= 3 and cappedMax = maxR or maxR > 3 and cappedMax = 3) and + (total <= cappedMax and sc = total or total > cappedMax and sc = cappedMax) and + (total > cappedMax and mc = total - cappedMax or total <= cappedMax and mc = 0) and + result = origMsg.replaceAll("$@", expansion(sc, mc)) + ) + } + + /** + * The combined problems query predicate. + * + * For each `(element, message)` pair in the input `Config::problems` predicate, this produces + * a single output row with an expanded message and up to 3 placeholder pairs. + * + * The message is expanded from the single `$@` in the input to a comma-separated list of `$@` + * placeholders (up to `Config::maxResults()`, capped at 3), with "and N more" appended if there + * are additional placeholders beyond the maximum. + * + * Placeholder pairs beyond the number of actual placeholders are filled with the first + * placeholder value and an empty string, so they are not highlighted in the UI. + */ + query predicate problems( + Element e, string outMsg, + Placeholder p1, string str1, + Placeholder p2, string str2, + Placeholder p3, string str3 + ) { + exists(string origMsg, int sc | + Config::problems(e, origMsg, _, _) and + outMsg = expandedMsg(e, origMsg) and + sc = showCount(e, origMsg) and + p1 = getNthPlaceholder(e, origMsg, 1) and + str1 = getNthPlaceholderStr(e, origMsg, 1) and + ( + sc >= 2 and + p2 = getNthPlaceholder(e, origMsg, 2) and + str2 = getNthPlaceholderStr(e, origMsg, 2) + or + sc < 2 and p2 = p1 and str2 = "" + ) and + ( + sc >= 3 and + p3 = getNthPlaceholder(e, origMsg, 3) and + str3 = getNthPlaceholderStr(e, origMsg, 3) + or + sc < 3 and p3 = p1 and str3 = "" + ) + ) + } +} diff --git a/test/qtil/list/PlaceholderListTest.expected b/test/qtil/list/PlaceholderListTest.expected new file mode 100644 index 0000000..2d9e6d0 --- /dev/null +++ b/test/qtil/list/PlaceholderListTest.expected @@ -0,0 +1 @@ +| All 7 tests passed. | diff --git a/test/qtil/list/PlaceholderListTest.ql b/test/qtil/list/PlaceholderListTest.ql new file mode 100644 index 0000000..98dad21 --- /dev/null +++ b/test/qtil/list/PlaceholderListTest.ql @@ -0,0 +1,158 @@ +import qtil.list.PlaceholderList +import qtil.testing.Qnit + +/** + * A finite stringable type for use as test elements. + */ +class TestElement extends int { + TestElement() { this in [1..5] } + + string toString() { exists(int n | n = this | result = n.toString()) } +} + +/** + * A finite stringable type for use as test placeholders. + */ +class TestPlaceholder extends int { + TestPlaceholder() { this in [1..6] } + + string toString() { exists(int n | n = this | result = n.toString()) } +} + +/** + * A test configuration using default maxResults (3). + * + * - Element 1: 1 placeholder (p=1) + * - Element 2: 2 placeholders (p=1,2) + * - Element 3: 3 placeholders (p=1,2,3) + * - Element 4: 4 placeholders (p=1,2,3,4) — more than maxResults + */ +module TestConfig implements PlaceholderListSig { + predicate problems(TestElement e, string msg, TestPlaceholder p, string pStr) { + e = 1 and p = 1 and pStr = p.toString() and msg = "$@ is cool" + or + e = 2 and p in [1 .. 2] and pStr = p.toString() and msg = "$@ is cool" + or + e = 3 and p in [1 .. 3] and pStr = p.toString() and msg = "$@ is cool" + or + e = 4 and p in [1 .. 4] and pStr = p.toString() and msg = "$@ is cool" + } +} + +module DefaultMaxInstance = PlaceholderList; + +/** + * A test configuration with maxResults = 2. + * + * - Element 5: 4 placeholders (p=1,2,3,4) — more than maxResults + */ +module TestConfigMaxTwo implements PlaceholderListSig { + predicate problems(TestElement e, string msg, TestPlaceholder p, string pStr) { + e = 5 and p in [1 .. 4] and pStr = p.toString() and msg = "$@ is related" + } + + int maxResults() { result = 2 } +} + +/** + * A test configuration with maxResults = 1. + * Tests the special case where only 1 placeholder is shown with overflow. + * + * - Element 5 (in this config): 3 placeholders — tests "N more" with no comma + */ +module TestConfigMaxOne implements PlaceholderListSig { + predicate problems(TestElement e, string msg, TestPlaceholder p, string pStr) { + e = 5 and p in [1 .. 3] and pStr = p.toString() and msg = "$@ is related" + } + + int maxResults() { result = 1 } +} + +module MaxOneInstance = PlaceholderList; + +module MaxTwoInstance = PlaceholderList; + +class TestMaxOneResults extends Test, Case { + override predicate run(Qnit test) { + // Element 5: 3 placeholders, maxResults=1 → shows 1 + "and 2 more" (no comma before "and") + if + MaxOneInstance::problems(5, "$@ and 2 more is related", 1, "1", 1, "", 1, "") + then test.pass("MaxResults=1 with overflow: correct message without comma before 'and'") + else test.fail("MaxResults=1 with overflow: incorrect output") + } +} + +class TestSinglePlaceholder extends Test, Case { + override predicate run(Qnit test) { + // Element 1: 1 placeholder → message unchanged, p1 filled, p2/p3 use p1 with "" + if + DefaultMaxInstance::problems(1, "$@ is cool", 1, "1", 1, "", 1, "") + then test.pass("Single placeholder: correct message and placeholder slots") + else test.fail("Single placeholder: incorrect output") + } +} + +class TestTwoPlaceholders extends Test, Case { + override predicate run(Qnit test) { + // Element 2: 2 placeholders → "$@ and $@ is cool", p1/p2 filled, p3 uses p1 with "" + if + DefaultMaxInstance::problems(2, "$@ and $@ is cool", 1, "1", 2, "2", 1, "") + then test.pass("Two placeholders: correct message and placeholder slots") + else test.fail("Two placeholders: incorrect output") + } +} + +class TestThreePlaceholders extends Test, Case { + override predicate run(Qnit test) { + // Element 3: 3 placeholders → "$@, $@, and $@ is cool", all slots filled + if + DefaultMaxInstance::problems(3, "$@, $@, and $@ is cool", 1, "1", 2, "2", 3, "3") + then test.pass("Three placeholders: correct message and placeholder slots") + else test.fail("Three placeholders: incorrect output") + } +} + +class TestExcessPlaceholders extends Test, Case { + override predicate run(Qnit test) { + // Element 4: 4 placeholders, maxResults=3 → shows 3 + "and 1 more", first 3 slots filled + if + DefaultMaxInstance::problems(4, "$@, $@, $@, and 1 more is cool", 1, "1", 2, "2", 3, "3") + then test.pass("Excess placeholders (4 with max 3): correct message and placeholder slots") + else test.fail("Excess placeholders (4 with max 3): incorrect output") + } +} + +class TestCustomMaxResults extends Test, Case { + override predicate run(Qnit test) { + // Element 5: 4 placeholders, maxResults=2 → shows 2 + "and 2 more", first 2 slots filled, p3 padded + if + MaxTwoInstance::problems(5, "$@, $@, and 2 more is related", 1, "1", 2, "2", 1, "") + then test.pass("Custom maxResults (4 with max 2): correct message and placeholder slots") + else test.fail("Custom maxResults (4 with max 2): incorrect output") + } +} + +class TestNoDuplicateRows extends Test, Case { + override predicate run(Qnit test) { + // Each element should produce exactly one output row + if + count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, + TestPlaceholder p3, string s3 | + DefaultMaxInstance::problems(1, msg, p1, s1, p2, s2, p3, s3) + ) = 1 and + count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, + TestPlaceholder p3, string s3 | + DefaultMaxInstance::problems(2, msg, p1, s1, p2, s2, p3, s3) + ) = 1 and + count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, + TestPlaceholder p3, string s3 | + DefaultMaxInstance::problems(3, msg, p1, s1, p2, s2, p3, s3) + ) = 1 and + count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, + TestPlaceholder p3, string s3 | + DefaultMaxInstance::problems(4, msg, p1, s1, p2, s2, p3, s3) + ) = 1 + then test.pass("Each element produces exactly one output row") + else test.fail("Some element produces more or fewer than one output row") + } +} From cf18211d64a8d7814c6d641da8c7cd9c2f280d9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:00:47 +0000 Subject: [PATCH 3/3] Address review feedback: max=5, refactor expansion, query predicate tests, README, remove .gitignore Co-authored-by: MichaelRFairhurst <1627771+MichaelRFairhurst@users.noreply.github.com> Agent-Logs-Url: https://github.com/advanced-security/codeql-qtil/sessions/a14c8c20-be9c-4b06-b4af-53039bacd071 --- .gitignore | 1 - README.md | 21 +++ src/qtil/list/PlaceholderList.qll | 98 +++++++++---- test/qtil/list/PlaceholderListTest.expected | 12 +- test/qtil/list/PlaceholderListTest.ql | 149 +++++--------------- 5 files changed, 133 insertions(+), 148 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2886f99..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -codeql_home/ diff --git a/README.md b/README.md index 7f94683..d0df7a6 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,27 @@ select listEntry.getItem().getName(), listEntry.getDivision().getName(), listEntry.getNext().getItem().getName() ``` +**PlaceholderList**: Collapses multiple `(element, placeholder)` input rows into a single alert row with a formatted list message. Useful when a single alert element is related to a variable number of placeholder elements. + +_Note: the input message must contain exactly one `$@` placeholder, which will be expanded._ + +```ql +module MyConfig implements Qtil::PlaceholderListSig { + predicate problems(Function f, string msg, Variable v, string vStr) { + v = f.getAParameter() and + msg = f.getName() + " has parameters $@." and + vStr = v.getName() + } +} + +// Import to get a `query predicate problems(...)` with up to 5 placeholder pairs: +import Qtil::PlaceholderList +// Result for a function with 3 params: "foo has parameters $@, $@, and $@.", p1, p2, p3 +// Result for a function with 7 params: "foo has parameters $@, $@, $@, $@, and $@ and 2 more.", p1-p5 +``` + +Custom `maxResults` (default 5) and `orderBy` are configurable via the signature module. + ### Strings **join(sep, ...)**: The first argument is used as a separator to join the remaining two to eight arguments. diff --git a/src/qtil/list/PlaceholderList.qll b/src/qtil/list/PlaceholderList.qll index 5d91231..06859a0 100644 --- a/src/qtil/list/PlaceholderList.qll +++ b/src/qtil/list/PlaceholderList.qll @@ -23,13 +23,13 @@ * If element `e` is related to three placeholders, the message will be: * "foo is related to $@, $@, and $@." * If element `e` is related to more than `maxResults()` placeholders, the message will be: - * "foo is related to $@, $@, $@, and N more." + * "foo is related to $@, $@, $@, $@, $@, and N more." * * To use this module, define a configuration module that implements `PlaceholderListSig`: * ```ql * module MyConfig implements PlaceholderListSig { * predicate problems(MyElement e, string msg, MyPlaceholder p, string pStr) { ... } - * int maxResults() { result = 3 } + * int maxResults() { result = 5 } * string orderBy(MyPlaceholder p) { result = p.toString() } * } * @@ -61,7 +61,7 @@ private import qtil.parameterization.SignatureTypes * msg = f.getName() + " has parameter $@." and * vStr = v.getName() * } - * int maxResults() { result = 3 } + * int maxResults() { result = 5 } * string orderBy(Variable v) { result = v.getName() } * } * ``` @@ -80,12 +80,12 @@ signature module PlaceholderListSig 0 and result = "$@ and " + moreCount + " more" + n = 4 and result = "$@, $@, $@, and $@" or - showCount = 2 and moreCount > 0 and result = "$@, $@, and " + moreCount + " more" + n = 5 and result = "$@, $@, $@, $@, and $@" + } + + /** + * Build the overflow suffix for the expansion string. + * + * Returns `""` when `moreCount = 0` (no overflow), or `" and N more"` when there are + * additional placeholders beyond the visible maximum. + */ + bindingset[moreCount] + private string moreString(int moreCount) { + moreCount = 0 and result = "" or - showCount = 3 and moreCount > 0 and result = "$@, $@, $@, and " + moreCount + " more" + moreCount > 0 and result = " and " + moreCount + " more" + } + + /** + * Build the full expansion string that replaces the single `$@` placeholder in the original + * message. + * + * `showCount` is the number of `$@` placeholders to include (1–5), and `moreCount` is the + * number of additional placeholders not shown (0 or more). + */ + bindingset[showCount, moreCount] + private string expansion(int showCount, int moreCount) { + result = placeholderExpansion(showCount) + moreString(moreCount) } /** * Get the effective number of placeholders to show for a given `(element, message)` pair. * - * This is the minimum of the total placeholder count and `Config::maxResults()`, capped at 3 + * This is the minimum of the total placeholder count and `Config::maxResults()`, capped at 5 * (the number of placeholder pairs in the output query predicate). */ private int showCount(Element e, string msg) { exists(int total, int maxR, int cappedMax | total = countPlaceholders(e, msg) and maxR = Config::maxResults() and - (maxR <= 3 and cappedMax = maxR or maxR > 3 and cappedMax = 3) and + (maxR <= 5 and cappedMax = maxR or maxR > 5 and cappedMax = 5) and (total <= cappedMax and result = total or total > cappedMax and result = cappedMax) ) } @@ -192,12 +213,10 @@ module PlaceholderList< * and optional "and N more" suffix. */ private string expandedMsg(Element e, string origMsg) { - exists(int total, int maxR, int cappedMax, int sc, int mc | + exists(int total, int sc, int mc | total = countPlaceholders(e, origMsg) and - maxR = Config::maxResults() and - (maxR <= 3 and cappedMax = maxR or maxR > 3 and cappedMax = 3) and - (total <= cappedMax and sc = total or total > cappedMax and sc = cappedMax) and - (total > cappedMax and mc = total - cappedMax or total <= cappedMax and mc = 0) and + sc = showCount(e, origMsg) and + (total > sc and mc = total - sc or total <= sc and mc = 0) and result = origMsg.replaceAll("$@", expansion(sc, mc)) ) } @@ -206,10 +225,10 @@ module PlaceholderList< * The combined problems query predicate. * * For each `(element, message)` pair in the input `Config::problems` predicate, this produces - * a single output row with an expanded message and up to 3 placeholder pairs. + * a single output row with an expanded message and up to 5 placeholder pairs. * * The message is expanded from the single `$@` in the input to a comma-separated list of `$@` - * placeholders (up to `Config::maxResults()`, capped at 3), with "and N more" appended if there + * placeholders (up to `Config::maxResults()`, capped at 5), with "and N more" appended if there * are additional placeholders beyond the maximum. * * Placeholder pairs beyond the number of actual placeholders are filled with the first @@ -219,7 +238,9 @@ module PlaceholderList< Element e, string outMsg, Placeholder p1, string str1, Placeholder p2, string str2, - Placeholder p3, string str3 + Placeholder p3, string str3, + Placeholder p4, string str4, + Placeholder p5, string str5 ) { exists(string origMsg, int sc | Config::problems(e, origMsg, _, _) and @@ -240,7 +261,22 @@ module PlaceholderList< str3 = getNthPlaceholderStr(e, origMsg, 3) or sc < 3 and p3 = p1 and str3 = "" + ) and + ( + sc >= 4 and + p4 = getNthPlaceholder(e, origMsg, 4) and + str4 = getNthPlaceholderStr(e, origMsg, 4) + or + sc < 4 and p4 = p1 and str4 = "" + ) and + ( + sc >= 5 and + p5 = getNthPlaceholder(e, origMsg, 5) and + str5 = getNthPlaceholderStr(e, origMsg, 5) + or + sc < 5 and p5 = p1 and str5 = "" ) ) } } + diff --git a/test/qtil/list/PlaceholderListTest.expected b/test/qtil/list/PlaceholderListTest.expected index 2d9e6d0..12e4115 100644 --- a/test/qtil/list/PlaceholderListTest.expected +++ b/test/qtil/list/PlaceholderListTest.expected @@ -1 +1,11 @@ -| All 7 tests passed. | +testDefault +| 1 | $@ is cool | 1 | 1 | 1 | | 1 | | 1 | | 1 | | +| 2 | $@ and $@ is cool | 1 | 1 | 2 | 2 | 1 | | 1 | | 1 | | +| 3 | $@, $@, and $@ is cool | 1 | 1 | 2 | 2 | 3 | 3 | 1 | | 1 | | +| 4 | $@, $@, $@, and $@ is cool | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 1 | | +| 5 | $@, $@, $@, $@, and $@ is cool | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | +| 6 | $@, $@, $@, $@, and $@ and 1 more is cool | 1 | 1 | 2 | 2 | 3 | 3 | 4 | 4 | 5 | 5 | +testMaxTwo +| 1 | $@ is related | 1 | 1 | 1 | | 1 | | 1 | | 1 | | +| 2 | $@ and $@ is related | 1 | 1 | 2 | 2 | 1 | | 1 | | 1 | | +| 3 | $@ and $@ and 1 more is related | 1 | 1 | 2 | 2 | 1 | | 1 | | 1 | | diff --git a/test/qtil/list/PlaceholderListTest.ql b/test/qtil/list/PlaceholderListTest.ql index 98dad21..ed69c6b 100644 --- a/test/qtil/list/PlaceholderListTest.ql +++ b/test/qtil/list/PlaceholderListTest.ql @@ -1,11 +1,10 @@ import qtil.list.PlaceholderList -import qtil.testing.Qnit /** * A finite stringable type for use as test elements. */ class TestElement extends int { - TestElement() { this in [1..5] } + TestElement() { this in [1..6] } string toString() { exists(int n | n = this | result = n.toString()) } } @@ -14,145 +13,65 @@ class TestElement extends int { * A finite stringable type for use as test placeholders. */ class TestPlaceholder extends int { - TestPlaceholder() { this in [1..6] } + TestPlaceholder() { this in [1..7] } string toString() { exists(int n | n = this | result = n.toString()) } } /** - * A test configuration using default maxResults (3). + * A test configuration using default maxResults (5). * * - Element 1: 1 placeholder (p=1) * - Element 2: 2 placeholders (p=1,2) * - Element 3: 3 placeholders (p=1,2,3) - * - Element 4: 4 placeholders (p=1,2,3,4) — more than maxResults + * - Element 4: 4 placeholders (p=1,2,3,4) + * - Element 5: 5 placeholders (p=1..5) + * - Element 6: 6 placeholders (p=1..6) — more than maxResults */ module TestConfig implements PlaceholderListSig { predicate problems(TestElement e, string msg, TestPlaceholder p, string pStr) { - e = 1 and p = 1 and pStr = p.toString() and msg = "$@ is cool" - or - e = 2 and p in [1 .. 2] and pStr = p.toString() and msg = "$@ is cool" - or - e = 3 and p in [1 .. 3] and pStr = p.toString() and msg = "$@ is cool" - or - e = 4 and p in [1 .. 4] and pStr = p.toString() and msg = "$@ is cool" + p in [1..e] and pStr = p.toString() and msg = "$@ is cool" } } -module DefaultMaxInstance = PlaceholderList; +module DefaultMax = PlaceholderList; + +query predicate testDefault( + TestElement e, string outMsg, + TestPlaceholder p1, string s1, + TestPlaceholder p2, string s2, + TestPlaceholder p3, string s3, + TestPlaceholder p4, string s4, + TestPlaceholder p5, string s5 +) { + DefaultMax::problems(e, outMsg, p1, s1, p2, s2, p3, s3, p4, s4, p5, s5) +} /** * A test configuration with maxResults = 2. * - * - Element 5: 4 placeholders (p=1,2,3,4) — more than maxResults + * - Element 1: 1 placeholder + * - Element 2: 2 placeholders (exact max) + * - Element 3: 3 placeholders — overflow by 1 */ module TestConfigMaxTwo implements PlaceholderListSig { predicate problems(TestElement e, string msg, TestPlaceholder p, string pStr) { - e = 5 and p in [1 .. 4] and pStr = p.toString() and msg = "$@ is related" + e in [1..3] and p in [1..e] and pStr = p.toString() and msg = "$@ is related" } int maxResults() { result = 2 } } -/** - * A test configuration with maxResults = 1. - * Tests the special case where only 1 placeholder is shown with overflow. - * - * - Element 5 (in this config): 3 placeholders — tests "N more" with no comma - */ -module TestConfigMaxOne implements PlaceholderListSig { - predicate problems(TestElement e, string msg, TestPlaceholder p, string pStr) { - e = 5 and p in [1 .. 3] and pStr = p.toString() and msg = "$@ is related" - } - - int maxResults() { result = 1 } -} - -module MaxOneInstance = PlaceholderList; - -module MaxTwoInstance = PlaceholderList; - -class TestMaxOneResults extends Test, Case { - override predicate run(Qnit test) { - // Element 5: 3 placeholders, maxResults=1 → shows 1 + "and 2 more" (no comma before "and") - if - MaxOneInstance::problems(5, "$@ and 2 more is related", 1, "1", 1, "", 1, "") - then test.pass("MaxResults=1 with overflow: correct message without comma before 'and'") - else test.fail("MaxResults=1 with overflow: incorrect output") - } -} - -class TestSinglePlaceholder extends Test, Case { - override predicate run(Qnit test) { - // Element 1: 1 placeholder → message unchanged, p1 filled, p2/p3 use p1 with "" - if - DefaultMaxInstance::problems(1, "$@ is cool", 1, "1", 1, "", 1, "") - then test.pass("Single placeholder: correct message and placeholder slots") - else test.fail("Single placeholder: incorrect output") - } +module MaxTwo = PlaceholderList; + +query predicate testMaxTwo( + TestElement e, string outMsg, + TestPlaceholder p1, string s1, + TestPlaceholder p2, string s2, + TestPlaceholder p3, string s3, + TestPlaceholder p4, string s4, + TestPlaceholder p5, string s5 +) { + MaxTwo::problems(e, outMsg, p1, s1, p2, s2, p3, s3, p4, s4, p5, s5) } -class TestTwoPlaceholders extends Test, Case { - override predicate run(Qnit test) { - // Element 2: 2 placeholders → "$@ and $@ is cool", p1/p2 filled, p3 uses p1 with "" - if - DefaultMaxInstance::problems(2, "$@ and $@ is cool", 1, "1", 2, "2", 1, "") - then test.pass("Two placeholders: correct message and placeholder slots") - else test.fail("Two placeholders: incorrect output") - } -} - -class TestThreePlaceholders extends Test, Case { - override predicate run(Qnit test) { - // Element 3: 3 placeholders → "$@, $@, and $@ is cool", all slots filled - if - DefaultMaxInstance::problems(3, "$@, $@, and $@ is cool", 1, "1", 2, "2", 3, "3") - then test.pass("Three placeholders: correct message and placeholder slots") - else test.fail("Three placeholders: incorrect output") - } -} - -class TestExcessPlaceholders extends Test, Case { - override predicate run(Qnit test) { - // Element 4: 4 placeholders, maxResults=3 → shows 3 + "and 1 more", first 3 slots filled - if - DefaultMaxInstance::problems(4, "$@, $@, $@, and 1 more is cool", 1, "1", 2, "2", 3, "3") - then test.pass("Excess placeholders (4 with max 3): correct message and placeholder slots") - else test.fail("Excess placeholders (4 with max 3): incorrect output") - } -} - -class TestCustomMaxResults extends Test, Case { - override predicate run(Qnit test) { - // Element 5: 4 placeholders, maxResults=2 → shows 2 + "and 2 more", first 2 slots filled, p3 padded - if - MaxTwoInstance::problems(5, "$@, $@, and 2 more is related", 1, "1", 2, "2", 1, "") - then test.pass("Custom maxResults (4 with max 2): correct message and placeholder slots") - else test.fail("Custom maxResults (4 with max 2): incorrect output") - } -} - -class TestNoDuplicateRows extends Test, Case { - override predicate run(Qnit test) { - // Each element should produce exactly one output row - if - count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, - TestPlaceholder p3, string s3 | - DefaultMaxInstance::problems(1, msg, p1, s1, p2, s2, p3, s3) - ) = 1 and - count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, - TestPlaceholder p3, string s3 | - DefaultMaxInstance::problems(2, msg, p1, s1, p2, s2, p3, s3) - ) = 1 and - count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, - TestPlaceholder p3, string s3 | - DefaultMaxInstance::problems(3, msg, p1, s1, p2, s2, p3, s3) - ) = 1 and - count(string msg, TestPlaceholder p1, string s1, TestPlaceholder p2, string s2, - TestPlaceholder p3, string s3 | - DefaultMaxInstance::problems(4, msg, p1, s1, p2, s2, p3, s3) - ) = 1 - then test.pass("Each element produces exactly one output row") - else test.fail("Some element produces more or fewer than one output row") - } -}