Commit 83ce168a68eaf63bb9d65042c88c388c3350d6d6

Authored by haohao
1 parent dea5e07e

fix(infra): 代码生成统一 LF 与尾逗号处理,修复 Windows 单测

yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
... ... @@ -457,10 +457,8 @@ public class CodegenEngine {
457 457 * @return 格式化后的代码
458 458 */
459 459 private String prettyCode(String content, String vmPath) {
460   - // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5、vue3_admin_uniapp)
461   - if (!StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) {
462   - content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }");
463   - }
  460 + // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(内含换行归一化)
  461 + content = CodegenFormatUtils.applyVueTrailingCommaFix(content, vmPath);
464 462 // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到
465 463 if (StrUtil.count(content, "dateFormatter") == 1) {
466 464 content = StrUtils.removeLineContains(content, "dateFormatter");
... ... @@ -482,7 +480,7 @@ public class CodegenEngine {
482 480 if (StrUtil.count(content, "DICT_TYPE.") == 0) {
483 481 content = StrUtils.removeLineContains(content, "DICT_TYPE");
484 482 }
485   - return content;
  483 + return CodegenFormatUtils.formatGeneratedContent(content);
486 484 }
487 485  
488 486 private Map<String, Object> initBindingMap(DbType dbType, CodegenTableDO table, List<CodegenColumnDO> columns,
... ...
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.codegen.inner;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +
  5 +/**
  6 + * 代码生成结果格式化工具
  7 + */
  8 +final class CodegenFormatUtils {
  9 +
  10 + /**
  11 + * 经典 Vue2 模板目录,与 {@link CodegenEngine#vueTemplatePath(String)} 一致
  12 + */
  13 + private static final String CLASSIC_VUE2_TEMPLATE_PREFIX = "codegen/vue/";
  14 +
  15 + /**
  16 + * 经典 Vue3 模板目录,与 {@link CodegenEngine#vue3TemplatePath(String)} 一致。
  17 + * 使用前缀 {@code codegen/vue3/} 而非子串匹配,避免误伤 {@code vue3_vben5}、{@code vue3_admin_uniapp} 等目录。
  18 + */
  19 + private static final String CLASSIC_VUE3_TEMPLATE_PREFIX = "codegen/vue3/";
  20 +
  21 + private CodegenFormatUtils() {
  22 + }
  23 +
  24 + /**
  25 + * 经典 Vue2 / Vue3 模板(Element UI / Element Plus 标准模版)
  26 + */
  27 + static boolean isClassicVueTemplate(String vmPath) {
  28 + if (StrUtil.isEmpty(vmPath)) {
  29 + return false;
  30 + }
  31 + return StrUtil.startWith(vmPath, CLASSIC_VUE2_TEMPLATE_PREFIX)
  32 + || StrUtil.startWith(vmPath, CLASSIC_VUE3_TEMPLATE_PREFIX);
  33 + }
  34 +
  35 + /**
  36 + * 是否处理 {@code ,\n}} / {@code ,\n }} 形式的末尾逗号(经典 Vue + vue3_vben2;排除 vben5、uniapp)
  37 + */
  38 + static boolean needsVueTrailingCommaBeforeBraceFix(String vmPath) {
  39 + if (StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) {
  40 + return false;
  41 + }
  42 + return isClassicVueTemplate(vmPath) || StrUtil.startWith(vmPath, "codegen/vue3_vben/");
  43 + }
  44 +
  45 + /**
  46 + * 是否处理 {@code ,\n)} 形式的末尾逗号(仅经典 Vue 的 .vue 视图模板)
  47 + */
  48 + static boolean needsReactiveTrailingCommaFix(String vmPath) {
  49 + return isClassicVueTemplate(vmPath) && StrUtil.endWith(vmPath, ".vue.vm");
  50 + }
  51 +
  52 + /**
  53 + * Vue 模板:统一去除对象/参数列表末尾多余逗号(与 {@link CodegenEngine#prettyCode} 历史行为一致)
  54 + */
  55 + static String applyVueTrailingCommaFix(String content, String vmPath) {
  56 + if (content == null) {
  57 + return null;
  58 + }
  59 + content = normalizeLineSeparators(content);
  60 + if (needsVueTrailingCommaBeforeBraceFix(vmPath)) {
  61 + content = content.replaceAll(",\\r?\\n}", "\n}")
  62 + .replaceAll(",\\r?\\n }", "\n }");
  63 + }
  64 + if (needsReactiveTrailingCommaFix(vmPath)) {
  65 + content = removeTrailingCommaBeforeCloseParen(content);
  66 + }
  67 + return content;
  68 + }
  69 +
  70 + /**
  71 + * 去除多行实参中 {@code ,\n)} 形式的末尾逗号(如 {@code foo(bar,\n)};{@code reactive} 的 {@code ,\n})} 由 {@code ,\n}} 规则处理)
  72 + */
  73 + static String removeTrailingCommaBeforeCloseParen(String content) {
  74 + if (content == null) {
  75 + return null;
  76 + }
  77 + return content.replaceAll(",\\r?\\n\\)", "\n)");
  78 + }
  79 +
  80 + /**
  81 + * 统一换行符为 LF
  82 + */
  83 + static String normalizeLineSeparators(String content) {
  84 + if (content == null) {
  85 + return null;
  86 + }
  87 + return content.replace("\r\n", "\n").replace('\r', '\n');
  88 + }
  89 +
  90 + /**
  91 + * 生产生成结果:LF + 保证文件以单个换行结尾(符合 POSIX / IDE 习惯)
  92 + */
  93 + static String formatGeneratedContent(String content) {
  94 + content = normalizeLineSeparators(content);
  95 + if (StrUtil.isEmpty(content)) {
  96 + return content;
  97 + }
  98 + while (content.endsWith("\n\n")) {
  99 + content = content.substring(0, content.length() - 1);
  100 + }
  101 + if (!content.endsWith("\n")) {
  102 + content = content + "\n";
  103 + }
  104 + return content;
  105 + }
  106 +
  107 + /**
  108 + * 单测断言:LF + 去掉末尾空白(与历史 fixture 无尾换行对齐)
  109 + */
  110 + static String normalizeForAssert(String content) {
  111 + if (content == null) {
  112 + return null;
  113 + }
  114 + return normalizeLineSeparators(content).stripTrailing();
  115 + }
  116 +}
... ...
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java
... ... @@ -18,12 +18,7 @@ import org.mockito.Spy;
18 18  
19 19 import java.io.ByteArrayInputStream;
20 20 import java.io.ByteArrayOutputStream;
21   -import java.util.ArrayList;
22   -import java.util.HashMap;
23   -import java.util.LinkedHashSet;
24   -import java.util.List;
25   -import java.util.Map;
26   -import java.util.Set;
  21 +import java.util.*;
27 22  
28 23 import static org.junit.jupiter.api.Assertions.assertEquals;
29 24  
... ... @@ -104,15 +99,14 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
104 99 .map(m -> (String) m.get("filePath"))
105 100 .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
106 101 assertEquals(expectedFiles, result.keySet(), "生成文件集合不匹配");
107   - // 校验每个文件;归一化 \r\n 为 \n,让断言不依赖文件落盘的换行风格
  102 + // 校验每个文件;归一化换行符,让断言不依赖文件落盘的换行风格
108 103 asserts.forEach(assertMap -> {
109 104 String contentPath = (String) assertMap.get("contentPath");
110 105 String filePath = (String) assertMap.get("filePath");
111   - String expected = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath)
112   - .replace("\r\n", "\n");
113   - String actual = result.get(filePath);
114   - assertEquals(expected, actual == null ? null : actual.replace("\r\n", "\n"),
115   - filePath + ":不匹配");
  106 + String expected = CodegenFormatUtils.normalizeForAssert(
  107 + ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath));
  108 + String actual = CodegenFormatUtils.normalizeForAssert(result.get(filePath));
  109 + assertEquals(expected, actual, filePath + ":不匹配");
116 110 });
117 111 }
118 112  
... ...
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFormatUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.codegen.inner;
  2 +
  3 +import org.junit.jupiter.api.Test;
  4 +
  5 +import static org.junit.jupiter.api.Assertions.*;
  6 +
  7 +class CodegenFormatUtilsTest {
  8 +
  9 + @Test
  10 + void testNormalizeLineSeparators() {
  11 + assertEquals("a\nb", CodegenFormatUtils.normalizeLineSeparators("a\r\nb"));
  12 + assertEquals("a\nb", CodegenFormatUtils.normalizeLineSeparators("a\rb"));
  13 + }
  14 +
  15 + @Test
  16 + void testFormatGeneratedContent() {
  17 + assertEquals("class A {\n}\n", CodegenFormatUtils.formatGeneratedContent("class A {\r\n}\r\n"));
  18 + assertEquals("x\n", CodegenFormatUtils.formatGeneratedContent("x"));
  19 + assertEquals("x\n", CodegenFormatUtils.formatGeneratedContent("x\n\n\n"));
  20 + assertEquals("", CodegenFormatUtils.formatGeneratedContent(""));
  21 + assertEquals(" \n", CodegenFormatUtils.formatGeneratedContent(" "));
  22 + }
  23 +
  24 + @Test
  25 + void testNormalizeForAssert() {
  26 + assertEquals("}", CodegenFormatUtils.normalizeForAssert("}\n"));
  27 + assertEquals("a\nb", CodegenFormatUtils.normalizeForAssert("a\r\nb\r\n"));
  28 + }
  29 +
  30 + @Test
  31 + void testNormalizeForAssert_matchesFormatGeneratedContentForAssert() {
  32 + String raw = "package demo;\r\n\r\npublic class A {\r\n private Long id;\r\n}\r\n";
  33 + String generated = CodegenFormatUtils.formatGeneratedContent(raw);
  34 + assertEquals(CodegenFormatUtils.normalizeForAssert(raw),
  35 + CodegenFormatUtils.normalizeForAssert(generated));
  36 + }
  37 +
  38 + @Test
  39 + void testRemoveTrailingCommaBeforeCloseParen() {
  40 + assertEquals("foo(bar\n)", CodegenFormatUtils.removeTrailingCommaBeforeCloseParen("foo(bar,\n)"));
  41 + }
  42 +
  43 + @Test
  44 + void testApplyVueTrailingCommaFix_classicVueReactive() {
  45 + String input = "const queryParams = reactive({\n createTime: [],\n})\n";
  46 + String expected = "const queryParams = reactive({\n createTime: []\n})\n";
  47 + assertEquals(expected, CodegenFormatUtils.applyVueTrailingCommaFix(
  48 + input, "codegen/vue3/views/index.vue.vm"));
  49 + }
  50 +
  51 + @Test
  52 + void testApplyVueTrailingCommaFix_skipVben5() {
  53 + String input = "const x = {\n a: 1,\n}\n";
  54 + assertEquals(input, CodegenFormatUtils.applyVueTrailingCommaFix(
  55 + input, "codegen/vue3_vben5_antd/general/views/index.vue.vm"));
  56 + }
  57 +
  58 + @Test
  59 + void testIsClassicVueTemplate() {
  60 + assertTrue(CodegenFormatUtils.isClassicVueTemplate("codegen/vue/views/index.vue.vm"));
  61 + assertTrue(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3/views/index.vue.vm"));
  62 + assertTrue(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3/api/api.ts.vm"));
  63 + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3_vben5_antd/schema/views/index.vue.vm"));
  64 + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3_vben/views/index.vue.vm"));
  65 + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue3_admin_uniapp/views/index.vue.vm"));
  66 + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/vue30/views/index.vue.vm")); // 非 codegen/vue3/ 前缀
  67 + assertFalse(CodegenFormatUtils.isClassicVueTemplate("codegen/java/controller/controller.vm"));
  68 + assertFalse(CodegenFormatUtils.isClassicVueTemplate(null));
  69 + }
  70 +
  71 + @Test
  72 + void testNeedsReactiveTrailingCommaFix() {
  73 + assertTrue(CodegenFormatUtils.needsReactiveTrailingCommaFix("codegen/vue3/views/index.vue.vm"));
  74 + assertFalse(CodegenFormatUtils.needsReactiveTrailingCommaFix("codegen/vue3/api/api.ts.vm"));
  75 + assertFalse(CodegenFormatUtils.needsReactiveTrailingCommaFix("codegen/vue3_vben5_antd/general/views/index.vue.vm"));
  76 + }
  77 +
  78 + @Test
  79 + void testNeedsVueTrailingCommaBeforeBraceFix() {
  80 + assertTrue(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3/views/index.vue.vm"));
  81 + assertTrue(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3_vben/views/index.vue.vm"));
  82 + assertFalse(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3_vben5_antd/general/views/index.vue.vm"));
  83 + assertFalse(CodegenFormatUtils.needsVueTrailingCommaBeforeBraceFix("codegen/vue3_admin_uniapp/views/index.vue.vm"));
  84 + }
  85 +}
  86 +
... ...