本文记录 compose-rules 接入 OpenHarmony 的完整过程,覆盖生态位盘点、构建期工具的适配路线选择、鸿蒙工具链换线、Kotlin 2.2 API 迁移、上游 190 项测试交叉验证、JVM 双引擎验收工程、ArkUI 报告页面、HAP 构建和真机验收。

compose-rules 是 Twitter 开源的 Jetpack Compose 静态检查规则库:17 条规则同时以 ktlint 规则集与 detekt 规则集两种形态发布,规则跑在构建期的 JVM 上,用 kotlin-compiler-embeddable 解析 Compose 源码、报告违规。本次适配与社区运行时库路线(qrose / KStore / kstatemachine 的"加 ohos target 补 actual")有本质区别:这个库没有任何一行代码会出现在设备上——ktlint 与 detekt 在任何平台上都是构建期 JVM 程序,鸿蒙工程照旧在 JVM 上跑它们。因此适配走的是 kotlinx-benchmark 先例确立的构建期工具路线:不给模块加 ohosArm64 / ohosX64,而是把规则引擎依赖的 kotlin-compiler-embeddable 整体换到鸿蒙社区 Kotlin 制品线 2.2.21-1.0.0,使 lint 引擎与 OpenHarmony 工程使用的 Kotlin 语言版本对齐;17 条规则逻辑零改动,上游 190 项测试全绿;再用一个 JVM 验收工程证明双引擎在鸿蒙线上真实咬合,一个 DevEco HAP 作为报告可视化载体完成真机验收。

项目地址: AtomGit/oh-tpc/compose-rules.git

上游仓库: twitter/compose-rules

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

compose-rules 为 Compose 团队提供"写法规范"这一件事的完整答案:@Composable 命名、modifier 参数约定、状态上提、remember 纪律、不稳定集合、CompositionLocal 命名与 allowlist、Preview 可见性……17 条规则把最常见的 Compose 反模式在 code review 之前拦下来。规则实现层(rules/common)是纯 Kotlin PSI 遍历,通过两个适配层接进 ktlint(TwitterKtlintRule)和 detekt(TwitterDetektRule),一条规则逻辑服务两个引擎。KMP/CMP 项目把这些检查用在共享 UI 层上,OpenHarmony 的 CMP 工程同样需要——但上游构建(Kotlin 1.8.0 / Gradle 7.4.2)的解析器比鸿蒙工具链落后整整四代,鸿蒙 CMP 工程用 HarmonyOS Kotlin 2.2.21 编译,源码里会出现 2.x 语法,1.8 的 lint 引擎与 2.2 的工程代码之间没有语言版本对齐的保证。

如果只把规则文档翻译一遍贴到鸿蒙社区,文档可以看,却无法证明规则引擎在鸿蒙工具链线上完成编译、解析和报告。适配需要同时验证规则库本体、引擎运行时兼容、验收工程、报告管线、ArkUI 页面和 HAP 交付链路。

障碍具体问题
工具链不一致上游 Kotlin 1.8.0 / Gradle 7.4.2,鸿蒙社区线 Kotlin 2.2.21-1.0.0 只发布在 eazytec Maven;KGP 2.2 还要求 Gradle 7.6.3+,wrapper 必须升。
API 断层Kotlin 2.2 移除了 KtStubElementTypes.FILE、把 KtPsiFactory(element) 构造器和 addToStdlib.cast 标记为废弃/不安全,上游开着 -Werror,三处源码必须迁移。
插件断层Gradle 8.14 下 shadow 7.1.2 与 spotless 6.13.0 使用已移除的 Gradle API,必须随 wrapper 一起升。
JVM 字节码校验KGP 2.2 强校验 Java/Kotlin 编译目标一致性,JDK 21 下 compileJava(21) 对 compileKotlin(11) 直接失败。
验收形态特殊lint 库没有可跑在设备上的运行时,不能照抄"so + NAPI + HAP"套路,验收载体要重新定义。
交付链路复杂报告管线、rawfile 资源、签名和 hdc 都要单独验收。

因此,本项目把边界放在:规则库整体换线编译、三处 API 迁移和双验收载体(JVM 扫描 + HAP 报告页)。规则语义完全由上游原版实现负责,ArkTS 只负责读取报告和展示结果。

1.2 库提供的能力

Demo 提供一次真实的静态扫描会话,对应适配的全部入口:

  • 双引擎扫描(ktlint 面):TwitterComposeRuleSetProvider 注册全部 17 条规则,KtLint.lint 逐文件解析样本并收集 LintError(行、列、规则 ID、消息);
  • 双引擎扫描(detekt 面):17 个 Compose*Check(Config.empty) 实例经 detekt-test 的 lint(code) 逐规则扫描同一份样本;
  • 门禁:每个引擎 ≥10 条违规且 ≥6 条不同规则,否则任务退出码 1——规则引擎哪天静默失效,验收立刻变红;
  • 报告管线:扫描结果落 compose_rules_report.json,经 packageLintReport 任务自动拷入 DevEco 工程 rawfile,HAP 报告页展示的是真机可见的真实数据;
  • 规则目录:17 条规则按 State / Composables / Modifiers 三组在 HAP 内静态呈现,含一句话说明与真实规则 ID。

