代码搜索、导航与结构化重构
一次看似简单的 API 重命名,常常会在发布后变成事故:开发者用全文搜索替换了直接调用,却漏掉别名导入、反射字符串、生成代码、默认分支之外的兼容分支,以及另一个仓库里的消费者。搜索结果很多不等于搜索完整,替换成功也不等于语义正确。真正困难的是证明“查了哪些语料、按什么身份匹配、改写影响什么语义、哪些证据允许发布”。
代码搜索与结构化重构应当是一条控制链,而不是若干互不相干的快捷键。它从仓库、分支、子模块、生成物和忽略规则的盘点开始,经过文本候选、文件收窄、符号身份、语法结构、类型归因与跨仓库索引,最后进入受控改写、差异审查、编译测试、分批发布和回滚。工具负责扩大人的观察与执行能力,架构师负责界定权威语料、可信证据和失败边界。
先判断搜索结论能承诺到哪一步
全文搜索回答“哪些字节或正则命中了”,不能直接证明“某个符号的全部引用都已找到”。LSP 引用查询理解项目模型和符号身份,却可能受未加载模块、条件编译、动态调用和语言服务器索引状态影响。AST 工具能够按语法形状匹配,但通常不理解完整类型关系;带类型归因的重写引擎更接近编译器语义,却需要正确的构建模型、依赖解析和更高运行成本。
因此,搜索结论应当带上能力等级:候选发现、语法匹配、符号引用、类型证明或运行事实。架构评审不能把低等级证据包装成高等级承诺。一次零结果至少可能表示五件事:目标确实不存在、语料未进入索引、查询被忽略规则过滤、当前身份无权读取、解析器没有理解目标语言。只有把这些分支逐一排除,零结果才是证据。
| 问题 | 首选能力 | 能证明什么 | 仍需补什么 |
|---|---|---|---|
| 精确文本出现在哪里 | rg | 当前可见语料中的文本命中 | 忽略项、编码、生成物和其他分支 |
| 哪些文件值得继续查 | fd + fzf | 文件候选和人工收窄 | 文件内容与符号身份 |
| 某个方法被谁调用 | LSP | 已加载项目模型中的符号引用 | 动态调用、反射与跨仓库消费者 |
| 离线大树如何快速跳转 | Ctags / GNU Global | 标签库或交叉引用库中的定义与引用 | 索引新鲜度与解析器边界 |
| 某种语法形状在哪里 | ast-grep / Comby | 结构或模板匹配 | 类型兼容与业务语义 |
| 类型 API 如何批量迁移 | OpenRewrite / JS、TS Codemod | 规则表达范围内的结构化改写 | 构建、测试与运行回归 |
| 多仓库影响面是什么 | GitHub / GitLab / Sourcegraph / OpenGrok | 已授权、已索引仓库中的命中 | 非默认分支、未同步权限与未索引宿主 |
| 如何把多仓库改动送达 | Batch Changes + 治理流程 | 变更批次、执行结果和 PR 状态 | 仓库 owner、发布顺序和回滚演练 |
从搜索面清单建立可信起点
搜索语料与覆盖面盘点先记录组织、代码宿主、仓库、默认分支、维护分支、子模块、稀疏检出、LFS、生成代码、Vendored 依赖、隐藏文件、二进制文件、编码与大文件策略。清单还要保留当前身份、索引时间、工作树提交和查询参数,使另一位工程师能够复现同一观察。
本地仓库先用 ripgrep 建立文本证据,再用 fd 与 fzf 缩小文件集合。rg 默认尊重忽略规则并跳过隐藏、二进制内容;fd 搜的是文件路径而不是正文;fzf 消费候选并执行预览命令。把三者的职责混在一起,会出现“预览看到了内容,所以候选一定完整”的错误推断。
搜索脚本应当保存查询、根目录、提交、工具版本、退出码和结果摘要。rg 的退出码 0 表示有命中,1 表示无命中,其他值表示错误;在 CI 中把 1 当作命令崩溃或把 2 当作零结果,都会制造错误结论。需要纳入被忽略或隐藏内容时,应明确增加参数并记录原因,不能把 -uuu 永久写进所有脚本,让缓存、凭证和大二进制文件无边界进入结果。
在文本、符号和结构之间逐级升级
当任务从“寻找字符串”变为“理解定义与引用”,进入 LSP 的符号、引用与调用层级。语言服务器依赖工作区根、构建配置、SDK、依赖和索引状态;导航失败首先检查项目模型,不应立即回退到全局替换。离线环境、超大代码树或编辑器无关场景,可以用 Universal Ctags 与 GNU Global 建立标签和交叉引用数据库,但必须定义重建时机、忽略清单和并发写保护。
结构化匹配处理的是“形状”。ast-grep 基于 Tree-sitter 查询语法树,适合可表达为节点关系的检查与修复;Comby 用语言感知模板和 hole 在多种代码中做结构替换,部署简单但不是类型系统。IntelliJ Structural Search and Replace 依赖 IDE 的 PSI、变量约束和搜索作用域,适合交互式发现与小批改写;正式重构仍要使用能更新引用的 IDE 重构动作或专用引擎。
跨类型和跨文件迁移需要更强模型。OpenRewrite 用带类型信息的 Lossless Semantic Tree 和 recipe 表达 Java 生态迁移;JavaScript 与 TypeScript Codemod 组合 jscodeshift、编译器 AST 或 ts-morph,处理导入、调用和类型声明。它们都不是“运行一次即可提交”的魔法:必须先 dry run,限定目录,保存变更统计,检查幂等性,再用格式化、类型检查、单元测试和构建验证输出。
把跨仓库搜索视为受权限约束的索引系统
GitHub Code Search 和 GitLab Code Search 适合各自宿主内的快速发现,但索引范围、默认分支、结果上限、文件大小、编码、生成代码和产品层级都会影响结论。平台搜索返回的是当前身份可见且被索引的结果,不应被描述为整个企业代码资产的完整扫描。
Sourcegraph Code Search面向多宿主和统一查询,权限同步、仓库同步、索引新鲜度与查询成本必须独立监控;认证并不自动等于仓库级授权,配置错误可能暴露本不应可见的代码。OpenGrok适合自托管源码浏览与交叉引用,代价是团队承担抓取、索引、增量更新、容量和访问控制。选择时不只比较查询语法,还要比较数据边界、权限模型、更新延迟、资源预算、审计和退役成本。
跨仓库写入比搜索多一个危险等级。Sourcegraph Batch Changes可以生成并跟踪多仓库变更集,但执行器、凭证、分支写权限、PR 创建、重试和撤销都要单独设计。大规模重构治理进一步规定 owner、批次、兼容窗口、合并顺序、发布观察、冻结条件和回滚责任,避免把“批量创建 PR”误当成迁移完成。
用正反实验校准工具边界
可复制的最小实验应当故意包含同名文本、别名导入、被忽略目录、生成文件、动态字符串和一个无法解析的模块。正向实验要求目标样本都被预期工具找到;反向实验故意使用错误工作区根、过期索引或受限身份,并观察缺失结果、诊断日志和退出码。实验目录使用虚构代码和临时凭证,完成后删除索引库、缓存、临时分支和访问令牌。
$lab = Join-Path $env:TEMP "code-search-lab"
New-Item -ItemType Directory -Force "$lab/src", "$lab/generated" | Out-Null
Set-Content "$lab/src/order.ts" 'export const submitOrder = () => "ok"'
Set-Content "$lab/src/use.ts" 'import { submitOrder as send } from "./order"; send()'
Set-Content "$lab/generated/client.ts" 'export const submitOrder = () => "generated"'
Set-Content "$lab/.gitignore" "generated/"
rg --json --glob '*.ts' 'submitOrder|send' $lab | Set-Content "$lab/result.jsonl"
$code = $LASTEXITCODE
"rg_exit=$code"
rg --hidden --no-ignore --glob '*.ts' 'submitOrder' $lab
Remove-Item -LiteralPath $lab -Recurse -Force第一条查询应命中 src 中的定义、导入和别名调用,但不会进入被忽略的 generated;第二条查询明确放宽边界后才看到生成文件。这能证明忽略策略会改变结果,不能证明 send() 在类型系统中一定指向 submitOrder。后者需要 LSP 或类型感知工具补充证据。
让改写具备可停止、可审查和可回退能力
结构化重构先产生候选,再产生补丁,最后才允许写入主分支。每个批次应固定基线提交、查询或 recipe 版本、目标仓库集合、最大文件数、最大 diff、owner 与验证命令。先在代表性仓库 dry run,审查误报和漏报;再选择低风险批次写入独立分支;最后通过编译、测试、契约检查和运行观察决定是否扩大范围。
回滚不能只写“执行 git revert”。数据库、消息协议、配置、缓存键和客户端兼容可能让代码回退失效。大规模改名宜采用 expand-and-contract:先新增兼容入口并观测消费者,再迁移调用,最后删除旧入口。任何批次一旦出现错误率、延迟、构建失败率或人工修正率超过阈值,应停止生成新变更,保留证据并回到上一兼容状态。
change_batch:
baseline: "<commit-sha>"
query_or_recipe: "rename-submit-order/v3"
repositories: ["service-a", "service-b"]
max_changed_files: 40
required_evidence:
- format
- compile
- unit-test
- contract-test
stop_when:
failed_repositories: 1
manual_fix_ratio: "> 10%"
runtime_error_rate: "> baseline + 0.2%"
rollback_owner: "platform-migration-owner"权限、敏感数据、容量与长期治理
本地搜索可能读到 .env、密钥、个人数据和客户样本;集中索引会把风险扩大到组织级。采集身份只读、按仓库授权、索引传输与存储加密、查询审计、日志脱敏和数据保留期限必须形成闭环。搜索结果、预览命令和 Batch 执行日志不得回显 Token;Fork、归档仓库和离职人员权限也要进入定期复核。
容量规划应观察仓库数、代码量、语言分布、增量频率、索引延迟、查询并发、昂贵查询比例和缓存命中。自托管方案还要给抓取、索引和查询分配隔离资源,防止一次全局正则拖垮交互搜索。SaaS 方案则要核对数据驻留、席位、索引额度、API 限流和退出时的数据清除能力,价格和产品能力以采购时的官方条款为准。
团队长期保留三类资产:搜索面清单、经过评审的查询或 recipe、变更批次证据。平台团队维护索引、执行器和通用规则;语言专家维护解析与迁移 recipe;仓库 owner 对业务语义、测试和发布负责;安全团队审查访问边界和敏感数据。职责分开后,搜索工具才能从个人技巧升级为可复用的工程能力。
上线前的工程核对
语料清单能够解释仓库、分支、子模块、生成物、忽略项和索引时间。查询结果标明能力等级,没有把文本命中冒充符号或类型证明。正向样本、反向样本、零结果和错误退出码都有可复现证据。
改写先 dry run,再限制批次,并通过格式化、编译、测试和 diff 审查。跨仓库索引按真实仓库权限过滤,权限同步延迟可观测。凭证不进入命令历史、查询日志、补丁、PR 描述和构建输出。
批次具有停止阈值、兼容窗口、发布顺序、运行观察和回滚 owner。工具退役时能撤销 Token、删除执行器、清理索引并证明数据已清除。
