先说个结论:如果你在 IntelliJ IDEA 或 Android Studio 里看到 “At least one of the problems in category ‘unused‘ is not analysed due to a compiler option being ignored” 这行提示,先别慌,它百分之九十不是编译错误,更不表示你代码里的未使用问题突然全消失了。这是工具在提醒你:由于某个编译器选项被忽略,在 “unused” 这一类问题中,至少有一个没有被分析到。
最近我在整理一个 Kotlin 多模块工程时,Problems 面板里就突然冒出过这句话。当时我刚调整完 Gradle 编译配置,正准备提交代码,面板里这个提示一直悬在那,不红不黄,既不影响构建又让人觉得哪哪不对劲。后来我专门花了几小时把它彻底查了一遍,发现这类问题在团队项目里其实挺常见的——某个人为了压缩编译输出改了一个编译参数,结果 IDE 的分析引擎在“unused”这一类检查上直接失效了,但表面看起来一切正常。
这篇文章我会顺着提示本身往下挖:它到底是谁发出的,背后涉及哪些编译机制,什么时候会触发,以及最后怎么定位和解决。内容主要面向用 Kotlin + Gradle 的 Android / JVM 项目,但对任何使用 IntelliJ 系 IDE 做 Kotlin 开发的工程都有参考价值。新手可以把这当作一个理解编译器诊断机制的小案例,老手也可以直接跳到后面的排查清单,看有没有踩过同样的坑。
1. 提示背后:不是 Bug,而是“诊断覆盖不完整”
1.1 这句话到底是谁在说
这个提示的来源,不是 Gradle 构建脚本,也不是 Kotlin 编译器直接打出来的 error,而是 IntelliJ IDEA / Android Studio 的代码分析引擎在整合编译器诊断后,发现自己没有覆盖某类问题,于是向使用者暴露的一条“信息”。它的关键点有两个:一是category ‘unused‘,二是compiler option being ignored。
先说unused。在 Kotlin 的编译器诊断体系里,unused不是某一个检查项,而是一整类检查的集合。最常见的包括未使用的局部变量、未使用的私有函数、未使用的构造参数、未使用的导入、未使用的 lambda 参数、未使用的 receiver,以及声明了但从未被引用的顶层函数或类。IDE 的 Inspections 面板里,对应的是 “Unused declarations” 这一组检查,你可以单独控制每个子项的开关和严重程度。
1.2 “compiler option being ignored” 意味着什么
再看后半句。它说某个“编译器选项被忽略了”。这里藏着一个容易被忽略的事实:编译器选项的“最终生效”不等于“配置写法存在”。我见过很多项目,在build.gradle.kts里写了一大堆freeCompilerArgs,但实际编译时压根没走到那一段配置——比如职责被另一个模块覆盖、DSL 写法更新后旧参数失效、或者版本升级后参数名被改名但不报错。配置存在,但选项被忽略,这就会让 IDE 的分析引擎得不到它想要的完整语义,因此在unused这一类诊断上只能给出部分结果。
从这里你也能看出,为何这行提示不是出现在编译日志里,而是出现在 IDE 的问题面板里:IDE 知道它拿到的分析结果是“不完整的”,但它不确定这是用户故意为之,还是配置意外错误,于是用一句客观描述提醒你。这种设计其实相当克制——它没有武断地说“你的项目有问题”,而是在说“这里存在一个不确定性,请你确认”。
1.3 为什么是 “at least one” 而不是具体数量
标题里还有一个词值得注意:at least one of the problems。为什么不直接说有多少个问题没被分析?因为分析引擎目前并不知道具体漏掉了多少个。可能只有一个,也可能有一百个,它只知道某一类诊断因为没有得到完整的编译器选项而无法继续。这种“未知数”的描述方式恰恰说明,处理这个提示的关键,不是去统计数量,而是先把让诊断无法完整运行的原因找出来。
这里我再补充一个场景。如果你在 IDE 里把 “Unused declaration” 检查的 Severity 调高,或者启用了allWarningsAsErrors,这个提示会显得更刺眼。因为它可能意味着你即将基于一份不完整的分析结果去修代码,甚至有人会误以为“当前代码没有任何 unused 问题”而把一些本该清理的私有方法留在仓库里。对于大型项目,这种不完整分析带来的长期维护成本,可比一个构建错误高得多。
2. 常见的触发场景:我什么时候会撞上它
2.1 场景一:Gradle 配置里显式屏蔽了 unused 警告
最直接也最常见的原因,是项目里有人为了“让编译输出干净一点”,在编译器参数中加入了对unused类警告的压制参数。Kotlin 编译器中有类似-Xsuppress-warning这类允许你指定诊断名称的开关。举例来说,如果你想压掉某个具体警告,可能会写:
kotlinOptions { freeCompilerArgs = freeCompilerArgs + "-Xsuppress-warning=UNUSED_VARIABLE" }为了叙述方便,我下面用-Xsuppress-warning=unused来代表“压制 unused 这一类诊断”的配置,实际工程里需要替换成具体的诊断名。
写这行配置的人,本意可能是想消除编译时满屏的警告噪音。但副作用是,IDE 的分析引擎在读到这一配置后,会对unused类问题停止完整分析。于是你在 inspection 面板里看不到任何 unused 问题,但 IDE 又认为有必要告诉你“分析不完整”,于是就有了这个提示。这种场景在团队项目里特别常见:某个人为了应付一次构建改了配置,然后项目里其它人开始收到这行提示。
2.2 场景二:增量编译/缓存让诊断结果落后于实际代码
第二种常见情况与缓存有关。现在的 Kotlin/Gradle 项目几乎都会开启增量编译,配合构建缓存和 IDE 的自身缓存,很多问题不会每次都重新分析。当你在分支之间切换,或者从远端拉了一版很大改动后,老旧的增量缓存和新的编译配置可能产生冲突。IDE 发现当前上下文里有一个编译器选项“应该被处理但实际没处理”,就会认为诊断覆盖不完整。
这类场景的典型特征是:提示出现在某次 Gradle Sync 之后,而且你去检查 Gradle 配置没有发现任何问题。处理方式相对温和:清理项目缓存、重启 IDE、或者执行一次./gradlew clean重新编译,提示通常就消失了。但我要提醒一点——如果清理缓存后它反复出现,那说明不是缓存问题,而是底层配置问题,别用“清缓存”作为唯一的解。
2.3 场景三:IDE 配置与 Gradle 编译配置不一致
第三种场景是 IDE 的 Inspection 设置和 Gradle 实际使用的编译器参数不一致。比如你在 IDE 里把 “Unused declarations” 检查开启了,但 Gradle 配置中传给 Kotlin 编译器的参数却包含某些影响 unused 分析的开关;或者反过来,IDE 已经被告知某个检查完全不用分析,而 Gradle 侧仍然在尝试分析。IDE 为了保持一个全局统一的分析模型,有时会退而求其次,只做有限分析,再把这个“降级”通过提示告诉你。
在我的经验里,这种不一致往往出现在项目从旧版 Kotlin 升级到新版之后。Kotlin 2.0 开始推荐使用compilerOptions {}DSL 而不是旧kotlinOptions {},如果项目里新旧两套配置并存,很容易出现某些参数在 IDE 里被视为“未知/忽略”,从而触发提示。
2.4 场景四:多模块项目里配置覆盖问题
第四种场景在单体仓库、多模块工程里最明显。假设 A 模块给 Kotlin 编译器加了一个影响 unused 分析的参数,B 模块没有,但 IDE 做跨模块分析时会把多个模块的编译选项合并在一套上下文里。此时 B 模块的代码中如果有 unused 问题,分析引擎可能无法判断应该按哪个编译选项来处理,于是打出“至少有一个问题没有被分析”的提示。这种场景排查起来更隐蔽,因为你单看 B 模块的 Gradle 配置是完全正常的,真正的问题在 A 模块。
这些触发场景说明一个核心观点:这个提示不是一个孤立 bug,而是“编译器选项在真实构建链路中未完全生效”的一种低配版告警。理解到这一步,排查思路就清晰了:我们要去检查编译链路中所有可能影响unused分析的因素。
3. 一步一步定位:从复现到找到元凶
3.1 先确认提示出现在哪里
不要一上来就改代码。先把提示的上下文看清楚。它出现在哪里?是 Gradle 构建输出里,还是 IDE 的 Problems 面板?如果只是 IDE 面板,那大概率是 IDE 分析层的问题;如果是命令行构建也输出同样的提示,那就要认真对待 Gradle 配置了。
区分方法很简单:在项目根目录执行一次完整编译。
./gradlew compileDebugKotlin --rerun-tasks再执行一次带告警统计的编译:
./gradlew compileDebugKotlin --rerun-tasks --info如果命令行输出里完全没有 “not analysed” 相关文字,说明问题只存在于 IDE 的分析引擎侧;如果命令行也提示,说明你的编译器参数或构建缓存确实出问题了。从这里开始,排查路线才会分叉。
3.2 审查 Kotlin 编译选项
下一步是打开模块的build.gradle.kts,检查带kotlinOptions或compilerOptions的部分。我把排查时常用的检查清单列出来,你可以直接照着过:
freeCompilerArgs里有没有-Xsuppress-warning、-Werror、-progressive这类影响诊断行为的参数?- 多个 build.gradle.kts 之间有没有对同一个属性重复赋值?比如子模块里
freeCompilerArgs += ...,而在根项目里用freeCompilerArgs = listOf(...)覆盖。 - 有没有在
gradle.properties里设置kotlin.incremental=false/kotlin.compiler.execution.strategy这类会影响编译行为的环境变量? - 如果项目使用 Kotlin 2.x,是否还在使用被废弃的
kotlinOptions?有没有可能新旧 DSL 混用导致参数被忽略?
3.3 最小化复现:砍掉所有“装饰性”配置
排查到这一步依然找不到问题的时候,我有一个很粗暴但有效的办法:搜索整个项目仓库里所有freeCompilerArgs、compilerOptions、kotlinOptions相关配置,然后逐个模块做最小化实验。具体做法是,在一个模块里临时把编译参数精简到最基础状态,只保留目标 JVM 版本和必要的语言版本,然后重新 Sync、重新构建,看提示是否消失。
如果提示消失,说明问题一定出在你删掉的某一项参数上;如果提示仍在,那就把怀疑对象扩大到 IDE 自身的 Inspection 设置和缓存。
3.4 用命令行构建结果当“照妖镜”
我一直强调一个原则:当 IDE 和命令行结论不一致时,以命令行构建结果为准。IDE 显示“未分析问题”时,命令行构建更接近真相,因为 Gradle 会真实地调用 Kotlin 编译器,所有配置都会被实际解释一遍。所以如果在上面第 3.1 步中命令行输出正常,说明你的代码和构建脚本大概率没有问题,提示只是 IDE 分析模型里的一个自我告警,处理优先级可以大幅降低。
如果你执行--info之后想更细地看编译器到底收到哪些参数,还可以在 Gradle 配置里临时打印:
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { doFirst { println("freeCompilerArgs: ${compilerOptions.freeCompilerArgs.get()}") } }编译时就能直观看到每个模块实际传给 Kotlin 编译器的参数列表,很多“配置存在但没生效”的问题在这一步就水落石出了。
4. 处理方案:按优先级推进的四个做法
4.1 先校正编译器参数,别急着清理缓存
如果你在配置里发现确实有压制 unused 警告的参数,先问一句:这个参数是故意的吗?如果是,请在注释里写明原因;如果不是,建议移出。尤其多人维护的项目,这类参数很容易是历史遗留——某个版本为了绕过问题暂时加上的,后来问题解决了但配置没删。
移除方式依据你用的 DSL 版本。新项目建议使用 Kotlin 2.x 的官方 DSL:
kotlin { compilerOptions { freeCompilerArgs.remove("-Xsuppress-warning=unused") } }在 Android 模块里,通常写成:
kotlinOptions { freeCompilerArgs = freeCompilerArgs.filterNot { it == "-Xsuppress-warning=unused" } }删除后重新 Sync 项目,再看 Problems 面板。如果 unused 类问题开始出现,说明配置恢复成功,之前的提示就是被这个参数压住的。
4.2 同步 IDE 检查项:让 inspection 和编译诊断一致
如果命令行构建正常、编译参数也没有问题,那就需要把视角切到 IDE。打开 Settings -> Editor -> Inspections,找到 Kotlin 下面的 “Unused declarations” 相关检查,确认它的 severity 符合你的预期。
这里有个容易踩的坑:很多人为了让“未使用代码”提示更醒目,会把 severity 设为 Error,同时又在 Gradle 里开了allWarningsAsErrors = true。这两个设置单独看都没问题,组合在一起之后,编译器会尝试把 unused 警告当作错误处理,但 IDE 又有自己的“该分析哪些类别”的边界,结果两边互相拉扯,提示就会出现。我的建议是:unused类检查保持 Warning 级别就好,不要开到 Error,别让本来无害的死代码管理升级成构建失败源头。等真的要推动清理时,可以单独通过 CI 脚本或专门的 Gradle task 来做。
4.3 清理项目缓存:一定要按顺序来
确实有一些场景是缓存导致提示暂时性出现。清理的顺序也有讲究,我踩过几次坑后形成的顺序是:
- 先执行
./gradlew clean,重新编译项目,观察提示是否仍在; - 如果还在,关掉 IDE,删除
.idea目录下除workspace.xml外的配置缓存(建议先备份),重新打开 IDE 让项目重新导入; - 只有在前面都无效时,再考虑
File -> Invalidate Caches / Restart,因为这个操作会清掉本地分析数据,重启后需要较长时间重建索引; - 最后检查
~/.gradle/caches和项目根目录build文件夹里是否存在异常庞大的缓存,这通常是构建缓存被污染的信号。
这里有一个需要强调的常识:不要因为一行提示,就把整个 Gradle 用户目录缓存删掉。删除后所有项目都要重新下载依赖和生成缓存,代价远大于收益。先做最小范围的清理。
4.4 如果想彻底“无视”这个提示
有些团队会决定不处理这个提示。如果确认不是配置问题,也不影响实际编译和测试,你可以在 IDE 中降低相关 Inspection 的优先级,或者把它放到 Ignored 分类里。具体是在 Problems 面板里右键该条提示,选择 Ignore 或调整 severity。但这属于治标不治本——后续如果有新人接手,看到这个被 ignore 的提示会一脸问号,所以我建议至少留下代码注释或团队 wiki 说明。
这里也说句实在话:如果项目非常庞大,分析性能一直紧张,主动忽略一些低价值诊断并不是不可接受的工程取舍。关键是,这个决定要有意识、有记录地做,而不是让问题一直悬着。
5. 实战经验:常见问题速查与心得
5.1 我遇到过的几个典型情况
在这里把我在实际项目里遇到的几个案例整理成一个速查表,方便你直接对照:
| 现象 | 可能的根因 | 处理建议 |
|---|---|---|
| 提示只出现在 IDE Problems 面板,命令行构建无任何异常 | IDE 分析模型与 Gradle 编译配置不一致,或分析缓存落后 | 先以命令行结果为准;清理 IDE 缓存,重新 Sync |
| 提示在多人协作后突然出现 | 有人提交了带编译参数改动的 Gradle 配置 | 用 git blame 定位改动者,确认加参数的目的 |
| 提示伴随大量 unused 警告一起消失 | 配置里存在压住 unused 诊断的参数 | 移除该参数,或改为定向 suppression |
| 提示反复出现在同一个模块 | 该模块 freeCompilerArgs 被覆盖或新旧 DSL 混用 | 统一 compilerOptions 配置,打印实际参数确认 |
| 提示在升级 Kotlin/AGP 版本之后出现 | 编译器参数改名或废弃,旧配置被静默忽略 | 查阅版本迁移文档,更新配置 |
5.2 几个排查过程中的小细节
第一,优先在--info构建输出里搜一下有没有unused相关字样。Kotlin 编译器在很多情况下会输出类似解释性文字,帮你判断是不是这个选项引起的。
第二,命令行构建时注意避免被增量编译蒙蔽。使用--rerun-tasks强制所有 task 重新执行,才有可能拿到完整的警告列表。如果你跑的是带缓存的增量构建,那等于还是在看旧结果。
第三,如果项目里有 compile avoidance 或 remote cache 配置,要特别留意。远程缓存会直接复用别人的构建产物,如果缓存命中,编译参数即使配置错误也可能完全不会暴露。这种情况最坑:你代码没问题、本地构建没问题,但 CI 上一直出现奇怪提示。
5.3 我能给的最实在的建议
最后分享一点个人体会。这类“诊断不完整”的提示,和普通编译错误不一样,它没有强制你说“必须马上改”,更像一位细心同事在旁边提醒你“你刚才看到的结论可能不完整”。为了不让这种不确定性长期存在,我会给自己定一个简单的处理原则:出现提示后先花不超过十五分钟排查配置,如果找不到原因,就直接用命令行构建结果做对照;命令行没有异常的话,把提示先降级处理,记在待办里,而不是盲目改代码。很多时候,它最终被证明是一个已经废弃的参数或一次缓存障碍,和代码质量没有半点关系。
我在实际项目中还有个习惯:把所有会影响编译器诊断的参数都集中到一个可注释的配置块里,并写明用途。这样后人看到某个全局编译选项时,能立刻明白它是用来做什么的,什么时候可以安全移除。遇到这个提示时,排查范围也会小很多。希望这套方法能让你下次再看到 “At least one of the problems in category ‘unused‘ is not analysed due to a compiler option being ignored” 时,少点焦虑,多点从容。