响应报告结构:

字段作用
toolchain本次扫描用的 Kotlin / Gradle / JDK / 规则版本(来自真实构建环境,非静态文字)。
ktlint.violations[] / detekt.violations[]逐条违规:文件名、行、列、规则 ID、消息。
summary两引擎违规总数。

规则侧完整能力(命名、modifier 约定、状态纪律、不稳定集合、allowlist 配置)全部在上游 rules/common 里原样保留,JVM 侧保留 190 项上游测试做交叉验证,证明换线后的规则逻辑没有漂移。

1.3 实现适配

维度要求
代码复用六个模块全部原样编译,不提取、不重写;仅 3 个文件 5 处调用点做 Kotlin 2.2 API 等价迁移,17 条规则逻辑零改动。
平台目标纯 JVM 构建,不加 ohos target(构建期工具,kotlinx-benchmark plugin 面先例);example/lint-demo 承担验收,JVM 即目标环境。
构建隔离仓库按已审核标准重排(eazytec 双仓置顶);Sonatype/vanniktech 发布链整体剥离;上游首页存 README.upstream.md。
管线稳定报告走 JSON 文件 + rawfile 资源,不传递对象引用;扫描门禁保证"没咬到违规就红"。
依赖纪律ktlint 0.48.2 / detekt 1.22.0 与上游完全一致(换线不换版),只把 org.jetbrains.kotlin 全线换到鸿蒙制品线,上游测试零改动全绿。
UI 完整单页三 Tab 覆盖全部验收面:规则目录、扫描报告(工具链/计数/逐条违规)、适配说明。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是规则引擎的运行宿主。compose-rules 是构建期工具,HAP 报告页是验收载体——把构建期扫描结果画出来,证明这条管线在鸿蒙工程语境里闭环。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认构建期工具路线、确定交付物
第 2 阶段:工具链换线     ── 仓库重排(eazytec 置前)、Kotlin 2.2.21-1.0.0 钉版、Gradle 8.14.1 + JDK 21、发布链剥离
第 3 阶段:API 迁移       ── KtPsiFactory.contextual、KtFileElementType.INSTANCE、cast → as,三文件五调用点
第 4 阶段:验收工程       ── lint-demo 双引擎 runner、违规样本、扫描门禁、JSON 报告
第 5 阶段:ArkUI 宿主     ── 三 Tab 页面、rawfile 报告读取、规则目录、适配说明
第 6 阶段:示例与验证     ── 190 项上游测试、验收门禁、HAP、签名、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
core-common/                          引擎无关规则核心(PSI visitor、Emitter)(进构建)
core-ktlint/  core-detekt/            ktlint / detekt 引擎适配层(进构建)
rules/common/                         17 条规则实现(进构建,除 1 行外零改动)
rules/ktlint/  rules/detekt/          规则集提供者 + shadow uber jar(进构建)
example/lint-demo/                    JVM 验收工程(样本 + 双引擎 runner + 报告管线)
example/harmonyApp/                   DevEco Stage、纯 ArkTS 三 Tab 页面
README.OpenHarmony.md / _CN.md        鸿蒙适配说明(中英双份)
README.upstream.md                    上游原文保存
docs/ .github/ spotless/ mkdocs.yml   上游文档站与 CI,留盘不动

构建拓扑对齐 cmp_qrose 已审核模式:settings.gradle 的 rootProject.name = "ohos_compose-rules",include 六个规则模块与 :example:lint-demo;上游的 gradle-enterprise 插件块、vanniktech 发布 classpath、Sonatype POM 键全部剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
KotlinHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,qrose / mordant / kotlinx-benchmark 同线)
JDKTemurin 21(org.gradle.java.home 写进 gradle.properties)
Gradle8.14.1(wrapper 从 7.4.2 升级,KGP 2.2 要求 7.6.3+)
引擎依赖ktlint 0.48.2 / detekt 1.22.0(与上游一致,换线不换版)
插件spotless 6.13.0 → 6.25.0、shadow 7.1.2 → 8.1.1(Gradle 8.14 兼容)
字节码Kotlin jvmTarget 11 + JavaCompile options.release = 11(与上游产物一致)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
Bundleorg.terminator.ohos.composerules
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
模块源码依赖形态处置
core-common引擎无关核心kotlin-compiler-embeddable(api)整体换线编译
core-ktlint / core-detekt引擎适配层ktlint-core / detekt-core(JVM-only)整体编译 + 1~2 处 API 迁移
rules/common17 条规则仅依赖 core-common整体编译 + 1 行迁移
rules/ktlint / rules/detekt提供者 + uber jarshadow 插件整体编译,插件升版

