显示导航

使用科德纳克在 Grails 应用程序中进行静态代码分析

在此指南中,您将学习如何使用科德纳克进行静态分析来改进您的代码。

作者:伊万·洛佩兹

Grails 版本 4.0.1

1 Grails 培训

Grails 培训 - 由创作并积极维护 Grails 框架的人开发和交付!

2 开始

在编写代码时,遵循一些规则、正确的惯例、样式规则等非常重要。然而,有时它并不那么简单。当我们团队协作时尤其重要,其中每位成员都有自己的偏好。改进此问题的一种方法是为代码添加一个静态分析工具。

在本指南中,您将安装并配置 科德纳克 来帮助您提高 Grails 代码的质量,您还将学习如何创建自定义的科德纳克规则。科德纳克会分析 Groovy 代码并报告潜在的 bug 和代码问题。

2.1 所需条件

要完成本指南,您需要具备以下条件

  • 一些时间

  • 一个不错的文本编辑器或 IDE

  • 安装了 JDK 1.8 或更高版本,并正确配置了 JAVA_HOME

2.2 如何完成该指南

开始时执行以下操作

Grails 指南代码库包含两个文件夹

  • initial 初始项目。通常是一个带有其他代码以帮助你快速开始的简单 Grails 应用程序。

  • complete 已完成的示例。是根据该指南给出的步骤实际操作并对 initial 文件夹应用那些更改的结果。

若要完成该指南,请转到 initial 文件夹

  • grails-guides/grails-codenarc/initialcd

并按照下一部分中的说明进行操作。

如果你在 grails-guides/grails-codenarc/completecd,则可以转到**已完成的示例**

3 编写应用程序

3.1 多项目构建

我们将使用 Grails 应用程序和一个自定义 CodeNarc 规则设置一个多项目构建,如下图所示

multiproject

查看我们的Grails 多项目构建指南以了解更多信息。

3.2 规则类型

当前的 CodeNarc 版本 (0.27.0) 包括分为 22 个类别的348 条规则

  • 基本:例如,检查是否存在空的 elsefinally 块。

  • 花括号:你见过多少次只有一条语句而没有花括号的 ifelse?我个人不喜欢没有花括号的代码,因为它是未来出现缺陷的根源。我们可以添加此类别中的规则来执行这些检查。

  • 约定:有一些规则用于检查某些约定:比如,当我们编写一个“反向”if、一个可以转换为 Elvis 运算符的 if 时,……

  • 异常:如果我们抛出 NullPointerException 之类的异常,这些规则就会失败。

还有更多类别,可以检查重复导入、未使用变量、不必要的 if,……当然,Grails 规则还有特定类别。

3.3 将 CodeNarc 添加到我们的项目

将 CodeNarc 添加到我们的项目是一项简单的任务,因为它有一个Gradle 插件

让我们修改 build.gradle 并添加以下内容

/app/build.gradle
apply from: "${rootProject.projectDir}/gradle/codenarc.gradle"

我们在 gradle/codenarc.gradle 中封装了所有 CodeNarc 配置

/gradle/codenarc.gradle
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 当只有一条违规时,我们不希望构建失败。

然后我们需要创建规则文件

/config/codenarc/rules.groovy
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

并且我们可以打开测试报告来检查冲突

report1
  • 首先,我们已经一个包含执行日期和所使用 CodeNarc 版本的部分。

  • 然后,那里还有具有包含违规文件总数以及优先级 1、2 和 3 的违规数的摘要的另一个部分。

  • 在此之后,对于每个文件又存在一个部分,我们可以在其中看到该文件中的所有违规(包括代码行和一小部分代码片段)。规则名称是用于访问更详细解释的链接。

让我们修复违规

修复 CodeNarc 违规有不同的方式

  • 修复问题:顾名思义,仅修复违规。

  • 禁用规则:有时候,我们不同意特定的 CodeNarc 违规。我们可以禁用它。

  • 忽略针对特定类或方法的规则:这种情况是我们不想禁用规则,而仅仅想在特定的类或方法中跳过规则。

修复问题

  • 要修复违规 FileEndsWithoutNewline,请在 UrlMappings 的末尾添加一行。

  • 要修复违规 SpaceBeforeOpeningBrace,请在大括号之前添加一个空格。

禁用规则

对于 ClassJavadocNoDef,我们可以通过修改 rules.groovy 文件来禁用规则

/app/src/codenarc/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 的规则

/app/grails-app/controllers/demo/UrlMappings.groovy
@SuppressWarnings(['UnnecessaryGString'])
class UrlMappings {

    static mappings = {
        "/$controller/$action?/$id?(.$format)?" {
            constraints {
                // apply constraints here
            }
        }

        "/"(view:"/index")
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}

检查结果

只需再次执行 check 任务并打开报告即可查看所有违规已消失

report2

3.4 最终配置

如果您在之前未采用过任何静态分析工具的中大型项目中安装 CodeNarc,您可能会遇到数百或数千个违规。有可能您的团队在编写代码的过程中不够仔细。此外,CodeNarc 的默认配置可能包含一些您不同意的规则。可能不适合该团队的规则。

我的建议是查看所有违规,阅读其文档,然后决定您是否要禁用它们,配置其他选项最终修复并遵守它们。

一旦团队确定了规则和配置,下一步是选择使编译失败的阈值。这些阈值允许成功编译出现一些违规,但是一旦我们达到这些级别,编译将失败。

要做到这一点,只需编辑 build.gradle 文件

/app/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 文件打包该规则。

/codenarc-rule/build.gradle
plugins {
    id 'groovy'
}

repositories {
    jcenter()
}

dependencies {
    implementation 'org.codenarc:CodeNarc:1.4' (1)
    testCompile 'junit:junit:4.12'
}
1 只需要 CodeNarc 依赖项

为了能够在我们自己的 Grails 应用程序中使用此新的 CodeNarc 规则,我们需要确保在 codenarcMain 任务中,jar 文件可用于类路径

/gradle/codenarc.gradle
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

定义规则

第一步是使用我们想要创建的规则信息创建一个元信息文件

/codenarc-rule/src/main/resources/rulesets/grails-extra.xml
<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 报告

/codenarc-rule/src/main/resources/codenarc-messages.properties
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>.

实施规则

最后,我们实施规则

/codenarc-rule/src/main/groovy/org/codenarc/rule/grails/GrailsTransactionalRule.groovy
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,因为如果没有设置,则不会添加违规

测试

当然,我们需要编写测试以确保一切按预期工作

/codenarc-rule/src/test/groovy/org/codenarc/rule/grails/GrailsTransactionalRuleTest.groovy
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 应用程序中了。

/app/src/codenarc/rules.groovy
ruleset {
    description 'Grails-CodeNarc Project RuleSet'

    ...
    ruleset('rulesets/grails-extra.xml')
}

现在我们可以创建一个服务并在其中使用 Spring @Transactional

/app/build.gradle
package demo

import org.springframework.transaction.annotation.Transactional

class DemoService {

    @Transactional
    void myMethod() {
        println 'Some business logic'
    }
}

最后一步是生成 CodeNarc 报告

report3

正如我们看到的,我们有一条新违规,因为我们创建的规则已经应用。

5 你需要使用 Grails 的帮助吗?

Object Computing, Inc. (OCI) 赞助了本指南的创作。提供各种咨询和支持服务。

OCI 是 Grails 的根据地

认识团队