Ohhnews

分类导航

$ cd ..
Jetbrains Blog原文

扩展 Qodana 功能:添加自定义代码检查

#qodana#jetbrains#代码检查#插件开发#静态分析

Qodana 是一个静态代码分析工具,它将 JetBrains IDE 中的代码检查和快速修复功能带入了持续集成领域。它可以在云端运行,从 Docker 容器执行集成到 CI/CD 流水线中,或者通过 JetBrains IDE 调用。

Qodana 已经提供了一套令人印象深刻的检查功能,但它并不局限于内置功能。您可以添加自定义检查来强制执行项目特定的规范和约定。

例如,设想一个具有特定代码约定的项目:

service 包中的每个 Kotlin 类都必须带有 Service 后缀。

在这种情况下,com.jetbrains.service.JetComponent 将不符合此约定,而 com.jetbrains.service.BrainComponentService 则完全没问题。在接下来的内容中,我们将构建一个实现此检查的插件,允许 Qodana 在未来的项目中强制执行此约定。

我们可以通过创建打包在插件中的自定义代码检查来实现此代码约定。Qodana 插件的开发方式与 JetBrains IDE 插件完全相同,也就是说,我们只需创建一个可以在 Qodana 中运行的 IntelliJ 平台插件。以下是我们要采取的步骤的简要概述:

  1. IntelliJ Platform Plugin Template 初始化项目。
  2. 调整项目属性和插件描述符以及必要的依赖项。
  3. 在插件描述符中声明本地检查并在 Kotlin 中实现它。
  4. 构建并打包插件。
  5. 在示例游乐场项目中,将插件工件放入适当的目录。
  6. 调整 Qodana 配置文件。
  7. 运行 Qodana 并查看报告!

准备插件项目

要引导项目,请访问 IntelliJ Platform Plugin Template 仓库并点击 Use this template 按钮以创建插件仓库。将其命名为 classname-inspection-qodana-plugin,复制项目 URL,并在 IntelliJ IDEA 中打开它。项目准备好后,通过根据需要声明 pluginGrouppluginNamepluginRepositoryUrl 来自定义 gradle.properties。记得点击 Sync Gradle Changes 悬浮按钮以应用更改。要修改唯一的插件标识符,请更改插件描述符 plugin.xml 中的 id 元素。

声明依赖项

我们的代码检查针对 Kotlin 类,因此我们需要将 Kotlin 插件添加到 Qodana 插件的依赖项中。gradle.properties 文件要求您声明:

platformBundledPlugins = org.jetbrains.kotlin

此外,插件描述符 plugin.xml 必须在其依赖项中包含相同的捆绑 Kotlin 插件:

<depends>org.jetbrains.kotlin</depends>

同样,记得通过点击悬浮按钮来同步 Gradle 更改。

此外,Kotlin 类检查需要支持 Kotlin K2 编译器,该编译器自 IntelliJ 平台 2025.1 版本起默认启用。在插件描述符中,声明 org.jetbrains.kotlin.supportsKotlinPluginMode 扩展。

<extensions defaultExtensionNs="org.jetbrains.kotlin">
    <supportsKotlinPluginMode supportsK2="true" />
</extensions>

创建代码检查

任何针对 Kotlin 类的代码检查的实际代码都需要三个步骤:

  1. 在插件描述符中声明一个 com.intellij.localInspection 扩展,以及必要的属性和对实现类的完全限定引用。
  2. 创建一个实现类,最好使用 Kotlin。
  3. 提供一个独立的 HTML 文件,其中包含检查描述、使用指南和示例。

声明扩展

将以下声明添加到 plugin.xml 插件描述符文件中:

<extensions defaultExtensionNs="com.intellij">
    <localInspection
        language="kotlin"
        implementationClass="org.intellij.sdk.codeInspection.ServicePackageClassNameInspection"
        enabledByDefault="true"
        displayName="SDK: Discouraged class name"
        groupName="Kotlin"
    />
</extensions>