关键判断在依赖形态:全仓 JVM-only 依赖,没有一处 expect/actual,没有一处平台源码集——这正是"构建期工具不加 ohos target"的技术依据(详见决策 1)。

1.4 页面边界
标题区          COMPOSERULES · OPENHARMONY / 副标题(Compose 静态检查规则库 · 17 条规则)
Tab 切换        规则 / 报告 / 适配
规则 Tab        State(4)/ Composables(9)/ Modifiers(4) 三组 17 条,每条规则名 + 说明 + 规则 ID
报告 Tab        工具链四行(Kotlin/Gradle/JDK/规则版本)
                两引擎违规计数(filesScanned / 违规列表长度)
                逐条违规(文件:行 + 规则 ID + 消息,可滚动)
适配 Tab        构建期工具定位、换线方式、验收路径、仓库与社区链接

页面启动时在 aboutToAppear 里同步读 rawfile 报告并填充 @State,占位数据带黄色横幅提示。

第 2 阶段:工具链换线

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上鸿蒙 Kotlin 线的制品是否齐全。用 POM 逐个验证:

org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.21-1.0.0   → HTTP 200
org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21-1.0.0         → eazytec 独有(中央仓库无此版本)

三个关键结论:其一,鸿蒙线 Kotlin 全家桶(gradle-plugin / stdlib / compiler-embeddable)带 -1.0.0 后缀只在 eazytec 发布,仓库顺序必须置前;其二,ktlint 0.48.2 与 detekt 1.22.0 是标准 JVM 制品,照常从 Maven Central 解析,与鸿蒙线无冲突;其三,kotlin-compiler-embeddable 2.2.21-1.0.0 就是规则引擎的解析器本体——版本目录里 kotlin = "2.2.21-1.0.0" 一处改动,构建插件与解析引擎同时换线。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

上游是 Kotlin 1.8.0——鸿蒙线没有 1.x 对应版本,必须跨代升级。选 2.2.21-1.0.0 而不是更新的 2.4.20-0.1.0-07,依据是"换线不换版、就低不就高"的已审核钉法:qrose、mordant、kotlinx-benchmark 三个仓库都用 2.2.21 线,本库规则源码是 1.8 时代代码,2.2.21 是兼容距离最小的鸿蒙线。Gradle 随之钉 8.14.1:KGP 2.2.21 要求 Gradle 7.6.3+,同时 2.2.21 线的全部先例都在 8.14.1(2.4.20 线才允许 Gradle 9.x),不越线。

settings.gradle 把 eazytec 仓库写在 pluginManagement 最前:

pluginManagement {
    repositories {
        // HarmonyOS Kotlin toolchain (eazytec community line) must come first.
        maven { url 'https://maven.eazytec-cloud.com/nexus/repository/maven-public/' }
        maven { url 'https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos' }
        gradlePluginPortal()
        mavenCentral()
    }
}

rootProject.name = "ohos_compose-rules"

根 build.gradle 的 buildscript / allprojects 仓库同步置前。gradle.properties 钉 JDK 并改版本:

org.gradle.java.home=C:/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot
GROUP=com.twitter.compose.rules
VERSION_NAME=0.0.26-ohos.1
2.3 换线第一轮编译的连环雷

wrapper 升 8.14.1、Kotlin 升 2.2.21 后,第一轮编译暴露四类问题,全部有解:

问题现象处置
JVM target 不一致compileJava (21) vs compileKotlin (11) 失败根构建加 JavaCompile options.release = 11
移除的增量属性useClasspathSnapshot 每次构建警告从 gradle.properties 删除
插件不兼容shadow 7.1.2 / spotless 6.13.0 用已移除 API升 8.1.1 / 6.25.0
源码 API 断层三文件编译失败(-Werror)见第 3 阶段
2.4 发布链剥离

上游通过 vanniktech 插件发 Maven Central。fork 侧:根 build.gradle 删 classpath,六个模块各删一行 apply plugin: "com.vanniktech.maven.publish",gradle.properties 删 SONATYPE_HOST / RELEASE_SIGNING_ENABLED / 全部 POM_* 键。产物消费走本地构建或社区 Maven。

第 3 阶段:API 迁移(技术难点)

上游开 allWarningsAsErrors = true,Kotlin 2.2 里三处旧 API 从"能用"变成"警告即错误",全部做语义等价替换——这是整个 fork 仅有的源码改动:

3.1 ComposeViewModelInjection.kt:KtPsiFactory 构造器废弃
// before(Kotlin 1.8)
val factory = KtPsiFactory(parameterList)
// after(Kotlin 2.2:官方迁移目标,行为一致——用元素的 project/module 上下文建工厂)
val factory = KtPsiFactory.contextual(parameterList)

这行在 vm-injection 规则的自动修复路径上,ktlint 面 103 项测试里含自动修复场景的用例直接覆盖它。

