关于 droidmap
droidmap 为 Android 项目构建包含 Room、Navigation、依赖注入和 Gradle 模块关系的图,适用于开发者、代码审查流程和编码 Agent。它可分析变更影响、定位规则问题并筛选受影响的测试,读取源文件但不会自行编译代码。
主要功能
- 01分析 Android 组件与模块依赖图
- 02检查 Room、Navigation 和依赖注入规则
- 03评估代码变更影响及受影响测试
- 04生成项目报告与流程图
- 05为拉取请求生成影响摘要
适用场景
- 01提交前检查数据库迁移和路由配置
- 02评估变更影响并选择需运行的测试
- 03在拉取请求中展示受影响模块与组件
- 04让编码 Agent 查询项目依赖关系
MCP 相关
droidmap 可通过 stdio MCP 服务向客户端提供影响分析、变更差异、规则检查、节点查询、搜索、报告和规则说明工具。文件变化时会更新图,以便 Agent 获取与代码编辑同步的分析结果。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
docker run -i --rm ghcr.io/hussainshirazi/droidmap:0.3.0本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
ghcr.io/hussainshirazi/droidmap:0.3.0stdio工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
droidmap
一张了解项目的 Android 专属地图,为你、你的拉取请求和你的 AI 编码 Agent 而打造。
通用代码图能识别类和调用,却看不到这些问题:
- Room 数据库的版本 4 没有迁移到版本 5 的迁移脚本,因此使用版本 4 的用户更新时会崩溃;
- 实体新增了一个列,但数据库版本没有递增,更新时也会崩溃;
- 迁移测试名为
migrationFrom1To4,实际却运行 1→2→3→4,而使用版本 1 的用户会触发MIGRATION_1_4,却没有任何测试覆盖它; navigate("settings")指向的路由没有在任何NavHost中注册;- 某个 ViewModel 注入了一个没有任何绑定的仓库,无论使用 Hilt 还是 Koin 都如此(使用 Koin 时会在运行时崩溃);
- 某个库在合并后的清单中添加了
ACCESS_NETWORK_STATE; - 某个功能模块通过
build-logic中的约定插件获取了:core:data,违反了你的模块规则。
droidmap 直接读取 Android 的连接关系(Gradle、Room、Navigation、Hilt、Koin、清单;支持 Kotlin 和 Java),并将其转换为一张图。你可以在 CI 中运行其规则,在每个拉取请求上获取说明变更影响范围的评论,在修改某个类前询问哪些内容依赖它,还可以通过 MCP 将它交给 AI Agent。
它只读取源文件:绝不会运行 Gradle、编译代码或向任何地方发送数据。(GitHub Action 只会在你要求时运行一个 Gradle 任务,用于获取合并后的清单。)
在真实应用中的表现
在 Now in Android(35 个模块,使用 Hilt、Room 和 Navigation 3)上,笔记本电脑每次运行耗时约 1.5 秒:
$ droidmap check
warning room-migration-untested core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt:66
NiaDatabase: 13 of 13 migration steps have no MigrationTestHelper test (1->2, 2->3, 3->4,
4->5, 5->6, 6->7, 7->8, 8->9, 9->10, 10->11, 11->12, 12->13, 13->14)
1 warning
对 OfflineFirstTopicsRepository.syncWith 做一行修改后(输出已缩短):
$ droidmap diff
Changed (1): OfflineFirstTopicsRepository (injectable)
Affected (what depends on the changed code):
Screens: ForYouScreen, InterestsScreen, TopicScreen
Navigation destinations: ForYouNavKey, InterestsNavKey, TopicNavKey
ViewModels: ForYouViewModel, InterestsViewModel, TopicViewModel
Dependency injection: DataModule.bindsTopicRepository, GetFollowableTopicsUseCase, SyncWorker
Modules: :app, :app-nia-catalog, :core:data, :core:data-test, :core:domain, ... and 12 more
Tests that cover it (1): OfflineFirstTopicsRepositoryTest
$ droidmap diff --format tests
:core:data:test --tests com.google.samples.apps.nowinandroid.core.data.repository.OfflineFirstTopicsRepositoryTest
最后一行可直接传给 CI 中的 ./gradlew:只运行一个测试类,而不是整个测试套件(参见仅运行变更可能影响的测试)。
安装
macOS 或 Linux:
curl -fsSL https://raw.githubusercontent.com/hussainshirazi/droidmap/main/install.sh | sh
Windows(PowerShell):
irm https://raw.githubusercontent.com/hussainshirazi/droidmap/main/install.ps1 | iex
Homebrew:运行 brew tap hussainshirazi/droidmap https://github.com/hussainshirazi/droidmap,然后运行 brew install droidmap。Scoop:运行 scoop bucket add droidmap https://github.com/hussainshirazi/droidmap,然后运行 scoop install droidmap/droidmap。
Docker,适用于 GitHub Actions 以外的 CI 系统(GitLab、Bitbucket、Jenkins、Bitrise):运行 docker run --rm -v "$PWD":/project ghcr.io/hussainshirazi/droidmap check(从版本 0.2.0 起)。
Gradle(从版本 0.3.0 起,发布到 Gradle Plugin Portal 后可用),在根目录的 build.gradle.kts 中添加配置;然后运行 ./gradlew droidmapCheck,有错误时构建会失败:
plugins { id("io.github.hussainshirazi.droidmap") version "0.3.0" }
droidmap {
failOn.set("warning") // error (the default), warning, info or never
mergedManifest.set(true) // run each processReleaseMainManifest first: library permissions are checked too
}
或者从最新版本下载 droidmap.jar,并运行 java -jar droidmap.jar。droidmap 需要 Java 17 或更高版本。droidmap 命令会使用 JAVA_HOME、PATH 中的 java,或者 Android Studio 自带的 Java,因此安装了 Android Studio 就无需进行任何配置。该 jar 文件约为 65 MB,因为其中包含 Kotlin 编译器的解析器。
快速开始
在项目内任意位置运行:
droidmap init # once: the GitHub workflow and the MCP config for AI agents
droidmap check # run the rules; exit code 1 on errors
droidmap impact TopicEntity # what depends on it, transitively
droidmap diff # what your uncommitted changes touch
droidmap diff --base main # what your branch touches
droidmap report -o REPORT.md # an overview, with module and screen-flow diagrams
在拉取请求中(GitHub Action)
droidmap init 会将此工作流写入 .github/workflows/droidmap.yml:
name: droidmap
on: [push, pull_request]
jobs:
droidmap:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # findings as code scanning alerts
pull-requests: write # the comment that says what the pull request touches
steps:
- uses: actions/checkout@v7
- uses: hussainshirazi/droidmap@v0
每次拉取请求中,Action 都会评论本次更改涉及的内容,并在每次推送时更新这条评论:
droidmap:本次更改涉及的内容
与
origin/main相比:更改了 1 个文件,其中 1 个由 droidmap 读取。已更改(1):
ItemEntityroom_entity受影响(依赖于已更改代码的内容):
屏幕 HomeScreenViewModel HomeViewModelRoom AppDatabase、ItemDao依赖注入 DataModule.bindItems、OfflineItemRepository模块 :app、:core:data、:core:database、:feature:detail-screen、:feature:home
当更改移除某个声明(目标位置、绑定或 DAO 方法)时,评论会在 已移除 下列出该声明,并在 受影响 下列出仍依赖它的内容。
检查结果会显示为代码扫描警报、已更改行上的注释以及作业摘要;遇到错误时,此步骤会失败。Action 无需设置 Java:它使用 GitHub 运行器已有的 JDK。若要同时检查库添加到合并后清单中的权限,请设置 merge-manifest: true:它会先运行 ./gradlew processReleaseMainManifest,因此该作业需要你构建所用的 JDK(actions/setup-java)。
| 输入 | 默认值 | |
|---|---|---|
path | . | Android 项目的文件夹(当它不是仓库根目录时) |
fail-on | error | 使用 warning 或 info 可提高严格程度;使用 never 仅报告问题 |
comment | true | 使用 false 可避免在拉取请求评论中显示影响摘要(摘要仍会保留在作业摘要中) |
upload-sarif | true | 对于没有 GitHub Advanced Security 的私有仓库,设为 false(公开仓库可免费使用代码扫描) |
args | 传递给 droidmap check 的额外参数,例如 --config tools/droidmap.toml | |
base | auto | 用于比较的 git ref:auto 表示拉取请求的目标分支,none 表示关闭此功能;其他 ref 必须先获取(例如使用 fetch-depth: 0) |
merge-manifest | off | true 会先运行 processReleaseMainManifest,也会检查库添加的权限;有产品风味时,使用对应变体的任务,例如 processProdReleaseMainManifest。此操作会运行你的构建,因此绝不要在 pull_request_target 工作流中使用 |
version | 固定的发布版本 | droidmap 发布版本,例如 0.2.0 |
仅运行更改可能影响的测试
droidmap diff --format tasks 会列出更改可能影响的每个模块的测试任务:包含有已更改文件的模块,以及所有依赖这些模块的模块(如果某个模块仅在测试中使用它们,则只会运行该模块的测试)。更改构建设置(settings、根构建文件、gradle/、build-logic)时,会列出所有模块。它仅读取 Gradle 文件,因此执行很快,也不会运行 Gradle:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # the base branch, to compare with
- run: curl -fsSL https://raw.githubusercontent.com/hussainshirazi/droidmap/main/install.sh | sh
- run: |
tasks=$(~/.local/bin/droidmap diff --base origin/${{ github.base_ref }} --format tasks --task testDebugUnitTest)
if [ -n "$tasks" ]; then ./gradlew $tasks; else echo "No module affected"; fi
--format modules 则会改为逐行输出模块路径。
--format tests 会进一步筛选:仅列出调用链能够到达已更改函数的测试类,并将它们作为 Gradle 参数输出( :core:data:testDebugUnitTest --tests com.example.OfflineRepositoryTest ...),因此 ./gradlew $(droidmap diff --base origin/main --format tests --task testDebugUnitTest) 只会运行这些测试。调用链会根据代码声明的内容进行追踪(属性或参数的类型、构造函数、函数的返回类型);接口上的调用会关联到所有实现,因此它倾向于运行更多测试。文本报告和 impact 会在“覆盖此项的测试”下列出相同的测试。
使用 AI Agent(MCP)
droidmap mcp 是一个通过 stdio 通信的 Model Context Protocol 服务。文件发生变化时,它会重建图(仅解析发生变化的文件),因此 Agent 编辑代码时,它的回答也会与代码保持同步。
Claude Code 插件(包含服务和一个技能,告诉 Claude 何时使用它:更改实体、路由或绑定前先分析影响,并在之后运行检查和测试)。除 Java 外,无需安装任何内容;它会找到 Android Studio 自带的 Java。首次使用时,它会下载与插件匹配的 droidmap 版本,并校验其 SHA-256。
/plugin install droidmap --marketplace hussainshirazi/droidmap
在 2.1.275 之前的 Claude Code 版本中,请先添加市场:/plugin marketplace add hussainshirazi/droidmap,然后运行 /plugin install droidmap@droidmap。该技能是一个普通的 SKILL.md 文件;其他能够读取技能的 Agent 也可以使用同一个文件。
任何 MCP 客户端。 droidmap init 会写入 .mcp.json(Claude Code),并在相应文件夹存在时写入 .cursor/mcp.json 或 .vscode/mcp.json;将这些文件提交后,团队中的每个人都会获得该服务。它绝不会修改已有文件:而是会打印需要添加的内容。手动配置:
claude mcp add droidmap -- droidmap mcp .
{ "mcpServers": { "droidmap": { "command": "droidmap", "args": ["mcp", "."] } } }
除 Java 外无需安装任何内容:"command": "java", "args": ["-jar", "/path/to/droidmap.jar", "mcp", "."]。使用 Docker:"command": "docker", "args": ["run", "--rm", "-i", "-v", "/path/to/project:/project", "ghcr.io/hussainshirazi/droidmap", "mcp"]。droidmap 也已列入 MCP Registry,名称为 io.github.hussainshirazi/droidmap(从 0.2.0 版本起)。
| 工具 | 用途 |
|---|---|
droidmap_impact | 更改实体、路由、ViewModel、绑定或模块前,查看依赖于它的内容 |
droidmap_diff | 查看未提交的更改(或某个分支)涉及哪些内容,以及哪些测试覆盖了这些内容 |
droidmap_check | 更改后运行规则检查 |
droidmap_show | 查看单个节点:其属性、依赖项和使用方 |
droidmap_find | 按名称搜索节点 |
droidmap_report | 获取项目的 Markdown 概览 |
droidmap_explain | 了解规则的重要性,以及如何修复发现的问题 |
droidmap 能理解什么
| 领域 | 可读取的内容 |
|---|---|
| Gradle | 来自 settings.gradle(.kts) 的模块;Kotlin DSL 和 Groovy 中的 project(":x") 和类型安全的 projects.x.y 依赖;版本目录中的插件;包含式构建(build-logic)中的约定插件为每个应用这些插件的模块添加的项目依赖;以及 Kotlin Multiplatform 源集(commonTest 中的依赖属于测试依赖)。 |
| Room | 数据库、实体、DAO(每个 @Query 使用的表,以及每个 @Insert、@Update、@Delete 或 @Upsert 写入的实体,包括继承的内容)、手动和自动迁移、导出的架构 JSON、每个实体拥有的列与该 JSON 中的列的对比,以及哪个 MigrationTestHelper 测试覆盖哪个迁移步骤。 |
| Navigation | 使用字符串路由(由常量、模板和辅助函数构建)或类型安全路由的 Compose Navigation、Navigation 3(entry<Key>,以及在对键执行的 when 中使用的 NavEntry(key))、XML 导航图(跨文件,包含深层链接、菜单项和只会弹回的操作),以及 Kotlin 和 Java 中的 navigate(...) 调用:使用路由、R.id 操作、Safe Args 方向(HomeFragmentDirections.actionHomeToDetail(id)),以及通过你自己的辅助函数(例如 navigateToTab(route)),最多深入五层。 |
| Hilt | @HiltViewModel、@Inject 构造函数和字段、带限定符的 @Provides 和 @Binds、入口点,以及哪些屏幕、Activity 和 Fragment 使用了哪些 ViewModel。 |
| Koin | 模块和定义(single、factory、viewModel、singleOf(::X)、bind)、Koin Annotations(支持 JSR-330;带注解的类仅在模块的 @ComponentScan 覆盖的包中才会被计入),以及每个定义通过 get() 获取的内容;koinViewModel、koinInject 和 by inject() 的用法。 |
| Manifest | 源清单中的权限(包括 tools:node="remove")和组件;如果构建已生成合并清单,还会读取其中的权限及添加每项权限的库。 |
Kotlin 和 Java 使用 Kotlin 编译器自己的解析器(detekt 和 ktlint 使用的解析器)进行读取,并通过多个线程处理。
命令
droidmap init [path] # write the GitHub workflow and MCP config (never overwrites)
droidmap check [path] [--format text|json|sarif] [--fail-on error|warning|info|never] [--sarif FILE] [--base REF]
droidmap impact <node> [path] [--depth N] [--json] # what depends on a node
droidmap diff [path] [--base REF] [--depth N] [--format text|markdown|json|modules|tasks|tests] [--task NAME] # what a change touches
droidmap show <node> [path] [--json] # a node, its dependencies and its users
droidmap find <text> [path] [--limit N] [--json] # search nodes
droidmap report [path] [-o FILE] # the Markdown report, with Mermaid diagrams
droidmap build [path] [-o DIR] [--node-link] # write droidmap-out/graph.json and REPORT.md
droidmap rules # list the rules and their default severities
droidmap explain <rule> # why a rule matters and how to fix what it finds
droidmap mcp [path] # run the MCP server over stdio
如果未提供 [path],droidmap 会处理当前文件夹周围的 Gradle 项目(当前文件夹及其上级目录中最近的 settings.gradle(.kts))。终端中的输出会带颜色;设置 NO_COLOR=1 可关闭颜色,设置 FORCE_COLOR=1 可开启颜色。
<node> 可以是 ID、名称或名称的一部分:TopicEntity、:core:data、"editor?id={id}"、HomeViewModel。check、impact、show、find 和 report 也接受 --config FILE 和 --graph FILE(使用已保存的 graph.json,而不进行扫描);build、diff 和 mcp 接受 --config FILE。
diff 会将工作树(无论是否已提交,包括新文件)与分支从 --base(默认为 HEAD:未提交的更改)分出时的状态进行比较。更改的行会计入其所在的最内层声明,因此编辑一个函数不会将其整个类都标记为有更改。
check --sarif FILE 还会将检查结果写入 SARIF。check --base REF 还会将 Room 架构文件与该提交进行比较;最后一个发布标签是最精确的选择,因为只有已发布的架构必须保持不变。如果使用 build,请将 droidmap-out/ 添加到 .gitignore。
退出代码:完成时为 0;check 发现达到或高于 --fail-on 设定级别的问题(或 find 未匹配到任何内容)时为 1;用法或配置错误时为 2;内部错误时为 3。
规则
| 规则 | 默认级别 | 检查内容 |
|---|---|---|
room-migration-path | error | 某个已知架构版本没有迁移到当前版本的路径 |
room-schema-missing | warning | 当前版本或更早版本没有导出的架构 JSON |
room-schema-stale | warning | 实体与当前版本的架构 JSON 不再匹配:实体变更但未提升版本,更新时会崩溃 |
room-schema-changed | warning | 使用 --base REF 时,REF 中已存在版本的架构 JSON 发生了变化;该版本发布后也会导致同样的崩溃 |
room-migration-untested | warning | 迁移步骤没有 MigrationTestHelper 测试运行 |
room-migration-unused | warning | 应用代码从未使用的手动 Migration(未传给 addMigrations?),即使有测试运行它 |
room-destructive-fallback | warning | fallbackToDestructiveMigration(),它会删除用户数据 |
room-insert-replace | off | @Insert(onConflict = REPLACE),它会删除并重新插入行 |
nav-unknown-target | error | navigate() 跳转到没有任何目标注册的路由或键 |
nav-unreachable | warning | 没有任何导航、启动位置或引用指向的目标,且没有深层链接或菜单项可以打开它 |
nav-deeplink-not-in-manifest | warning | 某个深层链接(navDeepLink、<deepLink>)的 scheme 和 host 没有任何 activity 的 intent filter 接受,因此该链接无法打开应用 |
hilt-missing-binding | warning | 被注入的项目类型没有 @Inject 构造函数、@Provides 或 @Binds |
hilt-duplicate-binding | error | 同一 source set 中,同一类型和限定符被绑定了两次 |
hilt-app-not-registered | error | 存在 @AndroidEntryPoint 类,但清单中的 <application android:name> 不是 @HiltAndroidApp 类(或不存在该类):这些类打开时会崩溃 |
hilt-fragment-host | error | @AndroidEntryPoint fragment 由非 @AndroidEntryPoint activity 承载(通过其导航图、布局或 fragment 事务):fragment 附加时会崩溃 |
workmanager-setup | warning | 某个 @HiltWorker 没有获得 HiltWorkerFactory,或者 WorkManager 从未请求某个 Configuration.Provider,因为清单中仍有默认初始化器 |
koin-missing-definition | warning | Koin 被要求提供的类型(get()、inject()、koinInject())没有对应的 Koin 定义:运行时会抛出 NoDefinitionFoundException |
gradle-unknown-project | error | 某个项目依赖了 settings 未包含的模块 |
gradle-forbidden-dependency | error | [[gradle.forbid]] 规则禁止的依赖 |
gradle-cycle | error | 彼此形成循环依赖的模块 |
manifest-permission | error | 不在 allowed_permissions 中的权限,无论它位于源清单中还是由库添加到合并后的清单中 |
gson-model-not-kept | warning | Gson 通过反射读取的类(fromJson、TypeToken、使用 GsonConverterFactory 的 Retrofit API),其字段在压缩发布构建中被 R8 重命名:没有 @SerializedName、@Keep 或 keep 规则。调试构建正常,发布构建读取到的字段为空 |
配置
在项目根目录放置 .droidmap.toml(所有内容均为可选项):
[rules]
"room-migration-untested" = "error" # error | warning | info | off
"room-insert-replace" = "error"
[manifest]
allowed_permissions = ["android.permission.INTERNET", "android.permission.POST_NOTIFICATIONS"]
[[gradle.forbid]]
from = ":feature:*"
to = ":feature:*"
reason = "features talk to each other through :core modules"
[ignore]
paths = ["benchmarks/**"]
未知的规则名称或严重级别会导致错误,因此拼写错误不会让规则悄然保持不变。from、to 和 ignore.paths 是 glob 模式,其中 * 也会匹配 : 和 /。禁止规则会跳过测试依赖,除非设置了 tests = true;allow_same_parent = true 则允许同级模块相互依赖,例如 :feature:a:impl 和 :feature:a:api。使用 allowed_permissions 时,如果某个库向合并后的清单添加权限,报告中会标明该库的名称,你可以移除该权限(tools:node="remove")或允许它。
图
droidmap build 会写入 droidmap-out/graph.json(架构为 droidmap/1),其中包含三个列表:
nodes:id、kind、label、file、line、module 和 attrs;edges:source、target、kind、file、line 和 attrs;unresolved:静态分析无法追踪的内容及其原因。
边始终表示源依赖于目标,因此影响分析会反向遍历。路径相对于项目根目录,输出具有确定性,因此可以清晰地比较差异。--node-link 还会写入 networkx 的 node-link 格式,graphify 等工具可以读取这种格式。
节点类型:module、room_database、room_entity、room_dao、room_migration、room_schema、test、nav_destination、nav_route、screen、function、viewmodel、injectable、hilt_module、hilt_provider、koin_module、koin_definition、entry_point、component、permission、class。
边类型:depends_on、contains_entity、contains_dao、queries、writes、has_schema、migrates、tests、registers、shows、navigates_to、starts_at、uses_viewmodel、injects、bound_by、requires、declared_in、uses_permission、declares。
限制
droidmap 读取源代码,不会运行 Gradle。这让它只需几秒就能完成分析,也能处理从未构建过的检出版本,并且永远不会运行项目的构建脚本;但这也意味着它无法获取只有构建过程才能生成的内容。此外,它有意采取保守策略:遇到无法解析的内容(存储在变量中的路由、非常量的版本、无法判断类型的 Koin 定义)时,会将其记录在 unresolved 中,而不是猜测,规则也不会对此触发。依赖猜测的规则会报告并不存在的问题。
- 不读取构建生成的代码(KSP、kapt)。Safe Args 不需要这类代码:
HomeFragmentDirections.actionHomeToDetail()会根据 Safe Args 为其生成的代码所用的名称,与对应操作匹配。 - 库添加的权限来自已解析的依赖,因此只有构建生成了合并清单时才会读取(
<module>/build/intermediates/merged_manifests/,或 Action 的merge-manifest输入)。如果合并清单早于构建文件,会对此作出标记。 - 名称按照 Kotlin 和 Java 的规则解析(导入、包、封闭类及其超类),但不进行类型推断,因为类型推断需要编译器和所有依赖。对变量的调用(例如
repo.load())会在变量类型已声明,或来自构造函数或函数的已声明返回类型时进行追踪;如果需要推断类型,则不会追踪(例如 lambda 中的it、泛型的类型实参、连续调用库函数)。 - 不会追踪运行时构造的值:从
var读取的路由,或在循环中构造的路由。常量、字符串模板、返回这些值的函数,以及最多五层深的辅助函数都会追踪。
贡献
运行 ./gradlew test fatJar 可执行测试并构建 build/libs/droidmap.jar。CONTRIBUTING.md 介绍了代码所在位置、新规则或修复的要求,以及 CI 中针对真实项目的检查。如果发现错误或遗漏,请使用 False positive or missed problem 问题表单提交;安全问题请按照 SECURITY.md 的说明私下报告。
许可证
Apache-2.0。请参阅 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。