Skip to content

Commit d58a5a0

Browse files
committed
Address PR review: add orderBy/andMoreText defaults, convert to query test, update docs
1 parent ba7bd9d commit d58a5a0

5 files changed

Lines changed: 78 additions & 159 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,35 @@ If you wish to perform a path search such as the above, but without reporting pr
323323
use the `Qtil::GraphPathSearch` module instead, which provides an efficient search algorithm
324324
without producing a `@kind path-problem` query.
325325

326+
**LimitResults**: A module for capping the number of related entities reported per finding while
327+
informing the user how many were omitted. This is useful for queries that find multiple related
328+
entities per finding (such as fields, parameters, or call sites) where listing all of them would
329+
produce too many results.
330+
331+
Only `problem` and `message` must be implemented. `placeholderString`, `orderBy`, `maxResults`, and
332+
`andMoreText` have sensible defaults. The instantiated module's `problems` query predicate is
333+
automatically part of the query output:
334+
335+
```ql
336+
module MyConfig implements Qtil::LimitResultsConfigSig<IncompleteInitialization, Field> {
337+
predicate problem(IncompleteInitialization init, Field f) { f = init.getField() }
338+
339+
bindingset[remaining]
340+
string message(IncompleteInitialization init, Field f, string remaining) {
341+
result = init.getKindStr() + " does not initialize non-static data member $@" + remaining + "."
342+
}
343+
}
344+
345+
module Results = Qtil::LimitResults<IncompleteInitialization, Field, MyConfig>;
346+
347+
// Results::problems is automatically part of the query output — no from/where/select needed.
348+
```
349+
350+
The `problems` query predicate yields `(finding, message, entity, entityStr)` tuples. At most
351+
`maxResults()` entities (default: 3) are reported per finding, ranked by `orderBy()` (default:
352+
`entity.toString()`). When some are omitted, `andMoreText(n)` (default: `" (and N more)"`) is
353+
appended to the message.
354+
326355
### Inheritance
327356

328357
**Instance**: A module to make `instanceof` inheritance easier in CodeQL, by writing

src/qtil/results/LimitResults.qll

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
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 `placeholderString()`), and the message notes how many
8-
* were omitted.
7+
* top `N` entities are reported (ordered by `orderBy()`), and the message notes how many were
8+
* omitted.
99
*
1010
* ## Usage
1111
*
1212
* Implement the `LimitResultsConfigSig` signature and instantiate the `LimitResults` module. Only
13-
* `problem` and `message` are required — `placeholderString` and `maxResults` have sensible
14-
* defaults:
13+
* `problem` and `message` are required — `placeholderString`, `orderBy`, `maxResults`, and
14+
* `andMoreText` have sensible defaults. The instantiated module's `problems` query predicate is
15+
* automatically part of the query output without any `from`/`where`/`select` boilerplate:
1516
*
1617
* ```ql
1718
* module MyConfig implements LimitResultsConfigSig<MyFinding, MyEntity> {
@@ -21,37 +22,28 @@
2122
*
2223
* bindingset[remaining]
2324
* string message(MyFinding finding, MyEntity entity, string remaining) {
24-
* result = "Finding $@ has entity $@" + remaining + "."
25+
* result = "Finding " + finding.getName() + " has entity $@" + remaining + "."
2526
* }
2627
* }
2728
*
2829
* module Results = LimitResults<MyFinding, MyEntity, MyConfig>;
2930
* ```
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:
33-
*
34-
* ```ql
35-
* module Results = LimitResults<MyFinding, MyEntity, MyConfig>;
36-
* // The query predicate Results::problems(...) is automatically part of the query output.
37-
* ```
3831
*/
3932

4033
private import qtil.parameterization.SignatureTypes
4134

