使用科德纳克在 Grails 应用程序中进行静态代码分析
在此指南中,您将学习如何使用科德纳克进行静态分析来改进您的代码。
作者:伊万·洛佩兹
Grails 版本 4.0.1
1 Grails 培训
Grails 培训 - 由创作并积极维护 Grails 框架的人开发和交付!
2 开始
在编写代码时,遵循一些规则、正确的惯例、样式规则等非常重要。然而,有时它并不那么简单。当我们团队协作时尤其重要,其中每位成员都有自己的偏好。改进此问题的一种方法是为代码添加一个静态分析工具。
在本指南中,您将安装并配置 科德纳克 来帮助您提高 Grails 代码的质量,您还将学习如何创建自定义的科德纳克规则。科德纳克会分析 Groovy 代码并报告潜在的 bug 和代码问题。
2.1 所需条件
要完成本指南,您需要具备以下条件
-
一些时间
-
一个不错的文本编辑器或 IDE
-
安装了 JDK 1.8 或更高版本,并正确配置了
JAVA_HOME
2.2 如何完成该指南
开始时执行以下操作
-
下载并解压源代码
或
-
克隆 Git 代码库
git clone https://github.com/grails-guides/grails-codenarc.git
Grails 指南代码库包含两个文件夹
-
initial
初始项目。通常是一个带有其他代码以帮助你快速开始的简单 Grails 应用程序。 -
complete
已完成的示例。是根据该指南给出的步骤实际操作并对initial
文件夹应用那些更改的结果。
若要完成该指南,请转到 initial
文件夹
-
在
grails-guides/grails-codenarc/initial
中cd
并按照下一部分中的说明进行操作。
如果你在 grails-guides/grails-codenarc/complete 中 cd ,则可以转到**已完成的示例** |
3 编写应用程序
3.1 多项目构建
我们将使用 Grails 应用程序和一个自定义 CodeNarc 规则设置一个多项目构建,如下图所示
查看我们的Grails 多项目构建指南以了解更多信息。
3.2 规则类型
当前的 CodeNarc 版本 (0.27.0) 包括分为 22 个类别的348 条规则
-
基本:例如,检查是否存在空的
else
或finally
块。 -
花括号:你见过多少次只有一条语句而没有花括号的
if
或else
?我个人不喜欢没有花括号的代码,因为它是未来出现缺陷的根源。我们可以添加此类别中的规则来执行这些检查。 -
约定:有一些规则用于检查某些约定:比如,当我们编写一个“反向”
if
、一个可以转换为 Elvis 运算符的if
时,…… -
异常:如果我们抛出
NullPointerException
之类的异常,这些规则就会失败。
还有更多类别,可以检查重复导入、未使用变量、不必要的 if,……当然,Grails 规则还有特定类别。
3.3 将 CodeNarc 添加到我们的项目
将 CodeNarc 添加到我们的项目是一项简单的任务,因为它有一个Gradle 插件。
让我们修改 build.gradle
并添加以下内容
apply from: "${rootProject.projectDir}/gradle/codenarc.gradle"
我们在 gradle/codenarc.gradle
中封装了所有 CodeNarc 配置
apply plugin: 'codenarc' (1)
codenarc {
toolVersion = '1.4' (2)
configFile = file("${rootProject.projectDir}/config/codenarc/rules.groovy") (3)
reportFormat = 'html' (4)
ignoreFailures = true (5)
}
1 | 应用 codenarc 插件。 |
2 | 设置我们想要使用的 CodeNarc 版本。 |
3 | 定义包含规则的主文件。默认情况下,CodeNarc 使用 config/codenarc/codenarc.xml ,但我们不想编写 XML 文件,对吗? |
4 | 我们想要的报告格式。对于人类可读格式,我们使用 html 。如果我们想将 CodeNarc 与 Jenkins 等集成,则需要将其更改为 xml 。 |
5 | 当只有一条违规时,我们不希望构建失败。 |
然后我们需要创建规则文件
ruleset {
description 'Grails-CodeNarc Project RuleSet'
ruleset('rulesets/basic.xml')
ruleset('rulesets/braces.xml')
ruleset('rulesets/convention.xml')
ruleset('rulesets/design.xml')
ruleset('rulesets/dry.xml')
ruleset('rulesets/exceptions.xml')
ruleset('rulesets/formatting.xml')
ruleset('rulesets/generic.xml')
ruleset('rulesets/imports.xml')
ruleset('rulesets/naming.xml')
ruleset('rulesets/unnecessary.xml')
ruleset('rulesets/unused.xml')
ruleset('rulesets/grails.xml')
}
有了此配置,我们就可以运行 check
任务了。
$ ./gradlew app:check
:check UP-TO-DATE
:complete:codenarcMain
CodeNarc rule violations were found. See the report at: file:///home/ivan/workspaces/oci/guides/grails-codenarc/complete/app/build/reports/codenarc/main.html
:complete:codenarcTest NO-SOURCE
:complete:compileJava NO-SOURCE
:complete:compileGroovy UP-TO-DATE
:complete:buildProperties UP-TO-DATE
:complete:processResources UP-TO-DATE
:complete:classes UP-TO-DATE
:complete:compileTestJava NO-SOURCE
:complete:compileTestGroovy NO-SOURCE
:complete:processTestResources NO-SOURCE
:complete:testClasses UP-TO-DATE
:complete:test NO-SOURCE
:complete:check
Total time: 1.953 secs
并且我们可以打开测试报告来检查冲突
-
首先,我们已经一个包含执行日期和所使用 CodeNarc 版本的部分。
-
然后,那里还有具有包含违规文件总数以及优先级 1、2 和 3 的违规数的摘要的另一个部分。
-
在此之后,对于每个文件又存在一个部分,我们可以在其中看到该文件中的所有违规(包括代码行和一小部分代码片段)。规则名称是用于访问更详细解释的链接。
让我们修复违规
修复 CodeNarc 违规有不同的方式
-
修复问题:顾名思义,仅修复违规。
-
禁用规则:有时候,我们不同意特定的 CodeNarc 违规。我们可以禁用它。
-
忽略针对特定类或方法的规则:这种情况是我们不想禁用规则,而仅仅想在特定的类或方法中跳过规则。
修复问题
-
要修复违规
FileEndsWithoutNewline
,请在UrlMappings
的末尾添加一行。 -
要修复违规
SpaceBeforeOpeningBrace
,请在大括号之前添加一个空格。
禁用规则
对于 ClassJavadoc
和 NoDef
,我们可以通过修改 rules.groovy
文件来禁用规则
ruleset {
description 'Grails-CodeNarc Project RuleSet'
...
ruleset('rulesets/convention.xml') {
'NoDef' {
enabled = false
}
}
...
ruleset('rulesets/formatting.xml') {
'ClassJavadoc' {
enabled = false
}
}
...
}
忽略规则
最后,我们可以忽略针对特定类的规则。在这种情况下,我们将使用 @SuppressWarnings
忽略 UrlMappings.groovy
针对 UnnecessaryGString
的规则
@SuppressWarnings(['UnnecessaryGString'])
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?" {
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}
检查结果
只需再次执行 check
任务并打开报告即可查看所有违规已消失
3.4 最终配置
如果您在之前未采用过任何静态分析工具的中大型项目中安装 CodeNarc,您可能会遇到数百或数千个违规。有可能您的团队在编写代码的过程中不够仔细。此外,CodeNarc 的默认配置可能包含一些您不同意的规则。可能不适合该团队的规则。
我的建议是查看所有违规,阅读其文档,然后决定您是否要禁用它们,配置其他选项最终修复并遵守它们。
一旦团队确定了规则和配置,下一步是选择使编译失败的阈值。这些阈值允许成功编译出现一些违规,但是一旦我们达到这些级别,编译将失败。
要做到这一点,只需编辑 build.gradle
文件
codenarc {
...
// We need to remove the following line
//ignoreFailures = true
maxPriority1Violations = 0
maxPriority2Violations = 5
maxPriority3Violations = 9
}
使用这种配置,一旦我们达到这些阈值,编译将失败。
4 编写自定义的规则
尽管 CodeNarc 提供了一些适用于 Grails 的 规则,但有时我们可能想要创建自己的规则。
在本例中,我们将创建一个规则来检查我们使用
grails.gorm.transactions.Transactional
而不是 @org.springframework.transaction.annotation.Transactional
。
你可能知道,Grails @Transactional
注释更好,因为它不会创建运行时代理。它是一种 AST 转换,在编译时应用,因此没有运行时开销。Grails 注释还提供一些 Spring 注释不具备的其他功能。
grails.gorm.transactions.Transactional 仅适用于最新版本的 GORM(本指南使用 GORM 7.0.2.RELEASE),对于以前版本,你应使用 @grails.transaction.Transactional 。 |
规则检查我们的代码。如果我们使用 Spring 注释,它会向报告中添加新的违规。
4.1 创建规则
创建项目
我们需要创建一个新的 Groovy 项目,因为我们希望使用 Gradle 以其自己的 jar 文件打包该规则。
plugins {
id 'groovy'
}
repositories {
jcenter()
}
dependencies {
implementation 'org.codenarc:CodeNarc:1.4' (1)
testCompile 'junit:junit:4.12'
}
1 | 只需要 CodeNarc 依赖项 |
为了能够在我们自己的 Grails 应用程序中使用此新的 CodeNarc 规则,我们需要确保在 codenarcMain
任务中,jar 文件可用于类路径
codenarcMain.dependsOn ':codenarc-rule:jar' (1)
tasks.withType(CodeNarc) { (2)
codenarcClasspath += files("${rootProject.projectDir}/codenarc-rule/build/libs/codenarc-rule.jar")
}
1 | CodeNarc 任务依赖于正在生成的 jar 文件 |
2 | 将 jar 添加至 codenarcClasspath |
定义规则
第一步是使用我们想要创建的规则信息创建一个元信息文件
<ruleset xmlns="http://codenarc.org/ruleset/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://codenarc.org/ruleset/1.0 http://codenarc.org/ruleset-schema.xsd"
xsi:noNamespaceSchemaLocation="http://codenarc.org/ruleset-schema.xsd">
<description>Extra Grails rules</description> (1)
<rule class='org.codenarc.rule.grails.GrailsTransactionalRule'/> (2)
</ruleset>
1 | 规则组的说明。 |
2 | 我们创建的每个规则有一个 rule 元素。 |
其次,我们使用纯文本和 html 创建一个带有规则说明的属性文件。消息将用于 html 报告
GrailsTransactional.description=Check that @org.springframework.transaction.annotation.Transactional is used instead of org.springframework.transaction.annotation.Transactional.
GrailsTransactional.description.html=Check that <em>@grails.gorm.transactions.Transactional</em> is used instead of <em>org.springframework.transaction.annotation.Transactional</em>.
实施规则
最后,我们实施规则
package org.codenarc.rule.grails
import groovy.transform.CompileStatic
import org.codehaus.groovy.ast.AnnotatedNode
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.ast.ImportNode
import org.codehaus.groovy.ast.ModuleNode
import org.codenarc.rule.AbstractAstVisitor
import org.codenarc.rule.AbstractAstVisitorRule
@CompileStatic
class GrailsTransactionalRule extends AbstractAstVisitorRule { (1)
int priority = 2 (2)
String name = 'GrailsTransactional' (3)
Class astVisitorClass = GrailsTransactionalVisitor (4)
}
@CompileStatic
class GrailsTransactionalVisitor extends AbstractAstVisitor { (5)
private static final String SPRING_TRANSACTIONAL = 'org.springframework.transaction.annotation.Transactional'
private static final String ERROR_MSG = 'Do not use Spring @Transactional, use @grails.gorm.transactions.Transactional instead'
@Override
void visitAnnotations(AnnotatedNode node) { (6)
node.annotations.each { AnnotationNode annotationNode ->
String annotation = annotationNode.classNode.text
if (annotation == SPRING_TRANSACTIONAL) {
addViolation(node, ERROR_MSG)
}
}
super.visitAnnotations(node)
}
@Override
void visitImports(ModuleNode node) { (7)
node.imports.each { ImportNode importNode ->
String importClass = importNode.className
if (importClass == SPRING_TRANSACTIONAL) {
node.lineNumber = importNode.lineNumber (8)
addViolation(node, ERROR_MSG)
}
}
super.visitImports(node)
}
}
1 | 规则需要从 AbstractAstVisitorRule 扩展 |
2 | 定义违规优先级 |
3 | 规则的名称。它需要与之前在元信息文件中定义的名称相同 |
4 | 实现规则的类 |
5 | 访问者类需要从 AbstractAstVisitor 扩展 |
6 | 检查类和方法的注释,并且在使用 Spring@Transactional 的情况下,添加新的违规 |
7 | 检查类的导入,并且在导入 Spring @Transactional 的情况下,添加新的违规 |
8 | 重要的是设置节点的 lineNumber ,因为如果没有设置,则不会添加违规 |
测试
当然,我们需要编写测试以确保一切按预期工作
package org.codenarc.rule.grails
import org.codenarc.rule.AbstractRuleTestCase
import org.codenarc.rule.Rule
import org.codenarc.rule.Violation
import org.junit.Test
class GrailsTransactionalRuleTest extends AbstractRuleTestCase { (1)
@Override
protected Rule createRule() { (2)
return new GrailsTransactionalRule()
}
@Test
void testGrailsTransactionalIsAllowedOnClassWithImport() {
final SOURCE = ''' (3)
import grails.gorm.transactions.Transactional
@Transactional
class TestService {
}
'''
assertNoViolations(SOURCE) (4)
}
@Test
void testGrailsTransactionalIsAllowedOnClassWithFullpackageAnnotation() {
final SOURCE = '''
@grails.gorm.transactions.Transactional
class TestService {
}
'''
assertNoViolations(SOURCE)
}
@Test
void testGrailsTransactionalIsAllowedOnMethodWithImport() {
final SOURCE = '''
import grails.gorm.transactions.Transactional
class TestService {
@Transactional
void foo() {
}
}
'''
assertNoViolations(SOURCE)
}
@Test
void testGrailsTransactionalIsAllowedOnMethodWithFullpackageAnnotation() {
final SOURCE = '''
class TestService {
@grails.gorm.transactions.Transactional
void foo() {
}
}
'''
assertNoViolations(SOURCE)
}
@Test
void testSpringTransactionalIsNotAllowedOnClassWithImport() {
final SOURCE = '''
import org.springframework.transaction.annotation.Transactional
@Transactional
class TestService {
}
'''
(5)
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
@Test
void testSpringTransactionalIsNotAllowedOnClassWithFullpackageAnnotation() {
final SOURCE = '''
@org.springframework.transaction.annotation.Transactional
class TestService {
}
'''
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
@Test
void testSpringTransactionalIsNotAllowedOnMethodWithImport() {
final SOURCE = '''
import org.springframework.transaction.annotation.Transactional
class TestService {
@Transactional
void foo() {
}
}
'''
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
@Test
void testSpringTransactionalIsNotAllowedOnMethodWithFullpackageAnnotation() {
final SOURCE = '''
class TestService {
@org.springframework.transaction.annotation.Transactional
void foo() {
}
}
'''
assertSingleViolation(SOURCE) { Violation violation ->
violation.rule.priority == 2 &&
violation.rule.name == 'GrailsTransactional'
}
}
}
1 | 测试类需要从AbstractRuleTestCase 扩展 |
2 | 实例化我们要测试的规则 |
3 | 将我们要测试的源代码写成一个 String |
4 | 在这种情况下,由于我们使用 Grails @Transactional ,我们希望没有违规 |
5 | 由于我们使用 Spring @Transactional ,我们希望会有一次违规 |
4.2 在 Grails 应用程序中检查该规则
一旦我们创建了该规则,并且在类路径上可用,就该将其添加到 Grails 应用程序中了。
ruleset {
description 'Grails-CodeNarc Project RuleSet'
...
ruleset('rulesets/grails-extra.xml')
}
现在我们可以创建一个服务并在其中使用 Spring @Transactional
package demo
import org.springframework.transaction.annotation.Transactional
class DemoService {
@Transactional
void myMethod() {
println 'Some business logic'
}
}
最后一步是生成 CodeNarc 报告
正如我们看到的,我们有一条新违规,因为我们创建的规则已经应用。