p2_fix_dubbo_collectionutils.py 9.82 KB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""
一次性纠偏:把 `p2_strip_dubbo.py` 第一版换错的 CollectionUtils 改回来。

## 错在哪

第一版把 `com.alibaba.dubbo.common.utils.CollectionUtils` 映射成了
`org.springframework.util.CollectionUtils`。Dubbo 那份是从 commons-collections
抄的,`isEmpty` 和 `isNotEmpty` 都有;Spring 那份**只有 `isEmpty`**——
从 3.1 到 7.0 的每个版本都没有 `isNotEmpty`(用 javap 逐个核对过 ~/.m2 里的
38 个 spring-core,全为 0)。存量代码里 `isNotEmpty` 有 938 处调用,
于是 legacy-common 一编译就报 8 个文件的「找不到符号 isNotEmpty」。

`p2_strip_dubbo.py` 里的映射已改为
`org.apache.commons.collections4.CollectionUtils`(与 Dubbo 那份同源)。
但那个脚本是「找不到就不改」的幂等设计,已经换过的文件它不会再动,
所以需要本脚本单独纠偏一次。

## 为什么不直接全局替换

不能全仓替换 `org.springframework.util.CollectionUtils`:存量代码里本来就有
若干文件是**真的**在用 Spring 那份(只用 `isEmpty`),那些是对的、不该动。
所以这里**以老仓库为准**枚举:凡是老文件 import 了 dubbo 那份的,
就把它在**新工程里的对应文件**改过来——老文件是唯一权威的事实来源。

## 映射与路径

与 `p2_import_legacy.py` 的 SOURCES 一致:老模块 → 新模块,包内相对路径不变。