3.2 TwitterKtlintRule.kt:FILE 常量消失 + cast 不安全

javap 检查 2.2.21 的 embeddable jar,KtStubElementTypes 里已无 FILE 字段,文件级元素类型移到单例 KtFileElementType.INSTANCE;addToStdlib.cast<T>() 被标记为错误级"unsafe cast"警告:

// before
KtStubElementTypes.FILE -> visitFile(psi.cast(), autoCorrect, emit.toEmitter())
// after
KtFileElementType.INSTANCE -> visitFile(psi as KtFile, autoCorrect, emit.toEmitter())

三处 psi.cast<...>() 逐一换成 as + @Suppress("UNCHECKED_CAST"),cast 目标类型与原来相同(KtFile / KtClass / KtFunction)。

3.3 TwitterDetektRule.kt:同款 cast 迁移

element.cast<KtFunction>() → element as KtFunction,一处。

五个调用点改完,190 项上游测试全绿——迁移等价性由测试直接证明(见第 6 阶段)。

第 4 阶段:验收工程(报告管线)

4.1 数据流
samples/Violations*.kt(故意违规的 Compose 样本,只解析不编译)
        ↓ TwitterComposeRuleSetProvider().getRuleProviders() / 17 个 Check(Config.empty)
LintDemoRunner.kt(ktlint 面 KtLint.lint + detekt 面 lint(code))
        ↓ 门禁:每引擎 ≥10 条违规且 ≥6 条不同规则,否则 exitProcess(1)
build/lint-report/compose_rules_report.json
        ↓ packageLintReport(Copy 任务,runLintDemo finalizedBy)
example/harmonyApp/entry/src/main/resources/rawfile/compose_rules_report.json
        ↓ DevEco 构建
HAP 报告页(ArkTS JSON.parse + @State 渲染)
4.2 样本设计

两份样本覆盖规则面的主干,全部从上游 docs/rules.md 的 Noncompliant 示例演化而来:

  • ViolationsProfileScreen.kt(状态类):小写 CompositionLocal(命名 + allowlist)、函数体里取 ViewModel(注入)、ViewModel 参数(转发)、mutableStateOf 不套 remember、小写 Composable、无 modifier 参数的 emitter;
  • ViolationsSettingsScreen.kt(组件类):sealed interface + 尾随逗号的现代语法、@Composable Modifier 工厂函数、modifier 无默认值、根布局复用传入 modifier、多 emitter、emit 同时返回值、ArrayList/MutableState 可变参数、List 不稳定集合、小写 multipreview 注解、公开 preview。

样本只被解析不被编译,不需要任何 Compose 依赖;放进 samples/ 目录而非源码集,避免被模块编译。

4.3 门禁与对称性
val gatesPassed = byEngine.entries.all { (_, v) -> v.size >= 10 } &&
    distinctRulesByEngine.entries.all { (_, rules) -> rules.size >= 6 }
if (!gatesPassed) exitProcess(1)

实测:两引擎各报 24 条、命中同一组 14 条规则。两个引擎共享同一套 rules/common 实现——计数对称是适配层的交叉验证:如果 ktlint 或 detekt 适配层被 2.2 迁移改坏,对称立刻破。未命中的 3 条规则(preview-public / parameter-order / vm-forwarding)是样本未构造出完整触发条件,它们在 190 项上游单测中全部有覆盖。

4.4 报告契约
{
  "generatedAt": "…", "toolchain": { "kotlin": "2.2.21-1.0.0", "gradle": "8.14.1", "jdk": "21", "rulesVersion": "0.0.26-ohos.1" },
  "ktlint":  { "filesScanned": 2, "violations": [ { "file": "…", "line": 12, "col": 5, "ruleId": "twitter-compose:modifier-missing-check", "message": "…" } ] },
  "detekt":  { "filesScanned": 2, "violations": [ … ] },
  "summary": { "ktlintViolations": 24, "detektViolations": 24 }
}

工具链四行来自构建环境注入的系统属性(gradle.version / rules.version / KotlinVersion.CURRENT),不是静态文字——报告页展示的就是本次真实构建的指纹。

第 5 阶段:ArkUI 宿主

5.1 读取 rawfile 报告
aboutToAppear(): void { this.loadReport(); }

private loadReport(): void {
  try {
    const context = this.getUIContext().getHostContext();
    if (context === undefined || context === null) { this.reportError = '…'; return; }
    const rm: resourceManager.ResourceManager = context.resourceManager;
    const bytes: Uint8Array = rm.getRawFileContentSync(REPORT_FILE);
    const text: string = util.TextDecoder.create('utf-8').decodeToString(bytes);
    const parsed = JSON.parse(text) as LintReport;
    /* 填充 @State:toolchainRows / ktlintViolations / detektViolations … */
  } catch (e) {
    const err = e as Error;
    this.reportError = `读取或解析 ${REPORT_FILE} 失败:${err.message ?? '未知错误'}`;
  }
}

