Skip to content

Commit ba7bd9d

Browse files
Address PR feedback: add query predicate, default maxResults/placeholderString
Co-authored-by: MichaelRFairhurst <1627771+MichaelRFairhurst@users.noreply.github.com>
1 parent 49c6b01 commit ba7bd9d

3 files changed

Lines changed: 85 additions & 34 deletions

File tree

src/qtil/results/LimitResults.qll

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,36 @@
44
*
55
* This is useful for queries that find multiple related entities per finding (such as fields,
66
* parameters, or call sites) but where listing all of them would be too noisy. Instead, only the
7-
* top `N` entities are reported (ordered by a configurable key), and the message notes how many
7+
* top `N` entities are reported (ordered by `placeholderString()`), and the message notes how many
88
* were omitted.
99
*
1010
* ## Usage
1111
*
12-
* Implement the `LimitResultsConfigSig` signature and instantiate the `LimitResults` module:
12+
* Implement the `LimitResultsConfigSig` signature and instantiate the `LimitResults` module. Only
13+
* `problem` and `message` are required — `placeholderString` and `maxResults` have sensible
14+
* defaults:
1315
*
1416
* ```ql
1517
* module MyConfig implements LimitResultsConfigSig<MyFinding, MyEntity> {
1618
* predicate problem(MyFinding finding, MyEntity entity) {
1719
* entity = finding.getAnEntity()
1820
* }
1921
*
22+
* bindingset[remaining]
2023
* string message(MyFinding finding, MyEntity entity, string remaining) {
2124
* result = "Finding $@ has entity $@" + remaining + "."
2225
* }
23-
*
24-
* string orderBy(MyEntity entity) { result = entity.getName() }
25-
*
26-
* int maxResults() { result = 3 }
2726
* }
2827
*
2928
* module Results = LimitResults<MyFinding, MyEntity, MyConfig>;
29+
* ```
30+
*
31+
* The instantiated module exposes a `problems` query predicate that can be used directly as
32+
* the output of a query without any `from`/`where`/`select` boilerplate:
3033
*
31-
* from MyFinding finding, MyEntity entity, string message
32-
* where Results::hasLimitedResult(finding, entity, message)
33-
* select finding, message, entity, entity.getName()
34+
* ```ql
35+
* module Results = LimitResults<MyFinding, MyEntity, MyConfig>;
36+
* // The query predicate Results::problems(...) is automatically part of the query output.
3437
* ```
3538
*/
3639