用法:python3 -I tools/p2_fix_dubbo_collectionutils.py [--dry-run]
"""

import argparse
import os
import re

CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LEGACY = os.path.join(os.path.dirname(CLOUD), "zteits-clouds")

DUBBO_IMPORT = "import com.alibaba.dubbo.common.utils.CollectionUtils;"
WRONG_IMPORT = "import org.springframework.util.CollectionUtils;"
RIGHT_IMPORT = "import org.apache.commons.collections4.CollectionUtils;"

# 第二种写法:存量代码里有一批是**全限定**调用 Spring 那份的 isNotEmpty
# (写 FQN 是为了跟同文件里另一个 CollectionUtils 区分开)。只换 import 治不了它。
FQN_WRONG = "org.springframework.util.CollectionUtils.isNotEmpty"
FQN_RIGHT = "org.apache.commons.collections4.CollectionUtils.isNotEmpty"

# 兜底扫描时跳过的目录:构建产物和 tools/recover 下的恢复副本
SKIP_DIR_NAMES = {"target", "out"}

# 「裸的」CollectionUtils.isNotEmpty( 调用:前面既不是 `.`(排除全限定写法)也不是标识符字符
BARE_ISNOTEMPTY = re.compile(r"(?<![.\w])CollectionUtils\.isNotEmpty\(")

# 与 p2_import_legacy.py 的 SOURCES 保持一致
MODULE_MAP = {
    "zteits-api": "parking-module-api",
    "zteits-framework": "parking-framework/parking-legacy-common",
    "zteits-park": "parking-module-park",
    "zteits-order": "parking-module-order",
    "zteits-pay": "parking-module-pay",
    "zteits-bill": "parking-module-bill",
    "zteits-charge": "parking-module-charge",
    "zteits-business": "parking-module-business",
    "zteits-sys": "parking-module-system",
}

SRC = "src/main/java"


def heal_spring_collectionutils(root, dry):
    """兜底:修掉「解析到 Spring 版 CollectionUtils 却调用 isNotEmpty」的文件。

    这种文件在**老仓库里就已经编译不过**——Spring 那份从来没有 isNotEmpty。
    实测 13 个(park 6 个、order 5 个、pay 2 个),是老仓库存量代码自身的构建问题,
    与本脚本要修的错误无关,但同样挡着编译,且修法完全一样(isNotEmpty 语义一致)。

    两种写法都要治:
      1. `org.springframework.util.CollectionUtils.isNotEmpty(...)` —— 全限定,直接改指向
      2. `import org.springframework.util.CollectionUtils;` + 裸的 `CollectionUtils.isNotEmpty(...)`

    ## ⚠️ 第 2 种**不能翻 import**,只能改调用点

    早先的实现是把裸调用那批文件的 `import` 从 Spring 换成 commons-collections4。这是错的,
    实测已被编译打脸(park 的 ParkingLotQueryServiceImpl 因此冒出 3 条
    「不兼容的类型: Map<...>无法转换为 Collection<?>」):

      * Spring 的 `CollectionUtils.isEmpty` 有 **Map 重载**;
      * commons-collections4 的 **没有**(它的 Map 版在 `MapUtils` 里)。

    于是「同一个文件里既用 Spring 的 isEmpty(Map)、又用 isNotEmpty(List)」这种混用,
    一翻 import 就把本来好好的 Map 判空打成编译错误——而 Map 调用远多于 isNotEmpty 调用
    (那个文件是 29 : 1),翻 import 是「为一个病人把 29 个健康人一起动了手术」。

    正确做法是**保留 Spring 的 import,只把这一个调用点改成 commons-collections4 的全限定名**。
    这正是老代码作者当年手工用的手法:老文件里那处调用的原文就是
    `com.alibaba.dubbo.common.utils.CollectionUtils.isNotEmpty(...)` —— 写成全限定,
    就是为了跟文件顶上那个 Spring import 区分开。
    """
    call_fixed, fqn_fixed = [], []
    for r, dirs, files in os.walk(root):
        dirs[:] = [d for d in dirs if d not in SKIP_DIR_NAMES]
        for f in files:
            if not f.endswith(".java"):
                continue
            p = os.path.join(r, f)
            with open(p, encoding="utf-8", errors="surrogateescape") as fh:
                src = fh.read()
            orig = src
            if FQN_WRONG in src:
                src = src.replace(FQN_WRONG, FQN_RIGHT)
                fqn_fixed.append(p)
            # 只处理「文件顶上是 Spring 那条 import」的:这才是真正解析错的场景。
            # 已经 import commons-collections4 的文件(老代码本来就用的那一批)不动——
            # 它们的裸调用本来就是对的,改成全限定名只会徒增几百处无意义 diff。
            if WRONG_IMPORT in src:
                # 只命中「裸的」调用:前面不能是 `.`(排除已是全限定的一类,如
                # org.apache.commons.collections4.CollectionUtils.isNotEmpty)也不能是标识符字符
                src, n = BARE_ISNOTEMPTY.subn(FQN_RIGHT + "(", src)
                if n:
                    call_fixed.append(p)
            if src != orig and not dry:
                with open(p, "w", encoding="utf-8", errors="surrogateescape") as fh:
                    fh.write(src)
    return fqn_fixed, call_fixed


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()

    fixed, already, missing_new, missing_import, stale = [], [], [], [], []

    for legacy_mod, new_mod in MODULE_MAP.items():
        base = os.path.join(LEGACY, legacy_mod, SRC)
        if not os.path.isdir(base):
            continue
        for root, _, files in os.walk(base):
            for f in files:
                if not f.endswith(".java"):
                    continue
                old_path = os.path.join(root, f)
                with open(old_path, encoding="utf-8", errors="surrogateescape") as fh:
                    if DUBBO_IMPORT not in fh.read():
                        continue
                rel = os.path.relpath(old_path, base)
                new_path = os.path.join(CLOUD, new_mod, SRC, rel)
                if not os.path.isfile(new_path):
                    missing_new.append((legacy_mod, rel))
                    continue
                with open(new_path, encoding="utf-8", errors="surrogateescape") as fh:
                    src = fh.read()
                if RIGHT_IMPORT in src:
                    already.append(rel)
                    continue
                if WRONG_IMPORT not in src:
                    # 既不是 dubbo 的、也不是 Spring 的:可能已被别的规则改过,人工看
                    missing_import.append((new_mod, rel))
                    continue
                if "isNotEmpty" not in src:
                    # 只用了 isEmpty:换成 commons-collections4 也对,
                    # 但留着 Spring 那份同样能跑。为消除"同一份文件两种来源"的困惑,
                    # 仍统一换掉,保持与 p2_strip_dubbo.py 的映射一致。
                    stale.append(rel)
                new_src = src.replace(WRONG_IMPORT, RIGHT_IMPORT, 1)
                if not args.dry_run:
                    with open(new_path, "w", encoding="utf-8", errors="surrogateescape") as fh:
                        fh.write(new_src)
                fixed.append(rel)

    print(f"✔ 纠偏 {len(fixed)} 个文件 -> {RIGHT_IMPORT}")
    if stale:
        print(f"  其中 {len(stale)} 个只用了 isEmpty(换成 commons-collections4 同样正确)")
    print(f"  已是目标 import(重复执行):{len(already)}")
    if missing_new:
        print(f"!! 新工程里找不到对应文件 {len(missing_new)} 个:")
        for m, r in missing_new[:20]:
            print(f"     {m}: {r}")
    if missing_import:
        print(f"!! import 既非 dubbo 也非 Spring,需人工确认 {len(missing_import)} 个:")
        for m, r in missing_import[:20]:
            print(f"     {m}: {r}")
    if args.dry_run:
        print("(--dry-run,未落盘)")

    # ---- 兜底:老仓库自身就编译不过的那批(解析到 Spring 版却调 isNotEmpty)----
    fqn_fixed, call_fixed = heal_spring_collectionutils(CLOUD, args.dry_run)
    if fqn_fixed or call_fixed:
        print("\n补救「Spring 版 CollectionUtils + isNotEmpty」(老仓库里本就编译不过):")
    if fqn_fixed:
        print(f"  老的全限定 Spring 调用改指向 commons-collections4:{len(fqn_fixed)} 个文件")
        for p in fqn_fixed:
            print(f"     {os.path.relpath(p, CLOUD)}")
    if call_fixed:
        print(f"  裸调用改写成 commons-collections4 全限定名(保留 Spring import):{len(call_fixed)} 个文件")
        for p in call_fixed:
            print(f"     {os.path.relpath(p, CLOUD)}")


if __name__ == "__main__":
    main()