Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/undefined-object-block-schema-variables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/theme-check-common': patch
---

Stop `UndefinedObject` from reporting schema settings used as bare variables in theme blocks. A block file can now use `{{ heading }}` for a `heading` setting without repeating it as a LiquidDoc `@param`.
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,148 @@ describe('Module: UndefinedObject', () => {
expect(offenses).toMatchObject([{ message: "Unknown object 'content' used." }]);
});

describe('block parameters in a theme block file', () => {
it('does not report a schema setting used as a bare variable', async () => {
const sourceCode = `
<h2>{{ heading }}</h2>
${schema([{ type: 'text', id: 'heading', label: 'Heading' }])}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid');

expect(offenses).toEqual([]);
});

it('does not report a schema setting and a LiquidDoc-only parameter in the same block', async () => {
const sourceCode = `
{% doc %}
@param {string} [eyebrow]
{% enddoc %}
<p>{{ eyebrow }}</p>
<h2>{{ heading }}</h2>
${schema([{ type: 'text', id: 'heading', label: 'Heading' }])}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid');

expect(offenses).toEqual([]);
});

it('does not report a name declared by both schema and LiquidDoc', async () => {
const sourceCode = `
{% doc %}
@param {string} heading
{% enddoc %}
<h2>{{ heading }}</h2>
${schema([{ type: 'text', id: 'heading', label: 'Heading' }])}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid');

expect(offenses).toEqual([]);
});

it('does not report content alongside schema and LiquidDoc variables', async () => {
const sourceCode = `
{% doc %}
@param {string} [eyebrow]
{% enddoc %}
<p>{{ eyebrow }}</p>
<h2>{{ heading }}</h2>
<div>{{ content }}</div>
${schema([{ type: 'text', id: 'heading', label: 'Heading' }])}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid');

expect(offenses).toEqual([]);
});

it('reports an unknown root alongside schema and LiquidDoc variables', async () => {
const sourceCode = `
{% doc %}
@param {string} [eyebrow]
{% enddoc %}
<p>{{ eyebrow }}</p>
<h2>{{ heading }}</h2>
<h3>{{ subtitle }}</h3>
${schema([{ type: 'text', id: 'heading', label: 'Heading' }])}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid');

expect(offenses).toMatchObject([{ message: "Unknown object 'subtitle' used." }]);
});

it('reports names from non-input schema entries and setting types', async () => {
const sourceCode = `
<div style="background: {{ background }}">
{{ layout_heading }}
{{ header }}
{{ color_background }}
</div>
${schema([
{ type: 'header', id: 'layout_heading', content: 'Layout' },
{ type: 'color_background', id: 'background', label: 'Background' },
])}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid');

expect(offenses).toMatchObject([
{ message: "Unknown object 'layout_heading' used." },
{ message: "Unknown object 'header' used." },
{ message: "Unknown object 'color_background' used." },
]);
});

it('keeps LiquidDoc parameters and content defined when the schema is invalid', async () => {
const sourceCode = `
{% doc %}
@param {string} [eyebrow]
{% enddoc %}
<p>{{ eyebrow }}</p>
<div>{{ content }}</div>
{% schema %}
{ "name": "Card", "settings": [
{% endschema %}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid');

expect(offenses).toEqual([]);
});

it('reports schema settings used as bare variables in a section file', async () => {
const sourceCode = `
<h2>{{ heading }}</h2>
${schema([{ type: 'text', id: 'heading', label: 'Heading' }])}
`;

const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'sections/hero.liquid');

expect(offenses).toMatchObject([{ message: "Unknown object 'heading' used." }]);
});

it('reports schema settings used as bare variables in an app block', async () => {
const sourceCode = `
<h2>{{ heading }}</h2>
${schema([{ type: 'text', id: 'heading', label: 'Heading' }])}
`;

const offenses = await runLiquidCheck(
UndefinedObject,
sourceCode,
'blocks/card.liquid',
{},
undefined,
'app',
);

expect(offenses).toMatchObject([{ message: "Unknown object 'heading' used." }]);
});
});

it('should not report an offense when a self defined variable is defined with a @param tag', async () => {
const sourceCode = `
{% doc %}
Expand Down Expand Up @@ -519,3 +661,7 @@ describe('Module: UndefinedObject', () => {
expect(offenses).to.be.empty;
});
});

function schema(settings: object[]): string {
return `{% schema %}${JSON.stringify({ name: 'Card', settings })}{% endschema %}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ import {
Position,
} from '@shopify/liquid-html-parser';
import { BLOCK_CONTENT_PARAMETER } from '../../block-parameters';
import { LiquidCheckDefinition, Mode, Severity, SourceCodeType, ThemeDocset } from '../../types';
import * as path from '../../path';
import { isBlock } from '../../to-schema';
import {
Context,
LiquidCheckDefinition,
Mode,
Severity,
SourceCodeType,
ThemeDocset,
} from '../../types';
import { isError, last } from '../../utils';
import { hasLiquidDoc } from '../../liquid-doc/liquidDoc';
import { isWithinRawTagThatDoesNotParseItsContents } from '../utils';
Expand Down Expand Up @@ -150,6 +159,7 @@ export const UndefinedObject: LiquidCheckDefinition = {
const objects = await globalObjects(themeDocset, relativePath, context.mode);

objects.forEach((obj) => fileScopedVariables.add(obj.name));
(await themeBlockParameterNames(context)).forEach((name) => fileScopedVariables.add(name));

variables.forEach((variable) => {
if (!variable.name) return;
Expand Down Expand Up @@ -194,6 +204,20 @@ function builtInVariables(relativePath: string): string[] {
return relativePath.startsWith('blocks/') ? [BLOCK_CONTENT_PARAMETER] : [];
}

/**
* A theme block's schema settings and LiquidDoc parameters are also plain
* variables in the block file. Returns no names when the block's parameters
* cannot be resolved, such as when its schema is invalid.
*/
async function themeBlockParameterNames(
context: Context<SourceCodeType.LiquidHtml>,
): Promise<string[]> {
if (!isBlock(context.file.uri)) return [];

const parameters = await context.getBlockParameters(path.basename(context.file.uri, '.liquid'));
return [...(parameters?.keys() ?? [])];
}

const BLOCK_CONTEXTUAL_OBJECTS = ['app', 'section', 'recommendations', 'block'];

function getContextualObjects(relativePath: string, mode: Mode = 'theme'): string[] {
Expand Down
Loading