SuperAPI AI 工具集

find-non-lambda-logs

编程与开发

查看项目

更新于 10-16 04:36

技能介绍

Find Non-Lambda Log Calls 是一个面向 Kotlin/Android 代码库的日志卫生审计技能,用于发现并修复三类相关的日志调用问题。

用途

该技能适用于以下场景:合并了新增日志的分支之后;对日志卫生进行周期性审计;将 android.util.Log 用法迁移到共享 Log 包装器之后。

主要能力

技能覆盖三类问题:

  1. 缺少 lambda 重载。使用字符串插值但未使用 lambda 重载的 Log.d/i/w/e 调用,在 release 构建中日志级别被过滤掉时仍会浪费字符串分配。

  2. catch 块中丢弃异常。catch (e: ...) 块内的 Log.w/e 调用插值了 ${e.message} 却没有传入 e,会丢失堆栈跟踪,并且在 e.message 为 null(NPE、无消息的 IOException 等)时记录不到有用信息。

  3. 仍在使用 android.util.Log。导入平台日志器的文件绕过了 Log.minLevel 和 LogSink,且没有 lambda 重载,因此上述两项修复都无法应用,需先迁移。

使用方式

技能以分步搜索命令的形式给出操作流程:

Step 0:先查找仍在使用 android.util.Log 的文件。使用两个模式——仅用全限定名模式会产生漏报,因为几乎没人写 android.util.Log.w(...),通常是导入 android.util.Log 后写 Log.w(...),因此 import 才是可靠信号:

grep -rln --include='*.kt' '^import android.util.Log$' . | grep -v '/build/' | grep -v PlatformLog

grep -rnE --include='*.kt' 'android.util.Log.(d|i|w|e|v)(' . | grep -v '/build/' | grep -v PlatformLog

需排除 PlatformLog.android.kt,它是包装器实现,必须调用 android.util.Log。

Step 0b:模式是按行锚定的,需单独扫描多行调用。用行尾左括号形式捕获,再逐条阅读:

grep -rnE --include='*.kt' 'Log.[diwe]($' . | grep -v '/build/'

rg -U --multiline --type kotlin 'Log.[diwe](\n[^)]*${'

优先处理循环内的调用点,而非单行调用。

Step 1:查找插值的 Log.d/Log.i(最高优先级,release 中会被过滤):

pattern: Log.(d|i)("[^"]+",\s*"[^"]*$ type: kotlin

pattern: Log.(d|i)(\w+,\s*"[^"]*$ type: kotlin

Step 2:查找未传异常的插值 Log.w/Log.e,然后手动排除第三个参数传入了异常的行。注意 it 是最容易漏掉的名字,Result.onFailure { ... } 是常见形态,多数正确调用以 , it) 结尾而非 , e)。异常也不一定在行尾,因此排除规则不应锚定 $:

THROWABLE='(e|t|it|ex|err|error|throwable|cause|tr)'

grep -rnE --include='.kt' 'Log.(w|e)([^,]+,\s"[^"]$' . | grep -vE ",\s$THROWABLE)"

Step 3:查找 catch 块中丢弃异常的 Log.w/e。在 Step 2 命中项中,插值 ${e.message}(或 ${t.message}、${throwable.message})但未传入异常本身的调用是另一类 bug:

pattern: Log.(w|e)([^)]*${(e|t|it|ex|err|throwable|cause).message} type: kotlin

随后逐条打开文件确认该行位于 catch (e: ...) 块内且未将 e 作为第三个参数传入。误报包括接受 e: SomeError 参数并转发到别处的扩展函数/辅助函数。Step 2 与 Step 3 可能标记同一行,应先处理 Step 3,再对剩余项应用 Step 2。

修复写法

lambda 重载(Step 1 + Step 2):

// Before Log.d("Tag", "Processing event ${event.id} from ${relay.url}")

// After Log.d("Tag") { "Processing event ${event.id} from ${relay.url}" }

异常重载(Step 3):改用 (tag, msg, throwable),lambda 重载不接受异常,因此必须使用即时字符串形式,并从消息文本中去掉冗余的 ${e.message}:

// Before — 丢失堆栈,e.message 为 null 时打印 "...failed: null" try { groupManager.clearAllState() } catch (e: Exception) { Log.w("MarmotManager", "clearAllState failed: ${e.message}") }

// After — 记录完整堆栈 try { groupManager.clearAllState() } catch (e: Exception) { Log.w("MarmotManager", "clearAllState failed", e) }

权衡:即使 warn 被过滤,消息字符串也会被即时分配,但 warn 级别的 catch 日志属于罕见事件路径,相比丢失诊断细节,这一开销可忽略。

不要转换的情况

不要转为 lambda:传入了 Throwable 参数的调用——lambda 重载 (tag) { message } 没有异常参数。

不要转为 lambda:任何导入了 android.util.Log 的文件中的调用。平台 Log 没有 lambda 重载,转换会编译失败并报 None of the following candidates is applicable。应先迁移文件,或保持调用不变。

无 $ 插值的静态字符串调用——没有分配收益。

被注释掉的日志调用。

catch 块之外有意记录 e.message 的信息性日志(罕见,通常表示异常已被处理,只有消息有意义)。

从 android.util.Log 迁移文件

这是为 Step 0 发现的文件解锁 Step 1–3 的前提。这是行为变更,应检查而非假设。迁移前需确认:文件使用了哪些级别(grep -hoE 'Log.[a-zA-Z]+' <files> | sort | uniq -c;包装器只有 d/i/w/e,没有 v,也没有 getStackTraceString,Log.v 调用没有直接等价物,需要决策而非重命名);门控是否会丢弃它们(LogLevel { DEBUG, INFO, WARN, ERROR },门控为 minLevel <= <level>,Amethyst.DEFAULT_LOG_LEVEL 在 debug 下为 INFO、release 下为 WARN,包装器自身默认为 DEBUG,因此 Log.w 和 Log.e 在所有构建类型和进程中都会保留,包括 Amethyst.init 运行之前,这对 :napplet 很重要;Log.d/Log.i 在 release 中会静默,需要有意决策);输出是否变化(不会,Android 上的 PlatformLogSink 委托给 android.util.Log,行原样进入 logcat);模块能否看到 quartz(nappletHost 已有 implementation(project(":quartz")),需先检查)。

然后:将 import android.util.Log 换成 import com.vitorpamplona.quartz.utils.Log,运行 ./gradlew spotlessApply(导入顺序会变化),仅将插值且无异常的调用转换为 lambda 形式。已传入异常的调用保持即时三参数形式——包装器的 w(tag, msg, throwable) 完全匹配,只需移动导入。

验证异常是否保留,因为粗心的重写可能静默丢弃第三个参数:

grep -hoE 'Log.[diwe]([^)],\s(e|it))' <files> | wc -l # 比较前后

适用场景与限制

该技能面向 Kotlin/Android 代码库,尤其是使用共享 Log 包装器(如 Amethyst 项目中的 quartz Log)的工程。搜索前需过滤噪声,否则统计会误导:丢弃 /build/、/androidTest/ 和 /src/test/(release 过滤不适用于测试),并丢弃首个非空白字符为 // 或 * 的行(注释掉的调用和 KDoc 示例都会匹配这些模式),可用 grep -vE ':[0-9]+: *(//|*)' 处理后者。标签可以是字符串字面量("Tag")或变量(tag、LOG_TAG),每一步都要运行两种模式。技能中的具体数字(如 2026-08-28 的扫描结果)为资料中记录的历史观察,非通用保证。