From 887a66f92a98dbea113f34f8c29fd64115730cb9 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Mon, 27 Apr 2026 09:17:23 -0700 Subject: [PATCH] fix(cli): clean up orphan whitespace and over-indent in generated models `wheels generate model Foo bar:string` produced template-fill leftovers: 4 consecutive blank-with-tabs lines inside config(), and validation lines indented at 4 tabs instead of 2. Two coordinated fixes: 1. Strip the leading 2-tab prefix from `buildModelValidations` content (CodeGen.cfc) and `generateRelationshipCode` (Templates.cfc). The template's `\t\t{{validations}}` placeholder already provides the first line's indent; subsequent lines join with `\n\t\t` so multi-line content stays aligned. 2. Add a `collapseEmptyLines` post-processing pass to processTemplate. Whitespace-only lines (left behind by empty placeholders like `\t\t{{hasManyRelationships}}` when no hasMany exists) become genuinely empty, then runs of 2+ consecutive blank lines collapse to one. Doesn't affect intentional single-blank separators between blocks. Fixes #2329. Tests added to CodeGenSpec covering: no orphan whitespace-only lines, no 2+ consecutive blank lines, validations indented at 2 tabs not 4, and multi-line validations stay aligned. --- cli/lucli/services/CodeGen.cfc | 11 ++-- cli/lucli/services/Templates.cfc | 35 ++++++++++- .../tests/specs/services/CodeGenSpec.cfc | 58 +++++++++++++++++++ 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/cli/lucli/services/CodeGen.cfc b/cli/lucli/services/CodeGen.cfc index 0e7b2708aa..7e877cf49a 100644 --- a/cli/lucli/services/CodeGen.cfc +++ b/cli/lucli/services/CodeGen.cfc @@ -77,15 +77,18 @@ component { arrayAppend(presenceProps, prop.name); var propType = structKeyExists(prop, "type") ? lCase(prop.type) : "string"; if (propType == "email") { - arrayAppend(formatLines, " validatesFormatOf(property=""#prop.name#"", type=""email"");"); + arrayAppend(formatLines, "validatesFormatOf(property=""#prop.name#"", type=""email"");"); } else if (propType == "url") { - arrayAppend(formatLines, " validatesFormatOf(property=""#prop.name#"", type=""URL"");"); + arrayAppend(formatLines, "validatesFormatOf(property=""#prop.name#"", type=""URL"");"); } } - var lines = [" validatesPresenceOf(""#arrayToList(presenceProps)#"");"]; + var lines = ["validatesPresenceOf(""#arrayToList(presenceProps)#"");"]; lines.append(formatLines, true); - return arrayToList(lines, chr(10)); + // Join with newline + 2 tabs so subsequent lines align with the template's + // `\t\t{{validations}}` placeholder indent. The first line gets its indent + // from the placeholder's leading whitespace at fill time. + return arrayToList(lines, chr(10) & chr(9) & chr(9)); } /** diff --git a/cli/lucli/services/Templates.cfc b/cli/lucli/services/Templates.cfc index f1f3be2578..3d0e14584b 100644 --- a/cli/lucli/services/Templates.cfc +++ b/cli/lucli/services/Templates.cfc @@ -164,9 +164,37 @@ component { processed = replace(processed, "|ObjectNamePluralC|", variables.helpers.pluralize(modelName), "all"); } + // Final cleanup: remove orphan whitespace-only lines left behind by + // empty placeholders (e.g. `\t\t{{hasManyRelationships}}` when no + // hasMany relationships exist), and collapse runs of consecutive blank + // lines to a single blank line. Without this, generated files end up + // with 4+ blank lines inside config() — see issue #2329. + processed = collapseEmptyLines(processed); + return processed; } + /** + * Normalise whitespace-only lines to genuinely empty, then collapse runs + * of 2+ consecutive empty lines down to one. Used after placeholder + * substitution to clean up template-fill leftovers. + */ + private string function collapseEmptyLines(required string content) { + var lines = listToArray(arguments.content, chr(10), true); + var cleaned = []; + var prevWasEmpty = false; + + for (var line in lines) { + var normalised = reFind("^[[:space:]]+$", line) ? "" : line; + var isEmpty = (normalised == ""); + if (isEmpty && prevWasEmpty) continue; + cleaned.append(normalised); + prevWasEmpty = isEmpty; + } + + return arrayToList(cleaned, chr(10)); + } + // ── Private helpers ────────────────────────────── /** @@ -275,9 +303,12 @@ component { private string function generateRelationshipCode(required string type, required string names) { var code = []; for (var rel in listToArray(arguments.names)) { - arrayAppend(code, " #arguments.type#('#trim(rel)#');"); + arrayAppend(code, "#arguments.type#('#trim(rel)#');"); } - return arrayToList(code, chr(10)); + // Join with newline + 2 tabs so subsequent lines align with the template's + // `\t\t{{...Relationships}}` placeholder indent. The first line gets its + // indent from the placeholder's leading whitespace at fill time. + return arrayToList(code, chr(10) & chr(9) & chr(9)); } /** diff --git a/cli/lucli/tests/specs/services/CodeGenSpec.cfc b/cli/lucli/tests/specs/services/CodeGenSpec.cfc index 4781392142..306ac9f5d8 100644 --- a/cli/lucli/tests/specs/services/CodeGenSpec.cfc +++ b/cli/lucli/tests/specs/services/CodeGenSpec.cfc @@ -97,6 +97,64 @@ component extends="wheels.wheelstest.system.BaseSpec" { expect(content).notToInclude("validatesFormatOf"); }); + it("produces no orphan whitespace-only lines (##2329)", () => { + codegen.generateModel( + name = "Layout", + properties = [{name: "title", type: "string"}], + force = true + ); + var content = fileRead(tempRoot & "/app/models/Layout.cfc"); + var lines = listToArray(content, chr(10), true); + var whitespaceOnly = []; + for (var i = 1; i <= arrayLen(lines); i++) { + if (reFind("^[[:space:]]+$", lines[i])) { + arrayAppend(whitespaceOnly, "line " & i & ": '" & lines[i] & "'"); + } + } + expect(arrayLen(whitespaceOnly)).toBe(0); + }); + + it("produces no consecutive blank-line runs (##2329)", () => { + codegen.generateModel( + name = "Spacer", + properties = [{name: "name", type: "string"}], + force = true + ); + var content = fileRead(tempRoot & "/app/models/Spacer.cfc"); + // 3+ consecutive newlines = 2+ consecutive blank lines + expect(content).notToInclude(chr(10) & chr(10) & chr(10)); + }); + + it("indents validations at 2 tabs, not 4 (##2329)", () => { + codegen.generateModel( + name = "Indent", + properties = [{name: "title", type: "string"}], + force = true + ); + var content = fileRead(tempRoot & "/app/models/Indent.cfc"); + // 2-tab indent is correct + expect(content).toInclude(chr(9) & chr(9) & "validatesPresenceOf"); + // 4-tab indent is the bug shape — must not be present + expect(content).notToInclude(chr(9) & chr(9) & chr(9) & chr(9) & "validatesPresenceOf"); + }); + + it("indents multi-line validations consistently at 2 tabs (##2329)", () => { + codegen.generateModel( + name = "MultiVal", + properties = [ + {name: "name", type: "string"}, + {name: "email", type: "email"} + ], + force = true + ); + var content = fileRead(tempRoot & "/app/models/MultiVal.cfc"); + // Both lines must be at the same 2-tab indent + expect(content).toInclude(chr(9) & chr(9) & "validatesPresenceOf"); + expect(content).toInclude(chr(9) & chr(9) & "validatesFormatOf"); + // And neither line should be at column 0 (subsequent-line bug) + expect(content).notToInclude(chr(10) & "validatesFormatOf"); + }); + }); describe("generateController()", () => {