Skip to content

Commit 27d41cb

Browse files
committed
QL: add ql/dead-code query
1 parent bbb2847 commit 27d41cb

File tree

7 files changed

+280
-42
lines changed

7 files changed

+280
-42
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import ql
2+
private import codeql_ql.ast.internal.Module
3+
4+
FileOrModule hasQueryRelation(ClasslessPredicate pred) {
5+
pred.hasAnnotation("query") and
6+
(
7+
result.asModule().getAMember() = pred
8+
or
9+
any(TopLevel top | top.getLocation().getFile() = result.asFile()).getAMember() = pred
10+
)
11+
}
12+
13+
FileOrModule importsQueryRelation(ClasslessPredicate pred) {
14+
result = hasQueryRelation(pred)
15+
or
16+
exists(Import i |
17+
not (i.hasAnnotation("private") and i.getLocation().getFile().getExtension() = "qll") and
18+
importsQueryRelation(pred) = i.getResolvedModule()
19+
|
20+
i = result.asModule().getAMember()
21+
or
22+
i = any(TopLevel top | top.getLocation().getFile() = result.asFile()).getAMember()
23+
)
24+
}
25+
26+
class Query extends File {
27+
Query() { this.getExtension() = "ql" }
28+
29+
predicate isPathProblem() {
30+
exists(QLDoc doc | doc.getLocation().getFile() = this |
31+
doc.getContents().matches("%@kind path-problem%")
32+
)
33+
}
34+
35+
predicate isProblem() {
36+
exists(QLDoc doc | doc.getLocation().getFile() = this |
37+
doc.getContents().matches("%@kind problem%")
38+
)
39+
}
40+
41+
predicate hasEdgesRelation(ClasslessPredicate pred) {
42+
importsQueryRelation(pred).asFile() = this and pred.getName() = "edges"
43+
}
44+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import ql
2+
import codeql_ql.bugs.PathProblemQueryQuery as PathProblemQuery
3+
import codeql_ql.ast.internal.Module
4+
5+
/** Gets something that can be imported by a ".qll" file. */
6+
private AstNode publicApi() {
7+
// base case - the toplevel is always "exported".
8+
result instanceof TopLevel
9+
or
10+
// recursive case. A public class/module/predicate/import that is a child of a public API.
11+
not result.hasAnnotation("private") and
12+
not result.getLocation().getFile().getExtension() = "ql" and // everything in ".ql" files is kinda private, as you can't import it. Query predicates/from-where-select is handled in `queryable`.
13+
result.getParent() = publicApi() and
14+
(
15+
result instanceof Class
16+
or
17+
result instanceof ClasslessPredicate
18+
or
19+
result instanceof Module
20+
or
21+
result instanceof Import
22+
)
23+
or
24+
result = publicApi().(Import).getResolvedModule().asModule()
25+
}
26+
27+
/**
28+
* Gets any AstNode that directly computes a result of a query.
29+
* I.e. a query predicate or the from-where-select.
30+
*/
31+
private AstNode queryable() {
32+
// result = query relation that is "transitively" imported by a .ql file.
33+
PathProblemQuery::importsQueryRelation(result).asFile().getExtension() = "ql"
34+
or
35+
// the from-where-select
36+
result instanceof Select
37+
or
38+
// child of the above.
39+
result = queryable().getAChild()
40+
}
41+
42+
AstNode hackyShouldBeTreatedAsAlive() {
43+
// Stages from the shared DataFlow impl are copy-pasted, so predicates that are dead in one stage are not dead in another.
44+
result = any(Module mod | mod.getName().matches("Stage%")).getAMember().(ClasslessPredicate) and
45+
result.getLocation().getFile().getBaseName().matches("DataFlowImpl%")
46+
or
47+
// Python stuff
48+
result.(Predicate).getName() = "quickEvalMe" // private predicate used for quick-eval
49+
or
50+
result.(Module).getName() = "FutureWork" // holder for later.
51+
or
52+
result = hackyShouldBeTreatedAsAlive().getAChild()
53+
}
54+
55+
/**
56+
* Gets an AST node that is alive.
57+
* That is, an API node that may in some way be part of or affect a query result or a publicly available API.
58+
*/
59+
private AstNode alive() {
60+
//
61+
// The 4 base cases.
62+
//
63+
// 1) everything that can be imported.
64+
result = publicApi()
65+
or
66+
// 2) everything that can be an output when running a query
67+
result = queryable()
68+
or
69+
// 3) A module with an import that imports another file, the import can activate a file.
70+
result.(Module).getAMember().(Import).getResolvedModule().getFile() !=
71+
result.getLocation().getFile()
72+
or
73+
// 4) Things that aren't really alive, but that this query treats as live.
74+
result = hackyShouldBeTreatedAsAlive()
75+
or
76+
//
77+
// The recursive cases.
78+
//
79+
result.getEnclosingPredicate() = alive()
80+
or
81+
result = alive().(Call).getTarget()
82+
or
83+
alive().(ClassPredicate).overrides(result)
84+
or
85+
result.(ClassPredicate).overrides(alive())
86+
or
87+
result = alive().(PredicateExpr).getResolvedPredicate()
88+
or
89+
// if a sub-class is alive, then the super-class is alive.
90+
result = alive().(Class).getASuperType().getResolvedType().(ClassType).getDeclaration()
91+
or
92+
// if the super class is alive and abstract, then any sub-class is alive.
93+
exists(Class sup | sup = alive() and sup.isAbstract() |
94+
sup = result.(Class).getASuperType().getResolvedType().(ClassType).getDeclaration()
95+
)
96+
or
97+
result = alive().(Class).getAChild() and
98+
not result.hasAnnotation("private")
99+
or
100+
result = alive().getAnAnnotation()
101+
or
102+
result = alive().getQLDoc()
103+
or
104+
// any imported module is alive. We don't have to handle the "import a file"-case, those are treated as public APIs.
105+
result = alive().(Import).getResolvedModule().asModule()
106+
or
107+
result = alive().(VarDecl).getType().getDeclaration()
108+
or
109+
result = alive().(FieldDecl).getVarDecl()
110+
or
111+
result = alive().(InlineCast).getType().getDeclaration()
112+
or
113+
// a class overrides some predicate, is the super-predicate is alive.
114+
exists(ClassPredicate pred, ClassPredicate sup |
115+
pred.hasAnnotation("override") and
116+
pred.overrides(sup) and
117+
result = pred.getParent() and
118+
sup.getParent() = alive()
119+
)
120+
or
121+
// if a class is alive, so is it's super-class
122+
result =
123+
[alive().(Class).getASuperType(), alive().(Class).getAnInstanceofType()]
124+
.getResolvedType()
125+
.getDeclaration()
126+
or
127+
// if a class is alive and abstract, then any sub-class is alive.
128+
exists(Class clz, Class sup | result = clz |
129+
clz.getASuperType().getResolvedType().getDeclaration() = sup and
130+
sup.isAbstract() and
131+
sup = alive()
132+
)
133+
or
134+
// a module containing something live, is also alive.
135+
result.(Module).getAMember() = alive()
136+
or
137+
result = alive().(Module).getAlias()
138+
or
139+
result.(NewType).getABranch() = alive()
140+
or
141+
result = alive().(TypeExpr).getAChild()
142+
or
143+
result = alive().(FieldAccess).getDeclaration()
144+
or
145+
result = alive().(VarDecl).getTypeExpr()
146+
or
147+
result.(Import).getParent() = alive()
148+
or
149+
result = alive().(NewType).getABranch()
150+
or
151+
result = alive().(ModuleExpr).getAChild()
152+
or
153+
result = alive().(ModuleExpr).getResolvedModule().asModule()
154+
or
155+
result = alive().(InstanceOf).getType().getResolvedType().getDeclaration()
156+
or
157+
result = alive().(Annotation).getAChild()
158+
or
159+
result = alive().(Predicate).getReturnType().getDeclaration()
160+
}
161+
162+
private AstNode deprecated() {
163+
result.hasAnnotation("deprecated")
164+
or
165+
result = deprecated().getQLDoc()
166+
or
167+
result = deprecated().getAnAnnotation()
168+
or
169+
result = deprecated().getAChild()
170+
}
171+
172+
// our type-resolution skips these, so ignore.
173+
private AstNode classUnion() {
174+
exists(result.(Class).getUnionMember())
175+
or
176+
exists(result.(Class).getAliasType())
177+
or
178+
result = classUnion().(Class).getUnionMember()
179+
or
180+
result = classUnion().(Class).getAliasType()
181+
or
182+
result = classUnion().getAnAnnotation()
183+
or
184+
result = classUnion().getQLDoc()
185+
or
186+
result = classUnion().(TypeExpr).getAChild()
187+
or
188+
result = classUnion().(ModuleExpr).getAChild()
189+
}
190+
191+
private predicate isDeadInternal(AstNode node) {
192+
not node = alive() and
193+
not node = deprecated() and
194+
not node = classUnion()
195+
}
196+
197+
predicate isDead(AstNode node) {
198+
isDeadInternal(node) and
199+
not isDeadInternal(node.getParent()) and
200+
not node instanceof BlockComment and
201+
exists(node.toString()) and // <- invalid code
202+
node.getLocation().getFile().getExtension() = ["ql", "qll"] and // ignore dbscheme files
203+
// cached-stages pattern
204+
not node.(Module).getAMember().(ClasslessPredicate).getName() = "forceStage" and
205+
not node.(ClasslessPredicate).getName() = "forceStage" and
206+
not node.getLocation().getFile().getBaseName() = "Caching.qll" and
207+
not node.getLocation().getFile().getRelativePath().matches("%/tutorials/%") // sometimes contains dead code - ignore
208+
}

ql/ql/src/queries/bugs/PathProblemQuery.ql

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,7 @@
1010
*/
1111

1212
import ql
13-
import codeql_ql.ast.internal.Module
14-
15-
FileOrModule hasEdgesRelation(ClasslessPredicate pred) {
16-
pred.getName() = "edges" and
17-
pred.hasAnnotation("query") and
18-
(
19-
result.asModule().getAMember() = pred
20-
or
21-
any(TopLevel top | top.getLocation().getFile() = result.asFile()).getAMember() = pred
22-
)
23-
}
24-
25-
FileOrModule importsEdges(ClasslessPredicate pred) {
26-
result = hasEdgesRelation(pred)
27-
or
28-
exists(Import i |
29-
not (i.hasAnnotation("private") and i.getLocation().getFile().getExtension() = "qll") and
30-
importsEdges(pred) = i.getResolvedModule()
31-
|
32-
i = result.asModule().getAMember()
33-
or
34-
i = any(TopLevel top | top.getLocation().getFile() = result.asFile()).getAMember()
35-
)
36-
}
37-
38-
class Query extends File {
39-
Query() { this.getExtension() = "ql" }
40-
41-
predicate isPathProblem() {
42-
exists(QLDoc doc | doc.getLocation().getFile() = this |
43-
doc.getContents().matches("%@kind path-problem%")
44-
)
45-
}
46-
47-
predicate isProblem() {
48-
exists(QLDoc doc | doc.getLocation().getFile() = this |
49-
doc.getContents().matches("%@kind problem%")
50-
)
51-
}
52-
53-
predicate hasEdgesRelation(ClasslessPredicate pred) { importsEdges(pred).asFile() = this }
54-
}
13+
import codeql_ql.bugs.PathProblemQueryQuery
5514

5615
from Query query, string msg, AstNode pred
5716
where

ql/ql/src/queries/style/DeadCode.ql

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @name Dead code
3+
* @description Code that cannot be reached should be deleted.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @id ql/dead-code
7+
* @precision very-high
8+
*/
9+
10+
import ql
11+
import codeql_ql.style.DeadCodeQuery
12+
13+
from AstNode node
14+
where isDead(node)
15+
select node, "Code is dead"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| Foo.qll:2:11:2:38 | ClasslessPredicate dead1 | Code is dead |
2+
| Foo.qll:6:3:6:30 | ClasslessPredicate dead2 | Code is dead |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/style/DeadCode.ql
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
private module Mixed {
2+
private predicate dead1() { none() }
3+
4+
predicate alive1() { none() }
5+
6+
predicate dead2() { none() }
7+
}
8+
9+
predicate usesAlive() { Mixed::alive1() }

0 commit comments

Comments
 (0)