4235
/**
4336
* A signature for configuring the `LimitResults` module.
4437
*
45-
* Only `problem` and `message` must be implemented. The predicates `placeholderString` and
46-
* `maxResults` have defaults and may be overridden.
38+
* Only `problem` and `message` must be implemented. The predicates `placeholderString`,
39+
* `orderBy`, `maxResults`, and `andMoreText` have defaults and may be overridden.
4740
*/
4841
signature module LimitResultsConfigSig<FiniteStringableType Finding, FiniteStringableType Entity> {
4942
/**
5043
* The relationship between findings and their associated entities.
5144
*
5245
* Defines which entities are relevant to a given finding. All entities satisfying this predicate
53-
* will be counted, but only the top `maxResults()` (ordered by `placeholderString()`) will be
54-
* reported.
46+
* will be counted, but only the top `maxResults()` (ordered by `orderBy()`) will be reported.
5547
*/
5648
predicate problem(Finding finding, Entity entity);
5749

@@ -66,25 +58,46 @@ signature module LimitResultsConfigSig<FiniteStringableType Finding, FiniteStrin
6658
string message(Finding finding, Entity entity, string remaining);
6759

6860
/**
69-
* The display string for an entity, also used as the ordering key (ascending).
61+
* The display string for an entity.
7062
*
71-
* Entities with smaller placeholder strings are reported first. When the total count exceeds
72-
* `maxResults()`, only the first `maxResults()` entities by this ordering are reported.
63+
* Used as the `entityStr` column in the `problems` query predicate output. Also used as the
64+
* default ordering key — see `orderBy`.
7365
*
7466
* Defaults to `entity.toString()`.
7567
*/
7668
default string placeholderString(Entity entity) { result = entity.toString() }
7769

70+
/**
71+
* The key to use when ordering entities within a finding (ascending).
72+
*
73+
* Entities with smaller order keys are reported first. When the total count exceeds
74+
* `maxResults()`, only the first `maxResults()` entities by this ordering are reported.
75+
*
76+
* Defaults to `placeholderString(entity)`.
77+
*/
78+
default string orderBy(Entity entity) { result = placeholderString(entity) }
79+
7880
/**
7981
* The maximum number of entities to report per finding.
8082
*
8183
* When the total number of entities for a finding exceeds this value, only the first
82-
* `maxResults()` entities (by `placeholderString()`) are reported, and the message includes a
83-
* `" (and N more)"` suffix indicating the number of omitted entities.
84+
* `maxResults()` entities (by `orderBy()`) are reported, and the message includes the
85+
* `andMoreText()` suffix indicating the number of omitted entities.
8486
*
8587
* Defaults to `3`.
8688
*/
8789
default int maxResults() { result = 3 }
90+
91+
/**
92+
* The suffix appended to the message when `n` entities are omitted.
93+
*
94+
* The parameter `n` is the number of omitted entities (i.e. `total - maxResults()`). Override
95+
* this to customise the "and N more" text, for example to use a different locale.
96+
*
97+
* Defaults to `" (and N more)"`.
98+
*/
99+
bindingset[n]
100+
default string andMoreText(int n) { result = " (and " + n + " more)" }
88101
}
89102

