p2_fix_poi3.py 7.25 KB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""
P2 纠偏:把存量 POI 3.x 的写法改到 POI 4.x。

## 为什么必须改

`parking-dependencies` 里 `poi-ooxml` 定的是 **4.1.2**(被 EasyExcel 3.3.4 要求,见 BOM 注释)。
但 BOM 注释里那句「3.9→4.1 未破坏」是**错的**:POI 4.0 一次性删掉了 3.x 里所有
「常量式」的旧 API,只保留枚举版。已用 javap 对 4.1.2 的 jar 逐个核对:

| 3.x 写法 | 4.1.2 实况 | 改成 |
|---|---|---|
| `CellStyle.BORDER_THIN` / `BORDER_DASHED` | 已删 | `BorderStyle.THIN` / `DASHED` |
| `Cell.CELL_TYPE_STRING` / `HSSFCell.CELL_TYPE_*` / `XSSFCell.CELL_TYPE_*` | 已删(`getCellType()` 现在返回 `CellType`) | `CellType.STRING` / … |
| `CellStyle.ALIGN_CENTER/LEFT/RIGHT` | 已删 | `HorizontalAlignment.CENTER/LEFT/RIGHT` |
| `CellStyle.VERTICAL_CENTER` | 已删 | `VerticalAlignment.CENTER` |
| `CellStyle.SOLID_FOREGROUND` | 已删 | `FillPatternType.SOLID_FOREGROUND` |
| `HSSFColor.GREY_25_PERCENT` / `ROSE` / `WHITE` | 内嵌类已删,改为枚举 `HSSFColorPredefined` | `HSSFColor.HSSFColorPredefined.XXX.getIndex()` |
| `font.setBoldweight(Font.BOLDWEIGHT_BOLD)` | 方法已删;`Font.BOLDWEIGHT_BOLD` 也没了(`HSSFFont` 那份变成包内可见) | `font.setBold(true)` |

⚠️ 最后一行的语义确认:`setBoldweight(BOLDWEIGHT_BOLD)` 就是「加粗」,等价于
`setBold(true)`;本仓库里 `setBoldweight` 的实参**只有** `BOLDWEIGHT_BOLD`
(`HSSFFont` 4 处 + `Font` 7 处,共 11 处,已全量核对),所以没有丢分支的风险。

⚠️ `setCellType(...)` **本身**在 4.1.2 还在(签名已改成收 `CellType`),所以只改传进去的常量。

## 为什么有两个包

`com/clouds/common/utils/` 下有 **两份**同源代码:`excle/`(6 个文件)与
`poiexcle/`(3 个文件)。第一次编译只报了 `excle/` 的 200 条错——
那是 javac 的默认 `-Xmaxerrs 200` **截断**,`poiexcle/` 的错被挡在后面没打印出来。
所以本脚本两个包一起改,不能只看报错文件清单。

## 幂等

全部规则都是「找不到就不改」,可重复执行。已改过的文件不会二次变形。

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

import argparse
import os
import re
import sys

CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE = os.path.join(
    CLOUD,
    "parking-framework/parking-legacy-common/src/main/java/com/clouds/common/utils",
)
DIRS = ["excle", "poiexcle"]

# 需要补的 import:枚举类型名 -> 全限定名
ENUM_IMPORTS = {
    "BorderStyle": "org.apache.poi.ss.usermodel.BorderStyle",
    "CellType": "org.apache.poi.ss.usermodel.CellType",
    "FillPatternType": "org.apache.poi.ss.usermodel.FillPatternType",
    "HorizontalAlignment": "org.apache.poi.ss.usermodel.HorizontalAlignment",
    "VerticalAlignment": "org.apache.poi.ss.usermodel.VerticalAlignment",
}