全 interface 建 model(LintReport / ToolchainInfo / KtlintViolation…),无 any;ForEach 带keyGenerator;报告缺字段、rawfile 缺文件、JSON 非法三种情况都有兜底,不崩溃。

5.2 Kit 导入的正确答案(真机调试踩出来的)

resourceManager 的 Kit 导入在真机编译时报错两次:先写 @kit.AbilityKit 报 has no exported member 'resourceManager',改 @kit.ResourceManagementKit 又报 Cannot find module。最终用本机 SDK 的 d.ts 文件定案——resourceManager 由 @kit.LocalizationKit 导出:

// E:/DE/DevEco Studio/sdk/default/openharmony/ets/kits/@kit.LocalizationKit.d.ts
export { i18n, intl, resourceManager, sendableResourceManager }

结论:Kit 归属以本机 SDK 的 d.ts 为准,网上示例代码的导入路径不能直接抄。

5.3 页面交互

三个 Tab 底部切换。规则 Tab 三组 17 条白卡片滚动;报告 Tab 顶部四行工具链、中部两引擎计数、下部逐条违规列表;适配 Tab 说明构建期工具定位、换线方式与验收路径,底部仓库 / 社区链接。占位报告(未跑验收时)带黄色横幅提示先执行 runLintDemo。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── lint-demo/
│   ├── build.gradle                       runLintDemo(JavaExec)+ packageLintReport(Copy → rawfile)
│   ├── samples/Violations*.kt             违规样本(只解析不编译)
│   └── src/main/kotlin/demo/LintDemoRunner.kt   双引擎 runner + 门禁 + JSON 报告
└── harmonyApp/
    ├── build-profile.json5                signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                 bundleName org.terminator.ohos.composerules、versionName 0.0.26-ohos.1
    └── entry/
        ├── src/main/ets/entryability/EntryAbility.ets
        ├── src/main/ets/pages/Index.ets   三 Tab 验收页面
        ├── src/main/resources/rawfile/compose_rules_report.json   真实扫描报告
        └── src/main/module.json5 / resources(base/element、media、profile)
6.2 构建、签名和安装
.\gradlew.bat build                            # 六模块编译 + 190 项测试 + spotless
.\gradlew.bat :example:lint-demo:runLintDemo   # 验收扫描,报告自动拷入 rawfile

# DevEco Studio 打开 example/harmonyApp(不要打开仓库根)
# File → Project Structure → Signing Configs 勾选自动签名
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc list targets -v
hdc -t <设备序列号> install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
  -a EntryAbility -b org.terminator.ohos.composerules -m entry

四、完整代码对照

4.1 整体架构

twitter/compose-rules(上游 17 条规则,除 3 文件 5 调用点外原样编译)
    │ rules/common:纯 Kotlin PSI 规则逻辑
    │ core-ktlint / core-detekt:引擎适配层
Kotlin 2.2.21-1.0.0 制品线(eazytec)
    │ kotlin-compiler-embeddable = 规则解析器本体
example/lint-demo
    │ 双引擎 runner + 门禁 + JSON 报告
packageLintReport
    │ 报告拷入 rawfile
example/harmonyApp
    │ ArkTS JSON.parse + @State 渲染
    ▼
ArkUI 真机三 Tab 页面(规则目录 / 扫描报告 / 适配说明)

4.2 文件清单

文件职责
settings.gradlepluginManagement eazytec 置前、ohos_compose-rules、include lint-demo、gradle-enterprise 剥离
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 钉版、spotless 6.25.0 / shadow 8.1.1、发布插件删除
build.gradle(根)仓库置前、vanniktech 剥离、:example 免 spotless、options.release = 11
gradle.propertiesJDK 21 钉版、VERSION_NAME=0.0.26-ohos.1、POM/Sonatype 键删除
rules/common/.../ComposeViewModelInjection.ktKtPsiFactory.contextual 迁移(1 处)
core-ktlint/.../TwitterKtlintRule.ktKtFileElementType.INSTANCE + cast → as(4 处)
core-detekt/.../TwitterDetektRule.ktcast → as(1 处)
example/lint-demo/build.gradlerunLintDemo + packageLintReport 报告管线
lint-demo/.../LintDemoRunner.kt双引擎扫描、门禁、JSON 报告
harmonyApp/.../pages/Index.etsArkUI 三 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(20 节)

4.3 关键 API 对照

层次API作用
ktlint 面TwitterComposeRuleSetProvider().getRuleProviders()一次性取全部 17 条规则的 Provider
ktlint 面KtLint.lint(KtLint.ExperimentalParams(...))0.48.2 的引擎入口(Params 是 0.49+ API,不能写错)
detekt 面Compose*Check(Config.empty) + lint(code)逐规则实例化扫描(每文件新实例,规则有状态)
规则层visitComposable(function, autoCorrect, emitter)引擎无关的 Composable 访问钩子(上游原样)
报告compose_rules_report.json两引擎统一契约,rawfile 交付
ArkTSgetRawFileContentSync + TextDecoder.decodeToStringrawfile 读取与解码
ArkTSresourceManager(from @kit.LocalizationKit)资源管理入口(Kit 归属以本机 SDK 为准)