90103
/**
@@ -118,22 +131,23 @@ module LimitResults<FiniteStringableType Finding, FiniteStringableType Entity, L
118131
*
119132
* At most `Config::maxResults()` entities are reported per finding. They are selected by ranking
120133
* all entities satisfying `Config::problem(finding, entity)` in ascending order of
121-
* `Config::placeholderString(entity)`, and taking those with rank <= `Config::maxResults()`.
134+
* `Config::orderBy(entity)`, and taking those with rank <= `Config::maxResults()`.
122135
*
123136
* The `message` is produced by `Config::message(finding, entity, remaining)`, where `remaining`
124-
* is `" (and N more)"` if the total exceeds `Config::maxResults()`, or `""` otherwise.
137+
* is `Config::andMoreText(n)` (with `n = total - maxResults()`) if the total exceeds
138+
* `Config::maxResults()`, or `""` otherwise.
125139
*/
126140
predicate hasLimitedResult(Finding finding, Entity entity, string message) {
127141
exists(int total, int ranked, string remaining |
128142
total = count(Entity e | Config::problem(finding, e)) and
129143
entity =
130144
rank[ranked](Entity e | Config::problem(finding, e) |
131-
e order by Config::placeholderString(e)
145+
e order by Config::orderBy(e)
132146
) and
133147
ranked <= Config::maxResults() and
134148
(
135149
total > Config::maxResults() and
136-
remaining = " (and " + (total - Config::maxResults()) + " more)"
150+
remaining = Config::andMoreText(total - Config::maxResults())
137151
or
138152
total <= Config::maxResults() and remaining = ""
139153
) and
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
| All 8 tests passed. |
1+
| BugA | BugA has field X | BugA.X | BugA.X |
2+
| BugB | BugB has field A | BugB.A | BugB.A |
3+
| BugB | BugB has field B | BugB.B | BugB.B |
4+
| BugB | BugB has field C | BugB.C | BugB.C |
5+
| BugC | BugC has field A (and 2 more) | BugC.A | BugC.A |
6+
| BugC | BugC has field B (and 2 more) | BugC.B | BugC.B |
7+
| BugC | BugC has field C (and 2 more) | BugC.C | BugC.C |
Lines changed: 2 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import qtil.results.LimitResults
2-
import qtil.testing.Qnit
32
import Bugs
43

54
module TestConfig implements LimitResultsConfigSig<Bug, BugField> {
@@ -13,134 +12,6 @@ module TestConfig implements LimitResultsConfigSig<Bug, BugField> {
1312

1413
module Results = LimitResults<Bug, BugField, TestConfig>;
1514

16-
/** BugA has 1 field (X), which is fewer than maxResults=3 (default), so all results are shown. */
17-
class TestBugAResultCount extends Test, Case {
18-
override predicate run(Qnit test) {
19-
if count(BugField f, string msg | Results::hasLimitedResult(any(Bug b | b = TBugA()), f, msg)) =
20-
1
21-
then test.pass("BugA: 1 result shown (fewer than maxResults)")
22-
else test.fail("BugA: wrong result count")
23-
}
24-
}
25-
26-
/** BugA shows field X with no suffix since total <= maxResults. */
27-
class TestBugANoSuffix extends Test, Case {
28-
override predicate run(Qnit test) {
29-
if
30-
exists(BugField f, string msg |
31-
Results::hasLimitedResult(any(Bug b | b = TBugA()), f, msg) and
32-
f.getFieldName() = "X" and
33-
msg = "BugA has field X"
34-
)
35-
then test.pass("BugA: correct message with no suffix")
36-
else test.fail("BugA: message incorrect")
37-
}
38-
}
39-
40-
/** BugB has 3 fields (A, B, C), exactly maxResults=3 (default), so all results are shown. */
41-
class TestBugBResultCount extends Test, Case {
42-
override predicate run(Qnit test) {
43-
if count(BugField f, string msg | Results::hasLimitedResult(any(Bug b | b = TBugB()), f, msg)) =
44-
3
45-
then test.pass("BugB: 3 results shown (exactly maxResults)")
46-
else test.fail("BugB: wrong result count")
47-
}
48-
}
49-
50-
/** BugB shows all 3 fields with no suffix since total <= maxResults. */
51-
class TestBugBNoSuffix extends Test, Case {
52-
override predicate run(Qnit test) {
53-
if
54-
forall(string fieldName |
55-
fieldName = ["A", "B", "C"] and
56-
exists(BugField f |
57-
f.getFieldName() = fieldName and
58-
f.getBugName() = "BugB"
59-
)
60-
|
61-
exists(BugField f, string msg |
62-
Results::hasLimitedResult(any(Bug b | b = TBugB()), f, msg) and
63-
f.getFieldName() = fieldName and
64-
msg = "BugB has field " + fieldName
65-
)
66-
)
67-
then test.pass("BugB: all 3 results shown with no suffix")
68-
else test.fail("BugB: some results have wrong message or are missing")
69-
}
15+
query predicate problems(Bug bug, string msg, BugField field, string fieldStr) {
16+
Results::problems(bug, msg, field, fieldStr)
7017
}
71-
72-
/** BugC has 5 fields (A, B, C, D, E), which exceeds maxResults=3 (default), so only 3 are shown. */
73-
class TestBugCResultCount extends Test, Case {
74-
override predicate run(Qnit test) {
75-
if count(BugField f, string msg | Results::hasLimitedResult(any(Bug b | b = TBugC()), f, msg)) =
76-
3
77-
then test.pass("BugC: 3 results shown (capped at maxResults)")
78-
else test.fail("BugC: wrong result count")
79-
}
80-
}
81-
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-
*/
86-
class TestBugCTopRanked extends Test, Case {
87-
override predicate run(Qnit test) {
88-
if
89-
forall(string fieldName | fieldName = ["A", "B", "C"] |
90-
exists(BugField f |
91-
Results::hasLimitedResult(any(Bug b | b = TBugC()), f, _) and
92-
f.getFieldName() = fieldName and
93-
f.getBugName() = "BugC"
94-
)
95-
) and
96-
not exists(BugField f |
97-
Results::hasLimitedResult(any(Bug b | b = TBugC()), f, _) and
98-
f.getFieldName() = ["D", "E"] and
99-
f.getBugName() = "BugC"
100-
)
101-
then test.pass("BugC: top 3 alphabetical fields shown, D and E omitted")
102-
else test.fail("BugC: wrong fields shown")
103-
}
104-
}
105-
106-
/** BugC shows a suffix " (and 2 more)" since 5 - 3 = 2 entities are omitted. */
107-
class TestBugCSuffix extends Test, Case {
108-
override predicate run(Qnit test) {
109-
if
110-
forall(BugField f, string msg |
111-
Results::hasLimitedResult(any(Bug b | b = TBugC()), f, msg)
112-
|
113-
msg.matches("% (and 2 more)")
114-
)
115-
then test.pass("BugC: all shown results have ' (and 2 more)' suffix")
116-
else test.fail("BugC: some results have wrong suffix")
117-
}
118-
}
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)