Skip to content

Commit c2d3e80

Browse files
committed
add QL-for-QL query for detecting unqueryable (unused) code
1 parent 82bbe67 commit c2d3e80

File tree

2 files changed

+79
-7
lines changed

2 files changed

+79
-7
lines changed

ql/ql/src/codeql_ql/style/DeadCodeQuery.qll

+64-7
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ private AstNode publicApi() {
3030
*/
3131
private AstNode queryPredicate() {
3232
// result = query relation that is "transitively" imported by a .ql file.
33-
PathProblemQuery::importsQueryRelation(result).asFile().getExtension() = "ql"
33+
// PathProblemQuery::importsQueryRelation(result).asFile().getExtension() = "ql"
34+
// any query predicate. Query predicates are usually meant to be used.
35+
result.(Predicate).hasAnnotation("query")
3436
or
3537
// the from-where-select
3638
result instanceof Select
@@ -208,8 +210,9 @@ private AstNode benign() {
208210
result instanceof Comment or
209211
not exists(result.toString()) or // <- invalid code
210212
// cached-stages pattern
211-
result.(Module).getAMember().(ClasslessPredicate).getName() = "forceStage" or
212-
result.(ClasslessPredicate).getName() = "forceStage" or
213+
result.(Module).getAMember().(ClasslessPredicate).getName() =
214+
["forceStage", "forceCachingInSameStageforceCachingInSameStage"] or
215+
result.(ClasslessPredicate).getName() = ["forceStage", "forceCachingInSameStage"] or
213216
result.getLocation().getFile().getBaseName() = "Caching.qll" or
214217
// sometimes contains dead code - ignore
215218
result.getLocation().getFile().getRelativePath().matches("%/tutorials/%") or
@@ -243,20 +246,74 @@ private AstNode queryable() {
243246
or
244247
result instanceof TopLevel // toplevel is always alive.
245248
or
249+
result = hackyShouldBeTreatedAsAlive()
250+
or
251+
// The below prevents the query from being too loud. The files below contain a lot of unqueryable code.
252+
// I think some of it is from some languages not using all features of a shared library, but I'm not sure (haven't look much into it).
253+
result
254+
.getLocation()
255+
.getFile()
256+
.getBaseName()
257+
.matches(["DataFlowImpl", "SsaImplCommon", "FlowSummary"] + "%")
258+
or
246259
// recurisve cases
247260
result = aliveStep(queryable())
248261
}
249262

263+
// The benign cases are mostly
264+
private AstNode benignUnqueryable() {
265+
result = benign() or
266+
// cached-stages pattern
267+
// sometimes contains dead code - ignore
268+
result.(Module).getName() = "Debugging" or
269+
result.getLocation().getFile() = benignUnqueryableFile()
270+
}
271+
272+
pragma[noinline]
273+
private File benignUnqueryableFile() {
274+
result.getAbsolutePath().matches("%/explore/%") or
275+
result.getRelativePath().matches("%/tutorials/%") or
276+
result.getRelativePath().matches("%/experimental/%") or
277+
result.getRelativePath().matches("%/frameworks/%") or // the frameworks have a lot of code that only exists for completeness.
278+
result.getBaseName() =
279+
[
280+
"Expr.qll", "TypeScript.qll", "YAML.qll", "Tokens.qll", "Instruction.qll", "Persistence.qll",
281+
"ES2015Modules.qll", "TaintTrackingPublic.qll", "TaintTrackingUtil.qll",
282+
] or // lots of classes that exist for completeness
283+
result.getBaseName() = ["CachedStages.qll", "Caching.qll", "tutorial.qll"] or
284+
result.getBaseName() = "PrettyPrintAst.qll" or // it's dead code, but seems intentional
285+
result.getBaseName() = ["CryptoAlgorithmNames.qll", "SensitiveDataHeuristics.qll"] or // not all langs use all the things
286+
// some more identical files
287+
result.getBaseName() = "ReachableBlock.qll" or
288+
// QL-for-QL tests contain plenty of unqueryable code on purpose
289+
result.getAbsolutePath().matches("%/ql/ql/test%")
290+
}
291+
292+
import NodeName
293+
250294
/**
251295
* Gets an AstNode that does not affect any query result.
252296
* Is interresting as an quick-eval target to investigate dead code.
253297
* (It is intentional that this predicate is a result of this predicate).
254298
*/
255-
AstNode unQueryable(string msg) {
299+
AstNode unQueryable() {
256300
not result = queryable() and
257301
not result = deprecated() and
258-
not result = benign() and
302+
not result = benignUnqueryable() and
259303
not result.getParent() = any(AstNode node | not node = queryable()) and
260-
msg = result.getLocation().getFile().getBaseName() and
261-
result.getLocation().getFile().getAbsolutePath().matches("%/javascript/%")
304+
// remove where a queryable feature with the "same" name exists.
305+
not exists(AstNode other, string name |
306+
name = getName(result, _) and
307+
name = oppositeFirstLetter(getName(other, _)) and
308+
other.getParent() = result.getParent()
309+
)
310+
}
311+
312+
bindingset[name]
313+
string oppositeFirstLetter(string name) {
314+
exists(string first | first = name.prefix(1) |
315+
if first.toUpperCase() = first
316+
then result = first.toLowerCase() + name.suffix(1)
317+
else result = first.toUpperCase() + name.suffix(1)
318+
)
262319
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @name Unqueryable code
3+
* @description Code that cannot affect the outcome of any query is suspicous.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id ql/unqueryable-code
7+
* @precision high
8+
*/
9+
10+
import ql
11+
import codeql_ql.style.DeadCodeQuery
12+
13+
from AstNode node
14+
where node = unQueryable()
15+
select node, "Code cannot affect the outcome of any query (including tests)."

0 commit comments

Comments
 (0)