4.4 验收 runner 与规则的边界

// ktlint 面
val ruleProviders = TwitterComposeRuleSetProvider().getRuleProviders()
KtLint.lint(
    KtLint.ExperimentalParams(
        fileName = sample.absolutePath,
        text = sample.readText(),
        ruleProviders = ruleProviders,
        cb = { err, _ -> errors.add(err) },
    ),
)
// detekt 面(每文件新实例:detekt 规则有状态,不能跨文件复用)
for (check in newDetektChecks()) {
    check.lint(code).forEach { finding -> results += Violation(…, finding.id, finding.message) }
}

两者之间只传文件文本与违规结果,不共享可变状态;LintError 与 Finding 各自映射进同一份 Violation model。

五、关键决策说明

决策 1:不加 ohosArm64/ohosX64,走构建期工具路线

加 ohos target 的前提是"这个库的代码会出现在设备上"。KStore 的 actual、qrose 的编码器满足前提;compose-rules 的全部依赖(kotlin-compiler-embeddable、ktlint-core、detekt-core)是 JVM-only,规则永远在构建期 JVM 里执行。给 JVM-only 模块强加 ohos target 编不出 klib,也没有运行场景。kotlinx-benchmark 的 plugin 模块已审核通过同款处置。适配的正确贡献是让规则理解鸿蒙工程的代码:解析器换到鸿蒙线 2.2.21,语言版本与 OpenHarmony 工程对齐。

决策 2:Kotlin 钉 2.2.21-1.0.0,Gradle 钉 8.14.1

上游 1.8.0 在鸿蒙线没有对应版本,必须跨代。2.2.21 线有三个已审核先例(qrose / mordant / kotlinx-benchmark),对 1.8 时代源码的兼容距离最小;2.4.20 线虽然更新,但先例全是 Kotlin 2.4 上游,且 Gradle 9.x 组合没有 2.2 代码的迁移先例。Gradle 8.14.1 是 2.2.21 线的标准搭配(该线不支持 Gradle 9)。插件(shadow / spotless)随 Gradle 升,依赖(ktlint / detekt)保持上游版本——“换线不换版”。

决策 3:三处源码迁移而不是关掉 -Werror

Kotlin 2.2 断层出现时,最省事的做法是把 allWarningsAsErrors 关掉——这会让 17 条规则与两个适配层暴露在真实的静默退化里。选择逐处迁移(5 个调用点、语义逐一等价),用 190 项测试收口:-Werror 保持开启,上游测试零改动全绿,迁移等价性才有证明。

决策 4:验收载体 = lint-demo(JVM)+ harmonyApp(HAP),不是 NAPI 桥

lint 库没有可桥接的运行时,照抄"so + NAPI + HAP"会制造一个假运行时。等价物是:lint-demo 用鸿蒙线解析器扫描鸿蒙风格的 Compose 代码(对应 qrose 的 nativeApp),harmonyApp 把扫描报告画到真机上(对应 harmonyApp 验收页)。报告页的数据源是构建期真实产物,不是摆设文字。

决策 5:报告走文件管线(rawfile),不走运行时桥

扫描发生在构建期,报告天然是静态产物。runLintDemo → compose_rules_report.json → packageLintReport → rawfile → HAP 的文件管线让"构建期扫描"与"真机展示"解耦:HAP 重新打包即可刷新报告,页面读取失败有完整兜底。这比在设备上跑一个假扫描诚实,也比手改 JSON 可复现。

六、测试与验证

6.1 测试环境

本次验证使用 Windows 10(x64)、Temurin JDK 21、Gradle 8.14.1、DevEco Studio 5.1.0 Release(SDK default,compatibleSdkVersion 5.1.0(18))、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机。

6.2 JVM 交叉验证

.\gradlew.bat build

上游全量测试 190 项全部通过(测试文件零改动):

模块用例数钉住什么
rules/ktlint(18 个测试类)103每条规则的触发 / 不触发 / 自动修复场景,经 ktlint 适配层
rules/detekt(18 个测试类)87同一套规则逻辑经 detekt 适配层

这组测试同时证明两件事:其一,三处 API 迁移没有改变规则行为;其二,ktlint 0.48.2 与 detekt 1.22.0 的引擎在 kotlin-compiler-embeddable 2.2.21-1.0.0(鸿蒙线)上运行时二进制兼容——PSI shaded 包在 1.8 → 2.2 之间保持了兼容,依赖图被抬到 2.2.21 后引擎照常工作。build 还包含 spotless 格式检查与 shadow uber jar 组装,产物形态完好。

