From 9cf4915d4942541c89abd9f832b1382d63b186e6 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Wed, 8 Jul 2026 13:27:45 +0200 Subject: [PATCH] =?UTF-8?q?refactor(web):=20hasApplicableFix=20=E6=94=B9?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=B0=93=E8=AF=8D=EF=BC=8C=E6=B6=88=E9=99=A4?= =?UTF-8?q?=20applyConflictFix=20=E5=BC=BA=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasApplicableFix 重写为对象入参类型谓词 patch is ApplicablePatch,谓词命中后 applyConflictFix 里 original/replacement 自动收窄为 string,删两处 as string;补收窄单测。 --- apps/web/lib/review/applyFix.test.ts | 26 ++++++++++++++----- apps/web/lib/review/applyFix.ts | 38 ++++++++++++++++++---------- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/apps/web/lib/review/applyFix.test.ts b/apps/web/lib/review/applyFix.test.ts index 4c70f1e..f1479b1 100644 --- a/apps/web/lib/review/applyFix.test.ts +++ b/apps/web/lib/review/applyFix.test.ts @@ -4,12 +4,26 @@ import { applyConflictFix, hasApplicableFix } from "./applyFix"; describe("hasApplicableFix", () => { it("requires non-empty original and string replacement", () => { - expect(hasApplicableFix("迈巴赫", "奔驰")).toBe(true); - expect(hasApplicableFix("删我", "")).toBe(true); // 空串=删除,仍可应用 - expect(hasApplicableFix(null, "奔驰")).toBe(false); - expect(hasApplicableFix("", "奔驰")).toBe(false); - expect(hasApplicableFix("迈巴赫", null)).toBe(false); - expect(hasApplicableFix(undefined, undefined)).toBe(false); + expect(hasApplicableFix({ original: "迈巴赫", replacement: "奔驰" })).toBe(true); + // 空串=删除,仍可应用 + expect(hasApplicableFix({ original: "删我", replacement: "" })).toBe(true); + expect(hasApplicableFix({ original: null, replacement: "奔驰" })).toBe(false); + expect(hasApplicableFix({ original: "", replacement: "奔驰" })).toBe(false); + expect(hasApplicableFix({ original: "迈巴赫", replacement: null })).toBe(false); + expect(hasApplicableFix({ original: undefined, replacement: undefined })).toBe( + false, + ); + }); + + it("narrows original/replacement to string when true (type predicate)", () => { + const patch: { original: string | null; replacement: string | null } = { + original: "迈巴赫", + replacement: "奔驰", + }; + if (!hasApplicableFix(patch)) throw new Error("expected applicable"); + // 编译期收窄:无需强转即可当 string 拼接(`as string` 已消除)。 + const combined: string = patch.original + patch.replacement; + expect(combined).toBe("迈巴赫奔驰"); }); }); diff --git a/apps/web/lib/review/applyFix.ts b/apps/web/lib/review/applyFix.ts index dc8a2a3..27b1873 100644 --- a/apps/web/lib/review/applyFix.ts +++ b/apps/web/lib/review/applyFix.ts @@ -6,15 +6,25 @@ export type FixOutcome = | { status: "not-found"; text: string } // 有补丁但终稿里找不到原文(作者已改稿/漂移):text 原样 | { status: "no-patch"; text: string }; // 该冲突无可自动应用的补丁:text 原样 +// 已确认可应用的补丁(original/replacement 均为 string)。 +export interface ApplicablePatch { + original: string; + replacement: string; +} + +// 松散补丁入参:original/replacement 可能缺失/为 null(审稿可局部修复时才带上)。 +interface LoosePatch { + original: string | null | undefined; + replacement: string | null | undefined; +} + // 是否带可应用补丁:original 非空 + replacement 为字符串(容许空串=删除)。 -export function hasApplicableFix( - original: string | null | undefined, - replacement: string | null | undefined, -): boolean { +// 类型谓词:命中即把 original/replacement 收窄为 string,调用方无需强转。 +export function hasApplicableFix(patch: LoosePatch): patch is ApplicablePatch { return ( - typeof original === "string" && - original.length > 0 && - typeof replacement === "string" + typeof patch.original === "string" && + patch.original.length > 0 && + typeof patch.replacement === "string" ); } @@ -25,18 +35,20 @@ export function applyConflictFix( original: string | null | undefined, replacement: string | null | undefined, ): FixOutcome { - if (!hasApplicableFix(original, replacement)) { + const patch: LoosePatch = { original, replacement }; + if (!hasApplicableFix(patch)) { return { status: "no-patch", text }; } - // hasApplicableFix 已收窄为 string。 - const from = original as string; - const to = replacement as string; - const idx = text.indexOf(from); + // 谓词已把 patch 收窄为 ApplicablePatch,original/replacement 皆为 string。 + const idx = text.indexOf(patch.original); if (idx === -1) { return { status: "not-found", text }; } return { status: "applied", - text: text.slice(0, idx) + to + text.slice(idx + from.length), + text: + text.slice(0, idx) + + patch.replacement + + text.slice(idx + patch.original.length), }; }