@@ -39,16 +42,16 @@ private import qtil.parameterization.SignatureTypes
3942
/**
4043
* A signature for configuring the `LimitResults` module.
4144
*
42-
* Implement this signature in a module to define the relationship between findings and entities,
43-
* the message format, the ordering of entities, and the maximum number of results to show per
44-
* finding.
45+
* Only `problem` and `message` must be implemented. The predicates `placeholderString` and
46+
* `maxResults` have defaults and may be overridden.
4547
*/
46-
signature module LimitResultsConfigSig<FiniteType Finding, FiniteType Entity> {
48+
signature module LimitResultsConfigSig<FiniteStringableType Finding, FiniteStringableType Entity> {
4749
/**
4850
* The relationship between findings and their associated entities.
4951
*
5052
* Defines which entities are relevant to a given finding. All entities satisfying this predicate
51-
* will be counted, but only the top `maxResults()` (ordered by `orderBy()`) will be reported.
53+
* will be counted, but only the top `maxResults()` (ordered by `placeholderString()`) will be
54+
* reported.
5255
*/
5356
predicate problem(Finding finding, Entity entity);
5457

@@ -63,40 +66,59 @@ signature module LimitResultsConfigSig<FiniteType Finding, FiniteType Entity> {
6366
string message(Finding finding, Entity entity, string remaining);
6467

6568
/**
66-
* The key to use when ordering entities within a finding (ascending).
69+
* The display string for an entity, also used as the ordering key (ascending).
6770
*
68-
* Entities with smaller order keys are reported first. When the total count exceeds
71+
* Entities with smaller placeholder strings are reported first. When the total count exceeds
6972
* `maxResults()`, only the first `maxResults()` entities by this ordering are reported.
73+
*
74+
* Defaults to `entity.toString()`.
7075
*/
71-
string orderBy(Entity entity);
76+
default string placeholderString(Entity entity) { result = entity.toString() }
7277

7378
/**
7479
* The maximum number of entities to report per finding.
7580
*
7681
* When the total number of entities for a finding exceeds this value, only the first
77-
* `maxResults()` entities (by `orderBy()`) are reported, and the message includes a
82+
* `maxResults()` entities (by `placeholderString()`) are reported, and the message includes a
7883
* `" (and N more)"` suffix indicating the number of omitted entities.
84+
*
85+
* Defaults to `3`.
7986
*/
80-
int maxResults();
87+
default int maxResults() { result = 3 }
8188
}
8289

8390
/**
8491
* A module that limits the number of results reported per finding, appending an "and N more"
8592
* suffix when additional entities exist beyond the configured maximum.
8693
*
87-
* Use `hasLimitedResult` as the body of a `where` clause in a `select` statement. Each result
88-
* tuple corresponds to one of the top-ranked entities for a finding, together with the formatted
89-
* message that includes the remaining count suffix when applicable.
94+
* The `problems` query predicate is the main entry point for use in a query. When this module is
95+
* instantiated as `module Results = LimitResults<...>`, the predicate `Results::problems` is
96+
* automatically part of the query output — no `from`/`where`/`select` boilerplate is needed.
9097
*
9198
* See `LimitResultsConfigSig` for configuration details.
9299
*/
93-
module LimitResults<FiniteType Finding, FiniteType Entity, LimitResultsConfigSig<Finding, Entity> Config> {
100+
module LimitResults<FiniteStringableType Finding, FiniteStringableType Entity, LimitResultsConfigSig<Finding, Entity> Config> {
101+
/**
102+
* A query predicate that reports findings alongside one of their top-ranked entities and a
103+
* formatted message. This is the primary way to use this module in a query.
104+
*
105+
* Each result tuple `(finding, msg, entity, entityStr)` corresponds to one of the top-ranked
106+
* entities for a finding. `entityStr` is `Config::placeholderString(entity)`, suitable for use
107+
* as the placeholder text in a `select` column alongside `entity`.
108+
*
109+
* At most `Config::maxResults()` entities are reported per finding.
110+
*/
111+
query predicate problems(Finding finding, string msg, Entity entity, string entityStr) {
112+
hasLimitedResult(finding, entity, msg) and
113+
entityStr = Config::placeholderString(entity)
114+
}
115+
94116
/**
95117
* Holds for each finding and one of its top-ranked entities, providing the formatted message.
96118
*
97119
* At most `Config::maxResults()` entities are reported per finding. They are selected by ranking
98120
* all entities satisfying `Config::problem(finding, entity)` in ascending order of
99-
* `Config::orderBy(entity)`, and taking those with rank <= `Config::maxResults()`.
121+
* `Config::placeholderString(entity)`, and taking those with rank <= `Config::maxResults()`.
100122
*
101123
* The `message` is produced by `Config::message(finding, entity, remaining)`, where `remaining`
102124
* is `" (and N more)"` if the total exceeds `Config::maxResults()`, or `""` otherwise.
@@ -105,7 +127,9 @@ module LimitResults<FiniteType Finding, FiniteType Entity, LimitResultsConfigSig
105127
exists(int total, int ranked, string remaining |
106128
total = count(Entity e | Config::problem(finding, e)) and
107129
entity =
108-
rank[ranked](Entity e | Config::problem(finding, e) | e order by Config::orderBy(e)) and
130+
rank[ranked](Entity e | Config::problem(finding, e) |
131+
e order by Config::placeholderString(e)
132+
) and
109133
ranked <= Config::maxResults() and
110134
(
111135
total > Config::maxResults() and
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
| All 7 tests passed. |
1+
| All 8 tests passed. |

test/qtil/results/LimitResultsTest.ql

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,11 @@ module TestConfig implements LimitResultsConfigSig<Bug, BugField> {
99
string message(Bug bug, BugField field, string remaining) {
1010
result = bug.getName() + " has field " + field.getFieldName() + remaining
1111
}
12-
13-
string orderBy(BugField field) { result = field.getFieldName() }
14-
15-
int maxResults() { result = 3 }
1612
}
1713

1814
module Results = LimitResults<Bug, BugField, TestConfig>;
1915

20-
/** BugA has 1 field (X), which is fewer than maxResults=3, so all results are shown. */
16+
/** BugA has 1 field (X), which is fewer than maxResults=3 (default), so all results are shown. */
2117
class TestBugAResultCount extends Test, Case {
2218
override predicate run(Qnit test) {
2319
if count(BugField f, string msg | Results::hasLimitedResult(any(Bug b | b = TBugA()), f, msg)) =
@@ -41,7 +37,7 @@ class TestBugANoSuffix extends Test, Case {
4137
}
4238
}
4339

44-
/** BugB has 3 fields (A, B, C), exactly maxResults=3, so all results are shown. */
40+
/** BugB has 3 fields (A, B, C), exactly maxResults=3 (default), so all results are shown. */
4541
class TestBugBResultCount extends Test, Case {
4642
override predicate run(Qnit test) {
4743
if count(BugField f, string msg | Results::hasLimitedResult(any(Bug b | b = TBugB()), f, msg)) =
@@ -73,7 +69,7 @@ class TestBugBNoSuffix extends Test, Case {
7369
}
7470
}
7571

76-
/** BugC has 5 fields (A, B, C, D, E), which exceeds maxResults=3, so only 3 are shown. */
72+
/** BugC has 5 fields (A, B, C, D, E), which exceeds maxResults=3 (default), so only 3 are shown. */
7773
class TestBugCResultCount extends Test, Case {
7874
override predicate run(Qnit test) {
7975
if count(BugField f, string msg | Results::hasLimitedResult(any(Bug b | b = TBugC()), f, msg)) =
@@ -83,7 +79,10 @@ class TestBugCResultCount extends Test, Case {
8379
}
8480
}
8581

86-
/** BugC shows only the first 3 fields alphabetically (A, B, C), not D or E. */
82+
/**
83+
* BugC shows only the first 3 fields alphabetically (A, B, C), not D or E, using the default
84+
* placeholderString ordering (toString() = "BugC.A", "BugC.B", ...).
85+
*/
8786
class TestBugCTopRanked extends Test, Case {
8887
override predicate run(Qnit test) {
8988
if
@@ -117,3 +116,31 @@ class TestBugCSuffix extends Test, Case {
117116
else test.fail("BugC: some results have wrong suffix")
118117
}
119118
}
119+
120+
/**
121+
* The `problems` query predicate returns (finding, msg, entity, entityStr) tuples, where
122+
* entityStr is the default placeholderString (toString()).
123+
*/
124+
class TestProblemsQueryPredicate extends Test, Case {
125+
override predicate run(Qnit test) {
126+
if
127+
// BugC: 3 problems shown, each entityStr = BugField.toString() = "BugC.<field>"
128+
count(Bug b, string msg, BugField f, string fstr |
129+
Results::problems(b, msg, f, fstr) and b = TBugC()
130+
) = 3 and
131+
forall(Bug b, string msg, BugField f, string fstr |
132+
Results::problems(b, msg, f, fstr) and b = TBugC()
133+
|
134+
fstr = "BugC." + f.getFieldName()
135+
) and
136+
// BugA: 1 problem, entityStr = "BugA.X"
137+
exists(Bug b, string msg, BugField f, string fstr |
138+
Results::problems(b, msg, f, fstr) and
139+
b = TBugA() and
140+
fstr = "BugA.X"
141+
)
142+
then test.pass("problems query predicate returns correct results with entityStr = toString()")
143+
else test.fail("problems query predicate returned unexpected results")
144+
}
145+
}
146+

0 commit comments

Comments
 (0)