6.3 验收工程验证

:example:lint-demo:runLintDemo
[ktlint] 24 violations, 14 distinct rules
[detekt] 24 violations, 14 distinct rules
Acceptance gates passed: ktlint=24 detekt=24
Report written to …\example\lint-demo\build\lint-report\compose_rules_report.json
BUILD SUCCESSFUL

两引擎计数完全对称(共享 rules/common),门禁通过,报告经 packageLintReport 自动拷入 harmonyApp rawfile。

6.4 HAP 验证

Build ArkTS: finished(Kit 导入修正为 @kit.LocalizationKit 后)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

HAP 为纯 ArkTS 工程,无 native 三件套,体积远小于带 so 的 CMP demo;签名由 DevEco 自动生成,证书不进仓库。

6.5 功能用例

  1. 启动后底部三 Tab 可切换:规则 / 报告 / 适配;
  2. 规则 Tab 三组 17 条规则完整滚动,每条含说明与真实规则 ID;
  3. 报告 Tab 无黄色占位横幅——说明已加载真实报告(预置占位数据会显示横幅与提示);
  4. 工具链四行显示 Kotlin 2.2.21-1.0.0 / Gradle 8.14.1 / JDK 21 / 规则版本 0.0.26-ohos.1,与构建环境一致;
  5. 两引擎计数各 24 条、14 条不同规则,逐条违规(文件:行 + 规则 ID + 消息)可滚动查看;
  6. 适配 Tab 展示构建期工具定位、换线方式与 AtomGit 仓库 / 社区链接。

6.6 验证结论