language 属性表示该检查适用于 Kotlin 源代码文件。重要的是默认显式启用该检查;否则,Qodana 将不会运行它。然后提供人类可读的描述性 displayName,以便在报告和设置中显示。groupName 属性设置检查类别,该类别将显示在 Qodana 报告和 IDE 设置中。最后,为实现类提供完全限定名称。

代码检查源代码

Kotlin 插件为 Kotlin 检查提供了一个有用的基础检查类:AbstractKotlinInspection。重写 buildVisitor 方法并提供一个 PSI 访问器实例,以类型安全的方式遍历 Kotlin 类元素。classVisitor 是一个方便的类似 DSL 的函数,它返回这种 PSI 访问器,并在您正在检查的任何项目中的任何 Kotlin 类上被调用。

package org.intellij.sdk.codeInspection

import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtVisitorVoid
import org.jetbrains.kotlin.psi.classVisitor

class ServicePackageClassNameInspection : AbstractKotlinInspection() {
    override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = classVisitor { klass ->
        val classNamePsi = klass.nameIdentifier ?: return@classVisitor
        val classFqn = klass.fqName?.asString() ?: return@classVisitor
        if (klass.packageLastComponent == "service" && !classFqn.endsWith("Service")) {
            holder.registerProblem(
                classNamePsi,
                "Class name in the 'service' package must have a 'Service' suffix"
            )
        }
    }

    private val KtClass.packageLastComponent: String
        get() = containingKtFile.packageFqName.shortName().asString()
}

访问器子类提取完全限定的 Kotlin 类名,检查最右边的包元素,并检查相应的后缀。任何不正确的类名都会向 ProblemsHolder 实例报告,并将封闭类作为 PSI 元素和人类可读的问题描述。

检查描述

每个本地检查都需要一个伴随描述文件,表示为 HTML。如果您使用 Create description file ServicePackageClassNameInspection.html 快速修复,将在正确位置创建一个名为 src/main/resources/inspectionDescriptions/ServicePackageClassName.html 的文件。您还必须提供将在 Qodana 报告和 IDE 设置中显示的描述。

<html>
<body>
Reports class names in the <code>service</code> packages that lack the <code>Service</code> suffix.
<p><b>Example:</b></p>
<pre><code>
  package com.example.foo.service
  class SomeComponent {
    /* class members */
  }
</code></pre>
</body>
</html>

构建插件

一切就绪——是时候构建了!执行 buildPlugin Gradle 任务,并查看 Gradle 输出目录中可用的 build/distributions/qodana-code-inspection-0.0.1.zip 工件。JAR 文件将用作 Qodana 扫描的主要工件。

注意插件工件类型

Qodana 不直接支持包含额外 JAR 归档或第三方依赖项的本地 ZIP 插件工件。任何插件都需要打包为单个 JAR 或解压缩到特定目录中。

在游乐场项目上运行 Qodana

让我们创建一个用 Kotlin 编写的游乐场项目,既然我们已经用插件扩展了 Qodana,现在就可以用 Qodana 检查它。要在本地运行 Qodana 插件,请确保您的系统上有两个软件组件可用:

我们的游乐场项目应该包含一个名为 src/main/kotlin/org/intellij/sdk/qodana/service/SomeComponent.kt 的类,它不符合我们的代码约定,因为它没有 Service 后缀。有两种方法可以将 Qodana 插件集成到 Qodana 中:

  • 在 JetBrains Marketplace 上发布
  • 为了更快的周转,将插件的 JAR 工件放入项目的 .qodana 目录中。

为了简化构建,将插件项目中的 build/distributions/qodana-code-inspection-0.0.1.zip 文件复制到游乐场项目中的 .qodana/qodana-code-inspection-0.0.1.zip 文件。如果 .qodana 目录尚不存在,请创建它。然后使用您喜欢的程序或工具解压存档。Qodana 能够访问 build/distributions/qodana-code-inspection 目录中的插件。

此外,需要配置 Qodana 以包含我们的自定义代码检查。更改游乐场项目根目录中的 qodana.yaml 文件,如下所示:

version: "1.0"
linter: qodana-jvm-community
include:
  - name: org.intellij.sdk.codeInspection.ServicePackageClassNameInspection

include 块需要引用插件中可用的代码检查的完全限定类名。现在执行以下命令从终端运行 Qodana:

