Skip to content

Commit 1013cb4

Browse files
QL: Query for finding missing or unwanted bidirectional imports of abstract classes
1 parent 1762394 commit 1013cb4

3 files changed

Lines changed: 92 additions & 1 deletion

File tree

ql/src/codeql_ql/ast/Ast.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,9 @@ class Class extends TClass, TypeDeclaration, ModuleDeclaration {
804804
result = this.getClassPredicate(name)
805805
)
806806
}
807+
808+
/** Holds if this class is abstract. */
809+
predicate isAbstract() { hasAnnotation(this, "abstract") }
807810
}
808811

809812
/**

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: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @name Bidirectional imports for abstract classes
3+
* @description An abstract class should import each of its subclasses, unless it is meant as an extension point, in which case it should import none of them.
4+
* @kind problem
5+
* @problem.severity error
6+
* @id ql/abstract-class-import
7+
* @tags correctness
8+
* performance
9+
* @precision high
10+
*/
11+
12+
import ql
13+
import codeql_ql.ast.internal.Module
14+
15+
File imports(File file) {
16+
exists(Import imp |
17+
imp.getLocation().getFile() = file and
18+
result = imp.getResolvedModule().getFile()
19+
)
20+
}
21+
22+
/** Gets a non-abstract subclass of `ab` that is defined in a different file */
23+
Class concreteExternalSubclass(Class ab) {
24+
ab.isAbstract() and
25+
not result.isAbstract() and
26+
result.getType().getASuperType*() = ab.getType() and
27+
result.getLocation().getFile() != ab.getLocation().getFile()
28+
}
29+
30+
/** Holds if there is a bidirectional import between the abstract class `ab` and its subclass `sub` */
31+
predicate bidirectionalImport(Class ab, Class sub) {
32+
sub = concreteExternalSubclass(ab) and
33+
sub.getLocation().getFile() = imports*(ab.getLocation().getFile())
34+
}
35+
36+
predicate stats(Class ab, int imports, int nonImports) {
37+
ab.isAbstract() and
38+
imports =
39+
strictcount(Class sub | sub = concreteExternalSubclass(ab) and bidirectionalImport(ab, sub)) and
40+
nonImports =
41+
strictcount(Class sub | sub = concreteExternalSubclass(ab) and not bidirectionalImport(ab, sub))
42+
}
43+
44+
predicate alert(Class ab, string msg, Class sub, Class sub2) {
45+
sub = concreteExternalSubclass(ab) and
46+
sub2 = concreteExternalSubclass(ab) and
47+
exists(int imports, int nonImports | stats(ab, imports, nonImports) |
48+
(imports < 10 or nonImports < 10) and // if this is not the case, it's likely intended
49+
(
50+
// report whichever of imports or nonimports there are more of; both if equal
51+
imports >= nonImports and
52+
not bidirectionalImport(ab, sub) and
53+
sub2 =
54+
min(Class other |
55+
other = concreteExternalSubclass(ab) and
56+
bidirectionalImport(ab, other)
57+
|
58+
other order by other.getLocation().toString()
59+
) and
60+
msg =
61+
"This abstract class doesn't import its subclass $@ but imports " + imports +
62+
" other subclasses, such as $@."
63+
or
64+
nonImports >= imports and
65+
bidirectionalImport(ab, sub) and
66+
sub2 =
67+
min(Class other |
68+
other = concreteExternalSubclass(ab) and
69+
not bidirectionalImport(ab, other)
70+
|
71+
other order by other.getLocation().toString()
72+
) and
73+
msg =
74+
"This abstract class imports its subclass $@ but doesn't import " + nonImports +
75+
" other subclasses, such as $@."
76+
)
77+
)
78+
}
79+
80+
from Class ab, string msg, Class sub, Class sub2
81+
where alert(ab, msg, sub, sub2)
82+
select ab, msg, sub, sub.getName(), sub2, sub2.getName()

0 commit comments

Comments
 (0)