规则库本体、双引擎运行时、验收门禁、报告管线、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,报告页展示了真实的构建期扫描会话(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-rules.git
cd compose-rules
export JAVA_HOME="/path/to/jdk-21"
./gradlew build                                   # 编译 + 190 项上游测试 + spotless
./gradlew :example:lint-demo:runLintDemo          # 双引擎验收扫描 + 报告落 rawfile
# DevEco Studio 打开 example/harmonyApp,配置签名后:
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.composerules -m entry

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改文档不算适配,必须让规则库在鸿蒙工具链线上真实编译、测试、扫描——但也不能反向硬加 ohos target:JVM-only 依赖编不出 klib,构建期工具没有设备端场景;
  2. settings.gradle 里 eazytec 双仓必须排在 pluginManagement 最前,否则鸿蒙线 Kotlin 插件解析不到,配置期直接失败;
  3. KGP 2.2 强校验 Java/Kotlin 编译目标一致性:JDK 21 下 compileJava 默认 21,必须 options.release = 11 配平 Kotlin 的 jvmTarget 11,否则 Inconsistent JVM Target Compatibility 直接失败;
  4. Kotlin 2.2 移除了 KtStubElementTypes.FILE(javap 确认),文件级元素类型用 KtFileElementType.INSTANCE;addToStdlib.cast 升级为错误级警告,换 as + @Suppress("UNCHECKED_CAST")——上游 -Werror 让这些问题全部前置到编译期暴露;
  5. Gradle 8.14 下 shadow 7.1.2 直接不可用,spotless 6.13.0 也有兼容问题;shadow → 8.1.1、spotless → 6.25.0 一次到位;
  6. Windows + core.autocrlf=true 检出的工作区是 CRLF,spotless 按上游 LF 规范校验会大面积红——跑一次 spotlessApply 归一化,对 git 提交内容无影响;
  7. ktlint 0.48.2 的引擎入口是 KtLint.lint(KtLint.ExperimentalParams(...)),KtLint.Params 是 0.49+ 的 API;引擎对象整体带 deprecation-for-removal 标记,runner 需要 @file:Suppress("DEPRECATION")(0.49 换 KtLintRuleEngine 是上游新版本的活);
  8. detekt 规则实例有状态,跨文件复用会串结果——每个文件重新实例化 17 个 Check;
  9. resourceManager 的 Kit 归属要以本机 SDK 为准:@kit.AbilityKit 不导出它、@kit.ResourceManagementKit 不存在,答案是 @kit.LocalizationKit(导出 { i18n, intl, resourceManager, sendableResourceManager }),用 SDK 的 d.ts 文件定案,不抄网上示例;
  10. 报告文件名两侧必须逐字符一致:runner 产出 compose-rules-report.json、HAP 读 compose_rules_report.json,差一个连字符页面就永远显示占位数据——统一为下划线命名;
  11. kotlin.incremental.useClasspathSnapshot 在 Kotlin 2.2 已移除,留在 gradle.properties 里每次构建都警告;
  12. 签名材料由 DevEco 自动写入 build-profile.json5,仓库内 signingConfigs 保持空数组,*.hap / *.p12 / *.cer 进 gitignore,证书绝不进入 AtomGit。

8.2 已知问题

  • ktlint 锁 0.48.2、detekt 锁 1.22.0(上游 0.0.26 的钉版;上游后续版本迁移 ktlint 0.49+ KtLintRuleEngine 与 detekt 1.23 新 Provider API,fork 跟随上游节奏,不抢跑);
  • 规则是 PSI 语法级的(与上游一致),不做完整类型推断,不检查 ArkTS(.ets)代码——检查对象是鸿蒙 CMP 工程共享 UI 层的 Kotlin Compose 源码;
  • 报告页数据在 HAP 打包时固化,重新扫描后需重新打包 HAP(构建期产物的天然属性);
  • 发布链已剥离,产物消费走本地构建或社区 Maven,不发 Maven Central;
  • HAP 报告页读取失败时显示错误卡片(设计如此),需要重跑 runLintDemo 后重打包。

8.3 未来优化方向

  • 把 com.twitter.compose.rules:*:0.0.26-ohos.1 发布到社区 Maven,鸿蒙工程直接消费坐标;
  • 接 CI:AtomGit 流水线跑 build + runLintDemo,双门禁化(测试 + 扫描对称性);
  • 跟踪上游 0.0.27+ 的 ktlint/detekt 大版本迁移,复用本次的三类 API 迁移方法;
  • 验收样本扩展到全部 17 条规则命中(补 preview-public / parameter-order / vm-forwarding 的触发构造);
  • 评估把扫描报告接入 DevEco 工程的 ohpm/hvigor 生命周期,让鸿蒙工程一条命令出报告。

九、总结

9.1 核心难点回顾

上游 compose-rules 0.0.26(Kotlin 1.8 / Gradle 7.4.2)
    → 路线判定:构建期纯 JVM 工具,不加 ohos target(kotlinx-benchmark 先例)
    → 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 + JDK 21(eazytec 置前)
    → 发布链剥离 + 插件升版(shadow 8.1.1 / spotless 6.25.0)
    → 三文件五调用点 API 迁移(-Werror 全程保持)
    → 190 项上游测试全绿(换线不换版的直接证据)
    → lint-demo 双引擎验收(24 + 24 对称 + 门禁)
    → 报告文件管线(rawfile)→ ArkTS 三 Tab 页面
    → HAP 构建、签名、真机验收

9.2 封装层次

  • 规则层:上游原版 17 条规则实现(rules/common),3 文件 5 调用点 API 等价迁移;
  • 引擎层:上游原版 ktlint / detekt 适配层,跑在鸿蒙线 kotlin-compiler-embeddable 2.2.21-1.0.0 上;
  • 构建层:eazytec 双仓置前、JDK 21 / Gradle 8.14.1 钉版、发布链剥离;
  • 验收层:lint-demo 双引擎 runner、违规样本、扫描门禁、JSON 报告;
  • 管线层:runLintDemo → compose_rules_report.json → rawfile;
  • ArkTS 层:三 Tab 页面、rawfile 读取、JSON 解析、@State 渲染;
  • DevEco 层:完成 HAP、签名、安装和真机运行。

9.3 三条经验

  1. 适配路线由"代码在哪执行"决定,不由"库叫什么"决定:KMP/CMP 库名头不能自动推出"加 ohos target"——compose-rules 顶着 Compose 的名字,却没有一行代码上设备;判据是依赖形态(JVM-only)与执行环境(构建期),kotlinx-benchmark 的 plugin 面早已给出先例,本次是把这条路线走到底;
  2. 构建期工具的"换线"要换到解析器本体:只升 Gradle 插件不升 kotlin-compiler-embeddable,lint 引擎还是旧语言版本;版本目录一处 kotlin = 2.2.21-1.0.0 让构建与解析同时换线,再用 190 项测试证明运行时兼容,这才是"鸿蒙线 lint"的完整含义;
  3. 验收要证明"真的咬合",不要演示"看起来在跑":lint-demo 的门禁(每引擎 ≥10 条违规、≥6 条规则)+ 双引擎计数对称,比任何截图都有说服力;HAP 报告页展示构建期真实数据(工具链指纹来自构建环境注入),占位数据明确标黄——静态工具的验收诚实度全靠这些细节。

9.4 适配成果

当前适配已完成:

  • compose-rules 0.0.26-ohos.1 在鸿蒙工具链线上的完整构建(三文件五调用点 API 迁移,17 条规则逻辑零改动);
  • HarmonyOS Kotlin 制品线在 lint 工具库上的落地(Kotlin 2.2.21-1.0.0 全家桶,Gradle 8.14.1 + JDK 21);
  • 190 项上游测试全绿(测试文件零改动)+ lint-demo 双引擎验收(24 + 24 对称、14 条规则命中、门禁通过);
  • DevEco Stage ArkUI 三 Tab 验收页面(规则目录、真实扫描报告、适配说明);
  • HAP 构建、签名、设备安装和真机验证(纯 ArkTS,无 native 依赖);
  • AtomGit 项目文档、OpenHarmony README(中英双份)、适配全流程博客和真机效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