qodana scan --volume $PWD/.qodana/qodana-code-inspection:/opt/idea/custom-plugins/qodana-code-inspection

这将下载相应的 Qodana Docker 镜像。将基于 Qodana 配置创建并运行 Docker 容器。为了使自定义插件在 Qodana 运行中可访问,请将 Qodana 插件目录从本地文件系统挂载到 Qodana Docker 容器内的适当目录。几分钟后,Qodana 将生成报告摘要并将其打印到标准输出。

Qodana - Detailed summary                                                                                                                                                                                                                
Analysis results: 1 problem detected                                                                                                                                                                                                     
By severity: High - 1                                                                                                                                                                                                                    
-------------------------------------------------------                                                                                                                              
Name                           Severity  Problems count                                                                                                                                                             
-------------------------------------------------------                                                                                                                              
SDK: Discouraged class name    High      1                                                                                                                                                             
-------------------------------------------------------

在浏览器中打开完整报告:

  Do you want to open the latest report [Y/n]Yes

!  Press Ctrl+C to stop serving the report

\  Showing Qodana report from http://localhost:8080/... (10s)

Qodana 将显示 SomeComponent 不符合 Qodana 插件中我们的本地检查提供的代码约定。 [LOADING...]

未来运行的提示

当修改并重新构建 Qodana 插件时,还需要重新创建 Qodana 缓存。在这种情况下,使用 --clear-cache CLI 开关重新加载所有 Qodana 运行的依赖项。

qodana scan --clear-cache --volume $PWD/.qodana/qodana-code-inspection-0.0.1.jar:/opt/idea/custom-plugins/codeinspection.jar

将 Qodana 插入您的 IDE

Qodana 插件 可以从磁盘安装 到 IDE 中。然后,对于项目中的任何 Kotlin 类,其代码检查会自动启用并调用。要检查它是否正常工作,请重新访问游乐场项目,打开 org.intellij.sdk.qodana.service.SomeComponent 类,并确保有问题的类名带有下划线。为了方便起见,将鼠标悬停在类名将显示代码检查结果以及问题描述。或者,您可以打开 Problems 工具窗口并在代码检查报告的所有问题列表中找到该问题。

代码检查现在的行为就像 IDE 提供的任何其他检查一样。在 Settings |Editor |Inspections | Kotlin 中,您会发现 SDK: Discouraged class name 检查,以及我们之前提供的 HTML 文件中的描述。

在 IDE 内运行 Qodana

安装插件后,您现在也可以从 IDE 运行 Qodana。在 Problems 工具窗口中,转到 Qodana 选项卡,然后点击 Try Locally 按钮。Qodana 将使用 qodana.yaml 文件进行配置并运行。Qodana 报告可以直接在工具窗口中找到。 [LOADING...]

Qodana 插件和 JetBrains Marketplace

构建和测试得当的自定义检查插件可以发布在 JetBrains Marketplace 上,从而无需从 .qodana 目录提供它们。相反,您只需要确保 Qodana 配置指定一个公共插件标识符,该标识符与插件描述符中的 id 元素匹配。

version: "1.0"
linter: qodana-jvm-community
plugins: 
  - id: com.github.novotnyr.qodanacodeinspection

Qodana 的 scan 命令被简化了,因为不再需要 .qodana 目录挂载。

qodana scan

Qodana 从 JetBrains Marketplace 下载此插件并运行其所有检查,生成控制台输出和可以在 Web 浏览器中显示的 HTML 报告。

总结

我们创建了一个带有代码检查的 Qodana 插件,用于检查特定的代码约定,我们有多种运行它的方法:

  • 作为放置在 .qodana 目录中并包含在 Qodana YAML 文件中的 JAR。
  • 作为对 JetBrains Marketplace 上公开可用插件的引用。
  • 作为安装在 IDE 中的 JAR,其中检查在本地 Qodana 运行中应用。
  • 作为安装在 IDE 中的 JAR,其中检查包含在集成的代码检查中。

有关 Qodana 插件和游乐场项目的简明示例,请参阅 IntelliJ SDK Code sample