Skip to content

Commit 370d75b

Browse files
authored
QL: Merge pull request github#104 from github/bidirectional-import
Query for finding missing or unwanted bidirectional imports of abstract classes
2 parents 7745a13 + 3fc0bed commit 370d75b

File tree

3 files changed

+115
-1
lines changed

3 files changed

+115
-1
lines changed

ql/src/codeql_ql/ast/Ast.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,9 @@ class Class extends TClass, TypeDeclaration, ModuleDeclaration {
781781
result = this.getClassPredicate(name)
782782
)
783783
}
784+
785+
/** Holds if this class is abstract. */
786+
predicate isAbstract() { hasAnnotation("abstract") }
784787
}
785788

786789
/**

ql/src/codeql_ql/ast/internal/Module.qll

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ private class ContainerOrModule extends TContainerOrModule {
1919
private class TFileOrModule = TFile or TModule;
2020

2121
/** A file or a module. */
22-
class FileOrModule extends TFileOrModule, ContainerOrModule { }
22+
class FileOrModule extends TFileOrModule, ContainerOrModule {
23+
abstract File getFile();
24+
}
2325

2426
private class File_ extends FileOrModule, TFile {
2527
File f;
@@ -41,6 +43,8 @@ private class File_ extends FileOrModule, TFile {
4143
endline = 0 and
4244
endcolumn = 0
4345
}
46+
47+
override File getFile() { result = f }
4448
}
4549

4650
private class Folder_ extends ContainerOrModule, TFolder {
@@ -90,6 +94,8 @@ class Module_ extends FileOrModule, TModule {
9094
) {
9195
m.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
9296
}
97+
98+
override File getFile() { result = m.getLocation().getFile() }
9399
}
94100

95101
private predicate resolveQualifiedName(Import imp, ContainerOrModule m, int i) {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* @name Bidirectional imports for abstract classes
3+
* @description An abstract class should import each of its subclasses, unless
4+
* it is meant as a configuration-style class, in which case it
5+
* should import none of them.
6+
* @kind problem
7+
* @problem.severity error
8+
* @id ql/abstract-class-import
9+
* @tags correctness
10+
* performance
11+
* @precision high
12+
*/
13+
14+
import ql
15+
import codeql_ql.ast.internal.Module
16+
17+
File imports(File file) {
18+
exists(Import imp |
19+
imp.getLocation().getFile() = file and
20+
result = imp.getResolvedModule().getFile()
21+
)
22+
}
23+
24+
predicate testFile(File f) { f.getRelativePath().matches("%/ql/test/%") }
25+
26+
predicate nonTestQuery(File f) { f.getBaseName().matches("%.ql") and not testFile(f) }
27+
28+
predicate liveNonTestFile(File f) {
29+
exists(File query | nonTestQuery(query) and f = imports*(query))
30+
}
31+
32+
predicate experimentalFile(File f) { f.getRelativePath().matches("%/experimental/%") }
33+
34+
Class getASubclassOfAbstract(Class ab) {
35+
ab.isAbstract() and
36+
result.getType().getASuperType() = ab.getType()
37+
}
38+
39+
/** Gets a non-abstract subclass of `ab` that contributes to the extent of `ab`. */
40+
Class concreteExternalSubclass(Class ab) {
41+
ab.isAbstract() and
42+
not result.isAbstract() and
43+
result = getASubclassOfAbstract+(ab) and
44+
// Heuristic: An abstract class with subclasses in the same file and no other
45+
// imported subclasses is likely intentional.
46+
result.getLocation().getFile() != ab.getLocation().getFile() and
47+
// Exclude subclasses in tests and libraries that are only used in tests.
48+
liveNonTestFile(result.getLocation().getFile())
49+
}
50+
51+
/** Holds if there is a bidirectional import between the abstract class `ab` and its subclass `sub` */
52+
predicate bidirectionalImport(Class ab, Class sub) {
53+
sub = concreteExternalSubclass(ab) and
54+
sub.getLocation().getFile() = imports*(ab.getLocation().getFile())
55+
}
56+
57+
predicate stats(Class ab, int imports, int nonImports) {
58+
ab.isAbstract() and
59+
imports =
60+
strictcount(Class sub | sub = concreteExternalSubclass(ab) and bidirectionalImport(ab, sub)) and
61+
nonImports =
62+
strictcount(Class sub | sub = concreteExternalSubclass(ab) and not bidirectionalImport(ab, sub))
63+
}
64+
65+
predicate alert(Class ab, string msg, Class sub, Class sub2) {
66+
sub = concreteExternalSubclass(ab) and
67+
sub2 = concreteExternalSubclass(ab) and
68+
exists(int imports, int nonImports | stats(ab, imports, nonImports) |
69+
(imports < 10 or nonImports < 10) and // if this is not the case, it's likely intended
70+
(
71+
// report whichever of imports or nonimports there are more of; both if equal
72+
imports >= nonImports and
73+
not bidirectionalImport(ab, sub) and
74+
sub2 =
75+
min(Class other |
76+
other = concreteExternalSubclass(ab) and
77+
bidirectionalImport(ab, other)
78+
|
79+
other order by other.getLocation().toString()
80+
) and
81+
msg =
82+
"This abstract class doesn't import its subclass $@ but imports " + imports +
83+
" other subclasses, such as $@."
84+
or
85+
nonImports >= imports and
86+
bidirectionalImport(ab, sub) and
87+
sub2 =
88+
min(Class other |
89+
other = concreteExternalSubclass(ab) and
90+
not bidirectionalImport(ab, other)
91+
|
92+
other order by other.getLocation().toString()
93+
) and
94+
msg =
95+
"This abstract class imports its subclass $@ but doesn't import " + nonImports +
96+
" other subclasses, such as $@."
97+
)
98+
) and
99+
// exclude results in experimental
100+
not experimentalFile(sub.getLocation().getFile())
101+
}
102+
103+
from Class ab, string msg, Class sub, Class sub2
104+
where alert(ab, msg, sub, sub2)
105+
select ab, msg, sub, sub.getName(), sub2, sub2.getName()

0 commit comments

Comments
 (0)