From 83ce168a68eaf63bb9d65042c88c388c3350d6d6 Mon Sep 17 00:00:00 2001 From: haohao <1036606149@qq.com> Date: Wed, 3 Jun 2026 14:54:01 +0800 Subject: [PATCH] fix(infra): 代码生成统一 LF 与尾逗号处理,修复 Windows 单测 --- yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java | 8 +++----- yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtils.java | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java | 18 ++++++------------ yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtilsTest.java | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtils.java create mode 100644 yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtilsTest.java diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index 70939a6..6e9563c 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -457,10 +457,8 @@ public class CodegenEngine { * @return 格式化后的代码 */ private String prettyCode(String content, String vmPath) { - // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5、vue3_admin_uniapp) - if (!StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) { - content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); - } + // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(内含换行归一化) + content = CodegenFormatUtils.applyVueTrailingCommaFix(content, vmPath); // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到 if (StrUtil.count(content, "dateFormatter") == 1) { content = StrUtils.removeLineContains(content, "dateFormatter"); @@ -482,7 +480,7 @@ public class CodegenEngine { if (StrUtil.count(content, "DICT_TYPE.") == 0) { content = StrUtils.removeLineContains(content, "DICT_TYPE"); } - return content; + return CodegenFormatUtils.formatGeneratedContent(content); } private Map initBindingMap(DbType dbType, CodegenTableDO table, List columns, diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtils.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtils.java new file mode 100644 index 0000000..7b5fbb4 --- /dev/null +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtils.java @@ -0,0 +1,116 @@ +package cn.iocoder.yudao.module.infra.service.codegen.inner; + +import cn.hutool.core.util.StrUtil; + +/** + * 代码生成结果格式化工具 + */ +final class CodegenFormatUtils { + + /** + * 经典 Vue2 模板目录,与 {@link CodegenEngine#vueTemplatePath(String)} 一致 + */ + private static final String CLASSIC_VUE2_TEMPLATE_PREFIX = "codegen/vue/"; + + /** + * 经典 Vue3 模板目录,与 {@link CodegenEngine#vue3TemplatePath(String)} 一致。 + * 使用前缀 {@code codegen/vue3/} 而非子串匹配,避免误伤 {@code vue3_vben5}、{@code vue3_admin_uniapp} 等目录。 + */ + private static final String CLASSIC_VUE3_TEMPLATE_PREFIX = "codegen/vue3/"; + + private CodegenFormatUtils() { + } + + /** + * 经典 Vue2 / Vue3 模板(Element UI / Element Plus 标准模版) + */ + static boolean isClassicVueTemplate(String vmPath) { + if (StrUtil.isEmpty(vmPath)) { + return false; + } + return StrUtil.startWith(vmPath, CLASSIC_VUE2_TEMPLATE_PREFIX) + || StrUtil.startWith(vmPath, CLASSIC_VUE3_TEMPLATE_PREFIX); + } + + /** + * 是否处理 {@code ,\n}} / {@code ,\n }} 形式的末尾逗号(经典 Vue + vue3_vben2;排除 vben5、uniapp) + */ + static boolean needsVueTrailingCommaBeforeBraceFix(String vmPath) { + if (StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) { + return false; + } + return isClassicVueTemplate(vmPath) || StrUtil.startWith(vmPath, "codegen/vue3_vben/"); + } + + /** + * 是否处理 {@code ,\n)} 形式的末尾逗号(仅经典 Vue 的 .vue 视图模板) + */ + static boolean needsReactiveTrailingCommaFix(String vmPath) { + return isClassicVueTemplate(vmPath) && StrUtil.endWith(vmPath, ".vue.vm"); + } + + /** + * Vue 模板:统一去除对象/参数列表末尾多余逗号(与 {@link CodegenEngine#prettyCode} 历史行为一致) + */ + static String applyVueTrailingCommaFix(String content, String vmPath) { + if (content == null) { + return null; + } + content = normalizeLineSeparators(content); + if (needsVueTrailingCommaBeforeBraceFix(vmPath)) { + content = content.replaceAll(",\\r?\\n}", "\n}") + .replaceAll(",\\r?\\n }", "\n }"); + } + if (needsReactiveTrailingCommaFix(vmPath)) { + content = removeTrailingCommaBeforeCloseParen(content); + } + return content; + } + + /** + * 去除多行实参中 {@code ,\n)} 形式的末尾逗号(如 {@code foo(bar,\n)};{@code reactive} 的 {@code ,\n})} 由 {@code ,\n}} 规则处理) + */ + static String removeTrailingCommaBeforeCloseParen(String content) { + if (content == null) { + return null; + } + return content.replaceAll(",\\r?\\n\\)", "\n)"); + } + + /** + * 统一换行符为 LF + */ + static String normalizeLineSeparators(String content) { + if (content == null) { + return null; + } + return content.replace("\r\n", "\n").replace('\r', '\n'); + } + + /** + * 生产生成结果:LF + 保证文件以单个换行结尾(符合 POSIX / IDE 习惯) + */ + static String formatGeneratedContent(String content) { + content = normalizeLineSeparators(content); + if (StrUtil.isEmpty(content)) { + return content; + } + while (content.endsWith("\n\n")) { + content = content.substring(0, content.length() - 1); + } + if (!content.endsWith("\n")) { + content = content + "\n"; + } + return content; + } + + /** + * 单测断言:LF + 去掉末尾空白(与历史 fixture 无尾换行对齐) + */ + static String normalizeForAssert(String content) { + if (content == null) { + return null; + } + return normalizeLineSeparators(content).stripTrailing(); + } +} diff --git a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java index 7d1be13..e4a553f 100644 --- a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java +++ b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java @@ -18,12 +18,7 @@ import org.mockito.Spy; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -104,15 +99,14 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest { .map(m -> (String) m.get("filePath")) .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)); assertEquals(expectedFiles, result.keySet(), "生成文件集合不匹配"); - // 校验每个文件;归一化 \r\n 为 \n,让断言不依赖文件落盘的换行风格 + // 校验每个文件;归一化换行符,让断言不依赖文件落盘的换行风格 asserts.forEach(assertMap -> { String contentPath = (String) assertMap.get("contentPath"); String filePath = (String) assertMap.get("filePath"); - String expected = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath) - .replace("\r\n", "\n"); - String actual = result.get(filePath); - assertEquals(expected, actual == null ? null : actual.replace("\r\n", "\n"), - filePath + ":不匹配"); + String expected = CodegenFormatUtils.normalizeForAssert( + ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath)); + String actual = CodegenFormatUtils.normalizeForAssert(result.get(filePath)); + assertEquals(expected, actual, filePath + ":不匹配"); }); } diff --git a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtilsTest.java b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtilsTest.java new file mode 100644 index 0000000..3d8f1a5 --- /dev/null +++ b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtilsTest.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.infra.service.codegen.inner; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class CodegenFormatUtilsTest { + + @Test + void testNormalizeLineSeparators() { + assertEquals("a\nb", CodegenFormatUtils.normalizeLineSeparators("a\r\nb")); + assertEquals("a\nb", CodegenFormatUtils.normalizeLineSeparators("a\rb")); + } + + @Test + void testFormatGeneratedContent() { + assertEquals("class A {\n}\n", CodegenFormatUtils.formatGeneratedContent("class A {\r\n}\r\n")); + assertEquals("x\n", CodegenFormatUtils.formatGeneratedContent("x")); + assertEquals("x\n", CodegenFormatUtils.formatGeneratedContent("x\n\n\n")); + assertEquals("", CodegenFormatUtils.formatGeneratedContent("")); + assertEquals(" \n", CodegenFormatUtils.formatGeneratedContent(" ")); + } + + @Test + void testNormalizeForAssert() { + assertEquals("}", CodegenFormatUtils.normalizeForAssert("}\n")); + assertEquals("a\nb", CodegenFormatUtils.normalizeForAssert("a\r\nb\r\n")); + } + + @Test + void testNormalizeForAssert_matchesFormatGeneratedContentForAssert() { + String raw = "package demo;\r\n\r\npublic class A {\r\n private Long id;\r\n}\r\n"; + String generated = CodegenFormatUtils.formatGeneratedContent(raw); + assertEquals(CodegenFormatUtils.normalizeForAssert(raw), + CodegenFormatUtils.normalizeForAssert(generated)); + } + + @Test + void testRemoveTrailingCommaBeforeCloseParen() { + assertEquals("foo(bar\n)", CodegenFormatUtils.removeTrailingCommaBeforeCloseParen("foo(bar,\n)")); + } + + @Test + void testApplyVueTrailingCommaFix_classicVueReactive() { + String input = "const queryParams = reactive({\n createTime: [],\n})\n"; + String expected = "const queryParams = reactive({\n createTime: []\n})\n"; + assertEquals(expected, CodegenFormatUtils.applyVueTrailingCommaFix( + input, "codegen/vue3/views/index.vue.vm")); + } + + @Test + void testApplyVueTrailingCommaFix_skipVben5() { + String input = "const x = {\n a: 1,\n}\n"; + assertEquals(input, CodegenFormatUtils.applyVueTrailingCommaFix( + input, "codegen/vue3_vben5_antd/general/views/index.vue.vm")); + } + + @Test + void testIsClassicVueTemplate() { + assertTrue(CodegenFormatUtils.isClassicVueTemplate("codegen/vue/views/index.vue.vm")); + assertTrue(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3/views/index.vue.vm")); + assertTrue(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3/api/api.ts.vm")); + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3_vben5_antd/schema/views/index.vue.vm")); + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3_vben/views/index.vue.vm")); + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3_admin_uniapp/views/index.vue.vm")); + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue30/views/index.vue.vm")); // 非 codegen/vue3/ 前缀 + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/java/controller/controller.vm")); + assertFalse(CodegenFormatUtils.isClassicVueTemplate(null)); + } + + @Test + void testNeedsReactiveTrailingCommaFix() { + assertTrue(CodegenFormatUtils.needsReactiveTrailingCommaFix("codegen/vue3/views/index.vue.vm")); + assertFalse(CodegenFormatUtils.needsReactiveTrailingCommaFix("codegen/vue3/api/api.ts.vm")); + assertFalse(CodegenFormatUtils.needsReactiveTrailingCommaFix("codegen/vue3_vben5_antd/general/views/index.vue.vm")); + } + + @Test + void testNeedsVueTrailingCommaBeforeBraceFix() { + assertTrue(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3/views/index.vue.vm")); + assertTrue(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3_vben/views/index.vue.vm")); + assertFalse(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3_vben5_antd/general/views/index.vue.vm")); + assertFalse(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3_admin_uniapp/views/index.vue.vm")); + } +} + -- libgit2 0.21.4