QL: Query for finding missing or unwanted bidirectional imports of abstract classes

This commit is contained in:
Joe Farebrother
2021-10-14 14:36:14 +01:00
parent 1762394b9b
commit 1013cb4ccb
3 changed files with 92 additions and 1 deletions

View File

@@ -804,6 +804,9 @@ class Class extends TClass, TypeDeclaration, ModuleDeclaration {
result = this.getClassPredicate(name)
)
}
/** Holds if this class is abstract. */
predicate isAbstract() { hasAnnotation(this, "abstract") }
}
/**

View File

@@ -19,7 +19,9 @@ private class ContainerOrModule extends TContainerOrModule {
private class TFileOrModule = TFile or TModule;
/** A file or a module. */
class FileOrModule extends TFileOrModule, ContainerOrModule { }
class FileOrModule extends TFileOrModule, ContainerOrModule {
abstract File getFile();
}
private class File_ extends FileOrModule, TFile {
File f;
@@ -41,6 +43,8 @@ private class File_ extends FileOrModule, TFile {
endline = 0 and
endcolumn = 0
}
override File getFile() { result = f }
}
private class Folder_ extends ContainerOrModule, TFolder {
@@ -90,6 +94,8 @@ class Module_ extends FileOrModule, TModule {
) {
m.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
override File getFile() { result = m.getLocation().getFile() }
}
private predicate resolveQualifiedName(Import imp, ContainerOrModule m, int i) {

View File

@@ -0,0 +1,82 @@
/**
* @name Bidirectional imports for abstract classes
* @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.
* @kind problem
* @problem.severity error
* @id ql/abstract-class-import
* @tags correctness
* performance
* @precision high
*/
import ql
import codeql_ql.ast.internal.Module
File imports(File file) {
exists(Import imp |
imp.getLocation().getFile() = file and
result = imp.getResolvedModule().getFile()
)
}
/** Gets a non-abstract subclass of `ab` that is defined in a different file */
Class concreteExternalSubclass(Class ab) {
ab.isAbstract() and
not result.isAbstract() and
result.getType().getASuperType*() = ab.getType() and
result.getLocation().getFile() != ab.getLocation().getFile()
}
/** Holds if there is a bidirectional import between the abstract class `ab` and its subclass `sub` */
predicate bidirectionalImport(Class ab, Class sub) {
sub = concreteExternalSubclass(ab) and
sub.getLocation().getFile() = imports*(ab.getLocation().getFile())
}
predicate stats(Class ab, int imports, int nonImports) {
ab.isAbstract() and
imports =
strictcount(Class sub | sub = concreteExternalSubclass(ab) and bidirectionalImport(ab, sub)) and
nonImports =
strictcount(Class sub | sub = concreteExternalSubclass(ab) and not bidirectionalImport(ab, sub))
}
predicate alert(Class ab, string msg, Class sub, Class sub2) {
sub = concreteExternalSubclass(ab) and
sub2 = concreteExternalSubclass(ab) and
exists(int imports, int nonImports | stats(ab, imports, nonImports) |
(imports < 10 or nonImports < 10) and // if this is not the case, it's likely intended
(
// report whichever of imports or nonimports there are more of; both if equal
imports >= nonImports and
not bidirectionalImport(ab, sub) and
sub2 =
min(Class other |
other = concreteExternalSubclass(ab) and
bidirectionalImport(ab, other)
|
other order by other.getLocation().toString()
) and
msg =
"This abstract class doesn't import its subclass $@ but imports " + imports +
" other subclasses, such as $@."
or
nonImports >= imports and
bidirectionalImport(ab, sub) and
sub2 =
min(Class other |
other = concreteExternalSubclass(ab) and
not bidirectionalImport(ab, other)
|
other order by other.getLocation().toString()
) and
msg =
"This abstract class imports its subclass $@ but doesn't import " + nonImports +
" other subclasses, such as $@."
)
)
}
from Class ab, string msg, Class sub, Class sub2
where alert(ab, msg, sub, sub2)
select ab, msg, sub, sub.getName(), sub2, sub2.getName()