From 2809035d04a8ffcb227f16927faa843775425633 Mon Sep 17 00:00:00 2001 From: "Charles-P. Clermont" Date: Wed, 30 Sep 2026 10:59:25 -0400 Subject: [PATCH] Add block settings migration diagnostic --- .changeset/block-settings-caller-migration.md | 5 ++ .../src/checks/liquid-syntax-error/block.ts | 22 ++++- .../checks/liquid-syntax-error/index.spec.ts | 85 +++++++++++++++---- 3 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 .changeset/block-settings-caller-migration.md diff --git a/.changeset/block-settings-caller-migration.md b/.changeset/block-settings-caller-migration.md new file mode 100644 index 000000000..6cd62bb44 --- /dev/null +++ b/.changeset/block-settings-caller-migration.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-common': patch +--- + +Tell authors to replace `block.settings.:` with `:` when calling a `block`. diff --git a/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts b/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts index 38ad7ba67..84c7126f1 100644 --- a/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts +++ b/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts @@ -52,7 +52,9 @@ export function blockTagSyntaxError( * Report the first one so body-form children stay outside the offense. */ const dottedArgument = markup.args.find(isDottedArgument); - if (dottedArgument) return syntaxProblem(dottedArgument.position, DOTTED_ARGUMENT); + if (dottedArgument) { + return syntaxProblem(dottedArgument.position, dottedArgumentMessage(dottedArgument.name)); + } if (markup.args.some(isInvalidBlockNameArgument)) { return syntaxProblem(node.position, SYNTAX_ERROR); @@ -109,3 +111,21 @@ function isInvalidBlockNameArgument(argument: BlockMarkup['args'][number]): bool function isDottedArgument(argument: BlockMarkup['args'][number]): boolean { return argument.name !== 'block.name' && argument.name.includes('.'); } + +/* + * Temporary migration guidance for the experimental block.settings. + * caller form. Remove it once authors have migrated to plain arguments. + */ +function dottedArgumentMessage(name: string): string { + const settingId = blockSettingId(name); + if (!settingId) return DOTTED_ARGUMENT; + + return `Liquid syntax error: in 'block' - Use '${settingId}:' instead of '${name}:' when calling a block.`; +} + +function blockSettingId(name: string): string | undefined { + const [object, property, settingId, ...rest] = name.split('.'); + if (object !== 'block' || property !== 'settings' || rest.length > 0) return undefined; + + return settingId; +} diff --git a/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts index 5c5abee28..cd52be330 100644 --- a/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts +++ b/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts @@ -18,6 +18,10 @@ const BARE_BRACKET_ACCESS = 'Bare bracket access is not allowed in strict2 mode' const DOTTED_ARGUMENT = "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value"; +function blockSettingArgumentMessage(settingId: string) { + return `Liquid syntax error: in 'block' - Use '${settingId}:' instead of 'block.settings.${settingId}:' when calling a block.`; +} + const UNCLOSED_DOC_PARSER_ERROR = "Attempting to end parsing before LiquidRawTag 'doc' was closed"; const UNOPENED_DOC_PARSER_ERROR = "Attempting to close LiquidTag 'doc' before it was opened without a matching 'doc'"; @@ -811,12 +815,9 @@ describe('LiquidSyntaxError', () => { describe('block caller arguments', () => { it.each([ - "block.settings.heading: 'Heading'", - 'block.content: body', - "block.settings.heading.label: 'Heading'", - "block.unknown: 'value'", - "heading.label: 'Heading'", - ])('rejects the unsupported dotted argument %s', async (argument) => { + ['heading', "block.settings.heading: 'Heading'"], + ['image_url', 'block.settings.image_url: product.featured_image'], + ])('tells authors to pass %s as a plain argument', async (settingId, argument) => { const template = `{% block 'card', ${argument} %}{% endblock %}`; const offenses = await runLiquidCheck( LiquidSyntaxError, @@ -827,16 +828,20 @@ describe('LiquidSyntaxError', () => { expect(offenses).toMatchObject([ { - message: DOTTED_ARGUMENT, + message: blockSettingArgumentMessage(settingId), start: { index: template.indexOf(argument) }, end: { index: template.indexOf(argument) + argument.length }, }, ]); }); - it('reports only the dotted argument in a body-form block', async () => { - const argument = "block.settings.heading: 'Heading'"; - const template = `{% block 'card', ${argument} %}\n Child content\n{% endblock %}`; + it.each([ + 'block.content: body', + "block.settings.heading.label: 'Heading'", + "block.unknown: 'value'", + "heading.label: 'Heading'", + ])('rejects the unsupported dotted argument %s', async (argument) => { + const template = `{% block 'card', ${argument} %}{% endblock %}`; const offenses = await runLiquidCheck( LiquidSyntaxError, template, @@ -853,9 +858,9 @@ describe('LiquidSyntaxError', () => { ]); }); - it('reports only the first of several dotted arguments', async () => { - const first = "block.settings.heading: 'Heading'"; - const template = `{% block 'card', title: 'Title', ${first}, block.content: body, heading.label: 'Label' %}Child{% endblock %}`; + it('reports only the dotted argument in a body-form block', async () => { + const argument = "block.settings.heading: 'Heading'"; + const template = `{% block 'card', ${argument} %}\n Child content\n{% endblock %}`; const offenses = await runLiquidCheck( LiquidSyntaxError, template, @@ -865,13 +870,50 @@ describe('LiquidSyntaxError', () => { expect(offenses).toMatchObject([ { - message: DOTTED_ARGUMENT, - start: { index: template.indexOf(first) }, - end: { index: template.indexOf(first) + first.length }, + message: blockSettingArgumentMessage('heading'), + start: { index: template.indexOf(argument) }, + end: { index: template.indexOf(argument) + argument.length }, }, ]); }); + it.each([ + [ + "block.settings.heading: 'Heading'", + "block.content: body, heading.label: 'Label'", + blockSettingArgumentMessage('heading'), + ], + [ + 'block.content: body', + "block.settings.heading: 'Heading', block.settings.count: 1", + DOTTED_ARGUMENT, + ], + [ + "block.settings.heading: 'Heading'", + 'block.settings.count: 1', + blockSettingArgumentMessage('heading'), + ], + ])( + 'reports only the first of several dotted arguments in %s, %s', + async (first, rest, message) => { + const template = `{% block 'card', title: 'Title', ${first}, ${rest} %}Child{% endblock %}`; + const offenses = await runLiquidCheck( + LiquidSyntaxError, + template, + 'templates/test.liquid', + NO_DOCSET, + ); + + expect(offenses).toMatchObject([ + { + message, + start: { index: template.indexOf(first) }, + end: { index: template.indexOf(first) + first.length }, + }, + ]); + }, + ); + it('accepts block.name with a string value alongside plain arguments', async () => { const offenses = await runLiquidCheck( LiquidSyntaxError, @@ -941,12 +983,19 @@ describe('LiquidSyntaxError', () => { }); it.each([ - ["block.settings.heading: 'Hello'", DOTTED_ARGUMENT], + ["block.settings.heading: 'Hello'", blockSettingArgumentMessage('heading')], + ['block.settings.count: 1', blockSettingArgumentMessage('count')], ['block.content: body', DOTTED_ARGUMENT], + ["block.settings.heading.label: 'Hello'", DOTTED_ARGUMENT], ["heading.label: 'Hello'", DOTTED_ARGUMENT], ['block.name: value', "Syntax error in 'block' tag"], ["block.name: value, heading.label: 'Hello'", DOTTED_ARGUMENT], - ["block.settings.heading: 'Hello', block.content: body", DOTTED_ARGUMENT], + ['block.name: value, block.settings.count: 1', blockSettingArgumentMessage('count')], + [ + "block.settings.heading: 'Hello', block.content: body", + blockSettingArgumentMessage('heading'), + ], + ["block.content: body, block.settings.heading: 'Hello'", DOTTED_ARGUMENT], ])('reports only one syntax error for %s', async (argument, message) => { const offenses = await checkBlockCall(`${argument}, count: 'many', count: 'more'`);