diff --git a/packages/host/app/tools/patch-code.ts b/packages/host/app/tools/patch-code.ts index 8f4583064a9..0f0b299225d 100644 --- a/packages/host/app/tools/patch-code.ts +++ b/packages/host/app/tools/patch-code.ts @@ -66,32 +66,52 @@ export default class PatchCodeTool extends HostBaseTool< lintIssues = lintResult.lintIssues ?? []; } - finalFileIdentifier = await this.determineFinalFileUrl( - fileUrl, - fileInfo, - hasEmptySearchPortion, - ); + // An applied patch always changed the pre-lint content, so ending up + // back at the original file means the autofix/format pass reverted + // the whole edit. Re-patching can never make progress from here — + // any further attempt round-trips to this same content — so say so + // alongside the lint issues instead of writing a no-op save that + // would keep the model trying. + let revertedByFormatter = + sourceContent !== '' && patchedCode === sourceContent; + if (revertedByFormatter) { + lintIssues = [ + ...lintIssues, + 'The automatic formatter reverted the applied changes, so the file is unchanged. Reformatting-only patches cannot fix the remaining issues; change the content itself or leave it as is.', + ]; + } else { + finalFileIdentifier = await this.determineFinalFileUrl( + fileUrl, + fileInfo, + hasEmptySearchPortion, + ); - let clientRequestId = this.toolService.trackAiAssistantCardRequest({ - action: 'patch-code', - roomId, - fileUrl: finalFileIdentifier, - }); + let clientRequestId = this.toolService.trackAiAssistantCardRequest({ + action: 'patch-code', + roomId, + fileUrl: finalFileIdentifier, + }); - let savedThroughOpenFile = await this.trySaveThroughOpenFile( - finalFileIdentifier, - patchedCode, - clientRequestId, - ); - if (!savedThroughOpenFile) { - this.cardService - .saveSource(new URL(finalFileIdentifier), patchedCode, 'bot-patch', { - resetLoader: hasExecutableExtension(finalFileIdentifier), - clientRequestId, - }) - .catch((error: unknown) => { - console.error('PatchCodeTool: failed to save source', error); - }); + let savedThroughOpenFile = await this.trySaveThroughOpenFile( + finalFileIdentifier, + patchedCode, + clientRequestId, + ); + if (!savedThroughOpenFile) { + this.cardService + .saveSource( + new URL(finalFileIdentifier), + patchedCode, + 'bot-patch', + { + resetLoader: hasExecutableExtension(finalFileIdentifier), + clientRequestId, + }, + ) + .catch((error: unknown) => { + console.error('PatchCodeTool: failed to save source', error); + }); + } } } diff --git a/packages/realm-server/tests/realm-endpoints/lint-test.ts b/packages/realm-server/tests/realm-endpoints/lint-test.ts index 93ad5e08ebc..93a85f8cd9b 100644 --- a/packages/realm-server/tests/realm-endpoints/lint-test.ts +++ b/packages/realm-server/tests/realm-endpoints/lint-test.ts @@ -621,6 +621,46 @@ export class MyCard extends CardDef { ); }); + test('does not flag the whitespace prettier introduces by wrapping long text', async function (assert) { + // Prettier wraps text nodes longer than its print width across + // indented lines; no-whitespace-for-layout flags exactly that wrap and + // has no autofix, so reporting it would hand back an issue no edit can + // clear — the formatter reverts every attempted whitespace fix. + let longText = + 'WELCOME TO MY HOMEPAGE     HOT SITE AWARD WINNER     BEST VIEWED IN 800x600    '; + let response = await request + .post('/_lint') + .set( + 'Authorization', + `Bearer ${createJWT(testRealm, 'john', ['read', 'write'])}`, + ) + .set('X-HTTP-Method-Override', 'QUERY') + .set('Accept', 'application/json') + .send(`import { CardDef } from '@cardstack/base/card-api'; +export class MyCard extends CardDef { +} + +`); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let responseJson = JSON.parse(response.text); + assert.ok( + /[^<]*\n[^<]*<\/span>/.test(responseJson.output), + 'prettier wrapped the long text node across lines', + ); + let whitespaceMessage = responseJson.messages.find( + (m: any) => m.ruleId === 'no-whitespace-for-layout', + ); + assert.notOk( + whitespaceMessage, + 'no-whitespace-for-layout is not reported for formatter-introduced wrapping', + ); + }); + test('handles nested template structures correctly', async function (assert) { let response = await request .post('/_lint') diff --git a/packages/runtime-common/tasks/lint.ts b/packages/runtime-common/tasks/lint.ts index 4505ed47460..21abc922c1a 100644 --- a/packages/runtime-common/tasks/lint.ts +++ b/packages/runtime-common/tasks/lint.ts @@ -155,7 +155,24 @@ async function initTemplateLinter(): Promise { const hostRequire = Module.createRequire(resolve(HOST_PKG, 'package.json')); const tlModule = hostRequire('ember-template-lint'); const TemplateLinter = (tlModule as any).default ?? tlModule; - return new TemplateLinter({ workingDir: HOST_PKG }); + // Host's config, minus rules the rest of this pipeline contradicts. + // Prettier (the pass right before template-lint) wraps text nodes longer + // than its print width across indented lines; no-whitespace-for-layout + // flags exactly that wrap and has no autofix, so for any over-width text + // node the pipeline would report an error that no edit can clear — the + // formatter reverts every attempted whitespace fix. The wrapped whitespace + // collapses at render, so nothing is lost by not flagging it. + const hostTemplateLintConfig = hostRequire('./.template-lintrc.js'); + return new TemplateLinter({ + workingDir: HOST_PKG, + config: { + ...hostTemplateLintConfig, + rules: { + ...hostTemplateLintConfig.rules, + 'no-whitespace-for-layout': false, + }, + }, + }); } // ---------------------------------------------------------------------------