# ---- 规则:(说明, 正则, 替换函数或字符串) ----
# 顺序有意义:CELL_TYPE / BORDER / ALIGN 的限定符可能是「类名」也可能是「实例变量名」
# (如 `hssfCell.CELL_TYPE_BOOLEAN`),所以 CELL_TYPE 用 \w+ 兜住两种情况,
# 其余三个按实测的类名枚举限定(避免误伤同名的业务常量)。
RULES = [
    (
        "Cell.CELL_TYPE_* → CellType.*",
        re.compile(r"\b[A-Za-z_]\w*\.CELL_TYPE_([A-Z_]+)\b"),
        lambda m: "CellType." + m.group(1),
    ),
    (
        "CellStyle.BORDER_* → BorderStyle.*",
        re.compile(r"\b(?:HSSFCellStyle|XSSFCellStyle|CellStyle)\.BORDER_([A-Z_]+)\b"),
        lambda m: "BorderStyle." + m.group(1),
    ),
    (
        "CellStyle.ALIGN_* → HorizontalAlignment.*",
        re.compile(r"\b(?:HSSFCellStyle|XSSFCellStyle|CellStyle)\.ALIGN_([A-Z_]+)\b"),
        lambda m: "HorizontalAlignment." + m.group(1),
    ),
    (
        "CellStyle.VERTICAL_* → VerticalAlignment.*",
        re.compile(r"\b(?:HSSFCellStyle|XSSFCellStyle|CellStyle)\.VERTICAL_([A-Z_]+)\b"),
        lambda m: "VerticalAlignment." + m.group(1),
    ),
    (
        "CellStyle.SOLID_FOREGROUND → FillPatternType.SOLID_FOREGROUND",
        re.compile(r"\b(?:HSSFCellStyle|XSSFCellStyle|CellStyle)\.(SOLID_FOREGROUND)\b"),
        lambda m: "FillPatternType." + m.group(1),
    ),
    (
        "HSSFColor.XXX.index → HSSFColorPredefined.XXX.getIndex()",
        re.compile(r"\bHSSFColor\.([A-Z][A-Z0-9_]*)\.index\b"),
        lambda m: f"HSSFColor.HSSFColorPredefined.{m.group(1)}.getIndex()",
    ),
    (
        "setBoldweight(X.BOLDWEIGHT_BOLD) → setBold(true)",
        re.compile(
            r"\.setBoldweight\(\s*(?:HSSFFont|Font)\.BOLDWEIGHT_BOLD\s*\)"
        ),
        lambda m: ".setBold(true)",
    ),
]


def add_imports(src, needed):
    """把缺的枚举 import 插进 import 区。已是 FQN 或已存在的不重复插。"""
    lines = src.split("\n")
    existing = {ln.strip() for ln in lines}
    missing = [
        (name, fqn)
        for name, fqn in sorted(ENUM_IMPORTS.items())
        if name in needed and f"import {fqn};" not in existing
    ]
    if not missing:
        return src, []

    # 定位插入点:最后一个 import 行之后;没有 import 就放在 package 行之后
    last_import = -1
    pkg = -1
    for i, ln in enumerate(lines):
        s = ln.strip()
        if s.startswith("import "):
            last_import = i
        elif s.startswith("package ") and pkg < 0:
            pkg = i
    at = (last_import if last_import >= 0 else pkg) + 1
    lines[at:at] = [f"import {fqn};" for _, fqn in missing]
    return "\n".join(lines), [n for n, _ in missing]


def migrate(path, dry):
    with open(path, encoding="utf-8", errors="surrogateescape") as fh:
        src = original = fh.read()

    hits = []
    for desc, pat, repl in RULES:
        src, n = pat.subn(repl, src)
        if n:
            hits.append(f"{desc} x{n}")

    # 兜底:还有 setBoldweight 就说明实参不是 BOLDWEIGHT_BOLD,需人工确认
    if ".setBoldweight(" in src:
        raise SystemExit(f"!! {path}: 仍有 setBoldweight 调用且实参非 BOLDWEIGHT_BOLD,需人工处理")

    if src == original:
        return None

    needed = {name for name in ENUM_IMPORTS if re.search(rf"\b{name}\.", src)}
    src, added = add_imports(src, needed)
    if added:
        hits.append("补 import: " + ", ".join(added))

    if not dry:
        with open(path, "w", encoding="utf-8", errors="surrogateescape") as fh:
            fh.write(src)
    return hits


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

    changed = 0
    for d in DIRS:
        base = os.path.join(BASE, d)
        if not os.path.isdir(base):
            print(f"!! 目录不存在: {base}")
            continue
        for f in sorted(os.listdir(base)):
            if not f.endswith(".java"):
                continue
            p = os.path.join(base, f)
            hits = migrate(p, args.dry_run)
            if hits:
                changed += 1
                print(f"  {d}/{f}")
                for h in hits:
                    print(f"      · {h}")
            else:
                print(f"  {d}/{f}  (无需改动)")

    print(f"\n改动文件 {changed} 个" + ("(--dry-run,未落盘)" if args.dry_run else ""))


if __name__ == "__main__":
    main()