Java 和 Kotlin 中的注解¶
Java/Kotlin 项目的 CodeQL 数据库包含有关附加到程序元素的所有注解的信息。
注意
CodeQL 对 Kotlin 的分析目前处于测试阶段。在测试期间,对 Kotlin 代码的分析及其相关文档的完整性将不如其他语言。
关于使用注解¶
注解由以下 CodeQL 类表示
- 类
Annotatable
表示所有可能附加有注解的实体(即,包、引用类型、字段、方法和局部变量)。 - 类
AnnotationType
表示 Java 注解类型,例如java.lang.Override
;注解类型是接口。 - 类
AnnotationElement
表示注解元素,即注解类型的成员。 - 类
Annotation
表示注解,例如@Override
;注解值可以通过成员谓词getValue
访问。
例如,Java/Kotlin 标准库定义了一个注解 SuppressWarnings
,它指示编译器不要发出某些类型的警告
package java.lang;
public @interface SuppressWarnings {
String[] value;
}
SuppressWarnings
被表示为一个 AnnotationType
,其唯一的 AnnotationElement
是 value
。
SuppressWarnings
的典型用法是用于防止使用原始类型发出警告的此注解
class A {
@SuppressWarnings("rawtypes")
public A(java.util.List rawlist) {
}
}
表达式 @SuppressWarnings("rawtypes")
被表示为一个 Annotation
。字符串字面量 "rawtypes"
用于初始化注解元素 value
,其值可以通过 getValue
谓词从注解中提取。
然后我们可以编写此查询来查找附加到构造函数的所有 @SuppressWarnings
注解,并返回注解本身及其 value
元素的值
import java
from Constructor c, Annotation ann, AnnotationType anntp
where ann = c.getAnAnnotation() and
anntp = ann.getType() and
anntp.hasQualifiedName("java.lang", "SuppressWarnings")
select ann, ann.getValue("value")
如果您正在分析的代码库使用 @SuppressWarnings
注解,您可以检查查询返回的注解元素的 value
。它们应该使用上面描述的 "rawtypes"
值。
作为另一个例子,此查询查找所有只有一个注解元素的注解类型,该元素的名称为 value
import java
from AnnotationType anntp
where forex(AnnotationElement elt |
elt = anntp.getAnAnnotationElement() |
elt.getName() = "value"
)
select anntp
示例:查找缺少的 @Override
注解¶
在较新的 Java 版本中,建议(虽然不是必需)您将覆盖另一个方法的方法用 @Override
注解进行注释。这些注解由编译器检查,用作文档,并帮助您避免意外的重载,而本应是覆盖。
例如,考虑此示例程序
class Super {
public void m() {}
}
class Sub1 extends Super {
@Override public void m() {}
}
class Sub2 extends Super {
public void m() {}
}
这里,Sub1.m
和 Sub2.m
都覆盖了 Super.m
,但只有 Sub1.m
用 @Override
进行注释。
现在我们将开发一个查询,用于查找像 Sub2.m
这样的方法,这些方法应该用 @Override
注解,但没有注释。
作为第一步,让我们编写一个查询,查找所有 @Override
注解。注解是表达式,因此可以使用 getType
访问其类型。另一方面,注解类型是接口,因此可以使用 hasQualifiedName
查询其限定名称。因此,我们可以像这样实现查询
import java
from Annotation ann
where ann.getType().hasQualifiedName("java.lang", "Override")
select ann
和往常一样,最好在 Java/Kotlin 项目的 CodeQL 数据库上尝试此查询,以确保它确实产生了一些结果。在前面的示例中,它应该找到 Sub1.m
上的注解。接下来,我们将 @Override
注解的概念封装为 CodeQL 类
class OverrideAnnotation extends Annotation {
OverrideAnnotation() {
this.getType().hasQualifiedName("java.lang", "Override")
}
}
这使得编写查找覆盖另一个方法但没有 @Override
注解的方法的查询变得非常容易:我们使用谓词 overrides
来确定一个方法是否覆盖另一个方法,并使用谓词 getAnAnnotation
(对任何 Annotatable
都可用)来检索某个注解。
import java
from Method overriding, Method overridden
where overriding.overrides(overridden) and
not overriding.getAnAnnotation() instanceof OverrideAnnotation
select overriding, "Method overrides another method, but does not have an @Override annotation."
在实践中,此查询可能会从编译的库代码中产生许多结果,这些结果并不十分有趣。因此,最好添加另一个合取词 overriding.fromSource()
来限制结果,仅报告有源代码可用的方法。
示例:查找对已弃用方法的调用¶
作为另一个例子,我们可以编写一个查询,查找对用 @Deprecated
注解标记的方法的调用。
例如,考虑此示例程序
class A {
@Deprecated void m() {}
@Deprecated void n() {
m();
}
void r() {
m();
}
}
这里,A.m
和 A.n
都被标记为已弃用。方法 n
和 r
都调用了 m
,但请注意,n
本身已弃用,因此我们可能不应该对此调用发出警告。
与前面的示例一样,我们将从定义一个用于表示 @Deprecated
注解的类开始
class DeprecatedAnnotation extends Annotation {
DeprecatedAnnotation() {
this.getType().hasQualifiedName("java.lang", "Deprecated")
}
}
现在我们可以定义一个用于表示已弃用方法的类
class DeprecatedMethod extends Method {
DeprecatedMethod() {
this.getAnAnnotation() instanceof DeprecatedAnnotation
}
}
最后,我们使用这些类来查找对已弃用方法的调用,排除自身出现在已弃用方法中的调用
import java
from Call call
where call.getCallee() instanceof DeprecatedMethod
and not call.getCaller() instanceof DeprecatedMethod
select call, "This call invokes a deprecated method."
在我们的示例中,此查询会标记 A.r
中对 A.m
的调用,但不会标记 A.n
中的调用。
有关类 Call
的更多信息,请参见“在调用图中导航”。
改进¶
Java/Kotlin 标准库提供了另一个注解类型 java.lang.SupressWarnings
,它可用于抑制某些类别的警告。特别是,它可以用于关闭对已弃用方法的调用的警告。因此,改进我们的查询以忽略从标记有 @SuppressWarnings("deprecation")
的方法内部对已弃用方法的调用是很有意义的。
例如,考虑此略微更新的示例
class A {
@Deprecated void m() {}
@Deprecated void n() {
m();
}
@SuppressWarnings("deprecation")
void r() {
m();
}
}
这里,程序员已明确抑制了 A.r
中关于已弃用调用的警告,因此我们的查询不应该再标记对 A.m
的调用了。
为此,我们首先引入一个类来表示所有 @SuppressWarnings
注解,其中字符串 deprecation
出现在要抑制的警告列表中
class SuppressDeprecationWarningAnnotation extends Annotation {
SuppressDeprecationWarningAnnotation() {
this.getType().hasQualifiedName("java.lang", "SuppressWarnings") and
this.getAValue().(Literal).getLiteral().regexpMatch(".*deprecation.*")
}
}
这里,我们使用 getAValue()
来检索任何注解值:实际上,注解类型 SuppressWarnings
只有一个注解元素,因此每个 @SuppressWarnings
注解只有一个注解值。然后,我们确保它是一个字面量,使用 getLiteral
获取其字符串值,并使用正则表达式匹配检查它是否包含字符串 deprecation
。
对于实际使用,此检查需要稍微概括一下:例如,OpenJDK Java 编译器允许 @SuppressWarnings("all")
注解来抑制所有警告。我们可能还想确保将 deprecation
匹配为一个完整的词,而不是另一个词的一部分,方法是将正则表达式更改为 ".*\\bdeprecation\\b.*"
。
现在我们可以扩展我们的查询以过滤掉包含 SuppressDeprecationWarningAnnotation
的方法中的调用
import java
// Insert the class definitions from above
from Call call
where call.getCallee() instanceof DeprecatedMethod
and not call.getCaller() instanceof DeprecatedMethod
and not call.getCaller().getAnAnnotation() instanceof SuppressDeprecationWarningAnnotation
select call, "This call invokes a deprecated method."
项目中通常包含对似乎已弃用方法的调用。