diff --git a/.changeset/remote-file-size-dependency.md b/.changeset/remote-file-size-dependency.md new file mode 100644 index 000000000..eca7b3936 --- /dev/null +++ b/.changeset/remote-file-size-dependency.md @@ -0,0 +1,5 @@ +--- +'@shopify/theme-check-common': minor +--- + +Add an optional `remoteFileSize` dependency: the size of a remote file, such as a script on a CDN. AssetSizeCSS and AssetSizeJavaScript use it, and by default it makes the same HEAD request as before. diff --git a/packages/theme-check-common/src/checks/app-block-missing-schema/index.spec.ts b/packages/theme-check-common/src/checks/app-block-missing-schema/index.spec.ts deleted file mode 100644 index f3bc4180d..000000000 --- a/packages/theme-check-common/src/checks/app-block-missing-schema/index.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { AppBlockMissingSchema } from './index'; - -describe('AppBlockMissingSchema', () => { - it('should report an error when schema tag is missing on a theme app extension (blocks only)', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck( - AppBlockMissingSchema, - sourceCode, - 'blocks/footer.liquid', - ); - expect(offenses).to.have.lengthOf(1); - }); - - it('should not report an error when schema tag is missing on a snippet in a theme app extension )', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck( - AppBlockMissingSchema, - sourceCode, - 'snippets/footer.liquid', - ); - expect(offenses).to.have.lengthOf(0); - }); - - it('should not report when the schema is present on a theme app extension', async () => { - const sourceCode = ` - - -{% schema %} -{ - "name": "Footer", - "max_blocks": 8, - "blocks": [ - { - "type": "link", - "name": "Link", - "settings": [ - { - "id": "linkurl", - "type": "url", - "label": "URL link" - }, - { - "id": "linktext", - "type": "text", - "label": "Link text" - } - ] - }, - { - "type": "custom-text", - "name": "Custom Text", - "settings": [ - { - "id": "custom-text-field", - "type": "textarea", - "label": "Text" - } - ] - } - ] -} -{% endschema %}`; - - const offenses = await runLiquidCheck( - AppBlockMissingSchema, - sourceCode, - 'blocks/footer.liquid', - ); - expect(offenses).to.have.lengthOf(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts b/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts index 8d87f9c76..5b8f4bec6 100644 --- a/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts +++ b/packages/theme-check-common/src/checks/app-block-valid-tags/index.spec.ts @@ -1,84 +1,7 @@ import { describe, it, expect } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { ForbiddenTag, AppBlockValidTags } from './index'; - -const blocksFilePath = 'blocks/app.liquid'; +import { ForbiddenTag } from './index'; describe('Module: AppBlockValidTags', () => { - it('should report an offense when the forbidden tag is used in blocks directory liquid file', async () => { - const tags = ['include', 'layout', 'section', 'sections']; - for (const tag of tags) { - const sourceCode = ` - {% ${tag} 'test' %} - {% schema %} - { } - {% endschema %} - `; - - const offenses = await runLiquidCheck(AppBlockValidTags, sourceCode, blocksFilePath); - - const expectedStart = { - index: 9, - line: 1, - character: 8, - }; - const tagEndOffset = 13; - const endOffset = tag.length + tagEndOffset; - - const expectedEnd = { - index: expectedStart.index + endOffset, - line: expectedStart.line, - character: expectedStart.character + endOffset, - }; - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: AppBlockValidTags.meta.code, - message: `Theme app extension blocks cannot contain '${tag}' tags`, - uri: 'file:///blocks/app.liquid', - severity: 0, - start: expectedStart, - end: expectedEnd, - }); - } - }); - - it('should report an offense when the forbidden tag is used with an end tag in blocks directory liquid file', async () => { - const tags = ['javascript', 'stylesheet']; - for (const tag of tags) { - const sourceCode = ` - {% ${tag} %} - {% end${tag} %} - {% schema %} - { } - {% endschema %} - `; - - const offenses = await runLiquidCheck(AppBlockValidTags, sourceCode, blocksFilePath); - - const expectedStart = { - index: 9, - line: 1, - character: 8, - }; - const expectedEnd = { - index: expectedStart.index + tag.length + 34, - line: 2, - character: expectedStart.character + tag.length + 9, - }; - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: AppBlockValidTags.meta.code, - message: `Theme app extension blocks cannot contain '${tag}' tags`, - uri: 'file:///blocks/app.liquid', - severity: 0, - start: expectedStart, - end: expectedEnd, - }); - } - }); - it('should contain specifically the following forbidden tags', async () => { const actualTagValues = Object.values(ForbiddenTag); const expectedTagValues = [ diff --git a/packages/theme-check-common/src/checks/asset-preload/index.spec.ts b/packages/theme-check-common/src/checks/asset-preload/index.spec.ts deleted file mode 100644 index b2e065a83..000000000 --- a/packages/theme-check-common/src/checks/asset-preload/index.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, highlightedOffenses } from '../../test'; -import { AssetPreload } from './index'; - -describe('Module: AssetPreload', () => { - it('no offense with link element', async () => { - const sourceCode = ` - - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(0); - }); - - it('reports stylesheet preloading', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal( - 'For better performance, prefer using the preload argument of the stylesheet_tag filter', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([``]); - }); - - it('reports image preloading', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal( - 'For better performance, prefer using the preload argument of the image_tag filter', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([``]); - }); - - it('allows high-priority image preloading', async () => { - // The preload_tag filter cannot generate responsive imagesrcset or - // imagesizes attributes, so LCP images may require an explicit - // high-priority preload link. - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(0); - }); - - it('reports image preloading when attribute values contain Liquid', async () => { - const sourceCode = ` - - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(2); - }); - - it('reports general preloading', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal( - 'For better performance, prefer using the preload_tag filter', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([``]); - }); - - it('should report offenses for manual preloading of assets', async () => { - const sourceCode = ` - - - - `; - - const offenses = await runLiquidCheck(AssetPreload, sourceCode); - expect(offenses).to.have.lengthOf(3); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([ - ``, - ``, - ``, - ]); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-app-block-css/index.spec.ts b/packages/theme-check-common/src/checks/asset-size-app-block-css/index.spec.ts deleted file mode 100644 index 744247871..000000000 --- a/packages/theme-check-common/src/checks/asset-size-app-block-css/index.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { AssetSizeAppBlockCSS } from '.'; -import { check, MockTheme } from '../../test'; - -describe('Module: AssetSizeAppBlockCSS', () => { - const extensionFiles: MockTheme = { - 'assets/app.css': '* { color: green } ', - 'blocks/app.liquid': ` - {% schema %} - { - "stylesheet": "app.css" - } - {% endschema %} - `, - }; - - it('should not report any offenses if CSS is smaller than threshold', async () => { - const offenses = await check(extensionFiles, [AssetSizeAppBlockCSS]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if CSS is larger than threshold', async () => { - const offenses = await check( - extensionFiles, - [AssetSizeAppBlockCSS], - {}, - { - AssetSizeAppBlockCSS: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `The file size for 'app.css' (19 B) exceeds the configured threshold (1 B)`, - uri: 'file:///blocks/app.liquid', - start: { index: 51 }, - end: { index: 58 }, - }); - }); - - it('should report an offense if the CSS file does not exist', async () => { - const extensionFiles: MockTheme = { - 'blocks/app.liquid': ` - {% schema %} - { - "stylesheet": "nonexistent.css" - } - {% endschema %} - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeAppBlockCSS]); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `'nonexistent.css' does not exist.`, - uri: 'file:///blocks/app.liquid', - start: { index: 57 }, - end: { index: 72 }, - }); - }); - - it('should reports offense if the CSS file does not exist and the asset has a trailing comma', async () => { - const extensionFiles: MockTheme = { - 'blocks/app.liquid': ` - {% schema %} - { - "stylesheet": "nonexistent.css", - } - {% endschema %} - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeAppBlockCSS]); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `'nonexistent.css' does not exist.`, - uri: 'file:///blocks/app.liquid', - start: { index: 57 }, - end: { index: 72 }, - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-app-block-javascript/index.spec.ts b/packages/theme-check-common/src/checks/asset-size-app-block-javascript/index.spec.ts deleted file mode 100644 index b5b91e6f5..000000000 --- a/packages/theme-check-common/src/checks/asset-size-app-block-javascript/index.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { AssetSizeAppBlockJavaScript } from '.'; -import { check, MockTheme } from '../../test'; - -describe('Module: AssetSizeAppBlockJavaScript', () => { - const extensionFiles: MockTheme = { - 'assets/app.js': 'console.log("Hello, world!");', - 'blocks/app.liquid': ` - {% schema %} - { - "javascript": "app.js" - } - {% endschema %} - `, - }; - - it('should not report any offenses if JavaScript is smaller than threshold', async () => { - const offenses = await check(extensionFiles, [AssetSizeAppBlockJavaScript]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if JavaScript is larger than threshold', async () => { - const offenses = await check( - extensionFiles, - [AssetSizeAppBlockJavaScript], - {}, - { - AssetSizeAppBlockJavaScript: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `The file size for 'app.js' (29 B) exceeds the configured threshold (1 B)`, - uri: 'file:///blocks/app.liquid', - start: { index: 51 }, - end: { index: 57 }, - }); - }); - - it('should report an offense if the JavaScript file does not exist', async () => { - const extensionFiles: MockTheme = { - 'blocks/app.liquid': ` - {% schema %} - { - "javascript": "nonexistent.js" - } - {% endschema %} - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeAppBlockJavaScript]); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: `'nonexistent.js' does not exist.`, - uri: 'file:///blocks/app.liquid', - start: { index: 57 }, - end: { index: 71 }, - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-css/index.spec.ts b/packages/theme-check-common/src/checks/asset-size-css/index.spec.ts deleted file mode 100644 index eb4382ae0..000000000 --- a/packages/theme-check-common/src/checks/asset-size-css/index.spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { vi, expect, describe, it, afterEach } from 'vitest'; -import { AssetSizeCSS } from '.'; -import { check, MockTheme } from '../../test'; -import { SchemaProp } from '../../types'; -import { hasRemoteAssetSizeExceededThreshold } from '../../utils/file-utils'; - -vi.mock('../../utils/file-utils', async (importOriginal) => { - const actual: any = await importOriginal(); - return { - ...actual, - hasRemoteAssetSizeExceededThreshold: vi.fn(), - }; -}); - -describe('Module: AssetSizeCSS', () => { - const extensionFiles: MockTheme = { - 'assets/theme.css': '* { color: green !important; }', - 'templates/index.liquid': ` - - - - - - `, - }; - - const httpTest: MockTheme = { - 'templates/index.liquid': ` - - - - - - `, - }; - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should not report any offenses if CSS is smaller than threshold', async () => { - const offenses = await check(extensionFiles, [AssetSizeCSS]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if CSS is larger than threshold', async () => { - const offenses = await check( - extensionFiles, - [AssetSizeCSS], - {}, - { - AssetSizeCSS: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 1 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 51 }, - end: { index: 80 }, - }); - }); - - it('should report a warning when the CSS file size exceeds the threshold', async () => { - vi.mocked(hasRemoteAssetSizeExceededThreshold).mockReturnValue(Promise.resolve(true)); - const offenses = await check( - httpTest, - [AssetSizeCSS], - {}, - { - AssetSizeCSS: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 1 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 51 }, - end: { index: 122 }, - }); - }); - - it('should not report any offenses if CSS is smaller than threshold 2', async () => { - vi.mocked(hasRemoteAssetSizeExceededThreshold).mockResolvedValue(false); - const extensionFiles: MockTheme = { - 'assets/theme.css': 'console.log("hello world");', - 'templates/index.liquid': ` - - - {{ 'theme.css' | asset_url | stylesheet_tag }} - {{ "https://example.com" | stylesheet_tag }} - - - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeCSS]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if CSS is larger than threshold 2', async () => { - vi.mocked(hasRemoteAssetSizeExceededThreshold).mockResolvedValue(true); - const extensionFiles: MockTheme = { - 'assets/theme.css': 'console.log("hello world");', - 'templates/index.liquid': ` - - - {{ 'theme.css' | asset_url | stylesheet_tag }} - {{ "https://example.com" | stylesheet_tag }} - - - `, - }; - - const offenses = await check( - extensionFiles, - [AssetSizeCSS], - {}, - { - AssetSizeCSS: { - enabled: true, - thresholdInBytes: 2, - }, - }, - ); - - expect(offenses).toHaveLength(2); - expect(offenses[0]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 2 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 48 }, - end: { index: 89 }, - }); - expect(offenses[1]).toMatchObject({ - message: 'The CSS file size exceeds the threshold of 2 bytes', - uri: 'file:///templates/index.liquid', - start: { index: 107 }, - end: { index: 128 }, - }); - }); - - it('should not report any offenses if there is no stylesheet', async () => { - const extensionFiles: MockTheme = { - 'templates/index.liquid': ` - - - - - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeCSS]); - - expect(offenses).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-css/index.ts b/packages/theme-check-common/src/checks/asset-size-css/index.ts index 93796effd..7a8942c5e 100644 --- a/packages/theme-check-common/src/checks/asset-size-css/index.ts +++ b/packages/theme-check-common/src/checks/asset-size-css/index.ts @@ -64,7 +64,7 @@ export const AssetSizeCSS: LiquidCheckDefinition = { const thresholdInBytes = context.settings.thresholdInBytes; async function checkRemoteAssetSize(url: string, position: { start: number; end: number }) { - if (await hasRemoteAssetSizeExceededThreshold(url, thresholdInBytes)) { + if (await hasRemoteAssetSizeExceededThreshold(context, url, thresholdInBytes)) { context.report({ message: `The CSS file size exceeds the threshold of ${thresholdInBytes} bytes`, startIndex: position.start, diff --git a/packages/theme-check-common/src/checks/asset-size-javascript/index.spec.ts b/packages/theme-check-common/src/checks/asset-size-javascript/index.spec.ts deleted file mode 100644 index 36bb3f230..000000000 --- a/packages/theme-check-common/src/checks/asset-size-javascript/index.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { expect, describe, it, afterEach, vi } from 'vitest'; -import { AssetSizeJavaScript } from '.'; -import { check, MockTheme } from '../../test'; -import { SchemaProp } from '../../types'; -import { hasRemoteAssetSizeExceededThreshold } from '../../utils/file-utils'; - -vi.mock('../../utils/file-utils', async (importOriginal) => { - const actual: any = await importOriginal(); - return { - ...actual, - hasRemoteAssetSizeExceededThreshold: vi.fn(), - }; -}); - -describe('Module: AssetSizeJavaScript', () => { - const theme: MockTheme = { - 'assets/theme.js': "console.log('hello world'); console.log('Oh. Hi Mark!')", - 'templates/index.liquid': ` - - - - - - `, - }; - - const httpTest: MockTheme = { - 'templates/index.liquid': ` - - - - - - `, - }; - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should not find file size for invalid URLs', async () => { - const invalidUrls = [ - 'https://{{ settings.url }}', - "{{ 'this_file_does_not_exist.js' | asset_url }}", - '{% if on_product %}https://hello.world{% else %}https://hi.world{% endif %}', - ]; - - for (const url of invalidUrls) { - const offenses = await check( - { 'templates/index.liquid': `` }, - [AssetSizeJavaScript], - ); - expect(offenses).toHaveLength(0); - } - }); - - it('should report a warning when the JavaScript http request exceeds the threshold', async () => { - vi.mocked(hasRemoteAssetSizeExceededThreshold).mockReturnValue(Promise.resolve(true)); - const offenses = await check( - httpTest, - [AssetSizeJavaScript], - {}, - { - AssetSizeJavaScript: { - enabled: true, - thresholdInBytes: 1, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: - 'JavaScript on every page load exceeds compressed size threshold (1 Bytes), consider using the import on interaction pattern.', - uri: 'file:///templates/index.liquid', - start: { index: 52 }, - end: { index: 121 }, - }); - }); - - it('should not report any offenses if JavaScript is smaller than threshold', async () => { - const offenses = await check(theme, [AssetSizeJavaScript]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if JavaScript is larger than threshold', async () => { - const offenses = await check( - theme, - [AssetSizeJavaScript], - {}, - { - AssetSizeJavaScript: { - enabled: true, - thresholdInBytes: 2, - }, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - message: - 'JavaScript on every page load exceeds compressed size threshold (2 Bytes), consider using the import on interaction pattern.', - uri: 'file:///templates/index.liquid', - start: { index: 52 }, - end: { index: 80 }, - }); - }); - - it('should not report any offenses for inline JavaScript', async () => { - const inlineTheme: MockTheme = { - 'templates/index.liquid': ` - - - - - - `, - }; - - const offenses = await check(inlineTheme, [AssetSizeJavaScript]); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense if JS is larger than threshold', async () => { - const extensionFiles: MockTheme = { - 'assets/theme.js': 'console.log("hello world");', - 'templates/index.liquid': ` - - - {{ 'theme.js' | asset_url | script_tag }} - {{ "https://example.com" | script_tag }} - - - `, - }; - - const offenses = await check( - extensionFiles, - [AssetSizeJavaScript], - {}, - { - AssetSizeJavaScript: { - enabled: true, - thresholdInBytes: 2, - }, - }, - ); - - expect(offenses).toHaveLength(2); - expect(offenses[0]).toMatchObject({ - message: - 'JavaScript on every page load exceeds compressed size threshold (2 Bytes), consider using the import on interaction pattern.', - uri: 'file:///templates/index.liquid', - start: { index: 48 }, - end: { index: 84 }, - }); - expect(offenses[1]).toMatchObject({ - message: - 'JavaScript on every page load exceeds compressed size threshold (2 Bytes), consider using the import on interaction pattern.', - uri: 'file:///templates/index.liquid', - start: { index: 102 }, - end: { index: 123 }, - }); - }); - - it('should not report any offenses if there is no javascript', async () => { - const extensionFiles: MockTheme = { - 'templates/index.liquid': ` - - - - - `, - }; - - const offenses = await check(extensionFiles, [AssetSizeJavaScript]); - - expect(offenses).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/asset-size-javascript/index.ts b/packages/theme-check-common/src/checks/asset-size-javascript/index.ts index f75d4e75e..b07159f28 100644 --- a/packages/theme-check-common/src/checks/asset-size-javascript/index.ts +++ b/packages/theme-check-common/src/checks/asset-size-javascript/index.ts @@ -62,7 +62,7 @@ export const AssetSizeJavaScript: LiquidCheckDefinition = { const thresholdInBytes = context.settings.thresholdInBytes; async function checkRemoteAssetSize(url: string, position: { start: number; end: number }) { - if (await hasRemoteAssetSizeExceededThreshold(url, thresholdInBytes)) { + if (await hasRemoteAssetSizeExceededThreshold(context, url, thresholdInBytes)) { context.report({ message: `JavaScript on every page load exceeds compressed size threshold (${thresholdInBytes} Bytes), consider using the import on interaction pattern.`, startIndex: position.start, diff --git a/packages/theme-check-common/src/checks/block-content-usage/index.spec.ts b/packages/theme-check-common/src/checks/block-content-usage/index.spec.ts index 31ef96fd2..35f84088d 100644 --- a/packages/theme-check-common/src/checks/block-content-usage/index.spec.ts +++ b/packages/theme-check-common/src/checks/block-content-usage/index.spec.ts @@ -1,63 +1,11 @@ import { describe, expect, it } from 'vitest'; import { recommended } from '../index'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; import { Severity } from '../../types'; import { BlockContentUsage } from './index'; -const MESSAGE = "Use the implicit 'content' parameter directly instead of 'block.content'."; - describe('BlockContentUsage', () => { it('is a recommended error', () => { expect(recommended).toContain(BlockContentUsage); expect(BlockContentUsage.meta.severity).toBe(Severity.ERROR); }); - - it.each([ - ['{{ block.content }}', 'block.content'], - ["{{ block['content'] | upcase }}", "block['content']"], - ['{{ block.content.size }}', 'block.content.size'], - ['{% if block.content != blank %}{{ content }}{% endif %}', 'block.content'], - ["{% render 'card', body: block.content %}", 'block.content'], - ['{% liquid\n echo block.content\n%}', 'block.content'], - ])('reports the block.content lookup in %j', async (source, highlight) => { - const offenses = await runLiquidCheck(BlockContentUsage, source, 'blocks/card.liquid'); - - expect(offenses).toMatchObject([{ message: MESSAGE, severity: Severity.ERROR }]); - expect(highlightedOffenses({ 'blocks/card.liquid': source }, offenses)).toEqual([highlight]); - }); - - it('accepts bare content and other block properties', async () => { - const source = [ - '{{ content }}', - '{{ block.id }}', - '{{ block.settings.content }}', - '{{ block.shopify_attributes }}', - '{{ product.content }}', - ].join('\n'); - - const offenses = await runLiquidCheck(BlockContentUsage, source, 'blocks/card.liquid'); - - expect(offenses).toEqual([]); - }); - - it.each([ - 'sections/main.liquid', - 'snippets/card.liquid', - 'templates/index.liquid', - 'layout/theme.liquid', - ])('does not report the ordinary block object in %s', async (fileName) => { - const source = '{% for block in section.blocks %}{{ block.content }}{% endfor %}'; - - const offenses = await runLiquidCheck(BlockContentUsage, source, fileName); - - expect(offenses).toEqual([]); - }); - - it('does not report block.content inside a tag that does not render Liquid', async () => { - const source = '{% javascript %}console.log("{{ block.content }}");{% endjavascript %}'; - - const offenses = await runLiquidCheck(BlockContentUsage, source, 'blocks/card.liquid'); - - expect(offenses).toEqual([]); - }); }); diff --git a/packages/theme-check-common/src/checks/block-id-usage/index.spec.ts b/packages/theme-check-common/src/checks/block-id-usage/index.spec.ts deleted file mode 100644 index cb9efa08d..000000000 --- a/packages/theme-check-common/src/checks/block-id-usage/index.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { BlockIdUsage } from './index'; - -describe('Module: ContentForHeaderModification', () => { - it('reports offense with the use of block.id in an if liquid tag', async () => { - const sourceCode = ` - {% if block.id == '123' %} - No bueno - {% endif %} - `; - - const offenses = await runLiquidCheck(BlockIdUsage, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - 'The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID.', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(["block.id == '123'"]); - }); - - it('reports offense with the use of block.id in an elseif liquid tag', async () => { - const sourceCode = ` - {% if potato == '123' %} - No bueno - {% elsif block.id == '123' %} - No bueno - {% endif %} - `; - - const offenses = await runLiquidCheck(BlockIdUsage, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - 'The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID.', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(["block.id == '123'"]); - }); - - it('reports offense with the use of block.id in an unless liquid tag', async () => { - const sourceCode = ` - {% unless block.id == '123' %} - No bueno - {% endunless %} - `; - - const offenses = await runLiquidCheck(BlockIdUsage, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - 'The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID.', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(["block.id == '123'"]); - }); - - it('reports offense with the use of block.id in a case liquid tag', async () => { - const sourceCode = ` - {% case block.id %} - {% when '123' %} - No bueno - {% endcase %} - `; - - const offenses = await runLiquidCheck(BlockIdUsage, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - 'The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID.', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['block.id']); - }); -}); diff --git a/packages/theme-check-common/src/checks/cdn-preconnect/index.spec.ts b/packages/theme-check-common/src/checks/cdn-preconnect/index.spec.ts deleted file mode 100644 index 2832b4cdb..000000000 --- a/packages/theme-check-common/src/checks/cdn-preconnect/index.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { CdnPreconnect } from './index'; - -describe('Module: CdnPreconnect', () => { - it('should report preconnect to cdn.shopify.com', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(CdnPreconnect, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Preconnecting to cdn.shopify.com is unnecessary and can lead to worse performance', - ); - }); - - it('should report crossorigin preconnect to cdn.shopify.com', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(CdnPreconnect, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Preconnecting to cdn.shopify.com is unnecessary and can lead to worse performance', - ); - }); - - it('should not report other link types', async () => { - const sourceCode = ` - - - - `; - - const offenses = await runLiquidCheck(CdnPreconnect, sourceCode); - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/content-for-header-modification/index.spec.ts b/packages/theme-check-common/src/checks/content-for-header-modification/index.spec.ts deleted file mode 100644 index 55bf67827..000000000 --- a/packages/theme-check-common/src/checks/content-for-header-modification/index.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { ContentForHeaderModification } from './index'; - -describe('Module: ContentForHeaderModification', () => { - it('reports a offense with the use of a filter', async () => { - const sourceCode = "{{ content_for_header | split: ',' }}"; - - const offenses = await runLiquidCheck(ContentForHeaderModification, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('Do not rely on the content of `content_for_header`'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - }); - - it('reports offense with the use of assign', async () => { - const sourceCode = '{% assign x = content_for_header %}'; - - const offenses = await runLiquidCheck(ContentForHeaderModification, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('Do not rely on the content of `content_for_header`'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['{% assign x = content_for_header %}']); - }); - - it('reports offense with the use of capture', async () => { - const sourceCode = ` - {% capture x %} - {{ content_for_header }} - {% endcapture %} - `; - - const offenses = await runLiquidCheck(ContentForHeaderModification, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('Do not rely on the content of `content_for_header`'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['{{ content_for_header }}']); - }); - - it('reports offense with the use of echo', async () => { - const sourceCode = ` - {% liquid - echo content_for_header | split: ',' - %} - `; - - const offenses = await runLiquidCheck(ContentForHeaderModification, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('Do not rely on the content of `content_for_header`'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(["echo content_for_header | split: ','"]); - }); - - it('does not report an offense with normal use', async () => { - const sourceCode = '{{ content_for_header }}'; - - const offenses = await runLiquidCheck(ContentForHeaderModification, sourceCode); - expect(offenses).toHaveLength(0); - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/deprecate-bgsizes/index.spec.ts b/packages/theme-check-common/src/checks/deprecate-bgsizes/index.spec.ts deleted file mode 100644 index 9668fa894..000000000 --- a/packages/theme-check-common/src/checks/deprecate-bgsizes/index.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { DeprecateBgsizes } from './index'; -import { runLiquidCheck, highlightedOffenses } from '../../test'; - -describe('Module: DeprecateBgsizes', () => { - it('should report offenses for deprecated attributes', async () => { - const sourceCode = ` -
-
- `; - - const offenses = await runLiquidCheck(DeprecateBgsizes, sourceCode); - expect(offenses).to.have.length(4); - - const errorMessages = offenses.map((offense) => offense.message); - expect(errorMessages).to.deep.equal([ - 'Use the native loading="lazy" attribute instead of lazysizes', - 'Use the CSS imageset attribute instead of data-bgset', - 'Use the native loading="lazy" attribute instead of lazysizes', - 'Use the CSS imageset attribute instead of data-bgset', - ]); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.deep.equal([ - 'lazyload', - 'data-bgset="image.jpg"', - 'lazyload', - 'data-bgset="image2.jpg"', - ]); - }); - - it('should not report offenses for non-deprecated attributes', async () => { - const sourceCode = ` -
-
- `; - - const offenses = await runLiquidCheck(DeprecateBgsizes, sourceCode); - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/deprecate-lazysizes/index.spec.ts b/packages/theme-check-common/src/checks/deprecate-lazysizes/index.spec.ts deleted file mode 100644 index 16287d801..000000000 --- a/packages/theme-check-common/src/checks/deprecate-lazysizes/index.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { DeprecateLazysizes } from './index'; - -describe('Module: DeprecateLazysizes', () => { - it('should report data-srcset and data-sizes attributes', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(DeprecateLazysizes, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Use the native loading="lazy" attribute instead of lazysizes', - ); - }); - - it('should not report srcset and sizes attributes', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(DeprecateLazysizes, sourceCode); - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/deprecated-filter/index.spec.ts b/packages/theme-check-common/src/checks/deprecated-filter/index.spec.ts deleted file mode 100644 index cc970ed3e..000000000 --- a/packages/theme-check-common/src/checks/deprecated-filter/index.spec.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, applySuggestions, runLiquidCheck, applyFix } from '../../test'; -import { DeprecatedFilter } from './index'; -import { Offense } from '../../types'; - -describe('Module: DeprecatedFilter', () => { - it('should report an offense when a deprecated filter is used', async () => { - const sourceCode = ` - {{ '#EA5AB9' | hex_to_rgba }} - {{ '#EA5AB9' | hex_to_rgba: 0.5 }} - `; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - expect(offenses.map((e) => e.message)).toEqual([ - "Deprecated filter 'hex_to_rgba', consider using 'color_to_rgb'.", - "Deprecated filter 'hex_to_rgba', consider using 'color_to_rgb'.", - ]); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['| hex_to_rgba', '| hex_to_rgba: 0.5']); - }); - - it('should not report an offense when a non-deprecated filter is used', async () => { - const sourceCode = ` - {{ '#EA5AB9' | color_to_rgb }} - {{ '#EA5AB9' | color_to_rgb | color_modify: 'alpha', 0.5 }} - `; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it("should suggest a fix to replace the deprecated 'hex_to_rgba' filter", async () => { - const sourceCode = '{{ "#EA5AB9" | hex_to_rgba }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const fixedCode = applyFix(sourceCode, offenses[0]); - - expect(fixedCode).toEqual('{{ "#EA5AB9" | color_to_rgb }}'); - }); - - it("should suggest a fix to replace the deprecated 'hex_to_rgba' filter when an alpha value is passed", async () => { - const sourceCode = '{{ "#EA5AB9" | hex_to_rgba: 0.5 }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const fixedCode = applyFix(sourceCode, offenses[0]); - - expect(fixedCode).toEqual(`{{ "#EA5AB9" | color_to_rgb | color_modify: 'alpha', 0.5 }}`); - }); - - it("should suggest a fix to replace the deprecated 'img_url' filter", async () => { - const sourceCode = ` - {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: '200x', scale: 2 }} - {{ product.featured_image | img_url: '200x' }} - {{ product.featured_image | img_url: '200x300' }} - {{ product.featured_image | img_url: 'x300' }} - {{ product.featured_image | img_url }} - {{ product.featured_image - | img_url: '200x' - }} - {{ product.featured_image - | img_url: '200x', - format: format - | image_tag - }} - {{product.featured_image | img_url: '200x'}} - {{-product.featured_image | img_url: '200x'-}} - {% assign url = product.featured_image | img_url: '200x' %} - {% assign url = - product.featured_image | img_url: '200x' - %} - {{ product.featured_image | img_url: 'master' }} - {{ product.featured_image | img_url: '4000x', scale: 2 }} - - // not supported: - {{ product.featured_image | img_url: variable }} - {{ product.featured_image | img_url: '200x', scale: variable }} - `; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const fixedCode = fixSourceCode(sourceCode, offenses); - - const expected = ` - {{ product.featured_image | image_url: width: 400, crop: 'center' }} - {{ product.featured_image | image_url: width: 400 }} - {{ product.featured_image | image_url: width: 200 }} - {{ product.featured_image | image_url: width: 200, height: 300 }} - {{ product.featured_image | image_url: height: 300 }} - {{ product.featured_image | image_url: width: 100, height: 100 }} - {{ product.featured_image - | image_url: width: 200 - }} - {{ product.featured_image - | image_url: width: 200, format: format - | image_tag - }} - {{product.featured_image | image_url: width: 200}} - {{-product.featured_image | image_url: width: 200-}} - {% assign url = product.featured_image | image_url: width: 200 %} - {% assign url = - product.featured_image | image_url: width: 200 - %} - {{ product.featured_image | image_url: width: 100 }} - {{ product.featured_image | image_url: width: 5760 }} - - // not supported: - {{ product.featured_image | img_url: variable }} - {{ product.featured_image | img_url: '200x', scale: variable }} - `; - - expect(fixedCode).toEqual(expected); - }); - - it("should suggest a fix to replace the deprecated 'img_url' filter with named sizes", async () => { - const sourceCode = ` - {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'pico' }} - - {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'icon' }} - - {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'thumb' }} - - {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'small' }} - - {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'compact' }} - - {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'medium' }} - - {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'large' }} - - {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'grande' }} - - {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'original' }} - - {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} - {{ product.featured_image | img_url: 'master' }} - `; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const fixedCode = fixSourceCode(sourceCode, offenses); - - const expected = ` - {{ product.featured_image | image_url: width: 32, height: 32, crop: 'center' }} - {{ product.featured_image | image_url: width: 16, height: 16 }} - - {{ product.featured_image | image_url: width: 64, height: 64, crop: 'center' }} - {{ product.featured_image | image_url: width: 32, height: 32 }} - - {{ product.featured_image | image_url: width: 100, height: 100, crop: 'center' }} - {{ product.featured_image | image_url: width: 50, height: 50 }} - - {{ product.featured_image | image_url: width: 200, height: 200, crop: 'center' }} - {{ product.featured_image | image_url: width: 100, height: 100 }} - - {{ product.featured_image | image_url: width: 320, height: 320, crop: 'center' }} - {{ product.featured_image | image_url: width: 160, height: 160 }} - - {{ product.featured_image | image_url: width: 480, height: 480, crop: 'center' }} - {{ product.featured_image | image_url: width: 240, height: 240 }} - - {{ product.featured_image | image_url: width: 960, height: 960, crop: 'center' }} - {{ product.featured_image | image_url: width: 480, height: 480 }} - - {{ product.featured_image | image_url: width: 1200, height: 1200, crop: 'center' }} - {{ product.featured_image | image_url: width: 600, height: 600 }} - - {{ product.featured_image | image_url: width: 2048, height: 2048, crop: 'center' }} - {{ product.featured_image | image_url: width: 1024, height: 1024 }} - - {{ product.featured_image | image_url: width: 100, crop: 'center' }} - {{ product.featured_image | image_url: width: 100 }} - `; - - expect(fixedCode).toEqual(expected); - }); - - it("should suggest a fix to replace the deprecated 'article_img_url' filter", async () => { - const sourceCode = '{{ article.image | article_img_url }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - expect(suggestions).toEqual(['{{ article.image | image_url: width: 100, height: 100 }}']); - }); - - it("should suggest a fix to replace the deprecated 'article_img_url' filter with named sizes", async () => { - const sourceCode = '{{ article.image | article_img_url: "grande" }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - expect(suggestions).toEqual(['{{ article.image | image_url: width: 600, height: 600 }}']); - }); - - it("should suggest a fix to replace the deprecated 'collection_img_url' filter", async () => { - const sourceCode = '{{ collection.image | collection_img_url }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - expect(suggestions).toEqual(['{{ collection.image | image_url: width: 100, height: 100 }}']); - }); - - it("should suggest a fix to replace the deprecated 'collection_img_url' filter with named sizes", async () => { - const sourceCode = '{{ collection.image | collection_img_url: "grande" }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - expect(suggestions).toEqual(['{{ collection.image | image_url: width: 600, height: 600 }}']); - }); - - it("should suggest a fix to replace the deprecated 'product_img_url' filter", async () => { - const sourceCode = '{{ p.featured_image | product_img_url }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - expect(suggestions).toEqual(['{{ p.featured_image | image_url: width: 100, height: 100 }}']); - }); - - it("should suggest a fix to replace the deprecated 'product_img_url' filter with named sizes", async () => { - const sourceCode = '{{ p.featured_image | product_img_url: "grande" }}'; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - expect(suggestions).toEqual(['{{ p.featured_image | image_url: width: 600, height: 600 }}']); - }); - - it("should suggest a fix to replace the deprecated 'img_tag' filter", async () => { - const sourceCode = ` - {{ product | img_tag }} - {{ product | img_tag: 'image alt text' }} - {{ product | img_tag: 'image alt text', 'my-css-class' }} - {{ product | img_tag: 'image alt text', 'my-css-class', '200x300' }} - {{ product | img_tag: 'image alt text', 'my-css-class', 'grande' }} - `; - - const offenses = await runLiquidCheck(DeprecatedFilter, sourceCode); - const fixedCode = fixSourceCode(sourceCode, offenses); - - const expected = ` - {{ product | image_url: width: 100 | image_tag }} - {{ product | image_url: width: 100 | image_tag: alt: 'image alt text' }} - {{ product | image_url: width: 100 | image_tag: alt: 'image alt text', class: 'my-css-class' }} - {{ product | image_url: width: 200, height: 300 | image_tag: width: 200, height: 300, alt: 'image alt text', class: 'my-css-class' }} - {{ product | image_url: width: 600, height: 600 | image_tag: width: 600, height: 600, alt: 'image alt text', class: 'my-css-class' }} - `; - - expect(fixedCode).toEqual(expected); - }); -}); - -function fixSourceCode(source: string, offenses: Offense[]) { - offenses - .reverse() - .filter((offense) => offense.suggest) - .forEach((offense) => { - source = applySuggestions(source, offense)?.join('') || source; - }); - - return source; -} diff --git a/packages/theme-check-common/src/checks/deprecated-fonts-on-sections-and-blocks/index.spec.ts b/packages/theme-check-common/src/checks/deprecated-fonts-on-sections-and-blocks/index.spec.ts deleted file mode 100644 index fa8b10f18..000000000 --- a/packages/theme-check-common/src/checks/deprecated-fonts-on-sections-and-blocks/index.spec.ts +++ /dev/null @@ -1,613 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck, check, MockTheme } from '../../test'; -import { DeprecatedFontsOnSectionsAndBlocks } from './index'; - -const DEFAULT_FILE_NAME = 'sections/file.liquid'; - -describe('Module: DeprecatedFonts', () => { - it('reports no warning when the setting has a non-deprecated font as default value', async () => { - const sourceCode = ` - {% schema %} - { - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "alegreya_n4" - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when setting has a deprecated font as default value', async () => { - const sourceCode = ` - {% schema %} - { - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "helvetica_n4" - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); - - it('reports a warning when multiple settings have a deprecated font as default value', async () => { - const sourceCode = ` - {% schema %} - { - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "helvetica_n4" - }, - { - "type": "font_picker", - "id": "footer_font", - "label": "Footer font", - "default": "helvetica_n4" - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(2); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - expect(offenses[1].message).toEqual( - "setting 'footer_font' is using deprecated font 'helvetica_n4'", - ); - }); - - it('does not report a warning when the local block setting has no deprecated font', async () => { - const sourceCode = ` - {% schema %} - { - "blocks": [{ - "type": "my-local-block", - "name": "My Local Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "alegreya_n4" - } - ] - }] - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the local block setting has a deprecated font', async () => { - const sourceCode = ` - {% schema %} - { - "blocks": [{ - "type": "my-local-block", - "name": "My Local Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "helvetica_n4" - } - ] - }] - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); - - it('reports no warning when the preset settings do not use deprecated fonts', async () => { - const sourceCode = ` - {% schema %} - { - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "alegreya_n4" - } - ], - "presets": [ - { - "name": "Preset 1", - "settings": { - "heading_font": "alegreya_n4" - } - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the preset setting has a deprecated font as default value', async () => { - const sourceCode = ` - {% schema %} - { - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "alegreya_n4" - } - ], - "presets": [ - { - "name": "Preset 1", - "settings": { - "heading_font": "helvetica_n4" - } - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); - - it('reports no warning when the preset block setting has no deprecated fonts (blocks as a hash)', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "presets": [ - { - "name": "Preset 1", - "blocks": { - "text-1": { - "type": "text", - "settings": { - "heading_font": "alegreya_n4" - } - } - } - } - ] - } - {% endschema %}`, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the preset block setting has a deprecated font (blocks as a hash)', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "presets": [ - { - "name": "Preset 1", - "blocks": { - "text-1": { - "type": "text", - "settings": { - "heading_font": "helvetica_n4" - } - } - } - } - ] - } - {% endschema %}`, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - }); - - it('reports a warning when the preset block setting has a deprecated font in a nested block (blocks as a hash)', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "presets": [ - { - "name": "Preset 1", - "blocks": { - "text-1": { - "type": "text", - "blocks": { - "text-2": { - "type": "text", - "settings": { "heading_font": "helvetica_n4" } - } - } - } - } - } - ] - } - {% endschema %}`, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - }); - - it('reports no warning when the preset block setting has no deprecated fonts (blocks as an array)', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "presets": [ - { - "name": "Preset 1", - "blocks": [ - { - "type": "text", - "settings": { "heading_font": "alegreya_n4" } - } - ] - } - ] - } - {% endschema %}`, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the preset block setting has a deprecated font (blocks as an array)', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "presets": [ - { - "name": "Preset 1", - "blocks": [ - { - "type": "text", - "settings": { "heading_font": "helvetica_n4" } - } - ] - } - ] - } - {% endschema %}`, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - }); - - it('reports a warning when the preset nested block setting has a deprecated fonts (blocks as an array)', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "presets": [ - { - "name": "Preset 1", - "blocks": [ - { - "type": "text", - "blocks": [ - { - "type": "text", - "settings": { "heading_font": "helvetica_n4" } - } - ] - } - ] - } - ] - } - {% endschema %}`, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - }); - - it('reports no warning when the section default setting has no deprecated fonts', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - "defaults": { - "settings": { - "heading_font": "alegreya_n4" - } - } - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the section default setting has a deprecated font', async () => { - const sourceCode = ` - {% schema %} - { - "name": "Section 1", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - "default": { - "settings": { - "heading_font": "helvetica_n4" - } - } - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); - - it('reports no warning when the section default block setting has no deprecated fonts', async () => { - const theme: MockTheme = { - 'sections/section-1.liquid': ` - {% schema %} - { - "name": "Section 1", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ], - "blocks": [ - { - "type": "text" - } - ], - "defaults": { - "blocks": { - "type": "text", - "settings": { - "heading_font": "alegreya_n4" - } - } - } - } - {% endschema %}`, - }; - - const offenses = await check(theme, [DeprecatedFontsOnSectionsAndBlocks]); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the section default block setting has a deprecated font', async () => { - const sourceCode = ` - {% schema %} - { - "name": "Section 1", - "blocks": [ - { - "type": "text", - "name": "Local Text Block", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font" - } - ] - } - ], - "default": { - "blocks": [{ - "type": "text", - "settings": { - "heading_font": "helvetica_n4" - } - }] - } - } - {% endschema %}`; - - const offenses = await runLiquidCheck( - DeprecatedFontsOnSectionsAndBlocks, - sourceCode, - DEFAULT_FILE_NAME, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "setting 'heading_font' is using deprecated font 'helvetica_n4'", - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); -}); diff --git a/packages/theme-check-common/src/checks/deprecated-fonts-on-settings-data/index.spec.ts b/packages/theme-check-common/src/checks/deprecated-fonts-on-settings-data/index.spec.ts deleted file mode 100644 index 648036631..000000000 --- a/packages/theme-check-common/src/checks/deprecated-fonts-on-settings-data/index.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { DeprecatedFontsOnSettingsData } from './index'; -import { runJSONCheck } from '../../test'; - -describe('Module: DeprecatedFontsOnSettingsData', () => { - it('reports a warning when settings_data.json has a deprecated font in current settings', async () => { - const sourceCode = JSON.stringify({ - current: { - heading_font: 'helvetica_n4', - body_font: 'alegreya_n4', - }, - }); - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsData, - sourceCode, - 'config/settings_data.json', - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('The font "helvetica_n4" is deprecated'); - }); - - it('reports warnings for deprecated fonts in presets', async () => { - const sourceCode = JSON.stringify({ - current: {}, - presets: { - Default: { - heading_font: 'helvetica_n4', - body_font: 'agmena_n4', - }, - }, - }); - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsData, - sourceCode, - 'config/settings_data.json', - ); - - expect(offenses).toHaveLength(2); - expect(offenses.map((o) => o.message)).toContain('The font "helvetica_n4" is deprecated'); - expect(offenses.map((o) => o.message)).toContain('The font "agmena_n4" is deprecated'); - }); - - it('reports no warning when settings_data.json has no deprecated fonts', async () => { - const sourceCode = JSON.stringify({ - current: { - heading_font: 'alegreya_n4', - body_font: 'bitter_n4', - }, - }); - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsData, - sourceCode, - 'config/settings_data.json', - ); - - expect(offenses).toHaveLength(0); - }); - - it('reports no warning for non-string values', async () => { - const sourceCode = JSON.stringify({ - current: { - show_header: true, - items_per_page: 12, - colors_body_bg: '#ffffff', - }, - }); - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsData, - sourceCode, - 'config/settings_data.json', - ); - - expect(offenses).toHaveLength(0); - }); - - it('does not run check on files other than settings_data.json', async () => { - const sourceCode = JSON.stringify({ - current: { - heading_font: 'helvetica_n4', - }, - }); - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsData, - sourceCode, - 'config/settings_schema.json', - ); - - expect(offenses).toHaveLength(0); - }); - - it('reports warnings for deprecated fonts deeply nested in preset blocks', async () => { - const sourceCode = JSON.stringify({ - current: { - blocks: { - 'block-id-1': { - type: 'header', - settings: { - heading_font: 'helvetica_n4', - }, - }, - }, - }, - }); - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsData, - sourceCode, - 'config/settings_data.json', - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('The font "helvetica_n4" is deprecated'); - }); -}); diff --git a/packages/theme-check-common/src/checks/deprecated-fonts-on-settings-schema/index.spec.ts b/packages/theme-check-common/src/checks/deprecated-fonts-on-settings-schema/index.spec.ts deleted file mode 100644 index 71bc4a1a4..000000000 --- a/packages/theme-check-common/src/checks/deprecated-fonts-on-settings-schema/index.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { DeprecatedFontsOnSettingsSchema } from './index'; -import { runJSONCheck } from '../../test'; - -describe('Module: DeprecatedFontsOnSettingsSchema', () => { - it('reports a warning when settings_schema.json has a deprecated font', async () => { - const sourceCode = `[ - { - "name": "Fonts", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "helvetica_n4" - } - ] - } - ]`; - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsSchema, - sourceCode, - 'config/settings_schema.json', - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('The font "helvetica_n4" is deprecated'); - }); - - it('reports no warning when settings_schema.json has non-deprecated fonts', async () => { - const sourceCode = `[ - { - "name": "Fonts", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "alegreya_n4" - } - ] - } - ]`; - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsSchema, - sourceCode, - 'config/settings_schema.json', - ); - - expect(offenses).toHaveLength(0); - }); - - it('reports no warning for non-font_picker settings', async () => { - const sourceCode = `[ - { - "name": "Fonts", - "settings": [ - { - "type": "text", - "id": "heading_text", - "label": "Heading text", - "default": "helvetica_n4" - } - ] - } - ]`; - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsSchema, - sourceCode, - 'config/settings_schema.json', - ); - - expect(offenses).toHaveLength(0); - }); - - it('does not run check on files other than settings_schema.json', async () => { - const sourceCode = `[ - { - "name": "Fonts", - "settings": [ - { - "type": "font_picker", - "id": "heading_font", - "label": "Heading font", - "default": "helvetica_n4" - } - ] - } - ]`; - - const offenses = await runJSONCheck( - DeprecatedFontsOnSettingsSchema, - sourceCode, - 'config/other_file.json', - ); - - expect(offenses).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/deprecated-tag/index.spec.ts b/packages/theme-check-common/src/checks/deprecated-tag/index.spec.ts deleted file mode 100644 index eb944fccb..000000000 --- a/packages/theme-check-common/src/checks/deprecated-tag/index.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { DeprecatedTag } from './index'; - -describe('Module: DeprecatedTag', () => { - it('should report an offense when include tag is used', async () => { - const sourceCode = ` - {% include 'templates/foo.liquid' %} - `; - const offenses = await runLiquidCheck(DeprecatedTag, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual(`Use the 'render' tag instead of 'include'`); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['include']); - }); - - it('should not report an offense when render tag is used', async () => { - const sourceCode = ` - {% render 'templates/foo.liquid' %} - `; - const offenses = await runLiquidCheck(DeprecatedTag, sourceCode); - - expect(offenses).toHaveLength(0); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toHaveLength(0); - }); - - it('should suggest replacing include tag with render tag', async () => { - const sourceCode = ` - {% include 'foo.liquid' %} - {% assign greeting = "hello world" %} - {% include 'greeting.liquid' %} - `; - const offenses = await runLiquidCheck(DeprecatedTag, sourceCode); - - expect(offenses).toHaveLength(2); - if (!offenses[0].suggest || !offenses[1].suggest) return; - - expect(offenses[0].suggest[0].message).toEqual(`Replace 'include' with 'render'`); - expect(offenses[1].suggest[0].message).toEqual(`Replace 'include' with 'render'`); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toHaveLength(2); - expect(highlights[0]).toBe('include'); - expect(highlights[1]).toBe('include'); - }); - - it('should report multiple offenses when multiple include tags are used in the same line', async () => { - const sourceCode = "{% include 'foo.liquid' %} Some text {% include 'bar.liquid' %}"; - const offenses = await runLiquidCheck(DeprecatedTag, sourceCode); - - expect(offenses).toHaveLength(2); - }); - - it('should report offenses when include tags are nested', async () => { - const sourceCode = "{% if true %} {% include 'foo.liquid' %} {% endif %}"; - const offenses = await runLiquidCheck(DeprecatedTag, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual(`Use the 'render' tag instead of 'include'`); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['include']); - }); -}); diff --git a/packages/theme-check-common/src/checks/duplicate-block-arguments/index.spec.ts b/packages/theme-check-common/src/checks/duplicate-block-arguments/index.spec.ts deleted file mode 100644 index 0fe99a15b..000000000 --- a/packages/theme-check-common/src/checks/duplicate-block-arguments/index.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { DuplicateBlockArguments } from './index'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; - -describe('DuplicateBlockArguments', () => { - it('reports duplicate arguments after interface sources are merged', async () => { - const template = "{% block 'card', heading: 'a', heading: 'b' %}{% endblock %}"; - const offenses = await run(template); - - expect(offenses).toMatchObject([ - { message: "Duplicate argument 'heading' in block tag for 'card'." }, - ]); - expect(highlightedOffenses({ 'templates/test.liquid': template }, offenses)).toEqual([ - "heading: 'b'", - ]); - }); - - it('does not report unique arguments', async () => { - const offenses = await run("{% block 'card', heading: 'a', content: body %}{% endblock %}"); - - expect(offenses).toEqual([]); - }); - - it.each([ - ['text', "{% block 'card', content: body %}Body{% endblock %}"], - ['comment', "{% block 'card', content: body %}{% comment %}x{% endcomment %}{% endblock %}"], - ])('warns that %s body content takes precedence over content:', async (_name, template) => { - const offenses = await run(template); - - expect(offenses).toMatchObject([ - { - message: - "The explicit 'content' argument has no effect because the inline block body takes precedence.", - }, - ]); - expect(highlightedOffenses({ 'templates/test.liquid': template }, offenses)).toEqual([ - 'content: body', - ]); - }); - - it('does not let a whitespace-only body override content:', async () => { - const offenses = await run("{% block 'card', content: body %}\n \t\n{% endblock %}"); - - expect(offenses).toEqual([]); - }); - - it('preserves the duplicate warning when body content also overrides content:', async () => { - const offenses = await run( - "{% block 'card', content: first, content: second %}Body{% endblock %}", - ); - - expect(offenses.map((offense) => offense.message)).toEqual([ - "The explicit 'content' argument has no effect because the inline block body takes precedence.", - "Duplicate argument 'content' in block tag for 'card'.", - "The explicit 'content' argument has no effect because the inline block body takes precedence.", - ]); - }); - - it('reports duplicates without resolving the target block', async () => { - const offenses = await run("{% block 'missing', value: 'a', value: 'b' %}{% endblock %}"); - - expect(offenses).toHaveLength(1); - }); -}); - -async function run(template: string) { - return runLiquidCheck(DuplicateBlockArguments, template, 'templates/test.liquid'); -} diff --git a/packages/theme-check-common/src/checks/duplicate-content-for-arguments/index.spec.ts b/packages/theme-check-common/src/checks/duplicate-content-for-arguments/index.spec.ts deleted file mode 100644 index bc05dfed1..000000000 --- a/packages/theme-check-common/src/checks/duplicate-content-for-arguments/index.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { DuplicateContentForArguments } from '.'; -import { runLiquidCheck, applySuggestions } from '../../test'; - -describe('Module: DuplicateContentForArguments', () => { - function runCheck(sourceCode: string) { - return runLiquidCheck(DuplicateContentForArguments, sourceCode); - } - - describe('detection', () => { - it('should report duplicate arguments in content_for tags', async () => { - const sourceCode = ` - {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[0].suggest).toBeDefined(); - expect(offenses[0].suggest!.length).toBe(1); - expect(offenses[0].suggest![0].message).toBe("Remove duplicate argument 'param1'"); - }); - - it('should report multiple duplicate arguments in content_for tags', async () => { - const sourceCode = ` - {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3', param2: 'value4', param1: 'value5' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(3); - expect(offenses[0].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[0].start.index).toBe(sourceCode.indexOf("param1: 'value3'")); - expect(offenses[1].message).toMatch(/Duplicate argument 'param2'/); - expect(offenses[1].start.index).toBe(sourceCode.indexOf("param2: 'value4'")); - expect(offenses[2].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[2].start.index).toBe(sourceCode.indexOf("param1: 'value5'")); - }); - }); - - describe('suggestions', () => { - it('should correctly suggest fixing all duplicate arguments except for the first', async () => { - const sourceCode = `{% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3', param1: 'value4' %}`; - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(2); - expect(offenses[0].start.index).toBe(sourceCode.indexOf("param1: 'value3'")); - expect(offenses[1].start.index).toBe(sourceCode.indexOf("param1: 'value4'")); - const suggestionResult = applySuggestions(sourceCode, offenses[0]); - expect(suggestionResult).toEqual([ - `{% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value4' %}`, - ]); - }); - }); - - describe('edge cases', () => { - it('should not report when there are no duplicate arguments', async () => { - const sourceCode = ` - {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param3: 'value3' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should not report for variable render tags where snippet name is a variable', async () => { - const sourceCode = ` - {% render my_variable, param1: 'value1', param1: 'value2' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should handle remove duplicate param when there are multiple render tags', async () => { - const sourceCode = ` - {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param3: 'value3' %} - {% content_for 'block', type: 'fake-block', param1: 'value4', param2: 'value5', param1: 'value6' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[0].start.index).toBe(sourceCode.indexOf("param1: 'value6'")); - const suggestionResult = applySuggestions(sourceCode, offenses[0]); - expect(suggestionResult).toEqual([ - ` - {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param3: 'value3' %} - {% content_for 'block', type: 'fake-block', param1: 'value4', param2: 'value5' %} - `, - ]); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/duplicate-render-snippet-arguments/index.spec.ts b/packages/theme-check-common/src/checks/duplicate-render-snippet-arguments/index.spec.ts deleted file mode 100644 index 850f8e688..000000000 --- a/packages/theme-check-common/src/checks/duplicate-render-snippet-arguments/index.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { DuplicateRenderSnippetArguments } from '.'; -import { runLiquidCheck, applySuggestions } from '../../test'; - -describe('Module: DuplicateRenderSnippetArguments', () => { - function runCheck(sourceCode: string) { - return runLiquidCheck(DuplicateRenderSnippetArguments, sourceCode); - } - - describe('detection', () => { - it('should report duplicate arguments in render tags', async () => { - const sourceCode = ` - {% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[0].suggest).toBeDefined(); - expect(offenses[0].suggest!.length).toBe(1); - expect(offenses[0].suggest![0].message).toBe("Remove duplicate argument 'param1'"); - }); - - it('should report multiple duplicate arguments in render tags', async () => { - const sourceCode = ` - {% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3', param2: 'value4', param1: 'value5' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(3); - expect(offenses[0].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[0].start.index).toBe(sourceCode.indexOf("param1: 'value3'")); - expect(offenses[1].message).toMatch(/Duplicate argument 'param2'/); - expect(offenses[1].start.index).toBe(sourceCode.indexOf("param2: 'value4'")); - expect(offenses[2].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[2].start.index).toBe(sourceCode.indexOf("param1: 'value5'")); - }); - - it('should not report when render tag is using `with/for` alias syntax', async () => { - const sourceCode = ` - {% render 'snippet' with 'string' as param1, param1: 'value1' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toMatch(/Duplicate argument 'param1'/); - }); - }); - - describe('suggestions', () => { - it('should correctly suggest fixing all duplicate arguments except for the first', async () => { - const sourceCode = `{% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3', param1: 'value4' %}`; - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(2); - expect(offenses[0].start.index).toBe(sourceCode.indexOf("param1: 'value3'")); - expect(offenses[1].start.index).toBe(sourceCode.indexOf("param1: 'value4'")); - const suggestionResult = applySuggestions(sourceCode, offenses[0]); - expect(suggestionResult).toEqual([ - `{% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value4' %}`, - ]); - }); - - it('should suggest removing duplicates when with / for alias is used', async () => { - const sourceCode = `{% render 'snippet' with 'string' as param2, param1: 'value1', param2: 'value2' %}`; - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(1); - const suggestionResult = applySuggestions(sourceCode, offenses[0]); - expect(suggestionResult).toEqual([ - `{% render 'snippet' with 'string' as param2, param1: 'value1' %}`, - ]); - }); - }); - - describe('edge cases', () => { - it('should not report when there are no duplicate arguments', async () => { - const sourceCode = ` - {% render 'snippet', param1: 'value1', param2: 'value2', param3: 'value3' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should not report for variable render tags where snippet name is a variable', async () => { - const sourceCode = ` - {% render my_variable, param1: 'value1', param1: 'value2' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should handle remove duplicate param when there are multiple render tags', async () => { - const sourceCode = ` - {% render 'snippet', param1: 'value1', param2: 'value2', param3: 'value3' %} - {% render 'snippet', param1: 'value4', param2: 'value5', param1: 'value6' %} - `; - - const offenses = await runCheck(sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toMatch(/Duplicate argument 'param1'/); - expect(offenses[0].start.index).toBe(sourceCode.indexOf("param1: 'value6'")); - const suggestionResult = applySuggestions(sourceCode, offenses[0]); - expect(suggestionResult).toEqual([ - ` - {% render 'snippet', param1: 'value1', param2: 'value2', param3: 'value3' %} - {% render 'snippet', param1: 'value4', param2: 'value5' %} - `, - ]); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/empty-block-content/index.spec.ts b/packages/theme-check-common/src/checks/empty-block-content/index.spec.ts deleted file mode 100644 index 4f0177641..000000000 --- a/packages/theme-check-common/src/checks/empty-block-content/index.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { EmptyBlockContent } from '.'; -import { check, MockTheme } from '../../test'; - -describe('Module: EmptyBlockContent', () => { - const paths = ['sections', 'blocks']; - - paths.forEach((path) => { - it(`should not report offenses when the blocks array is not empty in ${path}`, async () => { - const theme: MockTheme = { - [`${path}/test_block.liquid`]: ` -
- {% content_for 'blocks' %} -
- {% schema %} - { - "name": "Test", - "blocks": [ - { - "type": "test-block" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [EmptyBlockContent]); - expect(offenses).to.have.length(0); - }); - - it(`should report offenses when the blocks array is empty in ${path}`, async () => { - const theme: MockTheme = { - [`${path}/test_block.liquid`]: ` -
- {% content_for 'blocks' %} -
- {% schema %} - { - "name": "Test", - "blocks": [] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [EmptyBlockContent]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `The 'content_for "blocks"' tag is present, but the blocks array is empty.`, - ); - }); - - it(`should report offenses when the blocks array is empty with the correct indices in ${path}`, async () => { - const theme: MockTheme = { - [`${path}/test_block.liquid`]: ` -
- {% content_for 'blocks' %} -
- {% schema %} - { - "name": "Test", - "blocks": [] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [EmptyBlockContent]); - expect(offenses).to.have.length(1); - const content = theme[`${path}/test_block.liquid`]; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal("{% content_for 'blocks' %}"); - }); - - it(`should report offenses when the blocks array has not been defined in ${path}`, async () => { - const theme: MockTheme = { - [`${path}/test_block.liquid`]: ` -
- {% content_for 'blocks' %} -
- {% schema %} - { - "name": "Test" - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [EmptyBlockContent]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `The 'content_for "blocks"' tag is present, but the blocks array is not defined.`, - ); - }); - - it(`should not report offenses when 'content_for blocks' is not present in ${path}`, async () => { - const theme: MockTheme = { - [`${path}/test_block.liquid`]: ` - {% schema %} - { - "name": "Test", - "blocks": [ - { - "type": "test-block" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [EmptyBlockContent]); - expect(offenses).to.be.empty; - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/excessive-settings-count/index.spec.ts b/packages/theme-check-common/src/checks/excessive-settings-count/index.spec.ts deleted file mode 100644 index d5b99cefc..000000000 --- a/packages/theme-check-common/src/checks/excessive-settings-count/index.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ExcessiveSettingsCount, TOLERATED_SETTINGS_COUNT } from './index'; -import { Severity } from '../../types'; -import { check, runLiquidCheck } from '../../test'; -import type { Offense } from '../../types'; - -const DEFAULT_PATH = 'sections/test.liquid'; - -async function checkWithMaxSettings(template: string, maxSettings: number): Promise { - return check( - { [DEFAULT_PATH]: template }, - [ExcessiveSettingsCount], - {}, - { ExcessiveSettingsCount: { enabled: true, maxSettings } }, - ); -} - -function settingEntries( - count: number, - { indent = ' ', idPrefix = 'setting', labelPrefix = 'Setting ' } = {}, -): string[] { - return Array.from({ length: count }, (_, index) => { - const separator = index === count - 1 ? '' : ','; - - return `${indent}{ "type": "text", "id": "${idPrefix}_${index}", "label": "${labelPrefix}${index}" }${separator}`; - }); -} - -function schemaWithSettings(count: number): string { - return [ - '
section
', - '{% schema %}', - '{', - ' "name": "Test",', - ' "settings": [', - ...settingEntries(count), - ' ]', - '}', - '{% endschema %}', - ].join('\n'); -} - -describe('ExcessiveSettingsCount', () => { - describe('wrapper tolerated maximum', () => { - it('does not report at the tolerated maximum', async () => { - const offenses = await runLiquidCheck( - ExcessiveSettingsCount, - schemaWithSettings(TOLERATED_SETTINGS_COUNT), - DEFAULT_PATH, - ); - - expect(offenses).toEqual([]); - }); - - it('reports when settings exceed the tolerated maximum', async () => { - const offenses = await runLiquidCheck( - ExcessiveSettingsCount, - schemaWithSettings(TOLERATED_SETTINGS_COUNT + 1), - DEFAULT_PATH, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - check: 'ExcessiveSettingsCount', - severity: Severity.WARNING, - }); - expect(offenses[0].message).toBe( - `This schema declares ${TOLERATED_SETTINGS_COUNT + 1} settings, which exceeds the maximum of ${TOLERATED_SETTINGS_COUNT}. Consider splitting this section or block into smaller pieces, or grouping related options with a header.`, - ); - }); - }); - - it('reports when count exceeds a custom maximum', async () => { - const offenses = await checkWithMaxSettings(schemaWithSettings(3), 2); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - `This schema declares 3 settings, which exceeds the maximum of 2. Consider splitting this section or block into smaller pieces, or grouping related options with a header.`, - ); - }); - - it('does not report when count equals the configured maximum', async () => { - await expect(checkWithMaxSettings(schemaWithSettings(40), 40)).resolves.toEqual([]); - }); - - it('excludes header and paragraph entries that have no id', async () => { - const template = [ - '
section
', - '{% schema %}', - '{', - ' "settings": [', - ' { "type": "header", "content": "Group" },', - ' { "type": "text", "id": "a", "label": "A" },', - ' { "type": "paragraph", "content": "Note" },', - ' { "type": "text", "id": "b", "label": "B" },', - ' { "type": "text", "id": "c", "label": "C" }', - ' ]', - '}', - '{% endschema %}', - ].join('\n'); - - await expect(checkWithMaxSettings(template, 3)).resolves.toEqual([]); - }); - - it('ignores settings nested in blocks', async () => { - const blockSettings = settingEntries(20, { - indent: ' ', - idPrefix: 'nested', - labelPrefix: 'N', - }); - const template = [ - '
section
', - '{% schema %}', - '{', - ' "settings": [', - ...settingEntries(5), - ' ],', - ' "blocks": [', - ' {', - ' "type": "child",', - ' "settings": [', - ...blockSettings, - ' ]', - ' }', - ' ]', - '}', - '{% endschema %}', - ].join('\n'); - - await expect(checkWithMaxSettings(template, 6)).resolves.toEqual([]); - }); - - it('reports above the threshold and not at the threshold', async () => { - const over = await checkWithMaxSettings(schemaWithSettings(6), 5); - expect(over).toHaveLength(1); - - const atBoundary = await checkWithMaxSettings(schemaWithSettings(5), 5); - expect(atBoundary).toEqual([]); - }); - - it('does not report when schema is absent', async () => { - await expect(checkWithMaxSettings('
{{ product.title }}
', 1)).resolves.toEqual([]); - }); -}); diff --git a/packages/theme-check-common/src/checks/hardcoded-routes/index.spec.ts b/packages/theme-check-common/src/checks/hardcoded-routes/index.spec.ts deleted file mode 100644 index 37a73b35f..000000000 --- a/packages/theme-check-common/src/checks/hardcoded-routes/index.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { HardcodedRoutes } from './index'; -import { runLiquidCheck, highlightedOffenses } from '../../test'; - -describe('Module: HardcodedRoutes', () => { - it('should report offenses for hardcoded routes', async () => { - const sourceCode = ` - -
- - - -
  • {{ 'Socks' | link_to: '/collections/socks' }}
  • -
  • {{ 'Home' | link_to: '/' }}
  • -
  • {{ 'Cart (' | append: cart.item_count | append: ')' | link_to: '/cart' }}
  • - `; - - const offenses = await runLiquidCheck(HardcodedRoutes, sourceCode); - expect(offenses).to.have.length(8); - - const errorMessages = offenses.map((offense) => offense.message); - expect(errorMessages).to.deep.equal([ - 'Use routes object {{ routes.root_url }} instead of hardcoding /', - 'Use routes object {{ routes.search_url }} instead of hardcoding /search', - 'Use routes object {{ routes.account_login_url }} instead of hardcoding /account/login', - 'Use routes object {{ routes.collections_url }} instead of hardcoding /collections', - 'Use routes object {{ routes.cart_change_url }} instead of hardcoding /cart/change', - 'Use routes object {{ routes.collections_url }} instead of hardcoding /collections', - 'Use routes object {{ routes.root_url }} instead of hardcoding /', - 'Use routes object {{ routes.cart_url }} instead of hardcoding /cart', - ]); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.deep.equal([ - '/', - '/search', - '/account/login', - '/collections', - '/cart/change', - '/collections', - '/', - '/cart', - ]); - }); - - it('should not report offenses for route objects', async () => { - const sourceCode = ` -
    - - - -
  • {{ 'About' | link_to: '/pages/about' }}
  • - `; - - const offenses = await runLiquidCheck(HardcodedRoutes, sourceCode); - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/img-width-and-height/index.spec.ts b/packages/theme-check-common/src/checks/img-width-and-height/index.spec.ts deleted file mode 100644 index ef2215966..000000000 --- a/packages/theme-check-common/src/checks/img-width-and-height/index.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { ImgWidthAndHeight } from './index'; - -describe('Module: ImgWidthAndHeight', () => { - it('should report missing attributes on multiple img tags with mixed attributes', async () => { - const sourceCode = ` - - - - - `; - - const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode); - expect(offenses).to.have.lengthOf(3); - expect(offenses[0].message).to.equal('Missing height attribute on img tag'); - expect(offenses[1].message).to.equal('Missing width attribute on img tag'); - expect(offenses[2].message).to.equal('Missing width and height attributes on img tag'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([ - '', - '', - '', - ]); - }); - - it('should report missing width and height attributes on img tags', async () => { - const sourceCode = ` - content - - - `; - - const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal('Missing width and height attributes on img tag'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['']); - }); - - it('should report missing height attribute on img tags', async () => { - const sourceCode = ` - - - `; - - const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal('Missing height attribute on img tag'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['']); - }); - - it('should report missing width attribute on img tags', async () => { - const sourceCode = ` - - - `; - - const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].message).to.equal('Missing width attribute on img tag'); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['']); - }); - - it('should not report missing attributes on img tags if they are not missing', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(ImgWidthAndHeight, sourceCode); - expect(offenses).to.be.empty; - }); -}); diff --git a/packages/theme-check-common/src/checks/json-missing-block/index.spec.ts b/packages/theme-check-common/src/checks/json-missing-block/index.spec.ts deleted file mode 100644 index 1082f97c3..000000000 --- a/packages/theme-check-common/src/checks/json-missing-block/index.spec.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { JSONMissingBlock } from './index'; -import { check, MockTheme } from '../../test'; - -describe('Module: JsonMissingBlock', () => { - describe('File existence validation', () => { - it('should report an offense when a block does not exist', async () => { - const theme: MockTheme = { - 'templates/product.failing.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "text_tqQTNE": { - "type": "missing_block" - } - }, - "block_order": ["text_tqQTNE"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "missing_block" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Theme block 'blocks/missing_block.liquid' does not exist.", - ); - - const content = theme['templates/product.failing.json']; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"missing_block"'); - }); - - it('should not report an offense when block exists', async () => { - const theme: MockTheme = { - 'templates/product.valid.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "text_block": { - "type": "text" - } - }, - "block_order": ["text_block"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "text" - } - ] - } - {% endschema %} - `, - 'blocks/text.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.be.empty; - }); - - it('should report an offense when a nested block does not exist', async () => { - const theme: MockTheme = { - 'templates/product.nested.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "parent_block": { - "type": "text", - "blocks": { - "child_block": { - "type": "missing_nested" - } - } - } - }, - "block_order": ["parent_block"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "text" - } - ] - } - {% endschema %} - `, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "blocks": [ - { - "type": "missing_nested" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Theme block 'blocks/missing_nested.liquid' does not exist.", - ); - - const content = theme['templates/product.nested.json']; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"missing_nested"'); - }); - }); - - describe('Allowed block type validation', () => { - it('should report an offense when block exists but is not in the section liquid schema', async () => { - const theme: MockTheme = { - 'templates/product.valid.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "text_block": { - "type": "text" - } - }, - "block_order": ["text_block"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "image" - } - ] - } - {% endschema %} - `, - 'blocks/text.liquid': '', - 'blocks/image.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Block type 'text' is not allowed in 'sections/custom-section.liquid'.", - ); - - const content = theme['templates/product.valid.json']; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"text"'); - }); - - it('should report an offense when a nested block exists but is not in the block liquid schema', async () => { - const theme: MockTheme = { - 'templates/product.nested.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "parent_block": { - "type": "text", - "blocks": { - "child_block": { - "type": "missing_nested" - } - } - } - }, - "block_order": ["parent_block"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "text" - } - ] - } - {% endschema %} - `, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "blocks": [ - { - "type": "image" - } - ] - } - {% endschema %} - `, - 'blocks/image.liquid': '', - 'blocks/missing_nested.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Block type 'missing_nested' is not allowed in 'blocks/text.liquid'.", - ); - - const content = theme['templates/product.nested.json']; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"missing_nested"'); - }); - - it('should report an offense when a nested private block exists but is not in the block liquid schema', async () => { - const theme: MockTheme = { - 'templates/product.nested.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "parent_block": { - "type": "text", - "blocks": { - "child_block": { - "type": "_private_block" - } - } - } - }, - "block_order": ["parent_block"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "text" - } - ] - } - {% endschema %} - `, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "blocks": [ - { - "type": "@theme" - } - ] - } - {% endschema %} - `, - 'blocks/image.liquid': '', - 'blocks/_private_block.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Block type '_private_block' is not allowed in 'blocks/text.liquid'.", - ); - - const content = theme['templates/product.nested.json']; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"_private_block"'); - }); - - it('should not report an offense when a static block exists and is not in the block liquid schema', async () => { - const theme: MockTheme = { - 'templates/product.static.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "child_block": { - "type": "text" - }, - "static_block": { - "type": "static_block", - "static": true - } - }, - "block_order": ["child_block"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "text" - } - ] - } - {% endschema %} - `, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "blocks": [ - { - "type": "@theme" - } - ] - } - {% endschema %} - `, - 'blocks/static_block.liquid': ` - {% schema %} - { - "name": "Static Block", - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.have.length(0); - }); - }); - - describe('Edge case validation', () => { - it('should ignore non-template JSON files', async () => { - const theme: MockTheme = { - 'config/index.json': `{ - "blocks": { - "text_block": { - "type": "nonexistent" - } - } - }`, - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.be.empty; - }); - - it('should ignore templates with local blocks', async () => { - const theme: MockTheme = { - 'templates/product.nested.json': `{ - "sections": { - "custom-section": { - "type": "custom-section", - "blocks": { - "parent_block": { - "type": "text", - "blocks": { - "child_block": { - "type": "_private_block" - } - } - } - }, - "block_order": ["parent_block"] - } - }, - "order": ["custom-section"] - }`, - 'sections/custom-section.liquid': ` - {% schema %} - { - "name": "Custom Section", - "blocks": [ - { - "type": "text", - "name": "Text Block" - } - ] - } - {% endschema %} - `, - 'blocks/text.liquid': ` - {% schema %} - { - "name": "Text Block", - "blocks": [ - { - "type": "image", - "name": "Image Block" - } - ] - } - {% endschema %} - `, - 'blocks/image.liquid': '', - 'blocks/_private_block.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingBlock]); - expect(offenses).to.be.empty; - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/json-missing-section/index.spec.ts b/packages/theme-check-common/src/checks/json-missing-section/index.spec.ts deleted file mode 100644 index 084195c73..000000000 --- a/packages/theme-check-common/src/checks/json-missing-section/index.spec.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { JSONMissingSection } from './index'; -import { check, MockTheme } from '../../test'; - -describe('Module: JSONMissingSection', () => { - describe('JSON templates', () => { - it('should report an offense when a section file does not exist', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ - "sections": { - "hero_AAAAAA": { - "type": "featured-collection" - } - }, - "order": ["hero_AAAAAA"] - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Section type 'featured-collection' does not refer to an existing section file", - ); - - const content = theme['templates/index.json']; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"featured-collection"'); - }); - - it('should not report an offense when the section file exists', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ - "sections": { - "hero_AAAAAA": { - "type": "featured-collection" - } - }, - "order": ["hero_AAAAAA"] - }`, - 'sections/featured-collection.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - - it('should report an offense for each missing section', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ - "sections": { - "hero_AAAAAA": { - "type": "missing-one" - }, - "main": { - "type": "existing-section" - }, - "footer_BBBBBB": { - "type": "missing-two" - } - }, - "order": ["hero_AAAAAA", "main", "footer_BBBBBB"] - }`, - 'sections/existing-section.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal( - "Section type 'missing-one' does not refer to an existing section file", - ); - expect(offenses[1].message).to.equal( - "Section type 'missing-two' does not refer to an existing section file", - ); - }); - - it('should not report an offense for section types with platform defaults', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ - "sections": { - "apps_section": { - "type": "apps" - }, - "blocks_section": { - "type": "_blocks" - } - }, - "order": ["apps_section", "blocks_section"] - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - - it('should not report an offense when a section entry has no type', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ - "sections": { - "inherited_section": { - "settings": {} - } - }, - "order": ["inherited_section"] - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - }); - - describe('Section groups', () => { - it('should report an offense when a section file does not exist', async () => { - const theme: MockTheme = { - 'sections/header-group.json': `{ - "type": "header", - "name": "Header group", - "sections": { - "announcement": { - "type": "missing-announcement-bar" - } - }, - "order": ["announcement"] - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Section type 'missing-announcement-bar' does not refer to an existing section file", - ); - - const content = theme['sections/header-group.json']; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"missing-announcement-bar"'); - }); - - it('should not report an offense when the section file exists', async () => { - const theme: MockTheme = { - 'sections/header-group.json': `{ - "type": "header", - "name": "Header group", - "sections": { - "announcement": { - "type": "announcement-bar" - } - }, - "order": ["announcement"] - }`, - 'sections/announcement-bar.liquid': '', - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - - it('should not report an offense for the section group type itself', async () => { - const theme: MockTheme = { - 'sections/header-group.json': `{ - "type": "header", - "name": "Header group", - "sections": {}, - "order": [] - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - }); - - describe('Edge cases', () => { - it('should ignore JSON files outside templates/ and sections/', async () => { - const theme: MockTheme = { - 'config/settings_data.json': `{ - "sections": { - "main": { - "type": "nonexistent" - } - } - }`, - 'locales/en.default.json': `{ - "sections": { - "main": { "type": "nonexistent" } - } - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - - it('should not report an offense when sections is not an object', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ - "sections": "not-an-object", - "order": [] - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - - it('should report each duplicate section key at its own location', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ - "sections": { - "main": { - "type": "missing-one" - }, - "main": { - "type": "missing-two" - } - }, - "order": ["main"] - }`, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.have.length(2); - - const content = theme['templates/index.json']; - expect(offenses[0].message).to.equal( - "Section type 'missing-one' does not refer to an existing section file", - ); - expect(content.slice(offenses[0].start.index, offenses[0].end.index)).to.equal( - '"missing-one"', - ); - expect(offenses[1].message).to.equal( - "Section type 'missing-two' does not refer to an existing section file", - ); - expect(content.slice(offenses[1].start.index, offenses[1].end.index)).to.equal( - '"missing-two"', - ); - }); - - it('should not report an offense when the JSON is invalid', async () => { - const theme: MockTheme = { - 'templates/index.json': `{ "sections": { "main": { "type": `, - }; - - const offenses = await check(theme, [JSONMissingSection]); - expect(offenses).to.be.empty; - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/json-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/json-syntax-error/index.spec.ts deleted file mode 100644 index f6365ea60..000000000 --- a/packages/theme-check-common/src/checks/json-syntax-error/index.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { JSONSyntaxError } from './index'; -import { highlightedOffenses, runJSONCheck } from '../../test'; - -describe('Module: JSONSyntaxError', () => { - it('should report an error for invalid JSON (0)', async () => { - const invalidJson = `{ - "key1": "value1", - "key2": "value2",, - }`; - - const offenses = await runJSONCheck(JSONSyntaxError, invalidJson, 'file.json'); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal('Property name expected'); - expect(offenses[1].message).to.equal('Expecting a value'); - - const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses); - expect(highlights).to.have.length(2); - expect(highlights[0]).to.equal(','); - expect(highlights[1]).to.equal(','); - }); - - it('should report an error for invalid JSON (1)', async () => { - const invalidJson = `{ - "key1": "value1", - "key2": "value2" - `; - - const offenses = await runJSONCheck(JSONSyntaxError, invalidJson, 'file.json'); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Expecting a closing brace (})'); - - const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses); - expect(highlights).to.have.length(1); - expect(highlights[0]).to.equal(''); - }); - - it('should report an error for invalid JSON (2)', async () => { - const invalidJson = `{ - 'key1': "value1", - "key2": "value2" - }`; - - const offenses = await runJSONCheck(JSONSyntaxError, invalidJson, 'file.json'); - expect(offenses).to.have.length(3); - expect(offenses[0].message).to.equal('Invalid symbol'); - expect(offenses[1].message).to.equal('Property name expected'); - expect(offenses[2].message).to.equal('Expecting a value'); - - const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses); - expect(highlights).to.have.length(3); - expect(highlights[0]).to.equal("'key1'"); - }); - - it('should not report any errors for valid JSON', async () => { - const validJson = `{ - "key1": "value1", - "key2": "value2" - }`; - - const offenses = await runJSONCheck(JSONSyntaxError, validJson); - expect(offenses).to.be.empty; - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-complexity/index.spec.ts b/packages/theme-check-common/src/checks/liquid-complexity/index.spec.ts deleted file mode 100644 index b11e904d5..000000000 --- a/packages/theme-check-common/src/checks/liquid-complexity/index.spec.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { LiquidComplexity } from './index'; -import { Severity } from '../../types'; -import { check, runLiquidCheck } from '../../test'; -import type { Offense } from '../../types'; - -const DEFAULT_PATH = 'snippets/test.liquid'; - -async function checkWithMaxComplexity(template: string, maxComplexity: number): Promise { - return checkSources([{ path: DEFAULT_PATH, source: template }], maxComplexity); -} - -async function checkSources( - sources: { path: string; source: string }[], - maxComplexity: number, -): Promise { - const themeDesc = Object.fromEntries(sources.map(({ path, source }) => [path, source])); - - return check( - themeDesc, - [LiquidComplexity], - {}, - { LiquidComplexity: { enabled: true, maxComplexity } }, - ); -} - -function repeatedIfs(count: number): string { - return Array.from({ length: count }, (_, index) => `{% if enabled_${index} %}{% endif %}`).join( - '\n', - ); -} - -function expectComplexityMessage( - offense: Offense, - complexity: number, - maxComplexity: number, -): void { - expect(offense).toMatchObject({ - check: 'LiquidComplexity', - severity: Severity.WARNING, - }); - expect(offense.message).toContain( - `Liquid complexity is ${complexity}, which exceeds the maximum of ${maxComplexity}.`, - ); - expect(offense.message).toContain('pushed it over the limit'); - expect(offense.message).toContain('simplifying conditional logic'); - expect(offense.message).toContain( - 'moving self-contained decision logic into snippets with the render tag', - ); -} - -describe('LiquidComplexity', () => { - describe('wrapper tolerated maximum', () => { - it('does not report when complexity equals 120', async () => { - const offenses = await runLiquidCheck(LiquidComplexity, repeatedIfs(119), DEFAULT_PATH); - - expect(offenses).toEqual([]); - }); - - it('reports when complexity exceeds 120', async () => { - const offenses = await runLiquidCheck(LiquidComplexity, repeatedIfs(120), DEFAULT_PATH); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 121, 120); - }); - }); - - it('counts nested logical expressions', async () => { - const template = ` - {% if available and visible %} - Featured product - {% elsif featured or highlighted %} - Highlighted product - {% endif %} - `.trim(); - - // 1 base + 1 if + 1 and + 1 elsif + 1 or = 5. - await expect(checkWithMaxComplexity(template, 5)).resolves.toEqual([]); - - const offenses = await checkWithMaxComplexity(template, 4); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 5, 4); - }); - - it('counts logical expressions in unless conditions', async () => { - const template = ` - {% unless hidden or archived %} - Visible product - {% endunless %} - `.trim(); - - // 1 base + 1 unless + 1 or = 3. - await expect(checkWithMaxComplexity(template, 3)).resolves.toEqual([]); - - const offenses = await checkWithMaxComplexity(template, 2); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 3, 2); - }); - - it('does not count logical expressions in variable output or assign tags', async () => { - const template = ` - {% assign visible = product.available and customer %} - {{ product.available or customer }} - `.trim(); - - // 1 base. Non-branching assign and output boolean expressions do not count. - await expect(checkWithMaxComplexity(template, 1)).resolves.toEqual([]); - }); - - it('counts decision points inside liquid tags', async () => { - const template = ` - {% liquid - if enabled - echo 'Enabled' - elsif archived - echo 'Archived' - endif - %} - `.trim(); - - // 1 base + 1 if + 1 elsif = 3. - await expect(checkWithMaxComplexity(template, 3)).resolves.toEqual([]); - - const offenses = await checkWithMaxComplexity(template, 2); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 3, 2); - }); - - it('counts each nested condition as another decision point', async () => { - const template = ` - {% if product.available and product.published %} - {% unless customer.banned or customer.guest %} - {% if customer.tags contains 'vip' and settings.vip_enabled %} - VIP offer - {% elsif product.tags contains 'sale' or product.compare_at_price > product.price %} - Sale offer - {% endif %} - {% endunless %} - {% endif %} - `.trim(); - - // 1 base + 1 if + 1 and + 1 unless + 1 or + 1 nested if + 1 and + 1 elsif + 1 or = 9. - await expect(checkWithMaxComplexity(template, 9)).resolves.toEqual([]); - - const offenses = await checkWithMaxComplexity(template, 8); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 9, 8); - }); - - it('resets complexity for each file', async () => { - const source = '{% if enabled %}{% endif %}'; - - const offenses = await checkSources( - [ - { path: 'snippets/first.liquid', source }, - { path: 'snippets/second.liquid', source }, - ], - 2, - ); - - expect(offenses).toEqual([]); - }); - - it('tolerates parse errors', async () => { - const template = '{% if enabled %}'; - - await expect(checkWithMaxComplexity(template, 1)).resolves.toEqual([]); - }); - - it('reports plain files when maxComplexity is 0', async () => { - const template = 'Hello'; - - const offenses = await checkWithMaxComplexity(template, 0); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 1, 0); - expect(offenses[0].start.index).toBe(0); - expect(offenses[0].end.index).toBe(template.length); - }); - - it('counts case consistently with if and elsif', async () => { - const template = ` - {% case status %} - {% when 'active' %} - Active - {% when 'draft' %} - Draft - {% endcase %} - `.trim(); - - // 1 base + 1 case + 2 when branches = 4. - await expect(checkWithMaxComplexity(template, 4)).resolves.toEqual([]); - - const offenses = await checkWithMaxComplexity(template, 3); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 4, 3); - }); - - it('reports the range of the decision point that first exceeds the maximum', async () => { - const firstDecision = '{% if first %}'; - const secondDecision = '{% if second %}'; - const template = `${firstDecision}{% endif %}\n${secondDecision}{% endif %}`; - const expectedStartIndex = template.indexOf(secondDecision); - const expectedEndIndex = expectedStartIndex + secondDecision.length; - - const offenses = await checkWithMaxComplexity(template, 2); - - expect(offenses).toHaveLength(1); - expectComplexityMessage(offenses[0], 3, 2); - expect(offenses[0].start.index).toBe(expectedStartIndex); - expect(offenses[0].end.index).toBe(expectedEndIndex); - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-free-settings/index.spec.ts b/packages/theme-check-common/src/checks/liquid-free-settings/index.spec.ts deleted file mode 100644 index c4d27b11c..000000000 --- a/packages/theme-check-common/src/checks/liquid-free-settings/index.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { LiquidFreeSettings } from './index'; -import { check, MockTheme } from '../../test'; - -describe('LiquidFreeSettings validation', () => { - const paths = ['sections', 'blocks']; - paths.forEach((path) => { - it(`should not report errors for valid settings without liquid logic in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "Section name", - "settings": [ - { - "id": "text_value", - "type": "text", - "label": "Text Value", - "default": "Some text without liquid logic." - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).to.have.length(0); - }); - - it(`should not report errors for valid settings with dynamic source liquid logic in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "Section name", - "settings": [ - { - "id": "text_value", - "type": "text", - "label": "Text Value", - "default": "Title {{ product.title }}" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).to.have.length(0); - }); - - it(`should not report errors for valid settings with liquid type field in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "Section name", - "settings": [ - { - "id": "text_value", - "type": "liquid", - "label": "Text Value", - "default": "{% render 'block' %}" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).to.have.length(0); - }); - - it(`should report an error when settings value contains Liquid logic in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "Section name", - "settings": [ - { - "id": "input_with_logic", - "type": "text", - "label": "Input with Logic", - "default": "Hello {% if user %} User {% endif %}!" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Settings values cannot contain liquid logic.'); - }); - - it(`should report an error with correct indices when settings value contains Liquid logic in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "Section name", - "settings": [ - { - "id": "input_with_logic", - "type": "text", - "label": "Input with Logic", - "default": "Hello {% if user %} User {% endif %}!" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).to.have.length(1); - const content = theme[`${path}/test-section.liquid`]; - const errorContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(errorContent).to.equal('"Hello {% if user %} User {% endif %}!"'); - }); - - it(`should report an error when settings value contains Liquid logic in object format in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "test", - "settings": { - "text_block": { - "type": "text", - "id": "text_block", - "label": "Text Block", - "default": "Hello {% if user %} User {% endif %}!" - } - } - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Settings values cannot contain liquid logic.'); - }); - - it(`should report errors when preset block values contain Liquid logic in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "Section name", - "presets": [ - { - "name": "Default", - "block": [ - { - "type": "text", - "settings": { - "text": "Hello World!{% random liquid logic %}" - } - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Settings values cannot contain liquid logic.'); - }); - - it(`should not report errors when settings value is available_if and it contains liquid logic`, async () => { - const theme: MockTheme = { - [`${path}/test-section.liquid`]: ` - {% schema %} - { - "name": "test", - "settings": { - "text_block": { - "type": "text", - "id": "text_block", - "label": "Text Block", - "default": "Hello World!", - "visible_if": "{% if user %} true {% endif %}" - } - } - } - {% endschema %} - `, - }; - const offenses = await check(theme, [LiquidFreeSettings]); - expect(offenses).toEqual([]); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidBooleanExpression.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidBooleanExpression.spec.ts deleted file mode 100644 index fc0afd32a..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidBooleanExpression.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { applyFix, runLiquidCheck } from '../../../test'; -import { LiquidHTMLSyntaxError } from '..'; - -describe('detectTrailingAssignValue', async () => { - it('should not report when there are no trailing values', async () => { - const testCases = [`{% assign foo = '123' %}`, `{% assign foo = '123' | upcase %}`]; - - for (const sourceCode of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - } - }); - - it('should not report when the boolean expression is in a schema tag', async () => { - const sourceCode = `{% schema %} - { - "visible_if": "{{ this > that }}" - } - {% endschema %}`; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - }); - - it('should report all use of boolean expressions', async () => { - const testCases = [ - [`{% assign foo = something == else %}`, '{% assign foo = something %}'], - [`{% echo foo != bar %}`, '{% echo foo %}'], - [`{{ this > that }}`, '{{ this }}'], - [`{{ bool and cond }}`, '{{ bool }}'], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNode.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNode.spec.ts deleted file mode 100644 index d888bd05b..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNode.spec.ts +++ /dev/null @@ -1,385 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { runLiquidCheck, applyFix } from '../../../test'; -import { LiquidHTMLSyntaxError } from '..'; - -describe('Module: InvalidConditionalBooleanExpression', () => { - it('should not report an offense for valid boolean expressions', async () => { - const testCases = [ - '{% if 1 > 2 %}hello{% endif %}', - '{% if variable == 5 %}hello{% endif %}', - "{% if 'abc' contains 'a' %}hello{% endif %}", - "{% if product.title != '' %}hello{% endif %}", - '{% if 1 and 2 %}hello{% endif %}', - '{% if true or false %}hello{% endif %}', - '{% if 10 > 5 and user.active %}hello{% endif %}', - '{% if price >= 100 or discount %}hello{% endif %}', - "{% if user.name contains 'admin' or user.role == 'owner' %}hello{% endif %}", - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should not report an offense for valid single values', async () => { - const testCases = [ - '{% if variable %}hello{% endif %}', - '{% if user.active %}hello{% endif %}', - '{% if true %}hello{% endif %}', - '{% if false %}hello{% endif %}', - '{% if 1 %}hello{% endif %}', - '{% if 0 %}hello{% endif %}', - "{% if 'string' %}hello{% endif %}", - '{% if contains %}hello{% endif %}', - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should report an offense when parser stops at numbers', async () => { - const source = '{% if 7 1 > 100 %}hello{% endif %}'; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Syntax is not supported: Expression stops at truthy value '7', and will ignore: '1 > 100'", - ); - - const fixed = applyFix(source, offenses[0]); - expect(fixed).to.equal('{% if 7 %}hello{% endif %}'); - }); - - it('should report an offense when parser stops at strings', async () => { - const source = "{% if 'hello' 1 > 100 %}world{% endif %}"; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Syntax is not supported: Expression stops at truthy value ''hello'', and will ignore: '1 > 100'", - ); - - const fixed = applyFix(source, offenses[0]); - expect(fixed).to.equal("{% if 'hello' %}world{% endif %}"); - }); - - it('should report an offense when parser stops at liquid literals', async () => { - const testCases = [ - { source: '{% if true 1 > 0 %}hello{% endif %}', value: 'true' }, - { source: '{% if false 1 > 0 %}hello{% endif %}', value: 'false' }, - { source: '{% if nil 6 > 5 %}hello{% endif %}', value: 'nil' }, - { source: '{% if empty 123 456 %}hello{% endif %}', value: 'empty' }, - { source: '{% if blank 789 %}hello{% endif %}', value: 'blank' }, - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase.source); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain( - `Expression stops at truthy value '${testCase.value}'`, - ); - - const fixed = applyFix(testCase.source, offenses[0]); - expect(fixed).to.equal(`{% if ${testCase.value} %}hello{% endif %}`); - } - }); - - it('should report offenses in different liquid tag types', async () => { - const testCases = [ - '{% if 7 1 > 100 %}hello{% endif %}', - "{% unless 'test' 42 > 0 %}hello{% endunless %}", - '{% if false %}no{% elsif 7 1 > 100 %}hello{% endif %}', - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain('Expression stops at truthy value'); - } - }); - - it('should report an offense for malformed expression starting with invalid token', async () => { - const source = '{% if > 2 %}hello{% endif %}'; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Syntax is not supported: Conditional cannot start with '>'. Use a variable or value instead", - ); - - const fixed = applyFix(source, offenses[0]); - expect(fixed).to.equal('{% if false %}hello{% endif %}'); - }); - - it('should report an offense for bare operators with no operands', async () => { - const testCases = [ - { source: '{% if > %}hello{% endif %}', token: '>' }, - { source: '{% if == %}hello{% endif %}', token: '==' }, - { source: '{% if < %}hello{% endif %}', token: '<' }, - { source: '{% if != %}hello{% endif %}', token: '!=' }, - { source: '{% if >= %}hello{% endif %}', token: '>=' }, - { source: '{% if <= %}hello{% endif %}', token: '<=' }, - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase.source); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain(`Conditional cannot start with '${testCase.token}'`); - - const fixed = applyFix(testCase.source, offenses[0]); - expect(fixed).to.equal('{% if false %}hello{% endif %}'); - } - }); - - it('should report an offense for other invalid starting characters', async () => { - const testCases = [ - { source: '{% if @ %}hello{% endif %}', token: '@' }, - { source: '{% if # %}hello{% endif %}', token: '#' }, - { source: '{% if $ %}hello{% endif %}', token: '$' }, - { source: '{% if & %}hello{% endif %}', token: '&' }, - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase.source); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain(`Conditional cannot start with '${testCase.token}'`); - - const fixed = applyFix(testCase.source, offenses[0]); - expect(fixed).to.equal('{% if false %}hello{% endif %}'); - } - }); - - it('should report an offense for malformed expressions in complex expressions', async () => { - const testCases = [ - '{% if > 5 and true %}hello{% endif %}', - '{% if == 2 or false %}hello{% endif %}', - '{% if < 10 and variable %}hello{% endif %}', - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain('Conditional cannot start with'); - - const fixed = applyFix(testCase, offenses[0]); - expect(fixed).to.equal('{% if false %}hello{% endif %}'); - } - }); - - it('should report an offense for trailing tokens after comparison', async () => { - const source = '{% if 1 == 2 foobar %}hello{% endif %}'; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Syntax is not supported: Conditional is invalid. Anything after '1 == 2' will be ignored", - ); - - const fixed = applyFix(source, offenses[0]); - expect(fixed).to.equal('{% if 1 == 2 %}hello{% endif %}'); - }); - - it('should report an offense for malformed comparisons like missing quotes', async () => { - const testCases = [ - { - source: "{% if 'wat' == 'squat > 2 %}hello{% endif %}", - description: 'missing closing quote creates trailing comparison', - }, - { - source: "{% if 'wat' == 'squat' > 2 %}hello{% endif %}", - description: 'extra comparison after valid comparison', - }, - { - source: "{% if price == 'test' != 5 %}hello{% endif %}", - description: 'chained comparisons without logical operators', - }, - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase.source); - expect(offenses, `Failed for: ${testCase.description}`).to.have.length(1); - expect(offenses[0].message).to.contain('Anything after'); - } - }); - - it('should report an offense for multiple trailing tokens', async () => { - const source = '{% if 10 > 4 baz qux %}hello{% endif %}'; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain('Anything after'); - - const fixed = applyFix(source, offenses[0]); - expect(fixed).to.equal('{% if 10 > 4 %}hello{% endif %}'); - }); - - it('should report an offense for trailing junk with different operators', async () => { - const testCases = [ - "{% if 'abc' contains 'a' noise %}hello{% endif %}", - '{% if price <= 50 extra %}hello{% endif %}', - '{% if count != 0 junk %}hello{% endif %}', - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain('Anything after'); - } - }); - - it('should not report an offense for valid logical continuations', async () => { - const testCases = [ - '{% if 1 > 0 and 2 < 3 %}hello{% endif %}', - '{% if x == 5 or y != 10 %}hello{% endif %}', - '{% if price >= 100 and discount %}hello{% endif %}', - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should not report an offense for truthy values followed by logical operators', async () => { - const testCases = [ - '{% if true and variable %}hello{% endif %}', - '{% if false or variable %}hello{% endif %}', - '{% if 1 and user.active %}hello{% endif %}', - '{% if 0 or fallback %}hello{% endif %}', - "{% if 'string' and condition %}hello{% endif %}", - "{% if 'value' or default %}hello{% endif %}", - '{% if 42 and check %}hello{% endif %}', - '{% if 3.14 or backup %}hello{% endif %}', - '{% if nil and something %}hello{% endif %}', - '{% if empty or alternative %}hello{% endif %}', - '{% if blank and other %}hello{% endif %}', - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should not report an offense for complex expressions with truthy values and logical operators', async () => { - const testCases = [ - '{% if true and variable > 5 %}hello{% endif %}', - "{% if 'test' or user.name == 'admin' %}hello{% endif %}", - '{% if 42 and price <= 100 %}hello{% endif %}', - '{% if false or count != 0 %}hello{% endif %}', - "{% if empty and product.title contains 'sale' %}hello{% endif %}", - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should not report an offense for unknown operators errors after values (Liquid catches these)', async () => { - const testCases = [ - '{% if my_var word > 5 %}hello{% endif %}', - '{% if jake johnson > 5 %}hello{% endif %}', - "{% if 'test' invalid > thing %}hello{% endif %}", - "{% if user.name custom 'admin' %}hello{% endif %}", - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should not report an offense for unknown operators after variables', async () => { - const testCases = [ - '{% if variable unknown > 5 %}hello{% endif %}', - "{% if user.role badop 'admin' %}hello{% endif %}", - '{% if price fake 100 %}hello{% endif %}', - '{% if "str" blue == something %}hello{% endif %}', - '{% if red blue > something %}hello{% endif %}', - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should not report an offense for unknown operators in complex expressions', async () => { - const testCases = [ - "{% if user.active and name fake 'test' %}hello{% endif %}", - "{% unless 'test' some > thing %}hello{% endunless %}", - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should not report an offense for pipe filter expressions', async () => { - const testCases = ['{% if wat | something == something %}hello{% endif %}']; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); - - it('should report an offense for misspelled logical operators', async () => { - const testCases = [ - { - source: '{% if "wat" == "squat" adn "wat" == "squat" %}hello{% endif %}', - misspelled: 'adn', - expectedFix: '"wat" == "squat"', - }, - { - source: '{% if variable > 5 andd other < 10 %}hello{% endif %}', - misspelled: 'andd', - expectedFix: 'variable > 5', - }, - ]; - - for (const testCase of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase.source); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain('Anything after'); - - const fixed = applyFix(testCase.source, offenses[0]); - expect(fixed).to.contain(testCase.expectedFix); - } - }); - - it('should report special message for JavaScript-style operators after literal values', async () => { - const testCases = [ - '{% if true && false %}hello{% endif %}', - '{% if false || true %}hello{% endif %}', - '{% if "hello" && world %}hello{% endif %}', - '{% if 42 || something %}hello{% endif %}', - ]; - - for (const source of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.contain( - "Use 'and'/'or' instead of '&&'/'||' for multiple conditions", - ); - } - }); - - it('should NOT report an offense for valid logical operators', async () => { - const validCases = [ - '{% if price > 100 and discount < 50 %}hello{% endif %}', - '{% if user.active or user.premium %}hello{% endif %}', - '{% if x == 1 and y == 2 or z == 3 %}hello{% endif %}', - ]; - - for (const testCase of validCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, testCase); - expect(offenses).to.have.length(0); - } - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNodeParenthesis.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNodeParenthesis.spec.ts deleted file mode 100644 index 18d2a6499..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNodeParenthesis.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { applyFix, runLiquidCheck } from '../../../test'; -import { LiquidHTMLSyntaxError } from '../index'; - -describe('detectConditionalNodeUnsupportedParenthesis', async () => { - it('should not report no unsupported parenthesis in the markup', async () => { - const testCases = [`{% if condition %}{% endif %}`, `{% if range == (1..2) %}{% endif %}`]; - - for (const sourceCode of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - } - }); - - it('should report when there are unsupported parenthesis in the markup', async () => { - const testCases = [ - [ - `{% if (this and that) or other %}{% endif %}`, - '{% if this and that or other %}{% endif %}', - ], - [`{% if (a > b) and (c == d) %}{% endif %}`, '{% if a > b and c == d %}{% endif %}'], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); - - it('should report when there are multiple instances of the error', async () => { - const sourceCode = `{% if (a or c) %}{% endif %} {% if (a == b) %}{% endif %} {% if condition %}{% endif %}`; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal('Syntax is not supported'); - expect(offenses[1].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal( - `{% if a or c %}{% endif %} {% if (a == b) %}{% endif %} {% if condition %}{% endif %}`, - ); - - const fixed2 = applyFix(sourceCode, offenses[1]); - expect(fixed2).to.equal( - `{% if (a or c) %}{% endif %} {% if a == b %}{% endif %} {% if condition %}{% endif %}`, - ); - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidEchoValue.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidEchoValue.spec.ts deleted file mode 100644 index 03abd81b1..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidEchoValue.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { applyFix, runLiquidCheck } from '../../../test'; -import { LiquidHTMLSyntaxError } from '../index'; - -describe('detectInvalidEchoValue', async () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should not report when echo value is valid', async () => { - const testCases = [ - `{% echo '123' %}`, - `{% echo '123' | upcase %}`, - `{{ '123' }}`, - `{{ '123' | upcase }}`, - `{{ }}`, - `{{ echo }}`, - `{% liquid echo %}`, - ]; - - for (const sourceCode of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - } - }); - - it('should not report when there are no filters provided', async () => { - const sourceCode = `{% echo '123' %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - }); - - it('should report when there are multiple values (no filters)', async () => { - const testCases = [ - [`{% echo '123' 555 text %}`, "{% echo '123' %}"], - [`{% echo "123" 555 text %}`, '{% echo "123" %}'], - [`{% echo 123 555 text %}`, '{% echo 123 %}'], - [`{% echo true 555 text %}`, '{% echo true %}'], - [`{{ '123' 555 text }}`, `{{ '123' }}`], - [`{{ "123" 555 text }}`, `{{ "123" }}`], - [`{{ 123 555 text }}`, `{{ 123 }}`], - [`{{ true 555 text }}`, `{{ true }}`], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); - - it('should report when there are multiple values (with filters)', async () => { - const testCases = [ - [`{% echo '123' 555 text | upcase %}`, "{% echo '123' | upcase %}"], - [`{% echo "123" 555 text | upcase %}`, '{% echo "123" | upcase %}'], - [`{% echo 123 555 text | default: 0 %}`, '{% echo 123 | default: 0 %}'], - [`{% echo true 555 text | fake-filter: 'yes' %}`, "{% echo true | fake-filter: 'yes' %}"], - [`{{ '123' 555 text | upcase }}`, `{{ '123' | upcase }}`], - [`{{ "123" 555 text | default: 0 }}`, `{{ "123" | default: 0 }}`], - [`{{ 123 555 text | fake-filter: 'yes' }}`, `{{ 123 | fake-filter: 'yes' }}`], - [`{{ true 555 text | fake-filter: 'yes' }}`, `{{ true | fake-filter: 'yes' }}`], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); - - it('should report when there are multiple instances of the error', async () => { - const sourceCode = `{% echo zero %} {% echo one two %} {% echo one two %}`; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal('Syntax is not supported'); - expect(offenses[1].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(`{% echo zero %} {% echo one %} {% echo one two %}`); - - const fixed2 = applyFix(sourceCode, offenses[1]); - expect(fixed2).to.equal(`{% echo zero %} {% echo one two %} {% echo one %}`); - }); - - it('should report when there is no value', async () => { - const testCases = [ - [`{% echo | upcase %}`, '{% echo blank %}'], - [`{{ | upcase }}`, `{{ blank }}`], - [`{% liquid echo | upcase %}`, `{% liquid echo blank %}`], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidFilterName.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidFilterName.spec.ts deleted file mode 100644 index bd4073cd0..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidFilterName.spec.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, applyFix } from '../../../test'; -import { LiquidHTMLSyntaxError } from '../index'; - -describe('Module: InvalidFilterName', () => { - describe('Alphanumeric and underscore trailing characters', () => { - it('should not report on filters with alphanumeric trailing characters', async () => { - const sourceCode = `{{ 'HELLO' | append123: 'world' }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report on filters with underscore trailing characters', async () => { - const sourceCode = `{{ 'HELLO' | append_test }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - }); - - describe('Valid filters', () => { - it('should not report on valid filter names', async () => { - const sourceCode = `{{ 'hello' | append: 'world' }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report on unknown filters (handled by UnknownFilter check)', async () => { - const sourceCode = `{{ 'hello' | completely_unknown_filter: 'world' }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - }); - - describe('Special characters after filter name', () => { - it('should detect and fix append with trailing @ character', async () => { - const sourceCode = `{{ 'HELLO' | append@ }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toContain('append'); - expect(fixedCode).not.toContain('append@'); - }); - - it('should detect and fix filter with space followed by characters', async () => { - const sourceCode = `{{ 'HELLO' | append me: 'world' }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toContain('append:'); - expect(fixedCode).not.toContain('append me:'); - }); - }); - - describe('Filter combinations and chains', () => { - it('should handle normal filter followed by filter with special characters', async () => { - const sourceCode = `{{ 'test' | downcase | append@: 'world' }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters '@'"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{{ 'test' | downcase | append: 'world' }}`); - }); - - it('should handle filter with special characters followed by normal filter', async () => { - const sourceCode = `{{ 'test' | append!: 'hello' | upcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters '!'"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{{ 'test' | append: 'hello' | upcase }}`); - }); - - it('should handle two filters with special characters in a row', async () => { - const sourceCode = `{{ 'test' | append@: 'hello' | upcase# }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(2); - - expect(offenses[0].message).toContain("Filter 'append' has trailing characters '@'"); - expect(offenses[1].message).toContain("Filter 'upcase' has trailing characters '#'"); - - expect(offenses[0].fix).toBeDefined(); - expect(offenses[1].fix).toBeDefined(); - - // Test individual fixes - const firstFixed = applyFix(sourceCode, offenses[0]); - expect(firstFixed).toBe(`{{ 'test' | append: 'hello' | upcase# }}`); - - const secondFixed = applyFix(sourceCode, offenses[1]); - expect(secondFixed).toBe(`{{ 'test' | append@: 'hello' | upcase }}`); - }); - - it('should handle filter with space and characters followed by normal filter', async () => { - const sourceCode = `{{ 'test' | append xyz: 'hello' | upcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters ' xyz'"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{{ 'test' | append: 'hello' | upcase }}`); - }); - }); - - describe('Assign tag filters', () => { - it('should detect and fix assign with trailing @ character', async () => { - const sourceCode = `{% assign foo = 'HELLO' | append@ %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toContain('append'); - expect(fixedCode).not.toContain('append@'); - }); - - it('should detect and fix assign with space followed by characters', async () => { - const sourceCode = `{% assign bar = 'HELLO' | append me: 'world' %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toContain('append:'); - expect(fixedCode).not.toContain('append me:'); - }); - - it('should handle assign with multiple filters with special characters', async () => { - const sourceCode = `{% assign baz = 'test' | append@: 'hello' | upcase# %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(2); - - expect(offenses[0].message).toContain("Filter 'append' has trailing characters '@'"); - expect(offenses[1].message).toContain("Filter 'upcase' has trailing characters '#'"); - - expect(offenses[0].fix).toBeDefined(); - expect(offenses[1].fix).toBeDefined(); - }); - - it('should not report on valid assign filters', async () => { - const sourceCode = `{% assign valid = 'hello' | append: 'world' | upcase %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - }); - - describe('Echo tag filters', () => { - it('should detect and fix echo with trailing @ character', async () => { - const sourceCode = `{% echo 'HELLO' | append@ %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toContain('append'); - expect(fixedCode).not.toContain('append@'); - }); - - it('should detect and fix echo with space followed by characters', async () => { - const sourceCode = `{% echo 'HELLO' | append me: 'world' %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters"); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toContain('append:'); - expect(fixedCode).not.toContain('append me:'); - }); - - it('should handle echo with multiple filters with special characters', async () => { - const sourceCode = `{% echo 'test' | append@: 'hello' | upcase# %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(2); - - expect(offenses[0].message).toContain("Filter 'append' has trailing characters '@'"); - expect(offenses[1].message).toContain("Filter 'upcase' has trailing characters '#'"); - - expect(offenses[0].fix).toBeDefined(); - expect(offenses[1].fix).toBeDefined(); - }); - - it('should not report on valid echo filters', async () => { - const sourceCode = `{% echo 'hello' | append: 'world' | upcase %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - }); - - describe('Liquid tag filters', () => { - it('should detect and fix assign in liquid tag with trailing characters', async () => { - const sourceCode = `{% liquid - assign foo = 'test' | append@: 'hello' - %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters '@'"); - expect(offenses[0].fix).toBeDefined(); - }); - - it('should detect and fix echo in liquid tag with trailing characters', async () => { - const sourceCode = `{% liquid - echo 'test' | append@: 'hello' - %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Filter 'append' has trailing characters '@'"); - expect(offenses[0].fix).toBeDefined(); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.spec.ts deleted file mode 100644 index 29bacff93..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { applyFix, runLiquidCheck } from '../../../test'; -import { LiquidHTMLSyntaxError } from '../index'; - -describe('detectInvalidLoopArguments', async () => { - it('should not report when args are valid', async () => { - const testCases = [ - `{% for i in array reversed %}{% endfor %}`, - `{% for i in array reversed offset: 1 %}{% endfor %}`, - `{% tablerow x in array limit: 10 %}{% endtablerow %}`, - `{% tablerow x in array cols: 2 limit: 10 %}{% endtablerow %}`, - ]; - - for (const sourceCode of testCases) { - const offenses = await testCheck(sourceCode); - expect(offenses).to.have.length(0); - } - }); - - it('should report when invalid args are found', async () => { - const testCases = [ - [ - `{% for i in array limit1: 10 %}{% endfor %}`, - '{% for i in array %}{% endfor %}', - 'limit1: 10', - ], - [ - `{% for i in array !! range: (1..2) %}{% endfor %}`, - '{% for i in array %}{% endfor %}', - '!!, range: (1..2)', - ], - [ - `{% tablerow i in (1..10) limit1: 10 %}{{ i }}{% endtablerow %}`, - '{% tablerow i in (1..10) %}{{ i }}{% endtablerow %}', - 'limit1: 10', - ], - ]; - - for (const [sourceCode, expected, invalidArguments] of testCases) { - const offenses = await testCheck(sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).toEqual( - expect.stringContaining(`Invalid/Unknown arguments: ${invalidArguments}`), - ); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); - - it('should report when positional args are found after named/invalid args', async () => { - const testCases = [ - [ - `{% for i in array limit: 10 reversed %}{% endfor %}`, - '{% for i in array limit: 10 %}{% endfor %}', - 'reversed', - ], - [ - `{% for i in array reversed limit: 10 invalid %}{% endfor %}`, - '{% for i in array reversed limit: 10 %}{% endfor %}', - 'invalid', - ], - [ - `{% for i in array !! reversed limit: 10 invalid %}{% endfor %}`, - '{% for i in array limit: 10 %}{% endfor %}', - '!!, reversed, invalid', - ], - [ - `{% for i in array reversed: (1..2) %}{% endfor %}`, - '{% for i in array %}{% endfor %}', - 'reversed: (1..2)', - ], - ]; - - for (const [sourceCode, expected, invalidArguments] of testCases) { - const offenses = await testCheck(sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).toEqual( - expect.stringContaining(`Invalid/Unknown arguments: ${invalidArguments}`), - ); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); - - it('should report when there are multiple instances of the error', async () => { - const sourceCode = `{% for i in array reversed limit: 10 invalid %}{% endfor %} {% tablerow x in array cols: 2 limit: 10 invalid %}{% endtablerow %}`; - - const offenses = await testCheck(sourceCode); - expect(offenses).to.have.length(2); - for (const offense of offenses) { - expect(offense.message).toEqual( - expect.stringContaining(`Invalid/Unknown arguments: invalid`), - ); - } - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal( - '{% for i in array reversed limit: 10 %}{% endfor %} {% tablerow x in array cols: 2 limit: 10 invalid %}{% endtablerow %}', - ); - - const fixed2 = applyFix(sourceCode, offenses[1]); - expect(fixed2).to.equal( - '{% for i in array reversed limit: 10 invalid %}{% endfor %} {% tablerow x in array cols: 2 limit: 10 %}{% endtablerow %}', - ); - }); -}); - -function testCheck(sourceCode: string) { - return runLiquidCheck(LiquidHTMLSyntaxError, sourceCode, undefined, { - themeDocset: { - tags: () => - Promise.resolve([ - { - name: 'for', - parameters: [ - { - name: 'reversed', - positional: true, - description: '...', - required: false, - types: [], - }, - { name: 'limit', positional: false, description: '...', required: false, types: [] }, - { name: 'offset', positional: false, description: '...', required: false, types: [] }, - ], - }, - { - name: 'tablerow', - parameters: [ - { name: 'cols', positional: false, description: '...', required: false, types: [] }, - { name: 'limit', positional: false, description: '...', required: false, types: [] }, - { name: 'offset', positional: false, description: '...', required: false, types: [] }, - ], - }, - ]), - filters: () => Promise.resolve([]), - objects: () => Promise.resolve([]), - liquidDrops: () => Promise.resolve([]), - systemTranslations: () => Promise.resolve({}), - }, - }); -} diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopRange.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopRange.spec.ts deleted file mode 100644 index 4e4101214..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopRange.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { applyFix, runLiquidCheck } from '../../../test'; -import { LiquidHTMLSyntaxError } from '../index'; -import { INVALID_LOOP_RANGE_MESSAGE } from './InvalidLoopRange'; - -describe('detectInvalidLoopRange', async () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should not report when range is valid', async () => { - const testCases = [ - `{% for i in (1..10) %}{% endfor %}`, - `{% tablerow x in (a..b) %}{% endtablerow %}`, - ]; - - for (const sourceCode of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - } - }); - - it('should report when there is an extra `.` character in range', async () => { - const testCases = [ - [`{% for i in (1...10) %}{% endfor %}`, '{% for i in (1..10) %}{% endfor %}'], - [ - `{% tablerow x in (a...b) %}{% endtablerow %}`, - '{% tablerow x in (a..b) %}{% endtablerow %}', - ], - [ - `{% tablerow x in (a .. b) %}{% endtablerow %}`, - '{% tablerow x in (a..b) %}{% endtablerow %}', - ], - [ - `{% tablerow x in ( a..b ) %}{% endtablerow %}`, - '{% tablerow x in (a..b) %}{% endtablerow %}', - ], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal(INVALID_LOOP_RANGE_MESSAGE); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); - - it('should report when the range contains a real number', async () => { - const sourceCode = `{% for i in (-2.9..2.9) %}{% endfor %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal(INVALID_LOOP_RANGE_MESSAGE); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(`{% for i in (-2..2) %}{% endfor %}`); - }); - - it('should report when the range contains a nested variable', async () => { - const sourceCode = `{% for i in (some.var..some.other.var) %}{% endfor %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - }); - - it('should report when there are multiple instances of the error', async () => { - const sourceCode = `{% for i in (1...10) %}{% endfor %} {% tablerow x in (a...b) %}{% endtablerow %}`; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal(INVALID_LOOP_RANGE_MESSAGE); - expect(offenses[1].message).to.equal(INVALID_LOOP_RANGE_MESSAGE); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal( - `{% for i in (1..10) %}{% endfor %} {% tablerow x in (a...b) %}{% endtablerow %}`, - ); - - const fixed2 = applyFix(sourceCode, offenses[1]); - expect(fixed2).to.equal( - `{% for i in (1...10) %}{% endfor %} {% tablerow x in (a..b) %}{% endtablerow %}`, - ); - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidPipeSyntax.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidPipeSyntax.spec.ts deleted file mode 100644 index 3c24e6a2b..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidPipeSyntax.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, applyFix } from '../../../test'; -import { LiquidHTMLSyntaxError } from '../index'; - -describe('Module: InvalidPipeSyntax', () => { - describe('Double pipe patterns', () => { - it('should detect and fix double pipes in variable output', async () => { - const sourceCode = `{{ 'hello' | upcase | | downcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove extra `|` character(s)'); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{{ 'hello' | upcase | downcase }}`); - }); - - it('should detect and fix double pipes with extra whitespace', async () => { - const sourceCode = `{{ 'hello' | upcase | | downcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove extra `|` character(s)'); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{{ 'hello' | upcase | downcase }}`); - }); - - it('should detect and fix double pipes in assign tag', async () => { - const sourceCode = `{% assign result = 'hello' | upcase | | downcase %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove extra `|` character(s)'); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{% assign result = 'hello' | upcase | downcase %}`); - }); - - it('should detect and fix double pipes in echo tag', async () => { - const sourceCode = `{% echo 'hello' | upcase | | downcase %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove extra `|` character(s)'); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{% echo 'hello' | upcase | downcase %}`); - }); - - it('should handle multiple double pipes in one expression', async () => { - const sourceCode = `{{ 'hello' | upcase | | downcase | | reverse }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(2); - - expect(offenses[0].message).toContain('Remove extra `|` character(s)'); - expect(offenses[1].message).toContain('Remove extra `|` character(s)'); - - expect(offenses[0].fix).toBeDefined(); - expect(offenses[1].fix).toBeDefined(); - }); - }); - - describe('Trailing pipe patterns', () => { - it('should detect and fix trailing pipe in variable output', async () => { - const sourceCode = `{{ 'hello' | upcase | }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove the trailing `|` character'); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{{ 'hello' | upcase }}`); - }); - - it('should detect and fix trailing pipe in assign tag', async () => { - const sourceCode = `{% assign result = 'hello' | upcase | %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove the trailing `|` character'); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{% assign result = 'hello' | upcase %}`); - }); - - it('should detect and fix trailing pipe in echo tag', async () => { - const sourceCode = `{% echo 'hello' | upcase | %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove the trailing `|` character'); - expect(offenses[0].fix).toBeDefined(); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{% echo 'hello' | upcase %}`); - }); - - it('should detect and fix multiple trailing pipes', async () => { - const sourceCode = `{{ 'hello' | upcase | | }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain('Remove the trailing `|` character'); - - const fixedCode = applyFix(sourceCode, offenses[0]); - expect(fixedCode).toBe(`{{ 'hello' | upcase }}`); - }); - }); - - describe('Complex pipe scenarios', () => { - it('should handle mixed valid and invalid pipes', async () => { - const sourceCode = `{{ 'hello' | upcase | | downcase | reverse | }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(2); - - // First should be double pipe - expect(offenses[0].message).toContain('Remove extra `|` character(s)'); - // Second should be trailing pipe - expect(offenses[1].message).toContain('Remove the trailing `|` character'); - }); - - it('should not interfere with valid filter parameter syntax', async () => { - const sourceCode = `{{ 'hello' | append: 'world' | upcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should handle liquid tag with mixed pipe issues', async () => { - const sourceCode = `{% liquid - assign foo = 'test' | upcase | | downcase | - echo bar | reverse - %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(2); - expect(offenses[0].message).toContain('Remove extra `|` character(s)'); - expect(offenses[1].message).toContain('Remove the trailing `|` character'); - }); - }); - - describe('Valid syntax should not be flagged', () => { - it('should not report on valid filter chains', async () => { - const sourceCode = `{{ 'hello' | upcase | append: 'world' | downcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report on simple filters', async () => { - const sourceCode = `{{ 'hello' | upcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report on filters with arguments', async () => { - const sourceCode = `{{ product.title | append: ' - ' | append: shop.name }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report on valid assign with filters', async () => { - const sourceCode = `{% assign title = product.title | upcase | truncate: 50 %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report on valid echo with filters', async () => { - const sourceCode = `{% echo product.title | upcase | truncate: 50 %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - }); - - describe('Edge cases', () => { - it('should handle pipes in string literals correctly', async () => { - const sourceCode = `{{ 'hello | world' | upcase }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report on conditional expressions with pipes', async () => { - const sourceCode = `{{ product.title | default: 'No title' }}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).toHaveLength(0); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/MultipleAssignValues.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/MultipleAssignValues.spec.ts deleted file mode 100644 index fd4a7ccfd..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/MultipleAssignValues.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { applyFix, runLiquidCheck } from '../../../test'; -import { LiquidHTMLSyntaxError } from '../index'; - -describe('detectMultipleAssignValues', async () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should not report when there are no trailing values', async () => { - const testCases = [`{% assign foo = '123' %}`, `{% assign foo = '123' | upcase %}`]; - - for (const sourceCode of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - } - }); - - it('should report when there are multiple values (no filters)', async () => { - const testCases = [ - [`{% assign foo = '123' 555 text %}`, "{% assign foo = '123' %}"], - [`{% assign foo = "123" 555 text %}`, '{% assign foo = "123" %}'], - [`{% assign foo = 123 555 text %}`, '{% assign foo = 123 %}'], - [`{% assign foo = true 555 text %}`, '{% assign foo = true %}'], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); - - it('should not report when there are no filters provided', async () => { - const sourceCode = `{% assign foo = '123' %}`; - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(0); - }); - - it('should report when there are multiple instances of the error', async () => { - const sourceCode = `{% assign foo = blank %} {% assign foo = '123' 555 text %} {% assign foo = '123' 555 text %}`; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal('Syntax is not supported'); - expect(offenses[1].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal( - `{% assign foo = blank %} {% assign foo = '123' %} {% assign foo = '123' 555 text %}`, - ); - - const fixed2 = applyFix(sourceCode, offenses[1]); - expect(fixed2).to.equal( - `{% assign foo = blank %} {% assign foo = '123' 555 text %} {% assign foo = '123' %}`, - ); - }); - - it('should report when there are multiple values (with filters)', async () => { - const testCases = [ - [`{% assign foo = '123' 555 text | upcase %}`, "{% assign foo = '123' | upcase %}"], - [`{% assign foo = "123" 555 text | upcase %}`, '{% assign foo = "123" | upcase %}'], - [`{% assign foo = 123 555 text | default: 0 %}`, '{% assign foo = 123 | default: 0 %}'], - [ - `{% assign foo = true 555 text | fake-filter: 'yes' %}`, - "{% assign foo = true | fake-filter: 'yes' %}", - ], - ]; - - for (const [sourceCode, expected] of testCases) { - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Syntax is not supported'); - - const fixed = applyFix(sourceCode, offenses[0]); - expect(fixed).to.equal(expected); - } - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts deleted file mode 100644 index 55e99a452..000000000 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { expect, describe, it, vi } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { Offense } from '../../types'; -import { LiquidHTMLSyntaxError } from './index'; - -describe('Module: LiquidHTMLSyntaxError', () => { - it('should report unclosed Liquid tags', async () => { - const sourceCode = ` - {% capture some_variable %} - Hello, world! - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `Attempting to end parsing before LiquidTag 'capture' was closed`, - ); - }); - - it('should report unclosed HTML tags', async () => { - const sourceCode = ` - - Hello, world! - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `Attempting to end parsing before HtmlElement 'a' was closed`, - ); - }); - - it('should report closing the wrong node (html/html)', async () => { - const sourceCode = ` - - Hello, world! - - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `Attempting to close HtmlElement 'b' before HtmlElement 'a' was closed`, - ); - }); - - it('should report closing the wrong node (html/liquid)', async () => { - const sourceCode = ` - - Hello, world! - {% endif %} - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `Attempting to close LiquidTag 'endif' before HtmlElement 'a' was closed`, - ); - }); - - it('should report unexpected tokens (1)', async () => { - const sourceCode = ` - {% if cond }} - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal(`Expected LiquidTagClose but got EndOfInput`); - }); - - it('should report unexpected tokens (2)', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal(`Expected HtmlTagClose but got EndOfInput`); - }); - - it('should report unexpected tokens (3)', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal(`Expected LiquidTagClose but got EndOfInput`); - }); - - it('should not report syntax error in valid Liquid code', async () => { - const sourceCode = ` - {% if some_variable %} - Hello, world! - {% endif %} - {% schema %} - { - "settings": [{ "visible_if": "{{ foo.bar == true }}" }] - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should not report canonical inline app block paths', async () => { - const sourceCode = - "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}"; - - const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should highligh the error', async () => { - let offenses: Offense[]; - let highlights: string[]; - let source: string; - - source = `
    `; - offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - highlights = highlightedOffenses({ 'file.liquid': source }, offenses); - expect(highlights).to.include(''); - - source = ``; - offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - highlights = highlightedOffenses({ 'file.liquid': source }, offenses); - expect(highlights).to.include(''); - - source = ``; - offenses = await runLiquidCheck(LiquidHTMLSyntaxError, source); - highlights = highlightedOffenses({ 'file.liquid': source }, offenses); - expect(highlights).to.include('>'); - }); -}); diff --git a/packages/theme-check-common/src/checks/liquid-nesting-depth/index.spec.ts b/packages/theme-check-common/src/checks/liquid-nesting-depth/index.spec.ts deleted file mode 100644 index 43de8b9c4..000000000 --- a/packages/theme-check-common/src/checks/liquid-nesting-depth/index.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { LiquidNestingDepth, TOLERATED_LIQUID_NESTING_DEPTH } from './index'; -import { Severity } from '../../types'; -import { check, runLiquidCheck } from '../../test'; -import type { Offense } from '../../types'; - -const DEFAULT_PATH = 'snippets/test.liquid'; - -async function checkWithMaxDepth(template: string, maxDepth: number): Promise { - return check( - { [DEFAULT_PATH]: template }, - [LiquidNestingDepth], - {}, - { LiquidNestingDepth: { enabled: true, maxDepth } }, - ); -} - -function nestedIfs(depth: number): string { - return [ - ...Array.from({ length: depth }, (_, index) => `{% if condition_${index} %}`), - 'Nested content', - ...Array.from({ length: depth }, () => '{% endif %}'), - ].join('\n'); -} - -function expectNestingMessage(offense: Offense, depth: number, maxDepth: number): void { - expect(offense).toMatchObject({ - check: 'LiquidNestingDepth', - severity: Severity.WARNING, - }); - expect(offense.message).toBe( - `This Liquid block is nested ${depth} levels deep, which exceeds the maximum allowed depth of ${maxDepth}.`, - ); -} - -describe('LiquidNestingDepth', () => { - describe('wrapper tolerated maximum', () => { - it('does not report when nesting equals the tolerated depth', async () => { - const offenses = await runLiquidCheck( - LiquidNestingDepth, - nestedIfs(TOLERATED_LIQUID_NESTING_DEPTH), - DEFAULT_PATH, - ); - - expect(offenses).toEqual([]); - }); - - it('reports when nesting exceeds the tolerated depth', async () => { - const offenses = await runLiquidCheck( - LiquidNestingDepth, - nestedIfs(TOLERATED_LIQUID_NESTING_DEPTH + 1), - DEFAULT_PATH, - ); - - expect(offenses).toHaveLength(1); - expectNestingMessage( - offenses[0], - TOLERATED_LIQUID_NESTING_DEPTH + 1, - TOLERATED_LIQUID_NESTING_DEPTH, - ); - }); - }); - - it('does not report a plain Liquid template', async () => { - const template = '
    {{ product.title }}
    '; - - await expect(checkWithMaxDepth(template, 1)).resolves.toEqual([]); - }); - - it('reports the first tag that exceeds a custom threshold', async () => { - const threshold = 2; - const nestedUnless = '{% unless hidden %}'; - const template = ` - {% if product.available %} - {% for variant in product.variants %} - ${nestedUnless} - Available variant - {% endunless %} - {% endfor %} - {% endif %} - `.trim(); - const expectedStartIndex = template.indexOf(nestedUnless); - const expectedEndIndex = expectedStartIndex + nestedUnless.length; - - const offenses = await checkWithMaxDepth(template, threshold); - - expect(offenses).toHaveLength(1); - expectNestingMessage(offenses[0], 3, threshold); - expect(offenses[0].start.index).toBe(expectedStartIndex); - expect(offenses[0].end.index).toBe(expectedEndIndex); - }); - - it('reports each nested control-flow tag deeper than the threshold', async () => { - const offenses = await checkWithMaxDepth(nestedIfs(5), 3); - - expect(offenses).toHaveLength(2); - expectNestingMessage(offenses[0], 4, 3); - expectNestingMessage(offenses[1], 5, 3); - }); - - it('does not count elsif and else branches as additional nesting', async () => { - const template = ` - {% if product.available %} - Available - {% elsif product.tags contains 'coming-soon' %} - Coming soon - {% else %} - Unavailable - {% endif %} - `.trim(); - - await expect(checkWithMaxDepth(template, 1)).resolves.toEqual([]); - }); - - it('counts tablerow as a nesting tag', async () => { - const template = ` - {% if section.blocks.size > 0 %} - {% tablerow block in section.blocks %} - {% unless block.settings.hidden %} - {{ block.settings.title }} - {% endunless %} - {% endtablerow %} - {% endif %} - `.trim(); - - const offenses = await checkWithMaxDepth(template, 2); - - expect(offenses).toHaveLength(1); - expectNestingMessage(offenses[0], 3, 2); - }); - - it('counts nested case blocks consistently with if, unless, and for blocks', async () => { - const template = ` - {% case section.settings.layout %} - {% when 'grid' %} - {% for product in collection.products %} - {% if product.available %} - {% unless product.tags contains 'hidden' %} - {{ product.title }} - {% endunless %} - {% endif %} - {% endfor %} - {% endcase %} - `.trim(); - - const offenses = await checkWithMaxDepth(template, 3); - - expect(offenses).toHaveLength(1); - expectNestingMessage(offenses[0], 4, 3); - }); - - it('does not count HTML nesting as Liquid nesting depth', async () => { - const template = ` -
    -
    -
    -
    - {% if product.available %} - Available - {% endif %} -
    -
    -
    -
    - `.trim(); - - await expect(checkWithMaxDepth(template, 1)).resolves.toEqual([]); - }); -}); 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 8ed4d3c0e..6d897e299 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 @@ -1,10 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { check, runLiquidCheck } from '../../test'; -import { blockSource } from '../../test/block-fixtures'; -import { DuplicateBlockArguments } from '../duplicate-block-arguments'; -import { MissingBlockArguments } from '../missing-block-arguments'; -import { UnrecognizedBlockArguments } from '../unrecognized-block-arguments'; -import { ValidBlockArgumentTypes } from '../valid-block-argument-types'; +import { runLiquidCheck } from '../../test'; import { LiquidSyntaxError } from './index'; import { checkBlockParserError } from './block'; import { isDocParserError } from './doc'; @@ -13,15 +8,6 @@ import { checkPartialParserError } from './partial'; import { checkRawParserError } from './raw'; import { hasRubyAcceptedWhitespaceSeparatedQuotePrefix, liquidLineTagLocation } from './utils'; -const RENDER_SYNTAX_ERROR = "Syntax error in 'render' tag"; -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'"; @@ -40,1064 +26,11 @@ const UNOPENED_JAVASCRIPT_PARSER_ERROR = const NO_DOCSET = { themeDocset: undefined } as const; describe('LiquidSyntaxError', () => { - describe('unknown tags', () => { - it('does not report unknown tags like {% foobar %}', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% foobar %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('uses injected tag docs for unknown tags inside liquid blocks', async () => { - const customThemeDocset = { - filters: async () => [], - objects: async () => [], - liquidDrops: async () => [], - tags: async () => [{ name: 'custom_tag' }], - systemTranslations: async () => ({}), - }; - - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% liquid\n custom_tag\n%}', - 'snippets/test.liquid', - { themeDocset: customThemeDocset }, - ); - - expect(offenses).toEqual([]); - }); - - it('reports unknown tags inside liquid blocks', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% liquid\n hello world\n%}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Unknown tag 'hello'", - }); - }); - }); - - describe('misplaced branch keywords', () => { - it.each([ - ['else', '{% else %}', "Unknown tag 'else'"], - ['when', '{% when x %}', "Unknown tag 'when'"], - ['when inside if', '{% if true %}{% when x %}{% endif %}', "Unknown tag 'when'"], - ['when inside for', '{% for x in xs %}{% when x %}{% endfor %}', "Unknown tag 'when'"], - ['when inside liquid', '{% liquid\n when x\n%}', "Unknown tag 'when'"], - ['elsif inside liquid', '{% liquid\n elsif x\n%}', "Unknown tag 'elsif'"], - ])('reports misplaced branch keyword %s', async (_name, template, message) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message, - }); - }); - - it.each([ - '{% if true %}{% else %}{% endif %}', - '{% case x %}{% when x %}{% else %}{% endcase %}', - '{% for x in xs %}{% else %}{% endfor %}', - ])('does not report valid else branches in %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - }); - - describe('inline comment tags', () => { - it('reports no-space multiline comments without prefixed continuation lines', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% #hello\nworld %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe( - "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character", - ); - }); - - it('accepts no-space multiline comments with prefixed continuation lines', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% #hello\n#world %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it("accepts Ruby's blank-first-line single-continuation shape", async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% #\nworld %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - }); - - describe('valid assign tags', () => { - it('produces no diagnostics for {% assign x = 1 %}', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x = 1 %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it("produces no diagnostics for {% assign x = 'hello' %}", async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% assign x = 'hello' %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for {% assign x = foo.bar %}', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x = foo.bar %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for {% assign x = foo | filter %}', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x = foo | filter %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for {% assign x = %} (empty RHS)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x = %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for {% assign x extra = x %}', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x extra = x %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each(['{% assign "x = y %}', "{% assign 'x = y %}"])( - 'matches Ruby Liquid parity for unclosed assign quote prefix in %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }, - ); - }); - - describe('invalid assign tags', () => { - it('reports LiquidSyntaxError for {% assign = 1 %} (missing LHS)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign = 1 %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for {% assign x.y = 1 %} (dotted LHS)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x.y = 1 %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for {% assign !x = 1 %} (garbage before target)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign !x = 1 %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it.each(['{% assign x 42 = x %}', "{% assign x 'hi' = x %}", '{% assign x ? = x %}'])( - 'reports diagnostics for %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBeGreaterThan(0); - }, - ); - - it('reports LiquidSyntaxError for {% assign x = foo | %} (empty filter)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x = foo | %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - }); - - describe('capture tags', () => { - it('produces no diagnostics for valid capture tags inside liquid blocks', async () => { - const template = '{% liquid\n capture x\n endcapture\n%}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports LiquidSyntaxError for skipped capture markup inside liquid blocks', async () => { - const template = '{% liquid\n capture @x\n endcapture\n%}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - }); - - describe('include tags', () => { - it.each([ - '{% include 42 %}', - '{% include 3.14 %}', - '{% include -5 %}', - '{% include true %}', - '{% include false %}', - '{% include nil %}', - '{% include blank %}', - '{% include empty %}', - '{% include (1..5) %}', - ])('produces no diagnostics for %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports LiquidSyntaxError for {% include ? %}', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% include ? %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for bare bracket access in include bindings', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% include 42 for [0] %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for multipart include named arguments', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% include 42, key.secondkey: value %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - }); - - describe('empty echo and variable output markup', () => { - it.each(['{% echo %}', '{% echo %}', '{{ }}', '{{ }}'])( - 'produces no diagnostics for %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }, - ); - - it('reports LiquidSyntaxError for garbage-only variable output', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{{ @ }}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe('Syntax error in variable output'); - }); - }); - - describe('empty first filter arguments', () => { - it.each([ - '{{ product.title | append: }}', - '{% echo product.title | append: %}', - '{% assign title = product.title | append: %}', - ])('produces no diagnostics for %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each(['{{ x y | append: }}', '{% echo x y | append: %}', '{% assign a = x y | append: %}'])( - 'reports diagnostics for malformed prefixes in %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - }, - ); - - it.each(['{{ product.title | }}', '{% echo product.title | %}'])( - 'reports diagnostics for %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - }, - ); - }); - - describe('section tags', () => { - it('produces no diagnostics for section keyword arguments', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% section 'header', color: 'red' %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - /* - * Bare bracket access in section keyword argument values is caught by the - * parser / LiquidHTMLSyntaxError layer, not by the `LiquidSyntaxError` - * section check. Running `LiquidSyntaxError` alone reports nothing here. - */ - it('does not report bare bracket section keyword argument values on its own', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% section 'header', foo: [0] %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - }); - - describe('tablerow tags', () => { - it('reports LiquidSyntaxError for bare bracket tablerow named argument values', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% tablerow product in collection.products cols: [0] %}{% endtablerow %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('uses injected tag docs to allow known loop arguments', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% tablerow product in collection.products cols: 3 limit: 6 %}{% endtablerow %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - }); - - describe('trailing comma parity', () => { - it.each([ - '{% cycle x, %}', - '{% tablerow product in collection.products, %}{% endtablerow %}', - '{% paginate collection.products by 12, %}{% endpaginate %}', - ])('produces no diagnostics for %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - '{% cycle x,, %}', - '{% cycle x @, %}', - '{% tablerow product in collection.products,, %}{% endtablerow %}', - '{% tablerow product in collection.products @, %}{% endtablerow %}', - '{% paginate collection.products by 12,, %}{% endpaginate %}', - '{% paginate collection.products by 12 @, %}{% endpaginate %}', - ])('reports diagnostics for %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBeGreaterThan(0); - }); - }); - - describe('check precedence', () => { - /* - * In the target only the `LiquidSyntaxError` check runs, so the world's - * dual-check dedup no longer applies. The single check emits one offense - * for the malformed filter on this line. - */ - it('emits a single LiquidSyntaxError on the same line', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% assign x = foo | %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('keeps one offense per line when errors are on different lines', async () => { - const template = '{% assign x = foo | %}\n{% assign = 1 %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - const checks = offenses.map((o) => o.check).sort(); - expect(checks).toEqual(['LiquidSyntaxError', 'LiquidSyntaxError']); - }); - }); - - describe('bare bracket access', () => { - it('reports LiquidSyntaxError for {% case [0] %} (bare bracket in case expression)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% case [0] %}{% when 1 %}{% endcase %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for {% when [0] %} (bare bracket in when value)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% case x %}{% when [0] %}{% endcase %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for {% capture [0] %} (bare bracket in capture tag)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% capture [0] %}{% endcapture %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for {% decrement [0] %} (bare bracket in decrement tag)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% decrement [0] %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('reports LiquidSyntaxError for {{ [0] }} (bare bracket in variable output)', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{{ [0] }}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - }); - - describe('unless tags', () => { - it('produces no diagnostics for valid unless conditions', async () => { - const templates = [ - '{% unless product.available %}Sold out{% endunless %}', - "{% unless product.available or customer.tags contains 'vip' %}Hidden{% endunless %}", - '{% unless items[0] %}Empty{% endunless %}', - '{% unless\n product.available\n%}Sold out{% endunless %}', - "{% liquid\n unless product.available\n echo 'Sold out'\n endunless\n%}", - ]; - - for (const template of templates) { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - } - }); - - it('reports LiquidSyntaxError for skipped unless garbage', async () => { - const templates = [ - '{% unless product.available @ %}Sold out{% endunless %}', - '{% unless product.available @ and customer %}Hidden{% endunless %}', - '{% unless\n product.available\n @\n%}Sold out{% endunless %}', - ]; - - for (const template of templates) { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe("Syntax error in 'unless' tag"); - } - }); - - it('reports LiquidSyntaxError for bare unless bracket access', async () => { - const template = '{% unless [0] %}Hidden{% endunless %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe('Bare bracket access is not allowed in strict2 mode'); - }); - - it('reports LiquidSyntaxError for whitespace-separated unless quote prefixes', async () => { - const templates = [ - "{% unless ' product.available %}Sold out{% endunless %}", - '{% unless " product.available %}Sold out{% endunless %}', - ]; - - for (const template of templates) { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe("Syntax error in 'unless' tag"); - } - }); - - it.each([ - "{% unless 'product.available %}Sold out{% endunless %}", - '{% unless "product.available %}Sold out{% endunless %}', - ])('reports LiquidSyntaxError for adjacent unless quote prefixes in %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe("Syntax error in 'unless' tag"); - }); - }); - describe('quote detection parity', () => { it('classifies Ruby-accepted quote prefixes with tokenizer coverage', () => { expect(hasRubyAcceptedWhitespaceSeparatedQuotePrefix("' ")).toBe(true); expect(hasRubyAcceptedWhitespaceSeparatedQuotePrefix("'")).toBe(false); - expect(hasRubyAcceptedWhitespaceSeparatedQuotePrefix("' foo ")).toBe(false); - }); - - it.each([ - "{% if product.title == 'Hat' %}Hat{% endif %}", - "{% case product.type %}{% when 'shirt' %}Shirt{% endcase %}", - "{{ 'hello' }}", - ])('produces no diagnostics for closed string markup in %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - "{% if ' product.available %}Available{% endif %}", - "{% case product.type %}{% when ' shirt %}Shirt{% endcase %}", - "{{ 'hello }}", - ])('reports LiquidSyntaxError for unclosed string markup in %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBeGreaterThan(0); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - }); - - describe('app block paths', () => { - const APP_BLOCK_PATH = - 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000'; - - it.each([ - `{% block '${APP_BLOCK_PATH}' %}{% endblock %}`, - `{% block '${APP_BLOCK_PATH}' %}\n {% endblock %}`, - `{% block '${APP_BLOCK_PATH}', block.name: 'Example' %}{% endblock %}`, - ])('produces no diagnostics for %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - [ - `{% block '${APP_BLOCK_PATH}', heading: 'Hello' %}{% endblock %}`, - "Liquid syntax error: in 'block' - app blocks do not accept arguments", - ], - [ - `{% block '${APP_BLOCK_PATH}', block.settings.heading: 'Hello' %}{% endblock %}`, - "Liquid syntax error: in 'block' - app blocks do not accept arguments", - ], - [ - `{% block '${APP_BLOCK_PATH}' %}Hello{% endblock %}`, - "Liquid syntax error: in 'block' - app blocks do not accept content", - ], - [ - `{% block '${APP_BLOCK_PATH}' %}{{ product.title }}{% endblock %}`, - "Liquid syntax error: in 'block' - app blocks do not accept content", - ], - ])('reports %s', async (template, message) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toMatchObject([{ check: 'LiquidSyntaxError', message }]); - }); - - it.each(['1', 'variable'])( - 'rejects a non-string block.name on an app path: %s', - async (value) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - `{% block '${APP_BLOCK_PATH}', block.name: ${value} %}{% endblock %}`, - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toMatchObject([ - { check: 'LiquidSyntaxError', message: "Syntax error in 'block' tag" }, - ]); - }, - ); - - it('reports malformed app block paths', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% block 'shopify://apps/example_app/snippets/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}", - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toMatchObject([ - { check: 'LiquidSyntaxError', message: "Syntax error in 'block' tag" }, - ]); - }); - - it.each([ - `{% block '${APP_BLOCK_PATH}', heading: 'First', heading: 'Second' %}{% endblock %}`, - `{% block '${APP_BLOCK_PATH}', content: 'Explicit' %}Inline{% endblock %}`, - ])('reports only the app syntax error across block checks: %s', async (template) => { - const offenses = await check({ 'templates/test.liquid': template }, [ - LiquidSyntaxError, - MissingBlockArguments, - UnrecognizedBlockArguments, - ValidBlockArgumentTypes, - DuplicateBlockArguments, - ]); - - expect(offenses).toMatchObject([ - { - check: 'LiquidSyntaxError', - message: "Liquid syntax error: in 'block' - app blocks do not accept arguments", - }, - ]); - }); - - it('produces no block parameter diagnostics for app block paths', async () => { - const offenses = await check( - { 'templates/test.liquid': `{% block '${APP_BLOCK_PATH}' %}{% endblock %}` }, - [ - LiquidSyntaxError, - MissingBlockArguments, - UnrecognizedBlockArguments, - ValidBlockArgumentTypes, - DuplicateBlockArguments, - ], - ); - - expect(offenses).toEqual([]); - }); - }); - - describe('block caller arguments', () => { - it.each([ - ['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, - template, - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toMatchObject([ - { - message: blockSettingArgumentMessage(settingId), - start: { index: template.indexOf(argument) }, - end: { index: template.indexOf(argument) + argument.length }, - }, - ]); - }); - - 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, - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toMatchObject([ - { - message: DOTTED_ARGUMENT, - 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 %}`; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toMatchObject([ - { - 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, - "{% block 'card', block.name: 'Card', heading: 'Heading' %}{% endblock %}", - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - "{% block 'card', block.settings %}{% endblock %}", - "{% block 'card', block.name: value %}Child content{% endblock %}", - ])('reports other malformed block tags on the whole tag in %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'templates/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toMatchObject([ - { - message: "Syntax error in 'block' tag", - start: { index: 0 }, - end: { index: template.length }, - }, - ]); - }); - - describe('with the block parameter checks', () => { - const CARD_BLOCK = blockSource( - [ - { id: 'heading', type: 'text' }, - { id: 'count', type: 'number' }, - ], - ['@param {string} heading - Heading', '@param {string} tracking_id - Tracking id'], - ); - const BLOCK_CALL_CHECKS = [ - LiquidSyntaxError, - MissingBlockArguments, - UnrecognizedBlockArguments, - ValidBlockArgumentTypes, - DuplicateBlockArguments, - ]; - - it('reports parameter offenses for plain arguments', async () => { - const offenses = await checkBlockCall("unknown: true, count: 'many', count: 'more'"); - - expect(offenses.map((offense) => offense.check).sort()).toEqual([ - 'DuplicateBlockArguments', - 'MissingBlockArguments', - 'MissingBlockArguments', - 'UnrecognizedBlockArguments', - 'ValidBlockArgumentTypes', - 'ValidBlockArgumentTypes', - ]); - }); - - it('accepts block.name with a string value', async () => { - const offenses = await checkBlockCall( - "block.name: 'Card', heading: 'Hello', count: 1, tracking_id: 'id'", - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - ["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.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'`); - - expect(offenses).toHaveLength(1); - expect(offenses).toMatchObject([{ check: 'LiquidSyntaxError', message }]); - }); - - function checkBlockCall(args: string) { - return check( - { - 'templates/test.liquid': `{% block 'card', ${args} %}{% endblock %}`, - 'blocks/card.liquid': CARD_BLOCK, - }, - BLOCK_CALL_CHECKS, - ); - } + expect(hasRubyAcceptedWhitespaceSeparatedQuotePrefix("' foo ")).toBe(false); }); }); @@ -1149,117 +82,9 @@ describe('LiquidSyntaxError', () => { }); }, ); - - it.each([ - { - tagName: 'block', - template: '{% endblock %}', - message: - "Attempting to close LiquidTag 'block' before it was opened without a matching 'block'", - }, - { - tagName: 'partial', - template: '{% endpartial %}', - message: - "Attempting to close LiquidTag 'partial' before it was opened without a matching 'partial'", - }, - ])( - 'reports LiquidSyntaxError for standalone end$tagName tags', - async ({ template, message }) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].message).toBe(message); - }, - ); }); describe('doc tags', () => { - it('produces no diagnostics for doc closing markup', async () => { - const template = '{% doc %}content{% enddoc foo %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports later unclosed doc blocks after accepted doc closing markup', async () => { - const template = '{% doc %}content{% enddoc foo %}{% doc %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Attempting to end parsing before LiquidRawTag 'doc' was closed", - }); - }); - - it('accepts later balanced doc blocks after accepted doc closing markup', async () => { - const template = '{% doc %}content{% enddoc foo %}{% doc %}later{% enddoc %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for whitespace-control doc closing markup', async () => { - const template = '{% doc %}content{%- enddoc foo -%}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports nested doc tags with whitespace-control delimiters', async () => { - const template = '{% doc %}{%- doc -%}{% enddoc %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: 'Nested doc tags are not allowed', - }); - }); - - it('does not treat similarly-prefixed doc tag names as nested doc tags', async () => { - const template = '{% doc %}{% docx %}{% enddoc %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - it('does not treat text-only doc mentions as doc parser errors', () => { const error = new Error(UNCLOSED_DOC_PARSER_ERROR); @@ -1278,200 +103,7 @@ describe('LiquidSyntaxError', () => { }); }); - describe('comment tags', () => { - it('reports nested comment blocks masked by a later independent comment block', async () => { - const template = - '{% comment %}{% comment %}inner{% endcomment %}{% comment %}second{% endcomment %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Liquid syntax error: 'comment' tag was never closed", - }); - }); - - it('reports nested raw blocks masked by a later independent raw block', async () => { - const template = '{% comment %}{% raw %}inner{% endcomment %}{% raw %}second{% endraw %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBeGreaterThan(0); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('accepts nested comment and raw blocks closed immediately after the parsed comment closer', async () => { - const templates = [ - '{% comment %}{% comment %}inner{% endcomment %}{% endcomment %}', - '{% comment %}{% raw %}{% endcomment %}{% endraw %}{% endcomment %}', - ]; - - for (const template of templates) { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - } - }); - }); - describe('raw tags', () => { - it('produces no diagnostics for Liquid syntax inside a valid raw body', async () => { - const template = '{% raw %}{% if broken %}{% endraw %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for raw closing markup', async () => { - const template = '{% raw %}c{% endraw foo %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports later unclosed raw blocks after accepted raw closing markup', async () => { - const template = '{% raw %}c{% endraw foo %}{% raw %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Attempting to end parsing before LiquidRawTag 'raw' was closed", - }); - }); - - it('accepts later balanced raw blocks after accepted raw closing markup', async () => { - const template = '{% raw %}c{% endraw foo %}{% raw %}later{% endraw %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for whitespace-control raw closing markup', async () => { - const template = '{% raw %}c{%- endraw foo -%}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports parser errors for similarly-prefixed raw closing tags', async () => { - const template = '{% raw %}c{% endrawx foo %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - message: "Attempting to end parsing before LiquidRawTag 'raw' was closed", - }); - }); - - it('reports LiquidSyntaxError for raw opening markup', async () => { - const template = '{% raw foo %}{% endraw %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Syntax error in 'raw' tag", - start: { index: template.indexOf('foo') }, - end: { index: template.indexOf('foo') + 'foo'.length }, - }); - }); - - it('reports LiquidSyntaxError for skipped raw opening characters', async () => { - const template = '{% raw @#$ %}{% endraw %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Syntax error in 'raw' tag", - start: { index: template.indexOf('@#$') }, - end: { index: template.indexOf('@#$') + '@#$'.length }, - }); - }); - - it('reports LiquidSyntaxError for quoted raw opening markup', async () => { - const template = "{% raw 'hello' %}{% endraw %}"; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe("Syntax error in 'raw' tag"); - }); - - it('reports LiquidSyntaxError for unclosed quoted raw opening markup', async () => { - const template = "{% raw 'hello %}{% endraw %}"; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe("Syntax error in 'raw' tag"); - }); - it('reports the raw parser error for unclosed raw tags', () => { const reports: { message: string }[] = []; @@ -1510,82 +142,9 @@ describe('LiquidSyntaxError', () => { }, ]); }); - - it('reports LiquidSyntaxError for unclosed raw tags inside liquid blocks', async () => { - const template = '{% liquid\n raw\n%}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Unclosed raw tag 'raw' in {% liquid %} block", - }); - }); - - it('does not report raw parser errors when source text merely mentions raw', async () => { - const template = 'This text mentions raw and endraw without Liquid tags.'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); }); describe('javascript tags', () => { - it.each([ - '{% javascript %}{% endjavascript %}', - '{% javascript %}{% endjavascript %}', - '{%- javascript -%}{%- endjavascript -%}', - ])('produces no diagnostics for empty javascript markup in %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'sections/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each(['{% javascript foo %}{% endjavascript %}', '{% javascript @#$ %}{% endjavascript %}'])( - 'reports LiquidSyntaxError for invalid javascript opening markup: %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'sections/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: "Syntax Error in 'javascript' - Valid syntax: javascript", - }); - }, - ); - - it('does not treat similarly-prefixed javascript tags as javascript openings', async () => { - const template = '{% javascriptx foo %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - it('accepts javascript closing markup in parser-error handling', () => { const reports: { message: string }[] = []; @@ -1598,33 +157,6 @@ describe('LiquidSyntaxError', () => { expect(reports).toEqual([]); }); - it('produces no diagnostics for javascript closing markup', async () => { - const template = "{% javascript %}console.log('x');{% endjavascript foo %}"; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'sections/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports parser errors for similarly-prefixed javascript closing tags', async () => { - const template = "{% javascript %}console.log('x');{% endjavascriptx foo %}"; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - message: "'javascript' tag was never closed", - }); - }); - it('does not report parser errors when source text merely mentions javascript', () => { const reports: { message: string }[] = []; @@ -1677,321 +209,4 @@ describe('LiquidSyntaxError', () => { ]); }); }); - - describe('style and stylesheet tags', () => { - it.each(['{% style %}{% endstyle %}', '{% style %}{% endstyle %}'])( - 'produces no diagnostics for empty style markup in %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }, - ); - - it('reports LiquidSyntaxError for style arguments', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% style extra %}{% endstyle %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: `'style' tag does not accept any arguments in "extra"`, - }); - }); - - it.each([ - '{% stylesheet %}{% endstylesheet %}', - '{% stylesheet %}{% endstylesheet %}', - "{% stylesheet 'scss' %}{% endstylesheet %}", - ])('produces no diagnostics for valid stylesheet markup in %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'sections/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - ['{% stylesheet scss %}{% endstylesheet %}', 'scss'], - ['{% stylesheet scssx %}{% endstylesheet %}', 'scssx'], - ['{% stylesheet scss extra %}{% endstylesheet %}', 'scss extra'], - ['{% stylesheet scss? %}{% endstylesheet %}', 'scss?'], - ])( - 'reports LiquidSyntaxError for invalid stylesheet markup in %s', - async (template, markup) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'sections/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: `'stylesheet' tag can only accept the string argument 'scss' in "${markup}"`, - }); - }, - ); - }); - - describe('comment tags (raw bodies)', () => { - it.each([ - '{% comment %}{% comment %}inner{% endcomment %}', - '{%- comment -%}{%- comment -%}inner{%- endcomment -%}', - '{% comment %}{% raw %}hello{% endcomment %}', - '{% comment %}this is {{{ not }}} valid{{ liquid{% endcomment %}', - '{% comment %}this is {% if liquid{% endcomment %}', - ])('reports LiquidSyntaxError for Ruby-stricter comment raw bodies: %s', async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); - - it('produces no diagnostics for inert closed Liquid tags inside comment bodies', async () => { - const template = '{% comment %}{% if %}{% endcomment %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for closed raw tags inside comment bodies', async () => { - const template = '{% comment %}{% raw %}hello{% endraw %}{% endcomment %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('does not treat similarly-prefixed names as comment or raw tags', async () => { - const template = '{% comment %}{% commentx %}{% rawx %}{% endcomment %}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('does not report parser errors when source text merely mentions comment tags', async () => { - const template = 'This text mentions comment and endcomment without Liquid tags.'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - '{% comment %}{% comment %}inner{% endcomment %}{% endcomment %}', - '{% comment %}{% comment %}{% comment %}deep{% endcomment %}{% endcomment %}{% endcomment %}', - '{% comment %}{% raw %}{% endcomment %}{% endraw %}{% endcomment %}', - '{%- comment -%}{%- raw -%}{%- endcomment -%}{%- endraw -%}{%- endcomment -%}', - ])( - 'produces no diagnostics for Ruby-accepted inert comment raw bodies: %s', - async (template) => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }, - ); - }); - - describe('non-assign valid Liquid', () => { - it('produces no diagnostics for valid Liquid', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% if true %}hello{% endif %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for Liquid output tags', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{{ shop.name }}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - }); - - describe('valid render tags', () => { - it('produces no diagnostics for valid render markup', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% render 'snippet' %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('produces no diagnostics for valid render markup inside liquid blocks', async () => { - const template = "{% liquid\n render 'snippet', product: product\n%}"; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses).toEqual([]); - }); - - it('reports LiquidSyntaxError for skipped quote prefixes', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% render \"'snippet' %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe(RENDER_SYNTAX_ERROR); - }); - }); - - describe('invalid render tags', () => { - it('reports LiquidSyntaxError for malformed string markup', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% render %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe(RENDER_SYNTAX_ERROR); - }); - - it('reports LiquidSyntaxError for malformed string markup inside liquid blocks', async () => { - const template = '{% liquid\n render\n%}'; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe(RENDER_SYNTAX_ERROR); - }); - - it('reports LiquidSyntaxError for bare bracket access in render snippet lookups', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - '{% render [0] %}', - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe(BARE_BRACKET_ACCESS); - }); - - it('reports LiquidSyntaxError for bare bracket access in render binding values', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% render 'snippet' for [0] %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe(BARE_BRACKET_ACCESS); - }); - - it('reports LiquidSyntaxError for bare bracket access in render keyword values', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% render 'snippet', x: [0] %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe(BARE_BRACKET_ACCESS); - }); - - it('reports LiquidSyntaxError for skipped trailing garbage in render markup', async () => { - const offenses = await runLiquidCheck( - LiquidSyntaxError, - "{% render 'snippet' ? %}", - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - expect(offenses[0].message).toBe(RENDER_SYNTAX_ERROR); - }); - - it('reports LiquidSyntaxError for skipped trailing garbage inside liquid blocks', async () => { - const template = "{% liquid\n render 'snippet' ?\n%}"; - const offenses = await runLiquidCheck( - LiquidSyntaxError, - template, - 'snippets/test.liquid', - NO_DOCSET, - ); - - expect(offenses.length).toBe(1); - expect(offenses[0]).toMatchObject({ - check: 'LiquidSyntaxError', - message: RENDER_SYNTAX_ERROR, - start: { index: template.indexOf('render') }, - end: { index: template.indexOf('?') + '?'.length }, - }); - }); - }); }); diff --git a/packages/theme-check-common/src/checks/matching-translations/index.spec.ts b/packages/theme-check-common/src/checks/matching-translations/index.spec.ts deleted file mode 100644 index efea1e4e7..000000000 --- a/packages/theme-check-common/src/checks/matching-translations/index.spec.ts +++ /dev/null @@ -1,427 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { autofix, check, highlightedOffenses } from '../../test'; -import { MatchingTranslations } from '../../checks/matching-translations/index'; - -const prettyJSON = (json: any) => JSON.stringify(json, null, 2); - -describe('Module: MatchingTranslations', async () => { - it('should report offenses when the translation file is missing a key', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - world: 'World', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(1); - expect(offenses).to.containOffense("The translation for 'world' is missing"); - } - }); - - it('should report offenses when the default translation is missing a key', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - world: 'Mundo', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(1); - expect(offenses).to.containOffense("A default translation for 'world' does not exist"); - expect(offenses[0]!).to.suggest( - theme[`locales/pt-BR${prefix}.json`], - 'Delete unneeded translation key', - { - startIndex: 0, - endIndex: theme[`locales/pt-BR${prefix}.json`].length, - insert: prettyJSON({ - hello: 'Olá', - }), - }, - ); - } - }); - - it('should report offenses when nested translation keys do not exist', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { world: 'Hello, world!' }, - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: {}, - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(1); - expect(offenses).to.containOffense({ - message: "The translation for 'hello.world' is missing", - uri: `file:///locales/pt-BR${prefix}.json`, - }); - - const fixed = await autofix(theme, offenses); - expect(fixed[`locales/pt-BR${prefix}.json`]).to.eql( - prettyJSON({ - hello: { - world: 'TODO', - }, - }), - ); - } - }); - - it('should report offenses when translation shapes do not match', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { world: 'Hello, world!' }, - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(2); - expect(offenses).to.containOffense({ - message: "A default translation for 'hello' does not exist", - uri: `file:///locales/pt-BR${prefix}.json`, - }); - expect(offenses).to.containOffense({ - message: "The translation for 'hello.world' is missing", - uri: `file:///locales/pt-BR${prefix}.json`, - }); - - const fixed = await autofix(theme, offenses); - - expect(fixed[`locales/pt-BR${prefix}.json`]).to.eql( - prettyJSON({ - hello: { world: 'TODO' }, - }), - ); - } - }); - - it('should report offenses when nested translation keys do not match', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { world: 'Hello, world!' }, - }), - [`locales/fr${prefix}.json`]: JSON.stringify({ - hello: { monde: 'Bonjour, monde' }, - }), - [`locales/es-ES${prefix}.json`]: JSON.stringify({ - hello: { world: 'Hello, world!', mundo: { hola: '¡Hola, mundo!' } }, - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(3); - expect(offenses).to.containOffense({ - message: "A default translation for 'hello.monde' does not exist", - uri: `file:///locales/fr${prefix}.json`, - }); - expect(offenses).to.containOffense({ - message: "A default translation for 'hello.mundo.hola' does not exist", - uri: `file:///locales/es-ES${prefix}.json`, - }); - expect(offenses).to.containOffense({ - message: "The translation for 'hello.world' is missing", - uri: `file:///locales/fr${prefix}.json`, - }); - - const fixed = await autofix(theme, offenses); - expect(fixed[`locales/fr${prefix}.json`]).to.eql( - prettyJSON({ - hello: { monde: 'Bonjour, monde', world: 'TODO' }, - }), - ); - - // Default does not exist should be a suggestion and not autofixed. - expect(fixed[`locales/es-ES${prefix}.json`]).to.eql(theme[`locales/es-ES${prefix}.json`]); - } - }); - - it('should not report offenses when default translations do not exist', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(0); - } - }); - - it('should not report offenses when translations match', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - world: 'World', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - world: 'Mundo', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(0); - } - }); - - it('should not report offenses when nested translations match', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { world: 'Hello, world!' }, - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: { world: 'Olá, mundo!' }, - }), - [`locales/fr${prefix}.json`]: JSON.stringify({ - hello: { world: 'Bonjour, monde' }, - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(0); - } - }); - - it('should not report offenses and ignore pluralization', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { - one: 'Hello, you', - other: "Hello, y'all", - }, - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: { - zero: 'Estou sozinho :(', - few: 'Olá, galerinha :)', - }, - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(0); - } - }); - - it('should not report offenses and ignore keys provided by Shopify', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - shopify: { - checkout: { - general: { - page_title: 'Checkout', - }, - }, - }, - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - shopify: { - sentence: { - words_connector: 'hello world', - }, - }, - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(0); - } - }); - - it('should not report offenses and ignore keys provided by customer accounts', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - customer_accounts: { - order: { - title: 'Order', - }, - }, - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - customer_accounts: { - profile: { - title: 'Perfil', - }, - }, - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(0); - } - }); - - it('should not report offenses and ignore "*.schema.json" files', async () => { - const theme = { - 'locales/en.default.json': JSON.stringify({ hello: 'Hello' }), - 'locales/pt-BR.schema.json': JSON.stringify({}), - }; - - const offenses = await check(theme, [MatchingTranslations]); - - expect(offenses).to.be.of.length(0); - }); - - it('should highlight the proper element when the translation file is missing a key', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - world: 'World', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - const elements = highlightedOffenses(theme, offenses); - - expect(elements).to.deep.eq(['{"hello":"Olá"}']); - } - }); - - it('should highlight the proper element when the default translation is missing a key', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: 'Hello', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - world: 'Mundo', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - const elements = highlightedOffenses(theme, offenses); - - expect(elements).to.deep.eq(['"world":"Mundo"']); - } - }); - - it('should highlight the proper element when nested translation keys do not exist', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { - world: 'Hello, world!', - }, - welcome: 'Welcome', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: {}, - welcome: 'Bem-vinda', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - const elements = highlightedOffenses(theme, offenses); - - expect(elements).to.deep.eq(['"hello":{}']); - } - }); - - it('should highlight the proper element when nested translation keys do not exist and there is a sibling node', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { - shopify: 'Shopify!', - world: 'Hello, world!', - }, - welcome: 'Welcome', - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: { - shopify: 'Shopify!', - }, - welcome: 'Bem-vinda', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - const elements = highlightedOffenses(theme, offenses); - - expect(elements).to.deep.eq(['"hello":{"shopify":"Shopify!"}']); - } - }); - - it('should highlight the proper element when translation shapes do not match', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { world: 'Hello, world!' }, - }), - [`locales/pt-BR${prefix}.json`]: JSON.stringify({ - hello: 'Olá', - }), - }; - - const offenses = await check(theme, [MatchingTranslations]); - const elements = highlightedOffenses(theme, offenses); - - // We have two elements because we have two offenses: - // - A default translation for 'hello' does not exist" - // - The translation for 'hello.world' is missing" - expect(elements).to.deep.eq(['"hello":"Olá"', '"hello":"Olá"']); - } - }); - - it('should not highlight anything if the file is unparseable', async () => { - for (const prefix of ['', '.schema']) { - const theme = { - [`locales/en.default${prefix}.json`]: JSON.stringify({ - hello: { world: 'Hello, world!' }, - }), - [`locales/pt-BR${prefix}.json`]: `{"hello": }`, - }; - - const offenses = await check(theme, [MatchingTranslations]); - expect(offenses).to.have.length(0); - } - }); -}); diff --git a/packages/theme-check-common/src/checks/max-file-size/index.spec.ts b/packages/theme-check-common/src/checks/max-file-size/index.spec.ts deleted file mode 100644 index 43933482b..000000000 --- a/packages/theme-check-common/src/checks/max-file-size/index.spec.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { MaxFileSize, MaxFileSizeJSON } from './index'; -import { runLiquidCheck, runJSONCheck } from '../../test'; - -const KILOBYTE = 1024; -const MEGABYTE = 1024 * KILOBYTE; - -type FileLimitCase = { - key: string; - path: string; - limitBytes: number; -}; - -/* prettier-ignore */ /// ignoring Prettier here to keep comments readable. -const FILE_LIMIT_CASES: FileLimitCase[] = [ - { key: 'assets', path: 'assets/theme.css', limitBytes: 20 * MEGABYTE }, - { key: 'blocks', path: 'blocks/product-card.liquid', limitBytes: 256 * KILOBYTE }, - { key: 'config/settings_data.json', path: 'config/settings_data.json', limitBytes: 1.5 * MEGABYTE }, - { key: 'config/settings_schema.json', path: 'config/settings_schema.json', limitBytes: 512 * KILOBYTE }, - { key: 'config', path: 'config/markets.json', limitBytes: 256 * KILOBYTE }, - { key: 'layout', path: 'layout/theme.liquid', limitBytes: 256 * KILOBYTE }, - { key: 'snippets', path: 'snippets/card.liquid', limitBytes: 256 * KILOBYTE }, - { key: 'templates/*.json', path: 'templates/product.json', limitBytes: 512 * KILOBYTE }, - { key: 'templates', path: 'templates/product.liquid', limitBytes: 256 * KILOBYTE }, - { key: 'locales', path: 'locales/en.default.json', limitBytes: 1.5 * MEGABYTE }, - { key: 'sections/*.json', path: 'sections/product.json', limitBytes: 512 * KILOBYTE }, - { key: 'sections', path: 'sections/product.liquid', limitBytes: 256 * KILOBYTE }, -]; - -describe('MaxFileSize', () => { - it.each(FILE_LIMIT_CASES)( - 'accepts $key at the exact byte limit', - async ({ path, limitBytes }) => { - const offenses = await lint(path, sourceWithByteSize(path, limitBytes)); - - expect(offenses).toEqual([]); - }, - ); - - it.each(FILE_LIMIT_CASES)( - 'reports $key one byte over the limit', - async ({ path, limitBytes }) => { - const offenses = await lint(path, sourceWithByteSize(path, limitBytes + 1)); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - check: 'MaxFileSize', - uri: `file:///${path}`, - message: expect.stringContaining(`the limit is ${formatBytes(limitBytes)}`), - }); - }, - ); - - it('reports oversized files under a VFS-prefixed theme path', async () => { - const path = 'workspace/themes/123/templates/index.liquid'; - const offenses = await lint(path, 'a'.repeat(256 * KILOBYTE + 1)); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - check: 'MaxFileSize', - uri: `file:///${path}`, - message: expect.stringContaining('the limit is 256 KB'), - }); - }); - - it('excludes schema block bytes for template-table files', async () => { - const source = - 'a'.repeat(256 * KILOBYTE) + - '{% schema %}' + - JSON.stringify({ - name: 'Large schema', - settings: [{ type: 'textarea', id: 'copy', label: 'Copy', default: 'b'.repeat(KILOBYTE) }], - }) + - '{% endschema %}'; - - const offenses = await lint('sections/schema-test.liquid', source); - - expect(offenses).toEqual([]); - }); - - it('excludes whitespace-controlled schema block bytes for template-table files', async () => { - const source = - 'a'.repeat(256 * KILOBYTE) + - '{%- schema -%}' + - JSON.stringify({ - name: 'Large schema', - settings: [{ type: 'textarea', id: 'copy', label: 'Copy', default: 'b'.repeat(KILOBYTE) }], - }) + - '{%- endschema -%}'; - - const offenses = await lint('sections/schema-whitespace-control-test.liquid', source); - - expect(offenses).toEqual([]); - }); - - it.each(['_drafts/bfcm/templates/product.json', '_drafts/bfcm/sections/product.json'])( - 'ignores oversized draft file %s', - async (path) => { - const offenses = await lint(path, jsonWithByteSize(20 * MEGABYTE + 1)); - - expect(offenses).toEqual([]); - }, - ); - - it('measures UTF-8 bytes instead of JavaScript string length', async () => { - const source = 'é'.repeat(128 * KILOBYTE + 1); - - expect(source.length).toBeLessThan(256 * KILOBYTE); - expect(Buffer.byteLength(source)).toBeGreaterThan(256 * KILOBYTE); - - const offenses = await lint('snippets/multibyte.liquid', source); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - check: 'MaxFileSize', - message: expect.stringContaining('262146 bytes'), - }); - }); - - it('reports oversized files even when the Liquid parser cannot build an AST', async () => { - const source = 'a'.repeat(256 * KILOBYTE + 1) + '{% raw %}'; - - const offenses = await lint('snippets/broken.liquid', source); - - expect(offenses).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - check: 'MaxFileSize', - uri: 'file:///snippets/broken.liquid', - message: expect.stringContaining('the limit is 256 KB'), - }), - ]), - ); - }); -}); - -async function lint(path: string, source: string) { - // The check harness parses only `.liquid` files as LiquidHtml; every other - // extension (`.json`, `.css`, ...) is treated as a JSON source (see - // to-source-code.ts). Route each file to the check that will actually visit - // it, so the Liquid-vs-JSON selection matches the harness's own file typing. - if (path.endsWith('.liquid')) { - return runLiquidCheck(MaxFileSize, source, path); - } - - return runJSONCheck(MaxFileSizeJSON, source, path); -} - -function sourceWithByteSize(path: string, bytes: number): string { - if (path === 'layout/theme.liquid') { - const layoutSource = - '{{ content_for_header }}{{ content_for_layout }}'; - return layoutSource + 'a'.repeat(bytes - Buffer.byteLength(layoutSource)); - } - - if (path === 'config/settings_schema.json') { - return jsonArrayWithByteSize(bytes); - } - - if (path.endsWith('.json')) { - return jsonWithByteSize(bytes); - } - - return 'a'.repeat(bytes); -} - -function jsonArrayWithByteSize(bytes: number): string { - // Wrapping the object form in `[]` adds exactly 2 bytes. - return `[${jsonWithByteSize(bytes - 2)}]`; -} - -function jsonWithByteSize(bytes: number): string { - // Build {"d":"aaa..."} where the filler length is adjusted to hit the exact byte target. - const shell = '{"d":""}'; // 8 bytes - const filler = 'a'.repeat(bytes - shell.length); - return `{"d":"${filler}"}`; -} - -function formatBytes(bytes: number): string { - if (bytes % MEGABYTE === 0) { - return `${bytes / MEGABYTE} MB`; - } - - if (bytes % KILOBYTE === 0) { - return `${bytes / KILOBYTE} KB`; - } - - return `${bytes} bytes`; -} diff --git a/packages/theme-check-common/src/checks/missing-asset/index.spec.ts b/packages/theme-check-common/src/checks/missing-asset/index.spec.ts deleted file mode 100644 index de1bff3e5..000000000 --- a/packages/theme-check-common/src/checks/missing-asset/index.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { MissingAsset } from '.'; -import { check } from '../../test'; - -describe('Module: MissingAsset', () => { - it('should report the missing asset when assigned to a variable', async () => { - const file = ` - {% assign logo_url = 'logo.png' | asset_url %} - Logo - `; - const files = { - 'snippets/snippet.liquid': file, - 'snippets/existing.liquid': '', - }; - - const offenses = await check(files, [MissingAsset]); - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: MissingAsset.meta.code, - message: "'assets/logo.png' does not exist", - uri: 'file:///snippets/snippet.liquid', - }); - }); - - it('should report the missing asset when defined inline', async () => { - const file = ``; - const files = { - 'snippets/snippet.liquid': file, - 'snippets/existing.liquid': '', - }; - - const offenses = await check(files, [MissingAsset]); - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: MissingAsset.meta.code, - message: "'assets/styles.css' does not exist", - uri: 'file:///snippets/snippet.liquid', - }); - }); - - it('should report the missing asset when multiple filters applied', async () => { - const file = ``; - const files = { - 'snippets/snippet.liquid': file, - 'snippets/existing.liquid': '', - }; - - const offenses = await check(files, [MissingAsset]); - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: MissingAsset.meta.code, - message: "'assets/styles.css' does not exist", - uri: 'file:///snippets/snippet.liquid', - }); - }); - - it('should report no offenses when an asset file exists', async () => { - const file = ``; - const files = { - 'snippets/snippet.liquid': file, - 'assets/styles.css': '', - }; - - const offenses = await check(files, [MissingAsset]); - - expect(offenses).to.have.length(0); - }); - - it('should not report for compiled .js.liquid files', async () => { - const file = `{{ 'foo.js' | asset_url }} `; - const files = { - 'snippets/snippet.liquid': file, - 'assets/foo.js.liquid': 'console.log("{{ "hi" }}");', - }; - - const offenses = await check(files, [MissingAsset]); - - expect(offenses).to.have.length(0); - }); - - it('should not report for compiled .css.liquid files', async () => { - const file = `{{ 'foo.css' | asset_url }} `; - const files = { - 'snippets/snippet.liquid': file, - 'assets/foo.css.liquid': 'body { color: {{ "blue" }}; }', - }; - - const offenses = await check(files, [MissingAsset]); - - expect(offenses).to.have.length(0); - }); - - it('should not report for compiled .scss.liquid files', async () => { - const file = `{{ 'foo.scss.css' | asset_url }} `; - const files = { - 'snippets/snippet.liquid': file, - 'assets/foo.scss.liquid': 'html { & body { color: {{ "blue" }}; } }', - }; - - const offenses = await check(files, [MissingAsset]); - - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/missing-block-arguments/index.spec.ts b/packages/theme-check-common/src/checks/missing-block-arguments/index.spec.ts deleted file mode 100644 index 152fe5396..000000000 --- a/packages/theme-check-common/src/checks/missing-block-arguments/index.spec.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { MissingBlockArguments } from './index'; -import { - blockSource, - INVALID_SCHEMA_BLOCK, - liquidDocBlock, - runBlockCallCheck, -} from '../../test/block-fixtures'; - -const DEVELOPER_BLOCK = blockSource( - [{ id: 'title', type: 'text' }], - ['@param {string} developer_id - Developer-only id'], -); - -const REQUIRED_BUILT_IN_CONTENT_BLOCK = blockSource([], ['@param {string} content - Body']); -const REQUIRED_SCHEMA_CONTENT_BLOCK = blockSource( - [{ id: 'content', type: 'text' }], - ['@param {string} content - Body'], -); -const LIQUID_DOC_ONLY_BLOCK = liquidDocBlock(['@param {string} tracking_id - Tracking id']); - -describe('MissingBlockArguments', () => { - it.each([ - [ - 'implicit platform defaults', - [ - { id: 'title', type: 'text' }, - { id: 'enabled', type: 'checkbox' }, - ], - ], - [ - 'explicit schema defaults', - [ - { id: 'columns', type: 'range', min: 1, max: 4, step: 1, default: 2 }, - { id: 'heading_font', type: 'font_picker', default: 'assistant_n4' }, - ], - ], - [ - 'settings that forbid defaults', - [ - { id: 'product', type: 'product' }, - { id: 'image', type: 'image_picker' }, - ], - ], - ])('does not require schema settings with %s', async (_name, settings) => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - blockSource(settings), - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - [ - 'an implicit platform default', - { id: 'enabled', type: 'checkbox' }, - '@param {boolean} enabled - Enabled', - ], - [ - 'an explicit schema default', - { id: 'columns', type: 'range', min: 1, max: 4, step: 1, default: 2 }, - '@param {number} columns - Columns', - ], - [ - 'a setting that forbids defaults', - { id: 'image', type: 'image_picker' }, - '@param {image} image - Image', - ], - ])('requires a LiquidDoc echo of a schema setting with %s', async (_name, setting, doc) => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - blockSource([setting], [doc]), - ); - - expect(offenses).toMatchObject([ - { message: `Missing required argument '${setting.id}' in block tag for 'card'.` }, - ]); - }); - - it('keeps optional LiquidDoc echoes of schema settings optional', async () => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - blockSource([{ id: 'title', type: 'text' }], ['@param {string} [title] - Title']), - ); - - expect(offenses).toEqual([]); - }); - - it('applies requiredness to LiquidDoc-only developer parameters', async () => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - DEVELOPER_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Missing required argument 'developer_id' in block tag for 'card'." }, - ]); - }); - - it('requires LiquidDoc-only parameters from a schema-less block', async () => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - LIQUID_DOC_ONLY_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Missing required argument 'tracking_id' in block tag for 'card'." }, - ]); - }); - - it.each([ - ['built-in content', REQUIRED_BUILT_IN_CONTENT_BLOCK], - ['schema-backed content', REQUIRED_SCHEMA_CONTENT_BLOCK], - ])('accepts an explicit argument for required %s', async (_name, block) => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card', content: body %}{% endblock %}", - block, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - ['text', "{% block 'card' %}Body{% endblock %}"], - ['a comment', "{% block 'card' %}{% comment %}Hidden{% endcomment %}{% endblock %}"], - ['an assignment', "{% block 'card' %}{% assign value = 'hidden' %}{% endblock %}"], - ])('treats %s in the invocation body as required content', async (_name, template) => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - template, - REQUIRED_BUILT_IN_CONTENT_BLOCK, - ); - - expect(offenses).toEqual([]); - }); - - it('accepts a body for required schema-backed content', async () => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}Body{% endblock %}", - REQUIRED_SCHEMA_CONTENT_BLOCK, - ); - - expect(offenses).toEqual([]); - }); - - it('does not treat a whitespace-only body as required content', async () => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}\n \t\n{% endblock %}", - REQUIRED_BUILT_IN_CONTENT_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Missing required argument 'content' in block tag for 'card'." }, - ]); - }); - - it('requires schema-backed content when LiquidDoc declares it required', async () => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - REQUIRED_SCHEMA_CONTENT_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Missing required argument 'content' in block tag for 'card'." }, - ]); - }); - - it('keeps schema-backed content optional when LiquidDoc declares it optional', async () => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - blockSource([{ id: 'content', type: 'text' }], ['@param {string} [content] - Body']), - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - ['missing target', undefined], - ['invalid target schema', INVALID_SCHEMA_BLOCK], - ])('does not report missing arguments for an unreadable %s', async (_name, block) => { - const offenses = await runBlockCallCheck( - MissingBlockArguments, - "{% block 'card' %}{% endblock %}", - block, - ); - - expect(offenses).toEqual([]); - }); -}); diff --git a/packages/theme-check-common/src/checks/missing-content-for-arguments/index.spec.ts b/packages/theme-check-common/src/checks/missing-content-for-arguments/index.spec.ts deleted file mode 100644 index 7ac97a639..000000000 --- a/packages/theme-check-common/src/checks/missing-content-for-arguments/index.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { applySuggestions, runLiquidCheck } from '../../test'; -import { MissingContentForArguments } from '.'; - -function check(existingCode: string, source: string) { - return runLiquidCheck( - MissingContentForArguments, - source, - undefined, - {}, - { - 'blocks/card.liquid': existingCode, - }, - ); -} - -const blockSourceCode = ` - {% doc %} - @param {string} required_string - A required string - @param {number} required_number - A required number - @param {boolean} required_boolean - A required boolean - @param {object} required_object - A required object - @param {string} [optional_string] - An optional string - @param {number} [optional_number] - An optional number - @param {object} [optional_object] - An optional object - @param {boolean} [optional_boolean] - An optional boolean - {% enddoc %} -`; - -describe('Module: MissingContentForArguments', () => { - describe('missing required arguments', () => { - it('should report missing required arguments', async () => { - const offenses = await check(blockSourceCode, `{% content_for 'block', type: 'card' %}`); - - expect(offenses).toHaveLength(4); - expect(offenses[0].message).toBe( - "Missing required argument 'required_string' in content_for tag for static block 'card'.", - ); - expect(offenses[1].message).toBe( - "Missing required argument 'required_number' in content_for tag for static block 'card'.", - ); - expect(offenses[2].message).toBe( - "Missing required argument 'required_boolean' in content_for tag for static block 'card'.", - ); - expect(offenses[3].message).toBe( - "Missing required argument 'required_object' in content_for tag for static block 'card'.", - ); - }); - }); - - describe('suggestions', () => { - it('should suggest adding missing required arguments when none already exist', async () => { - const sourceCode = `{% content_for 'block', type: 'card' %}`; - const offenses = await check(blockSourceCode, sourceCode); - - expect(offenses).toHaveLength(4); - const x = applySuggestions(sourceCode, offenses[0]); - expect(x).toEqual([`{% content_for 'block', type: 'card', required_string: '' %}`]); - }); - }); - - describe('edge cases', () => { - it('should handle mixed case type annotations', async () => { - const sourceCode = `{% content_for 'block', type: 'card', text: "hello", count: 5, flag: true, data: product %}`; - const offenses = await runLiquidCheck( - MissingContentForArguments, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {String} text - The text - @param {NUMBER} count - The count - @param {BOOLEAN} flag - The flag - @param {Object} data - The data - {% enddoc %} -
    {{ text }}{{ count }}{{ flag }}{{ data }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not report when block has no doc comment', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - MissingContentForArguments, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': `

    This snippet has no doc comment

    `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when LiquidDoc definition has no defined params', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - MissingContentForArguments, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @description this is a description - @example this is an example - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when block name is a VariableLookup', async () => { - const sourceCode = `{% assign block_name = 'card' %}{% content_for 'block', type: block_name, title: 'My Card' %}`; - const offenses = await runLiquidCheck( - MissingContentForArguments, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - @param {string} description - The description of the card - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/missing-render-snippet-arguments/index.spec.ts b/packages/theme-check-common/src/checks/missing-render-snippet-arguments/index.spec.ts deleted file mode 100644 index 15d077c93..000000000 --- a/packages/theme-check-common/src/checks/missing-render-snippet-arguments/index.spec.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { applySuggestions, MockTheme, runLiquidCheck } from '../../test'; -import { MissingRenderSnippetArguments } from '.'; - -function check(snippet: string, source: string) { - return runLiquidCheck( - MissingRenderSnippetArguments, - source, - undefined, - {}, - { - 'snippets/card.liquid': snippet, - }, - ); -} - -const defaultSnippet = ` - {% doc %} - @param {string} required_string - A required string - @param {number} required_number - A required number - @param {boolean} required_boolean - A required boolean - @param {object} required_object - A required object - @param {string} [optional_string] - An optional string - @param {number} [optional_number] - An optional number - @param {object} [optional_object] - An optional object - @param {boolean} [optional_boolean] - An optional boolean - {% enddoc %} -`; - -describe('Module: MissingRenderSnippetArguments', () => { - describe('missing required arguments', () => { - it('should report missing required arguments', async () => { - const offenses = await check(defaultSnippet, `{% render 'card' %}`); - - expect(offenses).toHaveLength(4); - expect(offenses[0].message).toBe( - "Missing required argument 'required_string' in render tag for snippet 'card'.", - ); - expect(offenses[1].message).toBe( - "Missing required argument 'required_number' in render tag for snippet 'card'.", - ); - expect(offenses[2].message).toBe( - "Missing required argument 'required_boolean' in render tag for snippet 'card'.", - ); - expect(offenses[3].message).toBe( - "Missing required argument 'required_object' in render tag for snippet 'card'.", - ); - }); - }); - - describe('suggestions', () => { - it('should suggest adding missing required arguments when none already exist', async () => { - const sourceCode = `{% render 'card' %}`; - const offenses = await check(defaultSnippet, sourceCode); - - expect(offenses).toHaveLength(4); - const x = applySuggestions(sourceCode, offenses[0]); - expect(x).toEqual([`{% render 'card', required_string: '' %}`]); - }); - - it('should suggest adding missing argument after provided alias variable', async () => { - const mockTheme = { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - @param {string} description - The description of the card - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - } as MockTheme; - - let sourceCode = `{% render 'card' with 'my-card' as title %}`; - let offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - mockTheme, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Missing required argument 'description' in render tag for snippet 'card'.", - ); - - let result = applySuggestions(sourceCode, offenses[0]); - expect(result).toEqual([`{% render 'card' with 'my-card' as title, description: '' %}`]); - - sourceCode = `{% render 'card' for titles as title %}`; - offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - mockTheme, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Missing required argument 'description' in render tag for snippet 'card'.", - ); - - result = applySuggestions(sourceCode, offenses[0]); - expect(result).toEqual([`{% render 'card' for titles as title, description: '' %}`]); - }); - }); - - describe('edge cases', () => { - it('should handle mixed case type annotations', async () => { - const sourceCode = `{% render 'card', text: "hello", count: 5, flag: true, data: product %}`; - const offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {String} text - The text - @param {NUMBER} count - The count - @param {BOOLEAN} flag - The flag - @param {Object} data - The data - {% enddoc %} -
    {{ text }}{{ count }}{{ flag }}{{ data }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not report when snippet has no doc comment', async () => { - const sourceCode = `{% render 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': `

    This snippet has no doc comment

    `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when LiquidDoc definition has no defined params', async () => { - const sourceCode = `{% render 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @description this is a description - @example this is an example - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when snippet name is a VariableLookup', async () => { - const sourceCode = `{% assign snippet_name = 'card' %}{% render snippet_name, title: 'My Card' %}`; - const offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - @param {string} description - The description of the card - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should report when "with/for" alias syntax is used', async () => { - const mockTheme = { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - {% enddoc %} -
    {{ title }}
    - `, - } as MockTheme; - - let sourceCode = `{% render 'card' with 'my-card' as unknown %}`; - let offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - mockTheme, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "Missing required argument 'title' in render tag for snippet 'card'.", - ); - - sourceCode = `{% render 'card' for array as unknown %}`; - offenses = await runLiquidCheck( - MissingRenderSnippetArguments, - sourceCode, - undefined, - {}, - mockTheme, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "Missing required argument 'title' in render tag for snippet 'card'.", - ); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/missing-template/index.spec.ts b/packages/theme-check-common/src/checks/missing-template/index.spec.ts deleted file mode 100644 index f54bec6f8..000000000 --- a/packages/theme-check-common/src/checks/missing-template/index.spec.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { MissingTemplate } from '.'; -import { check } from '../../test'; - -describe('Module: MissingTemplate', () => { - it('should report missing template errors', async () => { - const testCases = [ - { - testCase: 'should report the missing snippet to be rendered with "render"', - file: ` - {% render 'existing' %} - {% render 'missing' with foo as arg %} - {% render myvariable %} - `, - expected: { - message: "'snippets/missing.liquid' does not exist", - uri: 'file:///snippets/snippet.liquid', - start: { index: 51, line: 2, character: 18 }, - end: { index: 60, line: 2, character: 27 }, - }, - filesWith: (file: string) => ({ - 'snippets/snippet.liquid': file, - 'snippets/existing.liquid': '', - }), - }, - { - testCase: 'should report the missing snippet to be rendered with "include"', - file: "{% include 'missing' %}", - expected: { - message: "'snippets/missing.liquid' does not exist", - uri: 'file:///snippets/snippet.liquid', - start: { index: 11, line: 0, character: 11 }, - end: { index: 20, line: 0, character: 20 }, - }, - filesWith: (file: string) => ({ - 'snippets/snippet.liquid': file, - 'snippets/existing.liquid': '', - }), - }, - { - testCase: 'should report the missing section to be rendered with "section"', - file: "{% section 'missing' %}", - expected: { - message: "'sections/missing.liquid' does not exist", - uri: 'file:///sections/section.liquid', - start: { index: 11, line: 0, character: 11 }, - end: { index: 20, line: 0, character: 20 }, - }, - filesWith: (file: string) => ({ - 'sections/section.liquid': file, - 'sections/existing.liquid': '', - }), - }, - ]; - for (const { testCase, file, expected, filesWith } of testCases) { - const offenses = await check(filesWith(file), [MissingTemplate]); - - expect(offenses).to.have.length(1); - expect(offenses, testCase).to.containOffense({ - check: MissingTemplate.meta.code, - ...expected, - }); - } - }); - - it('ignores missing files if ignored by ignore_missing pattern', async () => { - const contexts = [ - { - file: '{% render "ignored" %}', - ignoreMissing: ['snippets/ignored.liquid'], - }, - { - file: '{% render "ignored" %}', - ignoreMissing: ['snippets/*.liquid'], - }, - { - file: '{% render "ignored" %}', - ignoreMissing: ['*/ignored.liquid'], - }, - { - file: '{% section "ignored" %}', - ignoreMissing: ['sections/ignored.liquid'], - }, - { - file: '{% section "ignored" %}', - ignoreMissing: ['sections/ignored.liquid'], - }, - { - file: '{% section "ignored" %}', - ignoreMissing: ['*/ignored.liquid'], - }, - ]; - - for (const { file, ignoreMissing } of contexts) { - MissingTemplate; - const offenses = await check( - { 'file.liquid': file }, - [MissingTemplate], - {}, - { - MissingTemplate: { - enabled: true, - ignoreMissing, - }, - }, - ); - expect(offenses).to.be.empty; - } - }); -}); diff --git a/packages/theme-check-common/src/checks/orphaned-snippet/index.spec.ts b/packages/theme-check-common/src/checks/orphaned-snippet/index.spec.ts deleted file mode 100644 index e1f26cda8..000000000 --- a/packages/theme-check-common/src/checks/orphaned-snippet/index.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { OrphanedSnippet } from './index'; -import { runLiquidCheck } from '../../test'; - -describe('Module: OrphanedSnippet', () => { - describe('when the snippet is not referenced by any files', () => { - it('should report a warning', async () => { - const sourceCode = `
    Orphaned content
    `; - - const offenses = await runLiquidCheck( - OrphanedSnippet, - sourceCode, - 'snippets/orphaned.liquid', - { - getReferences: async (uri) => [], - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe('This snippet is not referenced by any other files'); - }); - }); - - describe('when the snippet is referenced by other files', () => { - it('should not report a warning', async () => { - const sourceCode = `
    Referenced snippet
    `; - - const offenses = await runLiquidCheck( - OrphanedSnippet, - sourceCode, - 'snippets/product-card.liquid', - { - getReferences: async (uri) => [ - { - source: { uri: 'templates/product.liquid' }, - target: { uri: 'snippets/product-card.liquid' }, - type: 'direct', - }, - ], - }, - ); - - expect(offenses).toHaveLength(0); - }); - }); - - describe('when checking non-snippet files', () => { - it('should not report warnings for templates', async () => { - const sourceCode = `{% section 'header' %}`; - - const offenses = await runLiquidCheck(OrphanedSnippet, sourceCode, 'templates/index.liquid', { - getReferences: async (uri) => [], - }); - - expect(offenses).toHaveLength(0); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/pagination-size/index.spec.ts b/packages/theme-check-common/src/checks/pagination-size/index.spec.ts deleted file mode 100644 index eadee30ee..000000000 --- a/packages/theme-check-common/src/checks/pagination-size/index.spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { PaginationSize } from './index'; - -describe('Module: PaginationSize', () => { - it('should report an offense when paginate size is less than minSize', async () => { - const sourceCode = ` - {% paginate collection.products by 0 %} - {% endpaginate %} - `; - const offenses = await runLiquidCheck(PaginationSize, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Pagination size must be a positive integer between 1 and 250.', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['0']); - }); - - it('should report an offense when paginate size is greater than maxSize', async () => { - const sourceCode = ` - {% paginate collection.products by 251 %} - {% endpaginate %} - `; - - const offenses = await runLiquidCheck(PaginationSize, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Pagination size must be a positive integer between 1 and 250.', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['251']); - }); - - it('should not report an offense when paginate size is within the minSize and maxSize range', async () => { - const sourceCode = ` - {% paginate collection.products by 25 %} - {% endpaginate %} - `; - - const offenses = await runLiquidCheck(PaginationSize, sourceCode); - expect(offenses).to.have.length(0); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should not report an offense when paginate size is a section setting with a valid default value', async () => { - const sourceCode = ` - {% paginate collection.products by section.settings.products_per_page %} - {% endpaginate %} - {% schema %} - { - "name": "test", - "settings": [ - { - "type": "number", - "id": "products_per_page", - "label": "Products per Page", - "default": 12 - } - ] - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(PaginationSize, sourceCode); - expect(offenses).to.have.length(0); - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should report an offense when paginate size is a section setting with an invalid default value', async () => { - const sourceCode = ` - {% paginate collection.products by section.settings.products_per_page %} - {% endpaginate %} - {% schema %} - { - "name": "test", - "settings": [ - { - "type": "number", - "id": "products_per_page", - "label": "Products per Page", - "default": 251 - } - ] - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(PaginationSize, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `This setting's default value should be between 1 and 250 but is currently 251.`, - ); - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['section.settings.products_per_page']); - }); - - it('should report an offense when paginate size is a section setting with a missing default value', async () => { - const sourceCode = ` - {% paginate collection.products by section.settings.products_per_page %} - {% endpaginate %} - {% schema %} - { - "name": "test", - "settings": [ - { - "type": "number", - "id": "products_per_page", - "label": "Products per Page" - } - ] - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(PaginationSize, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Default pagination size should be defined in the section settings.', - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['section.settings.products_per_page']); - }); - - it('should not report an offense when settings is not an array', async () => { - const sourceCode = ` - {% paginate collection.products by section.settings.products_per_page %} - {% endpaginate %} - {% schema %} - { - "name": "test", - "settings": { - "products_per_page": { - "type": "number", - "id": "products_per_page", - "label": "Products per Page", - "default": 51 - } - } - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(PaginationSize, sourceCode); - expect(offenses).to.have.length(0); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); -}); diff --git a/packages/theme-check-common/src/checks/parser-blocking-script/index.spec.ts b/packages/theme-check-common/src/checks/parser-blocking-script/index.spec.ts deleted file mode 100644 index 549a17328..000000000 --- a/packages/theme-check-common/src/checks/parser-blocking-script/index.spec.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { ParserBlockingScript } from '.'; -import { applySuggestions, check as reportOffenses } from '../../test'; - -describe('Module: ParserBlockingScript', () => { - it('should report the correct offense for the Liquid filter', async () => { - const file = "{{ 'asset' | script_tag }}"; - const startIndex = file.indexOf('script_tag'); - const endIndex = startIndex + 'script_tag'.length; - - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - - expect(offenses).to.have.length(1); - const { check, message, start, end } = offenses[0]; - expect(check).to.equal(ParserBlockingScript.meta.code); - expect(message).to.contain('Use a - `; - const startIndex = file.indexOf('') + ''.length; - - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - - expect(offenses).to.have.length(1); - const { check, message, start, end } = offenses[0]; - expect(check).to.equal(ParserBlockingScript.meta.code); - expect(message).to.contain('Avoid parser blocking scripts by adding `defer`'); - expect(start.index).to.equal(startIndex); - expect(end.index).to.equal(endIndex); - }); - - describe('Case: LiquidFilter corrections', () => { - it('should suggest to replace a LiquidVariableOutput with a script tag that has the expression as URL', async () => { - const file = "{{ 'asset' | script_tag }}"; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - - expect(offenses).to.have.length(1); - const offense = offenses[0]!; - expect(offense).to.suggest(file, `Use an HTML script tag with the defer attribute instead`, { - startIndex: 0, - endIndex: file.length, - insert: ``, - }); - expect(offense).to.suggest(file, `Use an HTML script tag with the async attribute instead`, { - startIndex: 0, - endIndex: file.length, - insert: ``, - }); - - const suggestions = applySuggestions(file, offense); - expect(suggestions).to.include(``); - expect(suggestions).to.include(``); - }); - - it('should suggest to replace a LiquidVariableOutput with a script tag that has the expression and previous filters as URL', async () => { - const file = "{{ 'asset' | asset_url | script_tag }}"; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - - expect(offenses).to.have.length(1); - const offense = offenses[0]; - expect(offense).to.suggest(file, `Use an HTML script tag with the defer attribute instead`, { - startIndex: 0, - endIndex: file.length, - insert: ``, - }); - expect(offense).to.suggest(file, `Use an HTML script tag with the async attribute instead`, { - startIndex: 0, - endIndex: file.length, - insert: ``, - }); - - const suggestions = applySuggestions(file, offense); - expect(suggestions).to.include(``); - expect(suggestions).to.include(``); - }); - - it('should not suggest anything if the script_tag appears in an echo tag', async () => { - const file = "{% echo 'asset' | asset_url | script_tag %}"; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - - expect(offenses).to.have.length(1); - const { suggest } = offenses[0]; - expect(suggest).not.to.exist; - }); - - it('should not suggest anything if the script_tag appears in an assign tag', async () => { - const file = "{% assign script = 'asset' | asset_url | script_tag %}"; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - - expect(offenses).to.have.length(1); - const { suggest } = offenses[0]; - expect(suggest).not.to.exist; - }); - - it('should not suggest anything if the script_tag appears in a liquid tag', async () => { - const file = "{% liquid\necho 'asset' | asset_url | script_tag %}"; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - - expect(offenses).to.have.length(1); - const { suggest } = offenses[0]; - expect(suggest).not.to.exist; - }); - }); - - describe('Case: script tag suggestion', () => { - it('should suggest adding both attributes at the end', async () => { - const file = ``; - const offenses = await reportOffenses({ 'code.liquid': file }, [ParserBlockingScript]); - - expect(offenses).to.have.length(1); - - const offense = offenses[0]; - expect(offense).to.suggest(file, `Use an HTML script tag with the defer attribute instead`, { - startIndex: file.indexOf('>'), - endIndex: file.indexOf('>'), - insert: ` defer`, - }); - expect(offense).to.suggest(file, `Use an HTML script tag with the async attribute instead`, { - startIndex: file.indexOf('>'), - endIndex: file.indexOf('>'), - insert: ` async`, - }); - - const suggestions = applySuggestions(file, offense); - expect(suggestions).to.include(''); - expect(suggestions).to.include(''); - }); - }); - - describe('No offenses', () => { - it('should not report any offense when async is set on a script tag', async () => { - const file = ` - - `; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - expect(offenses).to.have.length(0); - }); - - it('should not report any offense when defer is set on a script tag', async () => { - const file = ` - - `; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - expect(offenses).to.have.length(0); - }); - - it('should not report any offense when async and defer are set on a script tag', async () => { - const file = ` - - `; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - expect(offenses).to.have.length(0); - }); - - it('should not report any offense when using scripts of type module', async () => { - const file = ` - - - `; - const offenses = await reportOffenses( - { - 'code.liquid': file, - }, - [ParserBlockingScript], - ); - expect(offenses).to.have.length(0); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/raw-tags/index.spec.ts b/packages/theme-check-common/src/checks/raw-tags/index.spec.ts deleted file mode 100644 index c20259b8e..000000000 --- a/packages/theme-check-common/src/checks/raw-tags/index.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - JavascriptOncePerFile, - JavascriptTagInWrongFile, - SchemaOncePerFile, - SchemaSectionOrBlockOnly, - StylesheetOncePerFile, - StylesheetTagInWrongFile, -} from './index'; -import { runLiquidCheck } from '../../test'; - -const SCHEMA_TAG = '{% schema %}{% endschema %}'; -const JAVASCRIPT_TAG = '{% javascript %}{% endjavascript %}'; -const STYLESHEET_TAG = '{% stylesheet %}{% endstylesheet %}'; - -describe('raw tag checks', () => { - describe('schema', () => { - it.each(['sections/test.liquid', 'blocks/test.liquid'])( - 'allows schema tags in %s', - async (path) => { - const offenses = await runLiquidCheck(SchemaSectionOrBlockOnly, SCHEMA_TAG, path); - - expect(offenses).toEqual([]); - }, - ); - - it.each(['templates/index.liquid', 'snippets/test.liquid'])( - 'reports schema tags in %s', - async (path) => { - const offenses = await runLiquidCheck(SchemaSectionOrBlockOnly, SCHEMA_TAG, path); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - check: 'SchemaSectionOrBlockOnly', - message: '{% schema %} is only valid in section or block files.', - }); - }, - ); - - it('reports second and subsequent schema tags in a file', async () => { - const source = [SCHEMA_TAG, SCHEMA_TAG, SCHEMA_TAG].join('\n'); - - const offenses = await runLiquidCheck(SchemaOncePerFile, source, 'sections/test.liquid'); - - expect(offenses).toHaveLength(2); - expect(offenses).toEqual([ - expect.objectContaining({ - check: 'SchemaOncePerFile', - message: '{% schema %} can only appear once per file.', - }), - expect.objectContaining({ - check: 'SchemaOncePerFile', - message: '{% schema %} can only appear once per file.', - }), - ]); - }); - }); - - describe('javascript', () => { - it.each(['sections/test.liquid', 'blocks/test.liquid', 'snippets/test.liquid'])( - 'allows javascript tags in %s', - async (path) => { - const offenses = await runLiquidCheck(JavascriptTagInWrongFile, JAVASCRIPT_TAG, path); - - expect(offenses).toEqual([]); - }, - ); - - it.each(['templates/index.liquid'])('reports javascript tags in %s', async (path) => { - const offenses = await runLiquidCheck(JavascriptTagInWrongFile, JAVASCRIPT_TAG, path); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - check: 'JavascriptTagInWrongFile', - message: '{% javascript %} is only valid in section, block, or snippet files.', - }); - }); - - it('reports second and subsequent javascript tags in a file', async () => { - const source = [JAVASCRIPT_TAG, JAVASCRIPT_TAG, JAVASCRIPT_TAG].join('\n'); - - const offenses = await runLiquidCheck(JavascriptOncePerFile, source, 'sections/test.liquid'); - - expect(offenses).toHaveLength(2); - expect(offenses).toEqual([ - expect.objectContaining({ - check: 'JavascriptOncePerFile', - message: '{% javascript %} can only appear once per file.', - }), - expect.objectContaining({ - check: 'JavascriptOncePerFile', - message: '{% javascript %} can only appear once per file.', - }), - ]); - }); - }); - - describe('stylesheet', () => { - it.each(['sections/test.liquid', 'blocks/test.liquid', 'snippets/test.liquid'])( - 'allows stylesheet tags in %s', - async (path) => { - const offenses = await runLiquidCheck(StylesheetTagInWrongFile, STYLESHEET_TAG, path); - - expect(offenses).toEqual([]); - }, - ); - - it.each(['templates/index.liquid'])('reports stylesheet tags in %s', async (path) => { - const offenses = await runLiquidCheck(StylesheetTagInWrongFile, STYLESHEET_TAG, path); - - expect(offenses).toHaveLength(1); - expect(offenses[0]).toMatchObject({ - check: 'StylesheetTagInWrongFile', - message: '{% stylesheet %} is only valid in section, block, or snippet files.', - }); - }); - - it('reports second and subsequent stylesheet tags in a file', async () => { - const source = [STYLESHEET_TAG, STYLESHEET_TAG, STYLESHEET_TAG].join('\n'); - - const offenses = await runLiquidCheck(StylesheetOncePerFile, source, 'sections/test.liquid'); - - expect(offenses).toHaveLength(2); - expect(offenses).toEqual([ - expect.objectContaining({ - check: 'StylesheetOncePerFile', - message: '{% stylesheet %} can only appear once per file.', - }), - expect.objectContaining({ - check: 'StylesheetOncePerFile', - message: '{% stylesheet %} can only appear once per file.', - }), - ]); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/remote-asset/index.spec.ts b/packages/theme-check-common/src/checks/remote-asset/index.spec.ts deleted file mode 100644 index 2ee416ace..000000000 --- a/packages/theme-check-common/src/checks/remote-asset/index.spec.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, highlightedOffenses, check, MockTheme } from '../../test'; -import { RemoteAsset } from './index'; - -describe('Module: RemoteAsset', () => { - it('should report an offense when asset_url or img_url filters are not used', async () => { - const sourceCode = ` - - - - {{ url | img_tag }} - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.have.length(4); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql([ - 'src="{{ \'image.png\' }}"', - 'href="{{ \'style.css\' }}"', - 'src="{{ \'script.js\' }}"', - 'url | img_tag', - ]); - }); - - it('should not report an offense when asset_url or img_url filters are used', async () => { - const sourceCode = ` - - - - {{ url | img_tag | asset_url }}, - {{ 'bootstrap.min.css' | asset_url | stylesheet_tag }} - {{ product | image_url: width: 450 | img_tag }} - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should not report an offense for links, videos, iframes', async () => { - const sourceCode = ` - -
    - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should not report an offense when asset_url or img_url filters are used with other filters', async () => { - const sourceCode = ` - - - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should not report an offense for non-asset elements', async () => { - const sourceCode = ` -
    - Link - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should not report an offense when asset_url filters are used with deprecated assets', async () => { - const sourceCode = ` - {{ product.featured_image | product_img_url }} - {{ article.image | article_img_url }} - {{ collection.image | collection_img_url }} - {{ 'image.png' | img_url }} - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should not report an offense when asset_url filters are used with non deprecated assets', async () => { - const sourceCode = ` - {{ 'option_selection.js' | shopify_asset_url }} - {{ 'lightbox.js' | global_asset_url | script_tag }} - {{ 'disclaimer.pdf' | file_url }} - {{ 'potions-header.png' | file_img_url: 'large' }} - {{ 'cart.js' | asset_url }} - {{ 'red-and-black-bramble-berries.jpg' | asset_img_url }} - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should report the correct message and index for a single offense', async () => { - const sourceCode = ``; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.have.length(1); - - const offense = offenses[0]; - expect(offense.message).to.equal( - 'Use one of the asset_url filters to serve assets for better performance.', - ); - expect(offense.start.index).to.equal(5); - expect(offense.end.index).to.equal(28); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.eql(['src="{{ \'image.png\' }}"']); - }); - - it('should not report an offense when asset_url filters are used with known assets', async () => { - const sourceCode = ` - { - const sourceCode = ``; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Asset should be served by the Shopify CDN for better performance.', - ); - }); - - it('should report an offense for remote stylesheets', async () => { - const sourceCode = ` - - - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - - expect(offenses).to.have.length(3); - offenses.forEach((offense) => { - expect(offense.message).to.equal( - 'Asset should be served by the Shopify CDN for better performance.', - ); - }); - }); - - it('should report an offense when a non shopify cdn is used as a liquid filter and when a asset_url filter is not used.', async () => { - const sourceCode = ` - {{ "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" | stylesheet_tag }} - `; - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.have.length(2); - - expect(offenses[0].message).to.equal( - 'Asset should be served by the Shopify CDN for better performance.', - ); - - expect(offenses[1].message).to.equal( - 'Use one of the asset_url filters to serve assets for better performance.', - ); - }); - - it('should not report an offense for variable-based URLs', async () => { - const sourceCode = ` - - - - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should not report an offense for hash URLs', async () => { - const sourceCode = ` - - - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should not report an offense for base64 encoded data URIs', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should not report an offense for various base64 data URI image formats', async () => { - const sourceCode = ` - - - - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should still report an offense for remote images alongside base64 images', async () => { - const sourceCode = ` - - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Asset should be served by the Shopify CDN for better performance.', - ); - }); - - it('should not report an offense for data URIs used with img_tag filter', async () => { - const sourceCode = ` - {{ 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' | img_tag }} - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should still report an offense for string literals without asset_url filter', async () => { - const sourceCode = ` - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Use one of the asset_url filters to serve assets for better performance.', - ); - }); - - it('should not report an offence if url is a shopify CDN', async () => { - const sourceCode = ` - - - - `; - - const offenses = await runLiquidCheck(RemoteAsset, sourceCode); - expect(offenses).to.be.empty; - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).to.be.empty; - }); - - it('should report an offense if url is not listed in allowedDomains', async () => { - const themeFiles: MockTheme = { - 'layout/theme.liquid': ` - - `, - }; - - const offenses = await check( - themeFiles, - [RemoteAsset], - {}, - { - RemoteAsset: { - enabled: true, - allowedDomains: ['someotherdomain.com'], - }, - }, - ); - - expect(offenses).to.have.length(1); - }); - - it('should report an offense if the url in the config is malformed/missing protocol', async () => { - const themeFiles: MockTheme = { - 'layout/theme.liquid': ` - - - `, - }; - - const offenses = await check( - themeFiles, - [RemoteAsset], - {}, - { - RemoteAsset: { - enabled: true, - allowedDomains: ['www.domain.com', 'domain.com'], - }, - }, - ); - - expect(offenses).to.have.length(2); - }); - - it('should not report an offense if url is listed in allowedDomains', async () => { - const themeFiles: MockTheme = { - 'layout/theme.liquid': ` - - `, - }; - - const offenses = await check( - themeFiles, - [RemoteAsset], - {}, - { - RemoteAsset: { - enabled: true, - allowedDomains: ['https://domain.com', 'http://domain.com', 'https://www.domain.com'], - }, - }, - ); - - expect(offenses).to.be.empty; - }); -}); diff --git a/packages/theme-check-common/src/checks/required-layout-theme-object/index.spec.ts b/packages/theme-check-common/src/checks/required-layout-theme-object/index.spec.ts deleted file mode 100644 index a3b4235ff..000000000 --- a/packages/theme-check-common/src/checks/required-layout-theme-object/index.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -// test/checks/required-layout-theme-object.test.ts -import { expect, describe, it } from 'vitest'; -import { RequiredLayoutThemeObject } from './index'; -import { runLiquidCheck, applyFix } from '../../test'; - -describe('Module: RequiredLayoutThemeObject', () => { - it('should report an error if content_for_header is missing', async () => { - const input = ` - - - - - - {{ content_for_layout }} - - - `; - - const offenses = await runLiquidCheck(RequiredLayoutThemeObject, input, 'layout/theme.liquid'); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The required object '{{ content_for_header }}' is missing in layout/theme.liquid", - ); - - const fix = applyFix(input, offenses[0]); - expect(fix).to.exist; - expect(fix!).to.include('{{ content_for_header }}'); - }); - - it('should report an error if content_for_layout is missing', async () => { - const input = ` - - - {{ content_for_header }} - - - - - - `; - - const offenses = await runLiquidCheck(RequiredLayoutThemeObject, input, 'layout/theme.liquid'); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The required object '{{ content_for_layout }}' is missing in layout/theme.liquid", - ); - - const fix = applyFix(input, offenses[0]); - expect(fix).to.exist; - expect(fix!).to.include('{{ content_for_layout }}'); - }); - - it('should not report an error if both content_for_header and content_for_layout are present', async () => { - const input = ` - - - {{ content_for_header }} - - - {{ content_for_layout }} - - - `; - - const offenses = await runLiquidCheck(RequiredLayoutThemeObject, input, 'layout/theme.liquid'); - expect(offenses).to.have.length(0); - }); - - it('should not report an error or fix if head or body tags are missing', async () => { - const input = ` - - - {{ content_for_header }} - {{ content_for_layout }} - - `; - - const offenses = await runLiquidCheck(RequiredLayoutThemeObject, input, 'layout/theme.liquid'); - expect(offenses).to.have.length(0); - }); - - it('should not report an error for non-layout/theme.liquid files', async () => { - const input = `{% echo 'hello world' %}`; - const offenses = await runLiquidCheck( - RequiredLayoutThemeObject, - input, - 'snippets/example.liquid', - ); - expect(offenses).to.have.length(0); - }); - - it('should report for any layout/*.liquid file, not only layout/theme.liquid', async () => { - /* - * The check scope was broadened: LAYOUT_PATH_PATTERN now matches every - * +layout/*.liquid+ asset, not just +layout/theme.liquid+. A non-theme - * layout file missing the required objects must therefore be reported. - */ - const input = ` - - - - - - {{ content_for_layout }} - - - `; - - const offenses = await runLiquidCheck( - RequiredLayoutThemeObject, - input, - 'layout/checkout.liquid', - ); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The required object '{{ content_for_header }}' is missing in layout/theme.liquid", - ); - }); - - it('should not report an error if the file is unparseable', async () => { - const input = ` - - {{ content_for_header }} - {{ content_for_layout }}{% echo }} - - `; - - const offenses = await runLiquidCheck( - RequiredLayoutThemeObject, - input, - 'snippets/example.liquid', - ); - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/reserved-doc-param-names/index.spec.ts b/packages/theme-check-common/src/checks/reserved-doc-param-names/index.spec.ts deleted file mode 100644 index df1122605..000000000 --- a/packages/theme-check-common/src/checks/reserved-doc-param-names/index.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { ReservedDocParamNames } from './index'; -import { runLiquidCheck } from '../../test'; - -describe('Module: ReservedDocParamNames', () => { - it.each(['blocks/file.liquid', 'snippets/file.liquid'])( - 'reports Liquid literal parameter names in %s', - async (fileName) => { - const sourceCode = ` - {% doc %} - @param nil - Example param - @param null - Example param - @param true - Example param - @param false - Example param - @param blank - Example param - @param empty - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(ReservedDocParamNames, sourceCode, fileName); - - expect(offenses).to.have.length(6); - expect(offenses.map(({ message }) => message)).toEqual([ - "The parameter name 'nil' is reserved because Liquid parses it as a literal.", - "The parameter name 'null' is reserved because Liquid parses it as a literal.", - "The parameter name 'true' is reserved because Liquid parses it as a literal.", - "The parameter name 'false' is reserved because Liquid parses it as a literal.", - "The parameter name 'blank' is reserved because Liquid parses it as a literal.", - "The parameter name 'empty' is reserved because Liquid parses it as a literal.", - ]); - }, - ); - - it.each(['blocks/file.liquid', 'snippets/file.liquid'])( - 'allows non-literal parameter names in %s', - async (fileName) => { - const sourceCode = ` - {% doc %} - @param param1 - Example param - @param class - Example param - @param attributes - Example param - @param id - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(ReservedDocParamNames, sourceCode, fileName); - - expect(offenses).to.be.empty; - }, - ); -}); diff --git a/packages/theme-check-common/src/checks/schema-presets-block-order/index.spec.ts b/packages/theme-check-common/src/checks/schema-presets-block-order/index.spec.ts deleted file mode 100644 index bd5018f4f..000000000 --- a/packages/theme-check-common/src/checks/schema-presets-block-order/index.spec.ts +++ /dev/null @@ -1,344 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck, check } from '../../test'; -import { SchemaPresetsBlockOrder } from './index'; - -const DEFAULT_FILE_NAME = 'sections/file.liquid'; - -describe('Module: SchemaPresetsBlockOrder', () => { - it('reports no warning when the preset blocks are in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text" - }, - "block-2": { - "type": "icon" - } - }, - "block_order": ["block-1", "block-2"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when not all preset blocks are in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "name": "Test section", - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text" - }, - "block-2": { - "type": "icon" - } - }, - "block_order": ["block-1"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual("block 'block-2' is missing from the block_order"); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).toBe('["block-1"]'); - }); - - it('reports no warning when the preset blocks has static blocks that are not in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text" - }, - "block-2": { - "type": "icon", - "static": true - } - }, - "block_order": ["block-1"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when preset blocks has static blocks that are in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text" - }, - "block-2": { - "type": "icon", - "static": true - } - }, - "block_order": ["block-1", "block-2"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual("static block 'block-2' cannot be in the block_order"); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).toBe('["block-1", "block-2"]'); - }); - - it('reports no warning when the nested preset blocks are in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text", - "blocks": { - "nested-1": { - "type": "nested" - }, - "nested-2": { - "type": "nested" - } - }, - "block_order": ["nested-1", "nested-2"] - }, - "block-2": { - "type": "icon" - } - }, - "block_order": ["block-1", "block-2"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the nested preset blocks are not in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text", - "blocks": { - "nested-1": { - "type": "nested" - }, - "nested-2": { - "type": "nested" - } - }, - "block_order": ["nested-1"] - }, - "block-2": { - "type": "icon" - } - }, - "block_order": ["block-1", "block-2"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual("block 'nested-2' is missing from the block_order"); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).toBe('["nested-1"]'); - }); - - it('reports no warning when the nested preset blocks has static blocks that are not in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text", - "blocks": { - "nested-1": { - "type": "nested", - "static": true - }, - "nested-2": { - "type": "nested" - } - }, - "block_order": ["nested-2"] - }, - "block-2": { - "type": "icon" - } - }, - "block_order": ["block-1", "block-2"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(0); - }); - - it('reports a warning when the nested preset blocks has static blocks that are in the block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text", - "blocks": { - "nested-1": { - "type": "nested", - "static": true - }, - "nested-2": { - "type": "nested" - } - }, - "block_order": ["nested-1", "nested-2"] - }, - "block-2": { - "type": "icon" - } - }, - "block_order": ["block-1", "block-2"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual("static block 'nested-1' cannot be in the block_order"); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).toBe('["nested-1", "nested-2"]'); - }); - - it('reports a warning when there should be a block_order but it is missing', async () => { - const sourceCode = ` - {% schema %} - { - "name": "Test section", - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text" - } - } - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual('block_order is missing'); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); - - it('reports no warning we have all static blocks and no block_order', async () => { - const sourceCode = ` - {% schema %} - { - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text", - "static": true - } - } - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(0); - }); - - it("reports a warning when a block in 'block_order' is not in 'blocks'", async () => { - const sourceCode = ` - {% schema %} - { - "name": "Test section", - "presets": [ - { - "name": "Preset 1", - "blocks": { - "block-1": { - "type": "text" - } - }, - "block_order": ["block-1", "block-2"] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsBlockOrder, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual("block 'block-2' is missing from 'blocks'"); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).toBe('"block-2"'); - }); -}); diff --git a/packages/theme-check-common/src/checks/schema-presets-static-blocks/index.spec.ts b/packages/theme-check-common/src/checks/schema-presets-static-blocks/index.spec.ts deleted file mode 100644 index 7a5449904..000000000 --- a/packages/theme-check-common/src/checks/schema-presets-static-blocks/index.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck, check } from '../../test'; -import { SchemaPresetsStaticBlocks } from './index'; - -const DEFAULT_FILE_NAME = 'sections/file.liquid'; - -describe('Module: SchemaPresetsStaticBlocks', () => { - it('reports no errors when there are {% content_for "block" ... %} for each static block in the preset blocks array', async () => { - const sourceCode = ` - {% content_for "block" type:"text" id: "block-1" %} - {% content_for "block" type:"icon" id: "block-2" %} - {% schema %} - { - "name": "Test section", - "blocks": [{"type": "@theme"}], - "presets": [ - { - "name": "Preset with two static blocks", - "blocks": [ - { - "type": "text", - "static": true, - "id": "block-1" - }, - { - "type": "icon", - "static": true, - "id": "block-2" - } - ] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(0); - }); - - it('reports an error when there are {% content_for "block" ... %} missing for static blocks in the preset blocks array', async () => { - const sourceCode = ` - {% content_for "block" type:"text" id:"block-1" %} - {% comment %} here we are missing the other content_for block for block-2 {% endcomment %} - {% schema %} - { - "name": "Test section", - "blocks": [{"type": "@theme"}], - "presets": [ - { - "name": "Preset with two static blocks", - "blocks": [ - { - "type": "text", - "static": true, - "id": "block-1" - }, - { - "type": "icon", - "static": true, - "id": "block-2" - } - ] - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - console.log(offenses); - expect(offenses[0].message).toEqual( - 'Static block block-2 is missing a corresponding content_for "block" tag.', - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); - - it('reports no errors when there are {% content_for "block" ... %} for each static block in the preset blocks hash', async () => { - const sourceCode = ` - {% content_for "block" type:"text" id: "block-1" %} - {% content_for "block" type:"icon" id: "block-2" %} - {% schema %} - { - "name": "Test section", - "blocks": [{"type": "@theme"}], - "presets": [ - { - "name": "Preset with two static blocks", - "blocks": { - "block-1": { - "type": "text", - "static": true - }, - "block-2": { - "type": "icon", - "static": true - } - } - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(0); - }); - - it('reports an error when there are {% content_for "block" ... %} missing for static blocks in the preset blocks hash', async () => { - const sourceCode = ` - {% content_for "block" type:"text" id:"block-1" %} - {% comment %} here we are missing the other content_for block for block-2 {% endcomment %} - {% schema %} - { - "name": "Test section", - "blocks": [{"type": "@theme"}], - "presets": [ - { - "name": "Preset with two static blocks", - "blocks": { - "block-1": { - "type": "text", - "static": true - }, - "block-2": { - "type": "icon", - "static": true - } - } - } - ] - } - {% endschema %}`; - - const offenses = await runLiquidCheck(SchemaPresetsStaticBlocks, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - console.log(offenses); - expect(offenses[0].message).toEqual( - 'Static block block-2 is missing a corresponding content_for "block" tag.', - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).toHaveLength(1); - }); -}); diff --git a/packages/theme-check-common/src/checks/static-stylesheet-and-javascript-tags/index.spec.ts b/packages/theme-check-common/src/checks/static-stylesheet-and-javascript-tags/index.spec.ts deleted file mode 100644 index afc5769a9..000000000 --- a/packages/theme-check-common/src/checks/static-stylesheet-and-javascript-tags/index.spec.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { check, highlightedOffenses } from '../../test'; -import { expect, describe, it } from 'vitest'; -import { StaticStylesheetAndJavascriptTags } from './index'; - -describe('Module: StaticStylesheetAndJavascriptTags', () => { - it('should not report errors for static CSS content in stylesheet tags', async () => { - const theme = { - 'templates/test.liquid': ` - {% stylesheet %} - .button { - background-color: blue; - color: white; - padding: 10px 20px; - } - {% endstylesheet %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(0); - }); - - it('should not report errors for static JavaScript content in javascript tags', async () => { - const theme = { - 'templates/test.liquid': ` - {% javascript %} - function greet(name) { - console.log('Hello, ' + name + '!'); - } - greet('World'); - {% endjavascript %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(0); - }); - - it('should report error for liquid variable in stylesheet tag', async () => { - const theme = { - 'templates/test.liquid': ` - {% stylesheet %} - .button { - background-color: {{ settings.button_color }}; - color: white; - } - {% endstylesheet %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Liquid variable found in CSS block. {% stylesheet %} tags should only contain static CSS code.', - ); - - const highlights = highlightedOffenses(theme, offenses); - expect(highlights).to.eql(['{{ settings.button_color }}']); - }); - - it('should report error for liquid variable in javascript tag', async () => { - const theme = { - 'templates/test.liquid': ` - {% javascript %} - const apiUrl = '{{ shop.url }}'; - fetch(apiUrl); - {% endjavascript %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Liquid variable found in JavaScript block. {% javascript %} tags should only contain static JavaScript code.', - ); - - const highlights = highlightedOffenses(theme, offenses); - expect(highlights).to.eql(['{{ shop.url }}']); - }); - - it('should report error for liquid tag in stylesheet tag', async () => { - const theme = { - 'templates/test.liquid': ` - {% stylesheet %} - .button { - {% if settings.enable_hover %} - background-color: blue; - {% endif %} - } - {% endstylesheet %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Liquid tag found in CSS block. {% stylesheet %} tags should only contain static CSS code.', - ); - - const highlights = highlightedOffenses(theme, offenses); - expect(highlights).to.eql([ - `{% if settings.enable_hover %} - background-color: blue; - {% endif %}`, - ]); - }); - - it('should report error for liquid tag in javascript tag', async () => { - const theme = { - 'templates/test.liquid': ` - {% javascript %} - {% if settings.enable_analytics %} - console.log('Analytics enabled'); - {% endif %} - {% endjavascript %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Liquid tag found in JavaScript block. {% javascript %} tags should only contain static JavaScript code.', - ); - - const highlights = highlightedOffenses(theme, offenses); - expect(highlights).to.eql([ - `{% if settings.enable_analytics %} - console.log('Analytics enabled'); - {% endif %}`, - ]); - }); - - it('should report multiple errors for multiple liquid nodes', async () => { - const theme = { - 'templates/test.liquid': ` - {% stylesheet %} - .button { - background-color: {{ settings.button_color }}; - {% if settings.show_border %} - border: 1px solid {{ settings.border_color }}; - {% endif %} - } - {% endstylesheet %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.include('Liquid variable found in CSS block'); - expect(offenses[1].message).to.include('Liquid tag found in CSS block'); - - const highlights = highlightedOffenses(theme, offenses); - expect(highlights).to.eql([ - '{{ settings.button_color }}', - `{% if settings.show_border %} - border: 1px solid {{ settings.border_color }}; - {% endif %}`, - ]); - }); - - it('should not report errors for other raw tags like schema', async () => { - const theme = { - 'sections/test.liquid': ` - {% schema %} - { - "name": "Section", - "settings": [ - { - "id": "color", - "type": "color", - "default": "{{ settings.default_color }}" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(0); - }); - - it('should not report errors for liquid in regular content outside raw tags', async () => { - const theme = { - 'templates/test.liquid': ` -
    - {% if product.available %} - - {% endif %} -
    - - {% stylesheet %} - .static-style { - margin: 10px; - } - {% endstylesheet %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(0); - }); - - it('should report nested raw tags inside stylesheet tags', async () => { - const theme = { - 'templates/test.liquid': ` - {% stylesheet %} - /* This should trigger an error */ - {% raw %} - .nested { color: red; } - {% endraw %} - {% endstylesheet %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.include('Liquid tag found in CSS block'); - - const highlights = highlightedOffenses(theme, offenses); - expect(highlights).to.eql([ - `{% raw %} - .nested { color: red; } - {% endraw %}`, - ]); - }); - - it('should not report offenses for empty stylesheet and javascript blocks', async () => { - const theme = { - 'templates/test.liquid': ` - {% stylesheet %} - {% endstylesheet %} - - {% javascript %} - {% endjavascript %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(0); - }); - - it('should not report errors for liquid in style tags (different from stylesheet)', async () => { - const theme = { - 'templates/test.liquid': ` - {% style %} - .button { - color: {{ settings.color }}; - } - {% endstyle %} - `, - }; - - const offenses = await check(theme, [StaticStylesheetAndJavascriptTags]); - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/translation-key-exists/index.spec.ts b/packages/theme-check-common/src/checks/translation-key-exists/index.spec.ts deleted file mode 100644 index b3fdfab52..000000000 --- a/packages/theme-check-common/src/checks/translation-key-exists/index.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { TranslationKeyExists } from '.'; -import { check } from '../../test'; -import { expect, describe, it } from 'vitest'; - -describe('Module: TranslationKeyExists', () => { - it('should report all keys if default locale file does not exist', async () => { - const offenses = await check( - { - 'code.liquid': `{{"key" | t}} -{{"nested.key" | t}}`, - }, - [TranslationKeyExists], - ); - expect(offenses).to.have.length(2); - }); - - it('should report nothing if default locale file is invalid', async () => { - const offenses = await check( - { - 'locales/en.default.json': '{', - 'code.liquid': `{{"key" | t}} -{{"nested.key" | t}} -{% schema %} - { - "locales": { - "en": { - "key": "I will be ignored", - "nested": { - "key": "Me too!" - } - } - } - } -{% endschema %}`, - }, - [TranslationKeyExists], - ); - expect(offenses).to.have.length(0); - }); - - it('should report unknown keys even if schema is invalid', async () => { - const offenses = await check( - { - 'locales/en.default.json': '{}', - 'code.liquid': `{{"key" | t}} -{% schema %} - { -{% endschema %}`, - }, - [TranslationKeyExists], - ); - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: TranslationKeyExists.meta.code, - message: "'key' does not have a matching entry in 'locales/en.default.json'", - uri: 'file:///code.liquid', - }); - }); - - it('should ignore existing keys', async () => { - const offenses = await check( - { - 'locales/en.default.json': JSON.stringify({ - key: '', - nested: { key: '' }, - }), - 'code.liquid': `{{"key" | t}} -{{"nested.key" | t}}`, - }, - [TranslationKeyExists], - ); - expect(offenses).to.have.length(0); - }); - - it('should ignore keys included in schema', async () => { - const offenses = await check( - { - 'code.liquid': `{{"submit" | t}} -{{ "this.does.not.exist" | translate }} -{% schema %} - { - "locales": { - "en": { - "submit": "Subscribe" - } - } - } -{% endschema %}`, - }, - [TranslationKeyExists], - ); - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: TranslationKeyExists.meta.code, - message: - "'this.does.not.exist' does not have a matching entry in 'locales/en.default.json' or 'code.liquid'", - uri: 'file:///code.liquid', - start: { index: 20, line: 1, character: 3 }, - end: { index: 41, line: 1, character: 24 }, - }); - }); - - it('should not crash if the schema locales does not exist', async () => { - const offenses = await check( - { - 'code.liquid': ` - {{ "this.does.not.exist" | translate }} - {% schema %} - { - "name": "Slideshow", - "tag": "section", - "class": "slideshow", - "limit": 1 - } - {% endschema %}`, - }, - [TranslationKeyExists], - ); - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: TranslationKeyExists.meta.code, - message: "'this.does.not.exist' does not have a matching entry in 'locales/en.default.json'", - uri: 'file:///code.liquid', - start: { index: 14, line: 1, character: 13 }, - end: { index: 35, line: 1, character: 34 }, - }); - }); - - it('should only report unknown keys for translate filter', async () => { - const offenses = await check( - { - 'locales/en.default.json': '{}', - 'code.liquid': `{{ "unknownkey" | t }} -{{ "do.not.report.me" | unknown_filter }} -{{ "unknown.nested.key" | translate }}`, - }, - [TranslationKeyExists], - ); - - expect(offenses).to.have.length(2); - expect(offenses).to.containOffense({ - check: TranslationKeyExists.meta.code, - message: "'unknownkey' does not have a matching entry in 'locales/en.default.json'", - uri: 'file:///code.liquid', - start: { index: 3, line: 0, character: 3 }, - end: { index: 15, line: 0, character: 15 }, - }); - expect(offenses).to.containOffense({ - check: TranslationKeyExists.meta.code, - message: "'unknown.nested.key' does not have a matching entry in 'locales/en.default.json'", - uri: 'file:///code.liquid', - start: { index: 68, line: 2, character: 3 }, - end: { index: 88, line: 2, character: 23 }, - }); - }); - - it('should count Shopify-provided translations as defined', async () => { - const offenses = await check( - { - 'locales/en.default.json': '{}', - 'code.liquid': `{{ 'shopify.sentence.words_connector' | t }} -{{ 'shopify.this.does.not.exist' | t }}`, - }, - [TranslationKeyExists], - ); - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - check: TranslationKeyExists.meta.code, - message: - "'shopify.this.does.not.exist' does not have a matching entry in 'locales/en.default.json'", - uri: 'file:///code.liquid', - start: { index: 48, line: 1, character: 3 }, - end: { index: 77, line: 1, character: 32 }, - }); - }); - - it('should handle key conflicts', async () => { - const offenses = await check( - { - 'locales/en.default.json': JSON.stringify({ - product: { quantity: 'TODO' }, - }), - 'code.liquid': '{{"product.quantity.decrease" | t}}', - }, - [TranslationKeyExists], - ); - - expect(offenses).to.have.length(1); - }); -}); diff --git a/packages/theme-check-common/src/checks/unclosed-html-element/index.spec.ts b/packages/theme-check-common/src/checks/unclosed-html-element/index.spec.ts deleted file mode 100644 index 1ae137048..000000000 --- a/packages/theme-check-common/src/checks/unclosed-html-element/index.spec.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { UnclosedHTMLElement } from '.'; -import { runLiquidCheck, highlightedOffenses } from '../../test'; - -describe('Module: UnclosedHTMLElement', () => { - it('should not report for files without unbalanced tags', async () => { - const testCases = [ - ` -
    - {% if cond %} -
    - - {% else %} -

    - {% endif %} - - {% if cond %} -

    - {% else %} - - {% endif %} - - {% if cond %} -
    - {% endif %} -
    - `, - ` -
    - {% if cond %} - {% else %} -

    - {% endif %} - - {% unless cond %} -

    - {% endunless %} -
    - `, - ` -
    - {% case thing %} - {% when 'one', 'two' %} -

    - {% when 'three' %} -

    - {% else %} -

    - {% endcase %} - - {% case thing %} - {% when 'one','two' %} -

    - {% when 'three' %} - - {% else %} -

    - {% endunless %} -
    - `, - ]; - - for (const file of testCases) { - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses, file).to.have.length(0); - } - }); - - it('should not report offenses for similarly written, but not string-equal, conditions', async () => { - const testCases = [ - ['number>10', 'number > 10'], // whitespace insensitive - ['"string"', `'string'`], - ['x == nil', 'x==null'], // nil == null - ['x.a', 'x["a"]'], - ['(0..a)', '( 0 .. a )'], - ['a or b', 'a or b'], - ]; - for (const [cond1, cond2] of testCases) { - const file = ` - {% if ${cond1} %} -

    - {% endif %} - - {% if ${cond2} %} -

    - {% endif %} - `; - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - expect(offenses).to.be.empty; - } - }); - - it('should report an offense for a branch open tag without its close tag', async () => { - const file = ` -
    - {% if cond %} -
    - {% endif %} -
    - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - message: `Opening tag does not have a matching closing tag for condition \`cond\` in HtmlElement 'div'`, - }); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - }); - - it('should report an offense for a branch close tag without its open tag', async () => { - const file = ` -
    - {% if cond %} -
    - {% endif %} - - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense({ - message: `Closing tag does not have a matching opening tag for condition \`cond\` in HtmlElement 'div'`, - }); - expect(highlightedOffenses(file, offenses)).to.include(''); - }); - - it('should report offenses when branches are closed in the wrong order', async () => { - const file = ` -
    - {% if cond %} -
    - - {% endif %} - - {% if cond %} -
    - - {% endif %} -
    - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses).to.have.length(4); - expect(offenses).to.containOffense({ - message: `Closing tag does not have a matching opening tag for condition \`cond\` in HtmlElement 'div'`, - }); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - expect(highlightedOffenses(file, offenses)).to.include(''); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - expect(highlightedOffenses(file, offenses)).to.include(''); - }); - - it('should report offenses when branches are closed in the wrong node', async () => { - const file = ` -
    - {% if cond %} -
    - - {% endif %} - -
    - {% if cond %} -
    - - {% endif %} -
    - - - {% if cond %} - - - {% endif %} - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses).to.have.length(6); - expect(offenses).to.containOffense({ - message: `Closing tag does not have a matching opening tag for condition \`cond\` in HtmlElement 'div'`, - }); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - expect(highlightedOffenses(file, offenses)).to.include(''); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - expect(highlightedOffenses(file, offenses)).to.include(''); - }); - - it('should not report for paired conditional open/close in for-loop boundary conditions', async () => { - const testCases = [ - // Open on forloop.first, close on forloop.last - ` - {% for item in items %} - {% if forloop.first or item_modulo == 1 %} -
    - {% endif %} - -
    {{ item }}
    - - {% if forloop.last or item_modulo == 0 %} -
    - {% endif %} - {% endfor %} - `, - // Simpler variant: different conditions but balanced open/close in same grandparent - ` -
    - {% if condition_a %} -
    - {% endif %} - - {% if condition_b %} -
    - {% endif %} -
    - `, - ]; - - for (const file of testCases) { - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - expect(offenses, file).to.have.length(0); - } - }); - - it('should still report when cross-identifier tags do not balance by name', async () => { - const file = ` -
    - {% if condition_a %} -
    - {% endif %} - - {% if condition_b %} - - {% endif %} -
    - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - expect(offenses).to.have.length(2); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - expect(highlightedOffenses(file, offenses)).to.include(''); - }); - - it('should report offenses when conditions do not match', async () => { - const file = ` -
    - {% if some_condition %} -
    - {% else %} -
    - {% else %} - - {% endif %} -
    - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses).to.have.length(4); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - expect(highlightedOffenses(file, offenses)).to.include('
    '); - expect(highlightedOffenses(file, offenses)).to.include(''); - }); - - it('should report an offense for unclosed or unopened in case statements', async () => { - const file = ` -
    - {% case thing %} - {% when 'one' %} -

    - {% else %} -

    - {% endcase %} - - {% case thing %} - {% when 'two' %} -

    - {% endcase %} -
    - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses).to.have.length(3); - expect(highlightedOffenses(file, offenses)).to.include('

    '); - expect(highlightedOffenses(file, offenses)).to.include('

    '); - expect(highlightedOffenses(file, offenses)).to.include('

    '); - }); - - it('should report an offense for doubly unclosed of the same name', async () => { - const file = ` -
    - {% if thing %} - - {% endif %} - - {% if thing %} - - {% endif %} -
    - `; - - const offenses = await runLiquidCheck(UnclosedHTMLElement, file); - - expect(offenses).to.have.length(2); - expect(highlightedOffenses(file, offenses)).to.include(''); - }); -}); diff --git a/packages/theme-check-common/src/checks/undefined-object/index.spec.ts b/packages/theme-check-common/src/checks/undefined-object/index.spec.ts deleted file mode 100644 index 0f2ea6ebc..000000000 --- a/packages/theme-check-common/src/checks/undefined-object/index.spec.ts +++ /dev/null @@ -1,667 +0,0 @@ -import { expect, describe, it, assert } from 'vitest'; -import { UndefinedObject } from './index'; -import { runLiquidCheck, highlightedOffenses } from '../../test'; -import { Offense } from '../../types'; - -describe('Module: UndefinedObject', () => { - it('should report an offense when object is undefined', async () => { - const sourceCode = ` - {{ my_var }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_var' used."]); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['my_var']); - }); - - it('should report an offense when object with an attribute is undefined', async () => { - const sourceCode = ` - {{ my_var.my_attr }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_var' used."]); - }); - - it('should report an offense when undefined object is used as an argument', async () => { - const sourceCode = ` - {{ product[my_object] }} - {{ product[my_object] }} - - {% comment %} string arguments should not be reported {% endcomment %} - {{ product["my_object"] }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(2); - expect(offenses.map((e) => e.message)).toEqual([ - "Unknown object 'my_object' used.", - "Unknown object 'my_object' used.", - ]); - }); - - it('should report an offense when object is undefined in a Liquid tag', async () => { - const sourceCode = ` - {% liquid - echo my_var - %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_var' used."]); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual(['my_var']); - }); - - it('should not report an offense when object is defined with an assign tag', async () => { - const sourceCode = ` - {% assign my_var = "value" %} - {{ my_var }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when object is defined with an assign tag and it is used as an argument', async () => { - const sourceCode = ` - {% assign prop = "title" %} - {{ product[prop] }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when object is defined with an assign tag in a Liquid tag', async () => { - const sourceCode = ` - {% liquid - assign my_var = "value" - echo my_var - %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when object is defined with a capture tag', async () => { - const sourceCode = ` - {% capture my_var %} value {% endcapture %} - {{ my_var }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when object is defined in a for loop', async () => { - const sourceCode = ` - {% for c in collections %} - {{ c }} - {% endfor %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense when object is defined in a for loop but used outside of the scope', async () => { - const sourceCode = ` - {% for c in collections %} - {{ c }} - {% endfor %}{{ c }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'c' used."]); - }); - - it('should report an offense when object is defined in a for loop but used outside of the scope (in scenarios where the same variable has multiple scopes in the file)', async () => { - const sourceCode = ` - {% for c in collections %} - {% comment %} -- Scope 1 -- {% endcomment %} - {{ c }} - {% endfor %} - {{ c }} - {% for c in collections %} - {% comment %} -- Scope 2 -- {% endcomment %} - {{ c }} - {% endfor %} - {{ c }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(2); - expect(offenses.map((e) => e.message)).toEqual([ - "Unknown object 'c' used.", - "Unknown object 'c' used.", - ]); - }); - - it('should report an offense when undefined object defines another object', async () => { - const sourceCode = ` - {% assign my_object = my_var %} - {{ my_object }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_var' used."]); - }); - - it('should not report an offense when object is defined in a tablerow loop', async () => { - const sourceCode = ` - {% tablerow c in collections %} - {{ c }} - {% endtablerow %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense when object is defined in a tablerow loop but used outside of the scope', async () => { - const sourceCode = ` - {% tablerow c in collections %} - {{ c }} - {% endtablerow %}{{ c }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'c' used."]); - }); - - it('should contextually report on the undefined nature of the paginate object (defined in paginate tag, undefined outside)', async () => { - const sourceCode = ` - {% assign col = 'string' | split: '' %} - {% paginate col by 5 %} - {{ paginate }} - {% endpaginate %}{{ paginate }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'paginate' used."]); - }); - - it('should contextually report on the undefined nature of the form object (defined in form tag, undefined outside)', async () => { - const sourceCode = ` - {% form "cart" %} - {{ form }} - {% endform %}{{ form }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'form' used."]); - }); - - it('should support {% layout none %}', async () => { - const sourceCode = ` - {% layout none %} - {{ none }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'none' used."]); - }); - - it('should support {% increment var %} and {% decrement var %}', async () => { - for (const tag of ['increment', 'decrement']) { - const sourceCode = ` - {% ${tag} var %} - {{ var }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - expect(offenses).toHaveLength(0); - } - }); - - it('should not report an offense when object is undefined in a "snippet" file with no presense of doc tags', async () => { - const sourceCode = ` - {{ my_var }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'snippets/file.liquid'); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense when object is undefined in a "snippet" file with doc tags that are missing the associated param', async () => { - const sourceCode = ` - {% doc %} - {% enddoc %} - {{ my_var }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'snippets/file.liquid'); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_var' used."]); - }); - - it('should not report an offense when object is defined with @param in a snippet file', async () => { - const sourceCode = ` - {% doc %} - @param {string} text - {% enddoc %} - - {{ text }} - `; - - const filePath = 'snippets/file.liquid'; - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, filePath); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense when object is not global', async () => { - const sourceCode = ` - {{ image }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'image' used."]); - }); - - it('should not report an offense for forloop/tablerowloop variables when in the correct context', async () => { - for (const tag of ['for', 'tablerow']) { - const sourceCode = ` - {% ${tag} x in collections %} - {{ ${tag}loop }} - {% end${tag} %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'file.liquid'); - - expect(offenses).toHaveLength(0); - } - }); - - it('should support contextual exceptions', async () => { - let offenses: Offense[]; - const contexts: [string, string][] = [ - ['section', 'sections/section.liquid'], - ['predictive_search', 'sections/predictive-search.liquid'], - ['recommendations', 'sections/recommendations.liquid'], - ['comment', 'sections/main-article.liquid'], - ['block', 'blocks/theme-app-extension.liquid'], - ['app', 'blocks/theme-app-extension.liquid'], - ['app', 'snippets/theme-app-extension.liquid'], - ]; - for (const [object, goodPath] of contexts) { - offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, goodPath); - expect(offenses).toHaveLength(0); - offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, 'file.liquid'); - expect(offenses).toHaveLength(1); - } - }); - - it('should allow block-level objects in snippets when in app mode', async () => { - const blockLevelObjects = ['section', 'block', 'recommendations', 'app']; - for (const object of blockLevelObjects) { - const sourceCode = `{% doc %} @param {string} x - x {% enddoc %}{{ ${object} }}`; - const offenses = await runLiquidCheck( - UndefinedObject, - sourceCode, - 'snippets/my-snippet.liquid', - {}, - undefined, - 'app', - ); - expect(offenses, `Expected no offense for '${object}' in app mode snippet`).toHaveLength(0); - } - }); - - it('should allow app, section, and block in snippets when in theme mode', async () => { - const allowedObjects = ['app', 'section', 'block']; - for (const object of allowedObjects) { - const sourceCode = `{% doc %} @param {string} x - x {% enddoc %}{{ ${object} }}`; - const offenses = await runLiquidCheck( - UndefinedObject, - sourceCode, - 'snippets/my-snippet.liquid', - ); - expect(offenses, `Expected no offense for '${object}' in theme mode snippet`).toHaveLength(0); - } - }); - - it('should still flag block-only objects in snippets when in theme mode', async () => { - const blockOnlyObjects = ['recommendations']; - for (const object of blockOnlyObjects) { - const sourceCode = `{% doc %} @param {string} x - x {% enddoc %}{{ ${object} }}`; - const offenses = await runLiquidCheck( - UndefinedObject, - sourceCode, - 'snippets/my-snippet.liquid', - ); - expect(offenses, `Expected offense for '${object}' in theme mode snippet`).toHaveLength(1); - } - }); - - it('should support contextual exceptions for checkout.liquid', async () => { - let offenses: Offense[]; - const contexts: [string, string][] = [ - ['locale', 'layout/checkout.liquid'], - ['direction', 'layout/checkout.liquid'], - ['skip_to_content_link', 'layout/checkout.liquid'], - ['checkout_html_classes', 'layout/checkout.liquid'], - ['checkout_stylesheets', 'layout/checkout.liquid'], - ['checkout_scripts', 'layout/checkout.liquid'], - ['content_for_logo', 'layout/checkout.liquid'], - ['breadcrumb', 'layout/checkout.liquid'], - ['order_summary_toggle', 'layout/checkout.liquid'], - ['content_for_order_summary', 'layout/checkout.liquid'], - ['alternative_payment_methods', 'layout/checkout.liquid'], - ['content_for_footer', 'layout/checkout.liquid'], - ['tracking_code', 'layout/checkout.liquid'], - ]; - for (const [object, goodPath] of contexts) { - offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, goodPath); - expect(offenses).toHaveLength(0); - offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, 'file.liquid'); - expect(offenses).toHaveLength(1); - } - }); - - it('should report an offense for forloop/tablerowloop used outside of context', async () => { - const sourceCode = ` - {{ forloop }} - {{ tablerowloop }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'file.liquid'); - - expect(offenses).toHaveLength(2); - }); - - it('should not report an offenses when definitions for global objects are unavailable', async () => { - const sourceCode = ` - {{ my_var }} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'file.liquid', { - themeDocset: undefined, - }); - - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when object is defined with @param in a block file', async () => { - const sourceCode = ` - {% doc %} - @param {string} text - {% enddoc %} - - {{ text }} - `; - - const filePath = 'blocks/my-custom-block.liquid'; - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, filePath); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense when an undefined object is used alongside @param in a block file', async () => { - const sourceCode = ` - {% doc %} - @param {string} text - {% enddoc %} - - {{ text }} - {{ undefined_variable }} - `; - - const filePath = 'blocks/my-custom-block.liquid'; - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, filePath); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe("Unknown object 'undefined_variable' used."); - }); - - it('does not report the built-in content variable in a block file', async () => { - const sourceCode = ` - {{ content }} - {% if content != blank %}{{ content | upcase }}{% endif %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, 'blocks/card.liquid'); - - expect(offenses).toEqual([]); - }); - - it('reports content outside block files', async () => { - const offenses = await runLiquidCheck(UndefinedObject, '{{ content }}', 'sections/main.liquid'); - - 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 = ` -

    {{ heading }}

    - ${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 %} -

    {{ eyebrow }}

    -

    {{ heading }}

    - ${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 %} -

    {{ heading }}

    - ${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 %} -

    {{ eyebrow }}

    -

    {{ heading }}

    -
    {{ content }}
    - ${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 %} -

    {{ eyebrow }}

    -

    {{ heading }}

    -

    {{ subtitle }}

    - ${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 = ` -
    - {{ layout_heading }} - {{ header }} - {{ color_background }} -
    - ${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 %} -

    {{ eyebrow }}

    -
    {{ content }}
    - {% 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 = ` -

    {{ heading }}

    - ${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 = ` -

    {{ heading }}

    - ${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 %} - @param {string} text - {% enddoc %} - - {% assign text = text | default: "value" %} - `; - - const filePath = 'snippets/file.liquid'; - const offenses = await runLiquidCheck(UndefinedObject, sourceCode, filePath); - - expect(offenses).toHaveLength(0); - }); - - it('should report an offense when assigning an undefined variable to itself', async () => { - const sourceCode = ` - {% assign my_var = my_var | default: "fallback" %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe("Unknown object 'my_var' used."); - }); - - it('should not report an offense when using variables inside visible_if statements in a schema tag', async () => { - const sourceCode = ` - {% schema %} - { - "settings": [ - { - "type": "text", - "label": "foo", - "id": "foo", - } - { - "type": "text", - "label": "bar", - "id": "bar", - "visible_if": "{{ block.settings.foo }}", - } - ] - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(UndefinedObject, sourceCode); - - assert(offenses.length == 0); - expect(offenses).to.be.empty; - }); -}); - -function schema(settings: object[]): string { - return `{% schema %}${JSON.stringify({ name: 'Card', settings })}{% endschema %}`; -} diff --git a/packages/theme-check-common/src/checks/unique-doc-param-names/index.spec.ts b/packages/theme-check-common/src/checks/unique-doc-param-names/index.spec.ts deleted file mode 100644 index 6d58307fd..000000000 --- a/packages/theme-check-common/src/checks/unique-doc-param-names/index.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { UniqueDocParamNames } from './index'; -import { runLiquidCheck } from '../../test'; - -describe('Module: UniqueDocParamNames', () => { - it(`should not report an error when a all doc params have unique names`, async () => { - const sourceCode = ` - {% doc %} - @param param1 - Example param - @param param2 - Example param - @param param3 - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(UniqueDocParamNames, sourceCode); - - expect(offenses).to.be.empty; - }); - - it('should report an error when a param name appears more than once', async () => { - const sourceCode = ` - {% doc %} - @param param1 - Example param - @param param1 - Example param - @param param1 - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(UniqueDocParamNames, sourceCode); - - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal("The parameter 'param1' is defined more than once."); - expect(offenses[1].message).to.equal("The parameter 'param1' is defined more than once."); - }); -}); diff --git a/packages/theme-check-common/src/checks/unique-settings-id/index.spec.ts b/packages/theme-check-common/src/checks/unique-settings-id/index.spec.ts deleted file mode 100644 index 5235dda04..000000000 --- a/packages/theme-check-common/src/checks/unique-settings-id/index.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { UniqueSettingIds } from './index'; -import { highlightedOffenses, runJSONCheck } from '../../test'; -import { invalidJson, validJson } from './test-data'; - -describe('Module: UniqueSettingIds', () => { - it("Should report an error for duplicate id's in settings_schema (0)", async () => { - const offenses = await runJSONCheck(UniqueSettingIds, invalidJson, 'file.json'); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Duplicate setting id found: "nosto_account_id"'); - - const highlights = highlightedOffenses({ 'file.json': invalidJson }, offenses); - - expect(highlights).to.have.length(1); - expect(highlights[0]).to.equal('"id": "nosto_account_id"'); - }); - - it('should not report any errors for valid file', async () => { - const offenses = await runJSONCheck(UniqueSettingIds, validJson); - - expect(offenses).to.be.empty; - }); -}); diff --git a/packages/theme-check-common/src/checks/unique-settings-id/test-data.ts b/packages/theme-check-common/src/checks/unique-settings-id/test-data.ts deleted file mode 100644 index 6c0d23002..000000000 --- a/packages/theme-check-common/src/checks/unique-settings-id/test-data.ts +++ /dev/null @@ -1,1191 +0,0 @@ -export const invalidJson = ` -[ - { - "name": "theme_info", - "theme_name": "SKKN", - "theme_author": "Accenture", - "theme_version": "2.0.0", - "theme_documentation_url": "https://www.accenture.com/", - "theme_support_url": "https://www.accenture.com/" - }, - { - "name": "Colors", - "settings": [ - { - "type": "color", - "id": "theme_color", - "label": "Theme Primary Color", - "default": "#ff0000" - }, - { - "type": "color", - "id": "default_shade_color", - "label": "PDP default shade option", - "default": "#757575" - } - ] - }, - { - "name": "Logos", - "settings": [ - { - "type": "image_picker", - "id": "logo", - "label": "Logo image" - } - ] - }, - { - "name": "Favicon", - "settings": [ - { - "type": "image_picker", - "id": "favicon", - "label": "Favicon image", - "info": "Will be scaled down to 32 x 32px" - } - ] - }, - { - "name": "Social media", - "settings": [ - { - "type": "header", - "content": "Social sharing options" - }, - { - "type": "checkbox", - "id": "share_facebook", - "label": "Share on Facebook", - "default": true - }, - { - "type": "checkbox", - "id": "share_twitter", - "label": "Tweet on Twitter", - "default": true - }, - { - "type": "checkbox", - "id": "share_pinterest", - "label": "Pin on Pinterest", - "default": true - }, - { - "type": "header", - "content": "Social accounts" - }, - { - "type": "text", - "id": "social_twitter_link", - "label": "Twitter", - "info": "https://twitter.com/shopify" - }, - { - "type": "text", - "id": "social_facebook_link", - "label": "Facebook", - "info": "https://facebook.com/shopify" - }, - { - "type": "text", - "id": "social_pinterest_link", - "label": "Pinterest", - "info": "https://pinterest.com/shopify" - }, - { - "type": "text", - "id": "social_instagram_link", - "label": "Instagram", - "info": "http://instagram.com/shopify" - }, - { - "type": "text", - "id": "social_tiktok_link", - "label": "TikTok", - "info": "http://tiktok.com/shopify" - }, - { - "type": "text", - "id": "social_tumblr_link", - "label": "Tumblr", - "info": "http://shopify.tumblr.com" - }, - { - "type": "text", - "id": "social_snapchat_link", - "label": "Snapchat", - "info": "https://www.snapchat.com/add/shopify" - }, - { - "type": "text", - "id": "social_youtube_link", - "label": "YouTube", - "info": "https://www.youtube.com/shopify" - }, - { - "type": "text", - "id": "social_vimeo_link", - "label": "Vimeo", - "info": "https://vimeo.com/shopify" - } - ] - }, - { - "name": "Newsletter", - "settings": [ - { - "type": "header", - "content": "Attentive" - }, - { - "type": "text", - "id": "attentive_id_desktop", - "label": "Creative ID (Desktop)" - }, - { - "type": "text", - "id": "attentive_id_mobile", - "label": "Creative ID (Mobile)" - }, - { - "type": "richtext", - "id": "privacy_policy", - "label": "Content" - } - ] - }, - { - "name": "Nosto Account Id", - "settings": [ - { - "type": "text", - "id": "nosto_account_id", - "label": "Nosto account id", - "default": "shopify-62406787286", - "placeholder": "If empty, Nosto script won't load" - } - ] - }, - { - "name": "Image Sizes", - "settings": [ - { - "type": "range", - "id": "product_gallery_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Product Gallery", - "default": 800 - }, - { - "type": "range", - "id": "product_tile_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Product Tile", - "default": 800 - }, - { - "type": "range", - "id": "article_tile_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Article Tile", - "default": 800 - }, - { - "type": "range", - "id": "collection_tile_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Collection Tile", - "default": 800 - }, - { - "type": "range", - "id": "line_item_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Line Item Thumbnail", - "default": 400 - }, - { - "type": "text", - "id": "responsive_sizes", - "label": "Responsive Sized", - "default": "360,540,720,900,1080,1200,1440,1680,1920", - "info": "Comma-separated list of sizes" - } - ] - }, - { - "name": "Pagination", - "settings": [ - { - "type": "range", - "id": "products_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Collection Products", - "default": 12 - }, - { - "type": "range", - "id": "results_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Search Results", - "default": 12 - }, - { - "type": "range", - "id": "articles_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Blog Articles", - "default": 12 - }, - { - "type": "range", - "id": "comments_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Article Comments", - "default": 12 - }, - { - "type": "range", - "id": "addresses_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Customer Addresses", - "default": 12 - }, - { - "type": "range", - "id": "orders_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Customer Orders", - "default": 12 - } - ] - }, - { - "name": "Navigation", - "settings": [ - { - "type": "header", - "content": "Breadcrumbs" - }, - { - "type": "checkbox", - "id": "show_breadcrumbs_home", - "label": "Show Home link", - "default": true - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_page", - "label": "Enable on Page template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_product", - "label": "Enable on Product template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_collection", - "label": "Enable on Collection template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_blog", - "label": "Enable on Blog template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_article", - "label": "Enable on Article template", - "default": false - }, - { - "type": "text", - "id": "footer_handle", - "label": "Footer Handle" - }, - { - "type": "text", - "id": "checkout_footer_handle", - "label": "Checkout Footer Handle" - } - ] - }, - { - "name": "Nosto Account Id", - "settings": [ - { - "type": "text", - "id": "nosto_account_id", - "label": "Nosto account id", - "default": "shopify-62406787286", - "placeholder": "If empty, Nosto script won't load" - } - ] - }, - { - "name": "Loyalty", - "settings": [ - { - "type": "range", - "id": "loyalty_points_settingss", - "min": 0, - "max": 10, - "step": 0.5, - "label": "Loyalty Points", - "default": 0.5 - } - ] - }, - { - "name": "Cart", - "settings": [ - { - "type": "range", - "id": "free_shipping_threshold", - "min": 0, - "max": 150, - "step": 5, - "unit": "$", - "label": "Free Shipping Threshold", - "default": 75 - }, - { - "type": "product", - "id": "complete_collection_set_handle", - "label": "Complete Collection Set Handle" - }, - { - "type": "text", - "id": "complete_collection_set_text", - "label": "Complete Collection Set Message", - "default": "You’re receiving 5 complimentary samples with your purchase of The Complete Collection." - }, - { - "type": "text", - "id": "quantity_message_for_one_time_purchase", - "label": "Quantity Message For Onetime Purchase", - "default": "You can select a maximum of 10 products of this item per order" - }, - { - "type": "text", - "id": "quantity_message_for_subscription", - "label": "Quantity Message For Subscription", - "default": "You can select a maximum of 3 products of this item per order" - }, - { - "type": "number", - "id": "maximum_quantity_for_one_time_purchase", - "label": "Maximum Quantity For Onetime Purchase", - "default": 10 - }, - { - "type": "number", - "id": "maximum_quantity_for_subscription", - "label": "Maximum Quantity For Subscription", - "default": 3 - } - ] - }, - { - "name": "Global-E", - "settings": [ - { - "type": "checkbox", - "id": "enable_globale", - "label": "Enable Global-E", - "default": true - }, - { - "type": "select", - "id": "environment", - "label": "Environment", - "options": [ - { - "value": "INT", - "label": "Staging" - }, - { - "value": "PROD", - "label": "Production" - } - ], - "default": "INT" - }, - { - "type": "text", - "id": "globale_mid", - "label": "Global-E Merchant ID", - "default": "1164" - }, - { - "type": "text", - "id": "globale_emi", - "label": "Global-E EMI", - "default": "8uyw" - }, - { - "type": "text", - "id": "globale_site_id", - "label": "Global-E Site ID", - "default": "212b00eaaa7c" - }, - { - "type": "text", - "id": "globale_appurl", - "label": "Global-E App URL", - "default": "https://crossborder-integration-qa-int.bglobale.com/" - }, - { - "type": "text", - "id": "globale_geappurl", - "label": "Global-E GE App URL", - "default": "https://www.bglobale.com/" - }, - { - "type": "text", - "id": "globale_apiurl", - "label": "Global-E GE API URL", - "default": "https://connect.bglobale.com/" - }, - { - "type": "text", - "id": "globale_checkout_environment", - "label": "Global-E Checkout Environment ID", - "default": "www.bglobale.com" - }, - { - "type": "text", - "id": "globale_operated_countries_GLBE_PARAMS", - "label": "Operated Countries (GLBE_PARAMS)", - "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", - "default": "AD, AE, AT, BE, BG, BN, CA, CH, CN, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GL, GR, HR, HU, ID, IE, IS, IT, LT, LU, LV, MC, MT, MX, NL, NO, PL, PT, RO, SA, SE, SG, SI, SK, VA" - }, - { - "type": "text", - "id": "globale_operated_countries_outside_GLBE_PARAMS", - "label": "Operated Countries outside GLBE_PARAMS", - "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", - "default": "AD,AE,AT,BE,BG,BN,CA,CH,CN,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GL,GR,HR,HU,ID,IE,IS,IT,LT,LU,LV,MC,MT,MX,NL,NO,PL,PT,RO,SA,SE,SG,SI,SK,VA" - } - ] - }, - { - "name": "Checkout Killswitches", - "settings": [ - { - "type": "checkbox", - "id": "terms_of_service_checkout", - "label": "Enables Terms of Service at Checkout Payment Method Step" - } - ] - }, - { - "name": "Hide Discount Code", - "settings": [ - { - "type": "text", - "id": "bundle_not_eligible_for_welcome10_discount", - "label": "Display below message as WELCOME10 is Not eligible for Bundle Items", - "default": "Part of Bundle, Not eligible for discount" - }, - { - "type": "text", - "id": "tiered_off_promotion_code", - "label": "Tiered % Off promotion Code (should be exact) or leave it empty if there is no tiered % off promotion active", - "default": "TIEREDOFF" - } - ] - }, - { - "name": "Klaviyo - Back In Stock", - "settings": [ - { - "type": "text", - "id": "klaviyo_public_api_key", - "label": "public Klaviyo API Key", - "default": "UJ6swp" - }, - { - "type": "text", - "id": "listId", - "label": "list id", - "default": "S8qDcW" - } - ] - }, - { - "name": "Sample", - "settings": [ - { - "type": "text", - "id": "sample_message", - "label": "Display below message as when customer order again for sample" - }, - { - "type": "header", - "content": "Attentive Popup for Product Page" - }, - { - "type": "text", - "id": "attentive_id_desktop_sample", - "label": "Creative ID (Desktop)" - }, - { - "type": "text", - "id": "attentive_id_mobile_sample", - "label": "Creative ID (Mobile)" - } - ] - }, - { - "name": "Yotpo", - "settings": [ - { - "type": "header", - "content": "Yotpo - Checkout Redeem Points" - }, - { - "type": "text", - "id": "yotpo_checkout_widget_id", - "label": "Yotpo Checkout Widget ID", - "default": "JXB__Dq8om7IRNIBU7D8ag" - }, - { - "type": "text", - "id": "yotpo_checkout_instance_id", - "label": "Yotpo Checkout Instance ID", - "default": "472279" - }, - { - "type": "text", - "id": "yotpo_account_text", - "label": "Yotpo Account Text", - "default": "Apply your points at checkout to receive a discount on your purchase" - } - ] - } -]`; - -export const validJson = ` -[ - { - "name": "theme_info", - "theme_name": "SKKN", - "theme_author": "Accenture", - "theme_version": "2.0.0", - "theme_documentation_url": "https://www.accenture.com/", - "theme_support_url": "https://www.accenture.com/" - }, - { - "name": "Colors", - "settings": [ - { - "type": "color", - "id": "theme_color", - "label": "Theme Primary Color", - "default": "#ff0000" - }, - { - "type": "color", - "id": "default_shade_color", - "label": "PDP default shade option", - "default": "#757575" - } - ] - }, - { - "name": "Logos", - "settings": [ - { - "type": "image_picker", - "id": "logo", - "label": "Logo image" - } - ] - }, - { - "name": "Favicon", - "settings": [ - { - "type": "image_picker", - "id": "favicon", - "label": "Favicon image", - "info": "Will be scaled down to 32 x 32px" - } - ] - }, - { - "name": "Social media", - "settings": [ - { - "type": "header", - "content": "Social sharing options" - }, - { - "type": "checkbox", - "id": "share_facebook", - "label": "Share on Facebook", - "default": true - }, - { - "type": "checkbox", - "id": "share_twitter", - "label": "Tweet on Twitter", - "default": true - }, - { - "type": "checkbox", - "id": "share_pinterest", - "label": "Pin on Pinterest", - "default": true - }, - { - "type": "header", - "content": "Social accounts" - }, - { - "type": "text", - "id": "social_twitter_link", - "label": "Twitter", - "info": "https://twitter.com/shopify" - }, - { - "type": "text", - "id": "social_facebook_link", - "label": "Facebook", - "info": "https://facebook.com/shopify" - }, - { - "type": "text", - "id": "social_pinterest_link", - "label": "Pinterest", - "info": "https://pinterest.com/shopify" - }, - { - "type": "text", - "id": "social_instagram_link", - "label": "Instagram", - "info": "http://instagram.com/shopify" - }, - { - "type": "text", - "id": "social_tiktok_link", - "label": "TikTok", - "info": "http://tiktok.com/shopify" - }, - { - "type": "text", - "id": "social_tumblr_link", - "label": "Tumblr", - "info": "http://shopify.tumblr.com" - }, - { - "type": "text", - "id": "social_snapchat_link", - "label": "Snapchat", - "info": "https://www.snapchat.com/add/shopify" - }, - { - "type": "text", - "id": "social_youtube_link", - "label": "YouTube", - "info": "https://www.youtube.com/shopify" - }, - { - "type": "text", - "id": "social_vimeo_link", - "label": "Vimeo", - "info": "https://vimeo.com/shopify" - } - ] - }, - { - "name": "Newsletter", - "settings": [ - { - "type": "header", - "content": "Attentive" - }, - { - "type": "text", - "id": "attentive_id_desktop", - "label": "Creative ID (Desktop)" - }, - { - "type": "text", - "id": "attentive_id_mobile", - "label": "Creative ID (Mobile)" - }, - { - "type": "richtext", - "id": "privacy_policy", - "label": "Content" - } - ] - }, - { - "name": "Nosto Account Id", - "settings": [ - { - "type": "text", - "id": "nosto_account_id", - "label": "Nosto account id", - "default": "shopify-62406787286", - "placeholder": "If empty, Nosto script won't load" - } - ] - }, - { - "name": "Image Sizes", - "settings": [ - { - "type": "range", - "id": "product_gallery_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Product Gallery", - "default": 800 - }, - { - "type": "range", - "id": "product_tile_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Product Tile", - "default": 800 - }, - { - "type": "range", - "id": "article_tile_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Article Tile", - "default": 800 - }, - { - "type": "range", - "id": "collection_tile_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Collection Tile", - "default": 800 - }, - { - "type": "range", - "id": "line_item_image_max", - "min": 160, - "max": 1920, - "step": 40, - "label": "Line Item Thumbnail", - "default": 400 - }, - { - "type": "text", - "id": "responsive_sizes", - "label": "Responsive Sized", - "default": "360,540,720,900,1080,1200,1440,1680,1920", - "info": "Comma-separated list of sizes" - } - ] - }, - { - "name": "Pagination", - "settings": [ - { - "type": "range", - "id": "products_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Collection Products", - "default": 12 - }, - { - "type": "range", - "id": "results_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Search Results", - "default": 12 - }, - { - "type": "range", - "id": "articles_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Blog Articles", - "default": 12 - }, - { - "type": "range", - "id": "comments_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Article Comments", - "default": 12 - }, - { - "type": "range", - "id": "addresses_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Customer Addresses", - "default": 12 - }, - { - "type": "range", - "id": "orders_page_size", - "min": 4, - "max": 48, - "step": 1, - "label": "Customer Orders", - "default": 12 - } - ] - }, - { - "name": "Navigation", - "settings": [ - { - "type": "header", - "content": "Breadcrumbs" - }, - { - "type": "checkbox", - "id": "show_breadcrumbs_home", - "label": "Show Home link", - "default": true - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_page", - "label": "Enable on Page template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_product", - "label": "Enable on Product template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_collection", - "label": "Enable on Collection template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_blog", - "label": "Enable on Blog template", - "default": false - }, - { - "type": "checkbox", - "id": "enable_breadcrumbs_article", - "label": "Enable on Article template", - "default": false - }, - { - "type": "text", - "id": "footer_handle", - "label": "Footer Handle" - }, - { - "type": "text", - "id": "checkout_footer_handle", - "label": "Checkout Footer Handle" - } - ] - }, - { - "name": "Nosto Account Id", - "settings": [ - { - "type": "text", - "id": "nosto_account_id_fixed", - "label": "Nosto account id", - "default": "shopify-62406787286", - "placeholder": "If empty, Nosto script won't load" - } - ] - }, - { - "name": "Loyalty", - "settings": [ - { - "type": "range", - "id": "loyalty_points_settingss", - "min": 0, - "max": 10, - "step": 0.5, - "label": "Loyalty Points", - "default": 0.5 - } - ] - }, - { - "name": "Cart", - "settings": [ - { - "type": "range", - "id": "free_shipping_threshold", - "min": 0, - "max": 150, - "step": 5, - "unit": "$", - "label": "Free Shipping Threshold", - "default": 75 - }, - { - "type": "product", - "id": "complete_collection_set_handle", - "label": "Complete Collection Set Handle" - }, - { - "type": "text", - "id": "complete_collection_set_text", - "label": "Complete Collection Set Message", - "default": "You’re receiving 5 complimentary samples with your purchase of The Complete Collection." - }, - { - "type": "text", - "id": "quantity_message_for_one_time_purchase", - "label": "Quantity Message For Onetime Purchase", - "default": "You can select a maximum of 10 products of this item per order" - }, - { - "type": "text", - "id": "quantity_message_for_subscription", - "label": "Quantity Message For Subscription", - "default": "You can select a maximum of 3 products of this item per order" - }, - { - "type": "number", - "id": "maximum_quantity_for_one_time_purchase", - "label": "Maximum Quantity For Onetime Purchase", - "default": 10 - }, - { - "type": "number", - "id": "maximum_quantity_for_subscription", - "label": "Maximum Quantity For Subscription", - "default": 3 - } - ] - }, - { - "name": "Global-E", - "settings": [ - { - "type": "checkbox", - "id": "enable_globale", - "label": "Enable Global-E", - "default": true - }, - { - "type": "select", - "id": "environment", - "label": "Environment", - "options": [ - { - "value": "INT", - "label": "Staging" - }, - { - "value": "PROD", - "label": "Production" - } - ], - "default": "INT" - }, - { - "type": "text", - "id": "globale_mid", - "label": "Global-E Merchant ID", - "default": "1164" - }, - { - "type": "text", - "id": "globale_emi", - "label": "Global-E EMI", - "default": "8uyw" - }, - { - "type": "text", - "id": "globale_site_id", - "label": "Global-E Site ID", - "default": "212b00eaaa7c" - }, - { - "type": "text", - "id": "globale_appurl", - "label": "Global-E App URL", - "default": "https://crossborder-integration-qa-int.bglobale.com/" - }, - { - "type": "text", - "id": "globale_geappurl", - "label": "Global-E GE App URL", - "default": "https://www.bglobale.com/" - }, - { - "type": "text", - "id": "globale_apiurl", - "label": "Global-E GE API URL", - "default": "https://connect.bglobale.com/" - }, - { - "type": "text", - "id": "globale_checkout_environment", - "label": "Global-E Checkout Environment ID", - "default": "www.bglobale.com" - }, - { - "type": "text", - "id": "globale_operated_countries_GLBE_PARAMS", - "label": "Operated Countries (GLBE_PARAMS)", - "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", - "default": "AD, AE, AT, BE, BG, BN, CA, CH, CN, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GL, GR, HR, HU, ID, IE, IS, IT, LT, LU, LV, MC, MT, MX, NL, NO, PL, PT, RO, SA, SE, SG, SI, SK, VA" - }, - { - "type": "text", - "id": "globale_operated_countries_outside_GLBE_PARAMS", - "label": "Operated Countries outside GLBE_PARAMS", - "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", - "default": "AD,AE,AT,BE,BG,BN,CA,CH,CN,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GL,GR,HR,HU,ID,IE,IS,IT,LT,LU,LV,MC,MT,MX,NL,NO,PL,PT,RO,SA,SE,SG,SI,SK,VA" - } - ] - }, - { - "name": "Checkout Killswitches", - "settings": [ - { - "type": "checkbox", - "id": "terms_of_service_checkout", - "label": "Enables Terms of Service at Checkout Payment Method Step" - } - ] - }, - { - "name": "Hide Discount Code", - "settings": [ - { - "type": "text", - "id": "bundle_not_eligible_for_welcome10_discount", - "label": "Display below message as WELCOME10 is Not eligible for Bundle Items", - "default": "Part of Bundle, Not eligible for discount" - }, - { - "type": "text", - "id": "tiered_off_promotion_code", - "label": "Tiered % Off promotion Code (should be exact) or leave it empty if there is no tiered % off promotion active", - "default": "TIEREDOFF" - } - ] - }, - { - "name": "Klaviyo - Back In Stock", - "settings": [ - { - "type": "text", - "id": "klaviyo_public_api_key", - "label": "public Klaviyo API Key", - "default": "UJ6swp" - }, - { - "type": "text", - "id": "listId", - "label": "list id", - "default": "S8qDcW" - } - ] - }, - { - "name": "Sample", - "settings": [ - { - "type": "text", - "id": "sample_message", - "label": "Display below message as when customer order again for sample" - }, - { - "type": "header", - "content": "Attentive Popup for Product Page" - }, - { - "type": "text", - "id": "attentive_id_desktop_sample", - "label": "Creative ID (Desktop)" - }, - { - "type": "text", - "id": "attentive_id_mobile_sample", - "label": "Creative ID (Mobile)" - } - ] - }, - { - "name": "Yotpo", - "settings": [ - { - "type": "header", - "content": "Yotpo - Checkout Redeem Points" - }, - { - "type": "text", - "id": "yotpo_checkout_widget_id", - "label": "Yotpo Checkout Widget ID", - "default": "JXB__Dq8om7IRNIBU7D8ag" - }, - { - "type": "text", - "id": "yotpo_checkout_instance_id", - "label": "Yotpo Checkout Instance ID", - "default": "472279" - }, - { - "type": "text", - "id": "yotpo_account_text", - "label": "Yotpo Account Text", - "default": "Apply your points at checkout to receive a discount on your purchase" - } - ] - } -]`; diff --git a/packages/theme-check-common/src/checks/unique-static-block-id/index.spec.ts b/packages/theme-check-common/src/checks/unique-static-block-id/index.spec.ts deleted file mode 100644 index 038c6b01f..000000000 --- a/packages/theme-check-common/src/checks/unique-static-block-id/index.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { UniqueStaticBlockId } from './index'; - -describe('Module: UniqueStaticBlockId', () => { - it('should report an error when two static blocks have the same id', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "static-block" %} - {% content_for "block", type: "text", id: "static-block" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The id 'static-block' is already being used by another static block", - ); - }); - - it('should not report an error when two static blocks have different id', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "static-block1" %} - {% content_for "block", type: "text", id: "static-block2" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(0); - }); - - it('should report an error when two static blocks have same id and different types', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "static-block" %} - {% content_for "block", type: "url", id: "static-block" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The id 'static-block' is already being used by another static block", - ); - }); - - it('should not report an error when two static blocks have different id and different types', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "static-block1" %} - {% content_for "block", type: "url", id: "static-block2" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(0); - }); - - it('should not report an error when blocks share the same id but have different arbitrary params', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} - {% content_for "block", type: "text", id: "header-menu", variant: "navigation_bar" %} - {% content_for "block", type: "text", id: "header-menu" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(0); - }); - - it('should report an error when blocks share the same id and same arbitrary params', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} - {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The id 'header-menu' is already being used by another static block", - ); - }); - - it('should not report an error when blocks share the same id but have different values for the same param', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "sidebar", color: "blue" %} - {% content_for "block", type: "text", id: "sidebar", color: "red" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(0); - }); - - it('should report an error when blocks share the same id and multiple matching params', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "sidebar", color: "blue", size: "large" %} - {% content_for "block", type: "text", id: "sidebar", color: "blue", size: "large" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The id 'sidebar' is already being used by another static block", - ); - }); - - it('should report an error when blocks have same params in different order', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "sidebar", color: "blue", size: "large" %} - {% content_for "block", type: "text", id: "sidebar", size: "large", color: "blue" %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The id 'sidebar' is already being used by another static block", - ); - }); - - it('should not report an error when any arbitrary param is a variable lookup', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} - {% content_for "block", type: "text", id: "header-menu", variant: some_variable %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(0); - }); - - it('should not report an error when two blocks use the same variable lookup', async () => { - const sourceCode = ` - {% content_for "block", type: "text", id: "header-menu", variant: some_variable %} - {% content_for "block", type: "text", id: "header-menu", variant: some_variable %} - `; - - const offenses = await runLiquidCheck(UniqueStaticBlockId, sourceCode); - - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/unknown-filter/index.spec.ts b/packages/theme-check-common/src/checks/unknown-filter/index.spec.ts deleted file mode 100644 index 12d44878d..000000000 --- a/packages/theme-check-common/src/checks/unknown-filter/index.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, highlightedOffenses } from '../../test'; -import { UnknownFilter } from './index'; - -describe('Module: UnknownFilter', () => { - it('should report an offense when an unknown filter is used', async () => { - const sourceCode = `{{ "hello" | unknown_filter }}`; - const offenses = await runLiquidCheck(UnknownFilter, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe("Unknown filter 'unknown_filter' used."); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights[0]).to.eql('| unknown_filter'); - }); - - it('reports on unknown filter', async () => { - const sourceCode = `{{ "foo" | bar }}`; - const offenses = await runLiquidCheck(UnknownFilter, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe("Unknown filter 'bar' used."); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights[0]).to.eql('| bar'); - }); - - it('reports on unknown filter chained with known filters', async () => { - const sourceCode = `{{ "foo" | append: ".js" | bar }}`; - const offenses = await runLiquidCheck(UnknownFilter, sourceCode); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe("Unknown filter 'bar' used."); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights[0]).to.eql('| bar'); - }); - - it('does not report on chain of known filters', async () => { - const sourceCode = `{{ "foo" | append: ".js" | item_count_for_variant }}`; - const offenses = await runLiquidCheck(UnknownFilter, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when a known filter is used (0)', async () => { - const sourceCode = `{{ 'hello' | item_count_for_variant }}`; - const offenses = await runLiquidCheck(UnknownFilter, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when a known filter is used (1)', async () => { - const sourceCode = `{{ 'hello' | link_to_type }}`; - const offenses = await runLiquidCheck(UnknownFilter, sourceCode); - expect(offenses).toHaveLength(0); - }); - - it('should not report an offense when a known filter is used (2)', async () => { - const sourceCode = `{{ 'hello' | link_to_vendor }}`; - const offenses = await runLiquidCheck(UnknownFilter, sourceCode); - expect(offenses).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/unrecognized-block-arguments/index.spec.ts b/packages/theme-check-common/src/checks/unrecognized-block-arguments/index.spec.ts deleted file mode 100644 index 0f2d34762..000000000 --- a/packages/theme-check-common/src/checks/unrecognized-block-arguments/index.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { UnrecognizedBlockArguments } from './index'; -import { - blockSource, - INVALID_SCHEMA_BLOCK, - liquidDocBlock, - runBlockCallCheck, -} from '../../test/block-fixtures'; - -const MERGED_BLOCK = blockSource( - [{ id: 'heading', type: 'text' }], - ['@param {string} tracking_id - Developer-only tracking id'], -); -const SCHEMA_ONLY_BLOCK = blockSource([{ id: 'heading', type: 'text' }]); -const LIQUID_DOC_ONLY_BLOCK = liquidDocBlock([ - '@param {string} tracking_id - Developer-only tracking id', -]); - -describe('UnrecognizedBlockArguments', () => { - it('accepts schema, LiquidDoc-only, and built-in content parameters', async () => { - const offenses = await run( - "{% block 'card', heading: 'Hello', tracking_id: 'hero', content: body %}{% endblock %}", - MERGED_BLOCK, - ); - - expect(offenses).toEqual([]); - }); - - it('accepts a schema parameter without LiquidDoc', async () => { - const offenses = await run( - "{% block 'card', heading: 'Hello' %}{% endblock %}", - SCHEMA_ONLY_BLOCK, - ); - - expect(offenses).toEqual([]); - }); - - it('reports an argument outside the merged interface', async () => { - const offenses = await run( - "{% block 'card', heading: 'Hello', unknown: true %}{% endblock %}", - MERGED_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Unknown argument 'unknown' in block tag for 'card'." }, - ]); - }); - - it('validates the LiquidDoc-only interface of a schema-less block', async () => { - const offenses = await run( - "{% block 'card', tracking_id: 'hero', unknown: true %}{% endblock %}", - LIQUID_DOC_ONLY_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Unknown argument 'unknown' in block tag for 'card'." }, - ]); - }); - - it('preserves block.name as the supported caller-side system argument', async () => { - const offenses = await run( - "{% block 'card', block.name: 'Card' %}{% endblock %}", - MERGED_BLOCK, - ); - - expect(offenses).toEqual([]); - }); - - it.each([ - "{% block 'card', block.settings.heading: 'Hello', unknown: true %}{% endblock %}", - "{% block 'card', block.unknown: 'value', unknown: true %}{% endblock %}", - ])('leaves dotted arguments to LiquidSyntaxError in %s', async (template) => { - const offenses = await run(template, MERGED_BLOCK); - - expect(offenses).toEqual([]); - }); - - it.each([ - ['missing target', undefined], - ['invalid target schema', INVALID_SCHEMA_BLOCK], - ])('does not report unknown arguments for an unreadable %s', async (_name, block) => { - const offenses = await run("{% block 'card', unknown: 'value' %}{% endblock %}", block); - - expect(offenses).toEqual([]); - }); -}); - -function run(template: string, block: string | undefined) { - return runBlockCallCheck(UnrecognizedBlockArguments, template, block); -} diff --git a/packages/theme-check-common/src/checks/unrecognized-content-for-arguments/index.spec.ts b/packages/theme-check-common/src/checks/unrecognized-content-for-arguments/index.spec.ts deleted file mode 100644 index e1ae1414c..000000000 --- a/packages/theme-check-common/src/checks/unrecognized-content-for-arguments/index.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck } from '../../test'; -import { UnrecognizedContentForArguments } from '.'; -import { RESERVED_CONTENT_FOR_ARGUMENTS } from '../../tags/content-for'; - -function check(snippet: string, source: string) { - return runLiquidCheck( - UnrecognizedContentForArguments, - source, - undefined, - {}, - { - 'blocks/card.liquid': snippet, - }, - ); -} - -const defaultSnippet = ` - {% doc %} - @param {string} required_string - A required string - @param {number} required_number - A required number - @param {boolean} required_boolean - A required boolean - @param {object} required_object - A required object - @param {string} [optional_string] - An optional string - @param {number} [optional_number] - An optional number - @param {object} [optional_object] - An optional object - @param {boolean} [optional_boolean] - An optional boolean - {% enddoc %} -`; - -describe('Module: UnrecognizedContentForArguments', () => { - describe('unknown arguments', () => { - it('should report unknown arguments that are provided in the content_for markup', async () => { - const sourceCode = ` - {% content_for 'block', - type: 'card', - required_string: 'My Card', - required_number: 1, - required_boolean: true, - required_object: product, - unknown_param: 'unknown', - second_unknown_param: 'second unknown', - %} - `; - const offenses = await check(defaultSnippet, sourceCode); - - expect(offenses).toHaveLength(2); - expect(offenses[0].message).toBe( - "Unknown argument 'unknown_param' in content_for tag for static block 'card'.", - ); - expect(offenses[1].message).toBe( - "Unknown argument 'second_unknown_param' in content_for tag for static block 'card'.", - ); - }); - }); - - describe('edge cases', () => { - it('should not report when static block has no doc comment', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - UnrecognizedContentForArguments, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': `

    This block has no doc comment

    `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when LiquidDoc definition has no defined params', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - UnrecognizedContentForArguments, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @description this is a description - @example this is an example - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when block name is a VariableLookup', async () => { - const sourceCode = `{% assign block_name = 'card' %}{% content_for 'block', type: block_name, title: 'My Card' %}`; - const offenses = await runLiquidCheck( - UnrecognizedContentForArguments, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - @param {string} description - The description of the card - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when argument begins with `.closest`', async () => { - const offenses = await check( - ` - {% doc %} - @param {string} title - The title of the card - {% enddoc %} -
    {{ title }}
    - `, - `{% content_for 'block', type: 'card', id: '123', closest.product: product %}`, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when argument is a reserved word', async () => { - const offenses = await check( - ` - {% doc %} - @param {string} title - The title of the card - {% enddoc %} -
    {{ title }}
    - `, - `{% content_for 'block', type: 'card', id: '123', ${RESERVED_CONTENT_FOR_ARGUMENTS[0]}: product %}`, - ); - - expect(offenses).toHaveLength(0); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/unrecognized-render-snippet-arguments/index.spec.ts b/packages/theme-check-common/src/checks/unrecognized-render-snippet-arguments/index.spec.ts deleted file mode 100644 index 57aa21c26..000000000 --- a/packages/theme-check-common/src/checks/unrecognized-render-snippet-arguments/index.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { applySuggestions, MockTheme, runLiquidCheck } from '../../test'; -import { UnrecognizedRenderSnippetArguments } from '.'; - -function check(snippet: string, source: string) { - return runLiquidCheck( - UnrecognizedRenderSnippetArguments, - source, - undefined, - {}, - { - 'snippets/card.liquid': snippet, - }, - ); -} - -const defaultSnippet = ` - {% doc %} - @param {string} required_string - A required string - @param {number} required_number - A required number - @param {boolean} required_boolean - A required boolean - @param {object} required_object - A required object - @param {string} [optional_string] - An optional string - @param {number} [optional_number] - An optional number - @param {object} [optional_object] - An optional object - @param {boolean} [optional_boolean] - An optional boolean - {% enddoc %} -`; - -describe('Module: UnrecognizedRenderSnippetParams', () => { - describe('unknown arguments', () => { - it('should report unknown arguments that are provided in the render markup', async () => { - const sourceCode = ` - {% render 'card', - required_string: 'My Card', - required_number: 1, - required_boolean: true, - required_object: product, - unknown_param: 'unknown', - second_unknown_param: 'second unknown', - %} - `; - const offenses = await check(defaultSnippet, sourceCode); - - expect(offenses).toHaveLength(2); - expect(offenses[0].message).toBe( - "Unknown argument 'unknown_param' in render tag for snippet 'card'.", - ); - expect(offenses[1].message).toBe( - "Unknown argument 'second_unknown_param' in render tag for snippet 'card'.", - ); - }); - }); - - describe('edge cases', () => { - it('should not report when snippet has no doc comment', async () => { - const sourceCode = `{% render 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - UnrecognizedRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': `

    This snippet has no doc comment

    `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when LiquidDoc definition has no defined params', async () => { - const sourceCode = `{% render 'card', title: 'My Card' %}`; - const offenses = await runLiquidCheck( - UnrecognizedRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @description this is a description - @example this is an example - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should not report when snippet name is a VariableLookup', async () => { - const sourceCode = `{% assign snippet_name = 'card' %}{% render snippet_name, title: 'My Card' %}`; - const offenses = await runLiquidCheck( - UnrecognizedRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - @param {string} description - The description of the card - {% enddoc %} -
    {{ title }}
    -
    {{ description }}
    - `, - }, - ); - - expect(offenses).toHaveLength(0); - }); - - it('should report when "with/for" alias syntax is used', async () => { - const mockTheme = { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - {% enddoc %} -
    {{ title }}
    - `, - } as MockTheme; - - let sourceCode = `{% render 'card' with 'my-card' as unknown_param %}`; - let offenses = await runLiquidCheck( - UnrecognizedRenderSnippetArguments, - sourceCode, - undefined, - {}, - mockTheme, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unknown argument 'unknown_param' in render tag for snippet 'card'.", - ); - expect(offenses[0].start.index).toBe(sourceCode.indexOf('with')); - expect(offenses[0].end.index).toBe( - sourceCode.indexOf('unknown_param') + 'unknown_param'.length, - ); - - sourceCode = `{% render 'card' for array as unknown_param %}`; - offenses = await runLiquidCheck( - UnrecognizedRenderSnippetArguments, - sourceCode, - undefined, - {}, - mockTheme, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unknown argument 'unknown_param' in render tag for snippet 'card'.", - ); - expect(offenses[0].start.index).toBe(sourceCode.indexOf('for')); - expect(offenses[0].end.index).toBe( - sourceCode.indexOf('unknown_param') + 'unknown_param'.length, - ); - }); - - it('should correctly suggest removing aliases with variable whitespace', async () => { - let sourceCode = `{% render 'card' with 'my-card' as unknown_param %}`; - let offenses = await runLiquidCheck( - UnrecognizedRenderSnippetArguments, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title of the card - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - - expect(offenses).toHaveLength(1); - let result = applySuggestions(sourceCode, offenses[0]); - expect(result).toEqual([`{% render 'card' %}`]); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/unsupported-doc-tag/index.spec.ts b/packages/theme-check-common/src/checks/unsupported-doc-tag/index.spec.ts index 3a9f1b04c..6aa1b5950 100644 --- a/packages/theme-check-common/src/checks/unsupported-doc-tag/index.spec.ts +++ b/packages/theme-check-common/src/checks/unsupported-doc-tag/index.spec.ts @@ -32,14 +32,4 @@ describe('Module: UnsupportedDocTag', () => { expect(suggestions).to.include(`{{ 'hello' }}`); }); - - it('should not report an error when `doc` tag is used within snippets', async () => { - const offenses = await runLiquidCheck( - UnsupportedDocTag, - sourceCode, - 'file://snippets/file.liquid', - ); - - expect(offenses).to.be.empty; - }); }); diff --git a/packages/theme-check-common/src/checks/unsupported-filter-arguments/index.spec.ts b/packages/theme-check-common/src/checks/unsupported-filter-arguments/index.spec.ts deleted file mode 100644 index 0ac72427d..000000000 --- a/packages/theme-check-common/src/checks/unsupported-filter-arguments/index.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { UnsupportedFilterArguments } from './index'; - -describe('Module: UnsupportedFilterArguments', () => { - it('reports an offense when a render argument uses a filter', async () => { - const sourceCode = `{% render 'foo', param1: 'bar', param2: 'hello' | append: ' world' %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain( - "Filters cannot be used on arguments passed to the 'render' tag", - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual([`| append: ' world'`]); - }); - - it('reports an offense when an include argument uses a filter', async () => { - const sourceCode = `{% include 'foo', x: bar | upcase %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain( - "Filters cannot be used on arguments passed to the 'include' tag", - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual([`| upcase`]); - }); - - it('does not report when no filter is used', async () => { - const sourceCode = `{% render 'foo', param1: 'bar', param2: 'hello' %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('reports an offense when a content_for argument uses a filter', async () => { - const sourceCode = `{% content_for 'block', type: 'foo', id: 'bar' | append: 'x' %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain( - "Filters cannot be used on arguments passed to the 'content_for' tag", - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual([`| append: 'x'`]); - }); - - it('does not report a false positive for a pipe inside a string literal', async () => { - const sourceCode = `{% render 'foo', sep: 'a|b' %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('does not report a false positive for a quoted pipe even when strict parse fails', async () => { - // `invalid_argument` forces the strict parse to fail, so the raw-markup - // scanner runs while the only pipe is still inside a quoted string literal. - const sourceCode = `{% render 'foo', sep: 'a|b', invalid_argument %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('does not report a false positive for a double-quoted pipe even when strict parse fails', async () => { - const sourceCode = `{% render 'foo', sep: "a|b", invalid_argument %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('highlights the real filter when a quoted pipe precedes it', async () => { - const sourceCode = `{% render 'foo', sep: 'a|b', x: bar | upcase %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(1); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual([`| upcase`]); - }); - - it('reports distinct highlights for two render tags on one line', async () => { - const sourceCode = `{% render 'foo', x: a | upcase %} {% render 'bar', y: b | downcase %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(2); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual([`| upcase`, `| downcase`]); - }); - - it('reports an offense for a render statement inside a {% liquid %} tag', async () => { - const sourceCode = `{% liquid\n render 'foo', x: bar | upcase\n%}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(1); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights).toEqual([`| upcase`]); - }); - - it('does not report on a plain render with no arguments', async () => { - const sourceCode = `{% render 'foo' %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('reports offenses on nested render tags', async () => { - const sourceCode = `{% if true %}{% render 'foo', x: bar | upcase %}{% endif %}`; - const offenses = await runLiquidCheck(UnsupportedFilterArguments, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain( - "Filters cannot be used on arguments passed to the 'render' tag", - ); - }); -}); diff --git a/packages/theme-check-common/src/checks/unused-assign/index.spec.ts b/packages/theme-check-common/src/checks/unused-assign/index.spec.ts deleted file mode 100644 index 109470280..000000000 --- a/packages/theme-check-common/src/checks/unused-assign/index.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { UnusedAssign } from './index'; -import { runLiquidCheck, applySuggestions } from '../../test'; - -describe('Module: UnusedAssign', () => { - it('should report an error when a variable is assigned but not used', async () => { - const sourceCode = ` - {% assign unusedVar = "value" %} - {% assign usedVar = "anotherValue" %} - {{ usedVar }} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal("The variable 'unusedVar' is assigned but not used"); - }); - - it('should not report an error when a variable is assigned and used', async () => { - const sourceCode = ` - {% assign usedVar = "value" %} - {{ usedVar }} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - - expect(offenses).to.be.empty; - }); - - it('should provide a suggestion to remove the unused variable', async () => { - const sourceCode = ` - {% assign unusedVar = "value" %} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].suggest).to.have.length(1); - expect(offenses[0]!.suggest![0].message).to.equal("Remove the unused variable 'unusedVar'"); - }); - - it('should fix the code by removing the unused variable', async () => { - const sourceCode = ` - {% assign unusedVar = "value" %} - {% assign usedVar = "anotherValue" %} - {{ usedVar }} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - // We're not deleting the line, that's prettier's job. - const expectedFixedCode = ` - ${''} - {% assign usedVar = "anotherValue" %} - {{ usedVar }} - `; - - expect(suggestions).to.include(expectedFixedCode); - }); - - it('should not report unused assigns for things used in a capture tag', async () => { - const sourceCode = ` - {% assign usedVar = "anotherValue" %} - {% capture foo %} - {{ usedVar }} - {% endcapture %} - {{ foo }} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - expect(offenses).to.be.empty; - }); - - it('should report unused capture', async () => { - const sourceCode = ` - {% assign usedVar = "anotherValue" %} - {% capture foo %} - {{ usedVar }} - {% endcapture %} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - expect(offenses).not.to.be.empty; - }); - - it('should not report unused assigns for things used in a raw-like tag', async () => { - const tags = ['style']; - for (const tag of tags) { - const sourceCode = ` - {% assign usedVar = 1 %} - {% ${tag} %} - {{ usedVar }} - {% end${tag} %} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - expect(offenses).to.be.empty; - } - }); - - it('should report unused assigns for things used in raw code that gets stripped away (schema, etc)', async () => { - const tags = ['schema', 'javascript', 'stylesheet']; - for (const tag of tags) { - const sourceCode = ` - {% assign usedVar = 1 %} - {% ${tag} %} - {{ usedVar }} - {% end${tag} %} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - expect(offenses).to.not.be.empty; - } - }); - - it('should not report variables used by a schema visible_if expression', async () => { - const sourceCode = ` - {% assign has_logo_image = true %} - {% schema %} - { - "name": "Logo", - "settings": [ - { - "type": "range", - "id": "height", - "min": 16, - "max": 80, - "step": 4, - "default": 32, - "visible_if": "{{ has_logo_image == true }}" - } - ] - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode, 'blocks/logo.liquid'); - - expect(offenses).to.be.empty; - }); - - it('should still report variables mentioned only in other schema strings', async () => { - const sourceCode = ` - {% assign unused_var = true %} - {% schema %} - { - "name": "unused_var", - "settings": [] - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode, 'blocks/example.liquid'); - - expect(offenses).to.have.lengthOf(1); - }); - - it('should not report unused assigns for things used in a HTML raw-like tag', async () => { - const tags = ['style', 'script']; - for (const tag of tags) { - const sourceCode = ` - {% assign usedVar = 1 %} - <${tag}> - {{ usedVar }} - - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - expect(offenses).to.be.empty; - } - }); - - it('should not report unused assigns for variables starting with an underscore', async () => { - const sourceCode = ` - {% assign _ = 1 %} - {% assign _unusedVar = 1 %} - {% capture _ %} - {% form "cart", cart %} - {% assign some_var = cart.successfully_posted? %} - {% endform %} - {% endcapture %} - {{ some_var }} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - expect(offenses).to.have.lengthOf(0); - }); - - it('should report unused assign for things used in a {% raw %} tag', async () => { - const sourceCode = ` - {% assign unusedVar = 1 %} - {% # It's a trap. It's not really used %} - {% raw %} - {{ unusedVar }} - {% endraw %} - `; - - const offenses = await runLiquidCheck(UnusedAssign, sourceCode); - expect(offenses).to.have.lengthOf(1); - }); -}); diff --git a/packages/theme-check-common/src/checks/unused-doc-param/index.spec.ts b/packages/theme-check-common/src/checks/unused-doc-param/index.spec.ts deleted file mode 100644 index dc94f2dba..000000000 --- a/packages/theme-check-common/src/checks/unused-doc-param/index.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { UnusedDocParam } from './index'; -import { runLiquidCheck, applySuggestions } from '../../test'; -import { LoopNamedTags } from '@shopify/liquid-html-parser'; - -describe('Module: UnusedDocParam', () => { - it('should not report a warning when a variable is defined and used', async () => { - const sourceCode = ` - {% doc %} - @param param1 - Example param - {% enddoc %} - - {{ param1 }} - `; - - const offenses = await runLiquidCheck(UnusedDocParam, sourceCode); - - expect(offenses).to.be.empty; - }); - - it('should report a warning with suggestions when a variable is defined but not used', async () => { - const sourceCode = ` - {% doc %} - @param param1 - Example param - @param param2 - Example param - {% enddoc %} - - {{ param1 }} - `; - - const offenses = await runLiquidCheck(UnusedDocParam, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The parameter 'param2' is defined but not used in this file.", - ); - expect(offenses[0].suggest).to.have.length(1); - expect(offenses[0]!.suggest![0].message).to.equal("Remove unused parameter 'param2'"); - }); - - it('should apply suggestion when a variable is defined but not used', async () => { - const sourceCode = ` - {% doc %} - @param param1 - Example param - @param param2 - Example param - {% enddoc %} - - {{ param1 }} - `; - - const offenses = await runLiquidCheck(UnusedDocParam, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - expect(suggestions).to.include(` - {% doc %} - @param param1 - Example param - - {% enddoc %} - - {{ param1 }} - `); - }); - - LoopNamedTags.forEach((tag) => { - it(`should report a warning when a variable is defined but not used outside '${tag}' loop context`, async () => { - const sourceCode = ` - {% doc %} - @param param1 - Example param - @param param2 - Example param - {% enddoc %} - - {{ param1 }} - - {% ${tag} param2 in array %} - {{ param2 }} - {% end${tag} %} - `; - - const offenses = await runLiquidCheck(UnusedDocParam, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "The parameter 'param2' is defined but not used in this file.", - ); - expect(offenses[0].suggest).to.have.length(1); - expect(offenses[0]!.suggest![0].message).to.equal("Remove unused parameter 'param2'"); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-block-argument-types/index.spec.ts b/packages/theme-check-common/src/checks/valid-block-argument-types/index.spec.ts index 2bb99b546..1bedd891c 100644 --- a/packages/theme-check-common/src/checks/valid-block-argument-types/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-block-argument-types/index.spec.ts @@ -1,171 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; import { ValidBlockArgumentTypes } from './index'; -import { check, highlightedOffenses } from '../../test'; -import { - blockSource, - INVALID_SCHEMA_BLOCK, - liquidDocBlock, - runBlockCallCheck, -} from '../../test/block-fixtures'; - -const TYPED_BLOCK = blockSource( - [ - { id: 'heading', type: 'text' }, - { id: 'count', type: 'number' }, - { id: 'product', type: 'product' }, - { id: 'products', type: 'product_list' }, - ], - ['@param {number} developer_count - Developer-only count'], -); - -const LIQUID_DOC_ONLY_BLOCK = liquidDocBlock([ - '@param {number} developer_count - Developer-only count', -]); +import { check } from '../../test'; describe('ValidBlockArgumentTypes', () => { - describe('call-site values', () => { - it('checks literals against schema-derived primitive types', async () => { - const template = "{% block 'card', heading: 42, count: 'many' %}{% endblock %}"; - const offenses = await run(template, TYPED_BLOCK); - - expect(offenses).toMatchObject([ - { message: "Type mismatch for argument 'heading': expected string, got number" }, - { message: "Type mismatch for argument 'count': expected number, got string" }, - ]); - expect(highlightedOffenses({ 'templates/test.liquid': template }, offenses)).toEqual([ - '42', - "'many'", - ]); - }); - - it('accepts literals that match schema-derived types', async () => { - const offenses = await run( - "{% block 'card', heading: 'Title', count: 2, product: product, products: products %}{% endblock %}", - TYPED_BLOCK, - ); - - expect(offenses).toEqual([]); - }); - - it('checks literals against LiquidDoc-only developer parameter types', async () => { - const offenses = await run( - "{% block 'card', developer_count: 'many' %}{% endblock %}", - TYPED_BLOCK, - ); - - expect(offenses).toMatchObject([ - { - message: "Type mismatch for argument 'developer_count': expected number, got string", - }, - ]); - }); - - it('checks LiquidDoc-only parameter types from a schema-less block', async () => { - const offenses = await run( - "{% block 'card', developer_count: 'many' %}{% endblock %}", - LIQUID_DOC_ONLY_BLOCK, - ); - - expect(offenses).toMatchObject([ - { - message: "Type mismatch for argument 'developer_count': expected number, got string", - }, - ]); - }); - - it('checks resource and list settings exactly', async () => { - const offenses = await run( - "{% block 'card', product: 'handle', products: ['one', 'two'] %}{% endblock %}", - TYPED_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Type mismatch for argument 'product': expected product, got string" }, - { message: "Type mismatch for argument 'products': expected product[], got string[]" }, - ]); - }); - - it('requires arrays to match the element type exactly', async () => { - const offenses = await run( - "{% block 'card', products: ['one', 2] %}{% endblock %}", - TYPED_BLOCK, - ); - - expect(offenses).toMatchObject([ - { message: "Type mismatch for argument 'products': expected product[], got mixed[]" }, - ]); - }); - - it('does not speculate about variable lookups or empty and partly unknown arrays', async () => { - const offenses = await run( - "{% block 'card', product: selected_product, products: [], heading: [selected, 'x'] %}{% endblock %}", - TYPED_BLOCK, - ); - - expect(offenses).toEqual([]); - }); - - it('accepts any literal for an object parameter', async () => { - const offenses = await run( - "{% block 'card', a: 'x', b: 1, c: (1..2), d: ['x'] %}{% endblock %}", - liquidDocBlock([ - '@param {object} a', - '@param {object} b', - '@param {object} c', - '@param {object} d', - ]), - ); - - expect(offenses).toEqual([]); - }); - - it('accepts any scalar for a boolean parameter but not a range or array', async () => { - const offenses = await run( - "{% block 'card', a: 'yes', b: 1, c: nil, d: (1..2), e: [true] %}{% endblock %}", - liquidDocBlock([ - '@param {boolean} a', - '@param {boolean} b', - '@param {boolean} c', - '@param {boolean} d', - '@param {boolean} e', - ]), - ); - - expect(offenses).toMatchObject([ - { message: "Type mismatch for argument 'd': expected boolean, got object" }, - { message: "Type mismatch for argument 'e': expected boolean, got boolean[]" }, - ]); - }); - - it('checks explicit content as a string without LiquidDoc', async () => { - const offenses = await run("{% block 'card', content: 42 %}{% endblock %}", blockSource([])); - - expect(offenses).toMatchObject([ - { message: "Type mismatch for argument 'content': expected string, got number" }, - ]); - }); - - it('checks explicit content as a string when the schema content setting is not', async () => { - const offenses = await run( - "{% block 'card', content: 42 %}{% endblock %}", - blockSource([{ id: 'content', type: 'number' }]), - ); - - expect(offenses).toMatchObject([ - { message: "Type mismatch for argument 'content': expected string, got number" }, - ]); - }); - - it.each([ - ['missing target', undefined], - ['invalid target schema', INVALID_SCHEMA_BLOCK], - ])('does not report type mismatches for an unreadable %s', async (_name, block) => { - const offenses = await run("{% block 'card', heading: 42 %}{% endblock %}", block); - - expect(offenses).toEqual([]); - }); - }); - it('does not load block parameters when the file has no LiquidDoc parameters', async () => { const getBlockSchema = vi.fn(async () => { throw new Error('schema provider failed'); @@ -182,127 +19,4 @@ describe('ValidBlockArgumentTypes', () => { expect(getBlockSchema).not.toHaveBeenCalled(); expect(getDocDefinition).not.toHaveBeenCalled(); }); - - describe('merged declarations', () => { - it('accepts exact and less-specific compatible LiquidDoc types', async () => { - const offenses = await definitions( - blockSource( - [ - { id: 'product', type: 'product' }, - { id: 'products', type: 'product_list' }, - ], - ['@param {object} [product] - Product', '@param {product[]} [products] - Products'], - ), - ); - - expect(offenses).toEqual([]); - }); - - it('compares resource and list LiquidDoc types exactly', async () => { - const offenses = await definitions( - blockSource( - [ - { id: 'product', type: 'product' }, - { id: 'products', type: 'product_list' }, - { id: 'collections', type: 'collection_list' }, - ], - [ - '@param {collection} [product] - Product', - '@param {product} [products] - Products', - '@param {product[]} [collections] - Collections', - ], - ), - ); - - expect(offenses.map((offense) => offense.message)).toEqual([ - "The schema setting 'product' has Liquid type 'product', but LiquidDoc declares 'collection'. The schema setting type is authoritative.", - "The schema setting 'products' has Liquid type 'product[]', but LiquidDoc declares 'product'. The schema setting type is authoritative.", - "The schema setting 'collections' has Liquid type 'collection[]', but LiquidDoc declares 'product[]'. The schema setting type is authoritative.", - ]); - }); - - it('points incompatible type declarations at the LiquidDoc type', async () => { - const source = blockSource( - [{ id: 'heading', type: 'text' }], - ['@param {number} [heading] - Heading'], - ); - const offenses = await definitions(source); - - expect(offenses).toMatchObject([ - { - message: - "The schema setting 'heading' has Liquid type 'string', but LiquidDoc declares 'number'. The schema setting type is authoritative.", - }, - ]); - expect(highlightedOffenses({ 'blocks/card.liquid': source }, offenses)).toEqual(['{number}']); - }); - - it('accepts an optional LiquidDoc declaration of a schema setting', async () => { - const offenses = await definitions( - blockSource([{ id: 'heading', type: 'text' }], ['@param {string} [heading] - Heading']), - ); - - expect(offenses).toEqual([]); - }); - - it('keeps the schema type of a setting that LiquidDoc declares as a string enum', async () => { - const block = blockSource( - [{ id: 'variant', type: 'text' }], - ["@param {'heading' | 'small'} [variant] - Variant"], - ); - - expect(await definitions(block)).toEqual([]); - expect(await run("{% block 'card', variant: 'body' %}{% endblock %}", block)).toEqual([]); - expect(await run("{% block 'card', variant: 42 %}{% endblock %}", block)).toMatchObject([ - { message: "Type mismatch for argument 'variant': expected string, got number" }, - ]); - }); - - it('does not speculate about omitted LiquidDoc or unmapped schema types', async () => { - const offenses = await definitions( - blockSource([{ id: 'item', type: 'metaobject' }], ['@param [item] - Item']), - ); - - expect(offenses).toEqual([]); - }); - - it('requires LiquidDoc content types to remain string-compatible', async () => { - const offenses = await definitions(blockSource([], ['@param {number} [content] - Body'])); - - expect(offenses).toMatchObject([ - { - message: - "The built-in parameter 'content' has Liquid type 'string', but LiquidDoc declares 'number'. The built-in parameter type is authoritative.", - }, - ]); - }); - - it.each([ - ['without LiquidDoc', []], - ['with a LiquidDoc string echo', ['@param {string} [content] - Body']], - ])( - 'leaves a non-string schema content setting %s to ValidBlockContentSettingType', - async (_name, params) => { - const offenses = await definitions( - blockSource([{ id: 'content', type: 'number' }], params), - ); - - expect(offenses).toEqual([]); - }, - ); - - it('accepts a string-compatible schema content setting', async () => { - const offenses = await definitions(blockSource([{ id: 'content', type: 'text' }])); - - expect(offenses).toEqual([]); - }); - }); }); - -function run(template: string, block: string | undefined) { - return runBlockCallCheck(ValidBlockArgumentTypes, template, block); -} - -function definitions(source: string) { - return check({ 'blocks/card.liquid': source }, [ValidBlockArgumentTypes]); -} diff --git a/packages/theme-check-common/src/checks/valid-block-content-setting-type/index.spec.ts b/packages/theme-check-common/src/checks/valid-block-content-setting-type/index.spec.ts index 9abd98056..718d29c80 100644 --- a/packages/theme-check-common/src/checks/valid-block-content-setting-type/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-block-content-setting-type/index.spec.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from 'vitest'; import { recommended } from '../index'; -import { ValidBlockArgumentTypes } from '../valid-block-argument-types'; -import { check, highlightedOffenses } from '../../test'; -import { blockSource } from '../../test/block-fixtures'; import { Severity } from '../../types'; import { ValidBlockContentSettingType } from './index'; @@ -11,111 +8,4 @@ describe('ValidBlockContentSettingType', () => { expect(recommended).toContain(ValidBlockContentSettingType); expect(ValidBlockContentSettingType.meta.severity).toBe(Severity.ERROR); }); - - it.each([ - ['number', 'number'], - ['checkbox', 'boolean'], - ['product', 'product'], - ['product_list', 'product[]'], - ])('reports a %s schema setting named content on its type', async (settingType, liquidType) => { - const source = blockSource([{ id: 'content', type: settingType }]); - - const offenses = await check({ 'blocks/card.liquid': source }, [ValidBlockContentSettingType]); - - expect(offenses).toMatchObject([ - { - message: `Schema setting 'content' has Liquid type '${liquidType}', but the built-in 'content' parameter has type 'string'.`, - severity: Severity.ERROR, - }, - ]); - expect(highlightedOffenses({ 'blocks/card.liquid': source }, offenses)).toEqual([ - `"${settingType}"`, - ]); - }); - - it.each(['text', 'textarea', 'richtext', 'inline_richtext', 'html'])( - 'accepts a string-compatible %s schema setting named content', - async (settingType) => { - const offenses = await check( - { 'blocks/card.liquid': blockSource([{ id: 'content', type: settingType }]) }, - [ValidBlockContentSettingType], - ); - - expect(offenses).toEqual([]); - }, - ); - - it('does not speculate about an unmapped schema setting type named content', async () => { - const offenses = await check( - { 'blocks/card.liquid': blockSource([{ id: 'content', type: 'metaobject' }]) }, - [ValidBlockContentSettingType], - ); - - expect(offenses).toEqual([]); - }); - - it('accepts non-string schema settings with other IDs', async () => { - const offenses = await check( - { 'blocks/card.liquid': blockSource([{ id: 'count', type: 'number' }]) }, - [ValidBlockContentSettingType], - ); - - expect(offenses).toEqual([]); - }); - - it('does not report a non-string content setting outside theme block files', async () => { - const offenses = await check( - { 'sections/main.liquid': blockSource([{ id: 'content', type: 'number' }]) }, - [ValidBlockContentSettingType], - ); - - expect(offenses).toEqual([]); - }); - - it('reports the schema mismatch once alongside ValidBlockArgumentTypes', async () => { - const source = blockSource( - [{ id: 'content', type: 'number' }], - ['@param {string} [content] - Body'], - ); - - const offenses = await check({ 'blocks/card.liquid': source }, [ - ValidBlockArgumentTypes, - ValidBlockContentSettingType, - ]); - - expect(offenses).toMatchObject([ - { check: 'ValidBlockContentSettingType', severity: Severity.ERROR }, - ]); - }); - - it('names the built-in parameter as the string authority when schema and LiquidDoc agree on another type', async () => { - const source = blockSource( - [{ id: 'content', type: 'number' }], - ['@param {number} [content] - Body'], - ); - - const offenses = await check({ 'blocks/card.liquid': source }, [ - ValidBlockArgumentTypes, - ValidBlockContentSettingType, - ]); - // Checks report in no particular order. - offenses.sort((a, b) => a.check.localeCompare(b.check)); - - expect(offenses).toMatchObject([ - { - check: 'ValidBlockArgumentTypes', - message: - "The built-in parameter 'content' has Liquid type 'string', but LiquidDoc declares 'number'. The built-in parameter type is authoritative.", - }, - { - check: 'ValidBlockContentSettingType', - message: - "Schema setting 'content' has Liquid type 'number', but the built-in 'content' parameter has type 'string'.", - }, - ]); - expect(highlightedOffenses({ 'blocks/card.liquid': source }, offenses)).toEqual([ - '{number}', - '"number"', - ]); - }); }); diff --git a/packages/theme-check-common/src/checks/valid-block-tag-placement/index.spec.ts b/packages/theme-check-common/src/checks/valid-block-tag-placement/index.spec.ts index dd5a8d492..0bb662df5 100644 --- a/packages/theme-check-common/src/checks/valid-block-tag-placement/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-block-tag-placement/index.spec.ts @@ -1,123 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { LiquidSyntaxError } from '../liquid-syntax-error'; import { recommended } from '../index'; -import { check, highlightedOffenses, runLiquidCheck } from '../../test'; import { ValidBlockTagPlacement } from './index'; -const BLOCK_TAG = "{% block 'card' %}Card{% endblock %}"; -const INNER_BLOCK_TAG = "{% block 'inner' %}Inner{% endblock %}"; -const BLOCK_TAG_UNDER_CONTROL_FLOW = `{% if product.available %}${BLOCK_TAG}{% endif %}`; -const NESTED_BLOCK_TAGS = `{% block 'outer' %}Before${INNER_BLOCK_TAG}After{% endblock %}`; -const BLOCK_TAG_NESTED_THROUGH_CONTROL_FLOW = `{% block 'outer' %}{% if product.available %}${INNER_BLOCK_TAG}{% endif %}{% endblock %}`; -const NESTED_BLOCK_TAGS_INSIDE_LIQUID = [ - '{% liquid', - " block 'outer'", - " block 'inner'", - ' endblock', - ' endblock', - '%}', -].join('\n'); - -const BLOCK_TAG_CASES = [ - { - description: 'a direct block tag', - source: BLOCK_TAG, - highlights: [BLOCK_TAG], - }, - { - description: 'a block tag under control flow', - source: BLOCK_TAG_UNDER_CONTROL_FLOW, - highlights: [BLOCK_TAG], - }, - { - description: 'nested block tags', - source: NESTED_BLOCK_TAGS, - highlights: [NESTED_BLOCK_TAGS, INNER_BLOCK_TAG], - }, - { - description: 'a block tag nested through control flow', - source: BLOCK_TAG_NESTED_THROUGH_CONTROL_FLOW, - highlights: [BLOCK_TAG_NESTED_THROUGH_CONTROL_FLOW, INNER_BLOCK_TAG], - }, - { - description: 'nested block tags inside a liquid tag', - source: NESTED_BLOCK_TAGS_INSIDE_LIQUID, - highlights: [ - "block 'outer'\n block 'inner'\n endblock\n endblock", - "block 'inner'\n endblock", - ], - }, -]; - -const ALLOWED_FILES = [ - 'templates/index.liquid', - 'templates/metaobject/product.liquid', - 'templates/metaobject/custom/product.liquid', - 'layout/theme.liquid', - 'templates\\index.liquid', - 'templates\\metaobject\\product.liquid', - 'layout\\theme.liquid', -]; - -const DISALLOWED_FILES = [ - 'blocks/card.liquid', - 'sections/main.liquid', - 'snippets/card.liquid', - 'assets/card.liquid', - 'theme.liquid', - 'layout/custom/theme.liquid', - 'snippets\\card.liquid', -]; - -const MESSAGE = "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid."; - describe('ValidBlockTagPlacement', () => { it('is enabled in the recommended configuration', () => { expect(recommended).toContain(ValidBlockTagPlacement); }); - - describe('allowed template and layout files', () => { - it.each(blockTagMatrix(ALLOWED_FILES))( - 'allows $description in $fileName', - async ({ fileName, source }) => { - const offenses = await runLiquidCheck(ValidBlockTagPlacement, source, fileName); - - expect(offenses).toEqual([]); - }, - ); - }); - - describe('all other theme files', () => { - it.each(blockTagMatrix(DISALLOWED_FILES))( - 'reports $description in $fileName', - async ({ fileName, source, highlights }) => { - const offenses = await runLiquidCheck(ValidBlockTagPlacement, source, fileName); - - expect(offenses).toHaveLength(highlights.length); - expect(offenses).toMatchObject(highlights.map(() => ({ message: MESSAGE }))); - expect(highlightedOffenses({ [fileName.replace(/\\/g, '/')]: source }, offenses)).toEqual( - highlights, - ); - }, - ); - }); - - it.each([ - ['unstructured markup', '{% block card %}{% endblock %}'], - ['structured markup', "{% block 'card', block.bad: true %}Card{% endblock %}"], - ])('leaves malformed block tags with %s to LiquidSyntaxError', async (_name, source) => { - const theme = { - 'snippets/card.liquid': source, - }; - const offenses = await check(theme, [LiquidSyntaxError, ValidBlockTagPlacement]); - - expect(offenses).toHaveLength(1); - expect(offenses[0].check).toBe('LiquidSyntaxError'); - }); }); - -function blockTagMatrix(fileNames: string[]) { - return fileNames.flatMap((fileName) => - BLOCK_TAG_CASES.map((testCase) => ({ ...testCase, fileName })), - ); -} diff --git a/packages/theme-check-common/src/checks/valid-block-target/index.spec.ts b/packages/theme-check-common/src/checks/valid-block-target/index.spec.ts index aa5a1b17d..cae8eb825 100644 --- a/packages/theme-check-common/src/checks/valid-block-target/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-block-target/index.spec.ts @@ -1,789 +1,12 @@ import { expect, describe, it } from 'vitest'; import { ValidBlockTarget } from './index'; import { check, MockFileSystem, MockTheme } from '../../test'; -import { Preset, Setting } from '../../types/schemas'; describe('Module: ValidBlockTarget', () => { const paths = ['sections', 'blocks']; - describe('File Existence Tests', () => { - paths.forEach((path) => { - it(`should ignore @app and @theme block types in ${path} bucket`, async () => { - const theme: MockTheme = { - [`${path}/app-theme.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@app" - }, - { - "type": "@theme" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`should not report any errors when all block files exist (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/text.liquid': '', - [`${path}/valid.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "text" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`should report errors when root level block file does not exist (${path} bucket)`, async () => { - const theme: MockTheme = { - [`${path}/invalid-section.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "invalid" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal("Theme block 'blocks/invalid.liquid' does not exist."); - }); - - it(`should report an error when a preset defined block file does not exist (${path} bucket)`, async () => { - const theme: MockTheme = { - [`${path}/invalid-block.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "missing_block" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Theme block 'blocks/missing_block.liquid' does not exist.", - ); - }); - - if (path === 'sections') { - it(`should report an error when default defined block file does not exist`, async () => { - const theme: MockTheme = { - [`${path}/invalid-file.liquid`]: ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "@theme" - } - ], - "default": { - "title": "Default", - "blocks": [ - { - "name": "some block", - "type": "missing_block" - } - ] - } - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense( - "Theme block 'blocks/missing_block.liquid' does not exist.", - ); - }); - } - - it(`should not report subsequent errors in present defined blocks if error in root level (${path} bucket)`, async () => { - const theme: MockTheme = { - [`${path}/invalid-block.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "invalid" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "text" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal("Theme block 'blocks/invalid.liquid' does not exist."); - }); - - it(`should report an error when a preset defined block file does not exist with hash-style presets (${path} bucket)`, async () => { - const theme: MockTheme = { - [`${path}/invalid-block.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - } - ], - "presets": [ - { - "name": "Default", - "blocks": { - "missing_block_hash": { - "type": "missing_block" - } - }, - "block_order": ["missing_block_hash"] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - "Theme block 'blocks/missing_block.liquid' does not exist.", - ); - }); - - it(`should not report an error when a preset defined block file exists with hash-style presets (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/text.liquid': '', - [`${path}/invalid-block.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - } - ], - "presets": [ - { - "name": "Default", - "blocks": { - "text_block": { - "type": "text" - } - }, - "block_order": ["text_block"] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - }); - - it('should not report errors for locally scoped blocks at root level', async () => { - const theme: MockTheme = { - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "local_block", - "name": "Local block" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - }); - - describe('Allowed Targeting Tests', () => { - paths.forEach((path) => { - it(`should report an error when a preset defined block is not specified at root level (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/text.liquid': '', - [`${path}/invalid-preset.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "text" - } - ], - "presets": [ - { - "name": "Default preset", - "blocks": [ - { - "type": "invalid_preset_block" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(2); - expect(offenses).to.containOffense( - 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.', - ); - expect(offenses).to.containOffense( - "Theme block 'blocks/invalid_preset_block.liquid' does not exist.", - ); - }); - - if (path === 'sections') { - it(`should report an error when a default defined block is not specified at root level`, async () => { - const theme: MockTheme = { - 'blocks/text.liquid': '', - [`${path}/invalid-preset.liquid`]: ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "text" - } - ], - "default": { - "name": "Default", - "blocks": [ - { - "name": "invalid_preset_block", - "type": "invalid_preset_block" - } - ] - } - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(2); - expect(offenses).to.containOffense( - 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.', - ); - expect(offenses).to.containOffense( - "Theme block 'blocks/invalid_preset_block.liquid' does not exist.", - ); - }); - } - - it(`should report errors in presets for private blocks even when @theme is specified at root level (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/_private.liquid': '', - [`${path}/private-blocks.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "_private" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense( - 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.', - ); - }); - - if (path === 'sections') { - it(`should report errors in default for private blocks even when @theme is specified at root level`, async () => { - const theme: MockTheme = { - 'blocks/_private.liquid': '', - [`${path}/private-blocks.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - } - ], - "default": { - "name": "Default", - "blocks": [ - { - "name": "private", - "type": "_private" - } - ] - } - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses).to.containOffense( - 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.', - ); - }); - } - - it(`should report errors with correct indices (${path} bucket)`, async () => { - const theme: MockTheme = { - [`${path}/private-blocks.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "_private" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(2); - - for (const offense of offenses) { - const content = theme[`${path}/private-blocks.liquid`]; - const erroredContent = content.slice(offense.start.index, offense.end.index); - expect(erroredContent).to.equal('"_private"'); - } - }); - - it(`should not report errors for private blocks when listed at root level (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/_private.liquid': '', - [`${path}/private-blocks.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "_private" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "_private" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`should handle mixed private and public blocks correctly (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/_private.liquid': '', - 'blocks/custom.liquid': '', - [`${path}/mixed-blocks.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - }, - { - "type": "_private" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "_private" - }, - { - "type": "custom" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`should report an error when a preset defined block is not specified at root level with hash-style presets (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/text.liquid': '', - [`${path}/invalid-preset.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "text" - } - ], - "presets": [ - { - "name": "Default preset", - "blocks": { - "invalid_preset_block_hash": { - "type": "invalid_preset_block" - } - }, - "block_order": ["invalid_preset_block_hash"] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(2); - expect(offenses).to.containOffense( - 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.', - ); - expect(offenses).to.containOffense( - "Theme block 'blocks/invalid_preset_block.liquid' does not exist.", - ); - }); - - it(`should report errors for private blocks even when @theme is specified at root level with hash-style presets (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/_private.liquid': '', - [`${path}/private-blocks.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "@theme" - } - ], - "presets": [ - { - "name": "Default", - "blocks": { - "private_hash": { - "type": "_private" - } - }, - "block_order": ["private_hash"] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.', - ); - }); - - it(`should not report errors for private blocks when listed at root level with hash-style presets (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/_private.liquid': '', - [`${path}/private-blocks.liquid`]: ` - {% schema %} - { - "name": "${path === 'sections' ? 'Section' : 'Block'} name", - "blocks": [ - { - "type": "_private" - } - ], - "presets": [ - { - "name": "Default", - "blocks": { - "private_hash": { - "type": "_private" - } - }, - "block_order": ["private_hash"] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`should report errors for nested blocks when they are not allowed (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/image.liquid': '', - 'blocks/group.liquid': '', - 'blocks/text.liquid': '', - 'blocks/slide.liquid': ` - {% schema %} - { - "name": "Slide", - "blocks": [ - { - "type": "text" - }, - { - "type": "image" - } - ] - } - {% endschema %} - `, - [`${path}/slideshow.liquid`]: ` - {% schema %} - { - "name": "Slideshow", - "blocks": [ - { - "type": "slide" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "slide", - "blocks": [ - { - "type": "group" - } - ] - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.', - ); - }); - - it(`should report errors for nested private blocks when they are not allowed (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/image.liquid': '', - 'blocks/_private.liquid': '', - 'blocks/text.liquid': '', - 'blocks/slide.liquid': ` - {% schema %} - { - "name": "Slide", - "blocks": [ - { - "type": "text" - }, - { - "type": "image" - } - ] - } - {% endschema %} - `, - [`${path}/slideshow.liquid`]: ` - {% schema %} - { - "name": "Slideshow", - "blocks": [ - { - "type": "slide" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "slide", - "blocks": [ - { - "type": "_private" - } - ] - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Private block type "_private" is not allowed in "slide" blocks.', - ); - }); - - it(`should report errors for nested blocks when they are not allowed with hash-style presets (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/image.liquid': '', - 'blocks/group.liquid': '', - 'blocks/text.liquid': '', - 'blocks/slide.liquid': ` - {% schema %} - { - "name": "Slide", - "blocks": [ - { - "type": "text" - }, - { - "type": "image" - } - ] - } - {% endschema %} - `, - [`${path}/slideshow.liquid`]: ` - {% schema %} - { - "name": "Slideshow", - "blocks": [ - { - "type": "slide" - } - ], - "presets": [ - { - "name": "Default", - "blocks": { - "slide_hash": { - "type": "slide", - "blocks": { - "group_hash": { - "type": "group" - } - } - } - } - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.', - ); - }); - + describe('Allowed Targeting Tests', () => { + paths.forEach((path) => { it(`should report errors for further nested blocks when they are not allowed (${path} bucket)`, async () => { const theme: MockTheme = { 'blocks/image.liquid': '', @@ -856,614 +79,6 @@ describe('Module: ValidBlockTarget', () => { const fs = new SlowFileSystem(theme); expect(await check(theme, [ValidBlockTarget], { fs })).to.have.length(1); }); - - describe(`Static Blocks used in a ${path} file`, () => { - const textSetting: Setting.Text = { - type: 'text' as Setting.Type.Text, - id: 'text', - label: 'Text', - }; - - describe('When presets are defined as a hash', () => { - describe('When the static block is used in the same file', () => { - const makeTheme = ( - preset: Preset.PresetBlockHash, - blocks_order: string[] = [], - ): MockTheme => { - return { - [`${path}/slideshow.liquid`]: ` - # Static block is defined in the body of the file - {% content_for "block", type: "static", id: "static_id" %} - - {% schema %} - { - "name": "Slideshow", - "presets": [ - { - "name": "Default", - "blocks": ${JSON.stringify(preset, null, 2)}, - "block_order": ${JSON.stringify(blocks_order)} - } - ] - } - {% endschema %} - `, - 'blocks/static.liquid': ` - {% schema %} - { - "name": "some static block", - "settings": [${JSON.stringify(textSetting)}] - } - {% endschema %} - `, - }; - }; - - it(`is silent for valid static blocks references`, async () => { - const theme: MockTheme = makeTheme({ - static_id: { - type: 'static', - static: true, - settings: { - text: 'some text', - }, - }, - }); - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`reports a warning for missing (or mismatching) static blocks references`, async () => { - const theme: MockTheme = makeTheme({ - mismatching_id: { - type: 'static', - static: true, - settings: { - text: 'some text', - }, - }, - }); - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).not.to.be.empty; - expect(offenses).to.containOffense( - 'Could not find a static block of type "static" with id "mismatching_id" in this file.', - ); - }); - }); - - describe('When the static block preset is defined in a nested block', () => { - const makeTheme = ( - preset: Preset.PresetBlockHash, - blocks_order: string[] = [], - ): MockTheme => { - return { - [`${path}/slideshow-container.liquid`]: ` - # This file demonstrates how a static block can be used inside a nested block's preset - {% content_for "blocks" %} - - {% schema %} - { - "name": "Slideshow container", - "blocks": [{ "type": "slideshow" }], - "presets": [{ - "name": "default", - "block_order": ["slideshow1"], - "blocks": { - "slideshow1": { - "type": "slideshow", - "blocks": ${JSON.stringify(preset, null, 2)}, - "block_order": ${JSON.stringify(blocks_order)} - } - } - }] - } - {% endschema %} - `, - [`blocks/slideshow.liquid`]: ` - # Static block is defined in the body of the file - {% content_for "block", type: "static", id: "static_id" %} - # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - {% schema %} - { "name": "Slideshow" } - {% endschema %} - `, - 'blocks/static.liquid': ` - {% schema %} - { - "name": "some static block", - "settings": [${JSON.stringify(textSetting)}] - } - {% endschema %} - `, - }; - }; - - it('is silent for valid static blocks references', async () => { - const theme = makeTheme({ - static_id: { - type: 'static', - static: true, - }, - }); - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it('reports warnings when using a mismatching (or nonexisting) static block', async () => { - const theme = makeTheme({ - mismatching_id: { - type: 'static', - static: true, - }, - }); - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).not.to.be.empty; - expect(offenses).to.containOffense( - 'Could not find a static block of type "static" with id "mismatching_id" in "blocks/slideshow.liquid".', - ); - }); - }); - }); - - describe('When preset are defined as an array', () => { - describe('When the static block is used in the same file', () => { - const makeTheme = (preset: Preset.PresetBlockForArray[]): MockTheme => { - return { - 'blocks/static.liquid': ` - {% schema %} - { - "name": "some static block", - "settings": [{ "label": "some setting", "type": "text", "id": "text" }], - } - {% endschema %} - `, - [`${path}/slideshow.liquid`]: ` - - # Static block is defined in the body of the file - {% content_for "block", type: "static", id: "static_id" %} - # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - {% schema %} - { - "name": "Slideshow", - "presets": [ - { - "name": "Default", - "blocks": ${JSON.stringify(preset, null, 2)} - } - ] - } - {% endschema %} - `, - }; - }; - - it(`is silent for valid static blocks references (${path} bucket)`, async () => { - const theme: MockTheme = makeTheme([ - { - type: 'static', - id: 'static_id', - static: true, - settings: { - text: 'some text', - }, - }, - ]); - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`reports a warning for missing (or mismatching) static blocks references (${path} bucket)`, async () => { - const theme: MockTheme = makeTheme([ - { - type: 'static', - id: 'mismatching_id', - static: true, - settings: { - text: 'some text', - }, - }, - ]); - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).not.to.be.empty; - expect(offenses).to.containOffense( - 'Could not find a static block of type "static" with id "mismatching_id" in this file.', - ); - }); - }); - }); - - describe('When the static block preset is defined in a nested block', () => { - const makeTheme = ( - preset: Preset.PresetBlockForArray[], - blocks_order: string[] = [], - ): MockTheme => { - return { - [`${path}/slideshow-container.liquid`]: ` - # This file demonstrates how a static block can be used inside a nested block's preset - {% content_for "blocks" %} - - {% schema %} - { - "name": "Slideshow container", - "blocks": [{ "type": "slideshow" }], - "presets": [{ - "name": "default", - "blocks": [ - { - "type": "slideshow", - "id": "slideshow1", - "blocks": ${JSON.stringify(preset, null, 2)}, - } - ] - }] - } - {% endschema %} - `, - [`blocks/slideshow.liquid`]: ` - # Static block is defined in the body of the file - {% content_for "block", type: "static", id: "static_id" %} - # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - {% schema %} - { "name": "Slideshow" } - {% endschema %} - `, - 'blocks/static.liquid': ` - {% schema %} - { - "name": "some static block", - "settings": [${JSON.stringify(textSetting)}] - } - {% endschema %} - `, - }; - }; - - it('is silent for valid static blocks references', async () => { - const theme = makeTheme([ - { - id: 'static_id', - type: 'static', - static: true, - }, - ]); - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it('reports warnings when using a mismatching (or nonexisting) static block', async () => { - const theme = makeTheme([ - { - id: 'mismatching_id', - type: 'static', - static: true, - }, - ]); - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).not.to.be.empty; - expect(offenses).to.containOffense( - 'Could not find a static block of type "static" with id "mismatching_id" in "blocks/slideshow.liquid".', - ); - }); - }); - }); - - it(`should report errors with correct indices for nested blocks when they are not allowed (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/image.liquid': '', - 'blocks/group.liquid': '', - 'blocks/text.liquid': '', - 'blocks/slide.liquid': ` - {% schema %} - { - "name": "Slide", - "blocks": [ - { - "type": "text" - }, - { - "type": "image" - } - ] - } - {% endschema %} - `, - [`${path}/slideshow.liquid`]: ` - {% schema %} - { - "name": "Slideshow", - "blocks": [ - { - "type": "slide" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "slide", - "blocks": [ - { - "type": "group" - } - ] - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.length(1); - const content = theme[`${path}/slideshow.liquid`]; - const erroredContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(erroredContent).to.equal('"group"'); - }); - - it(`should not report errors for nested blocks when they are allowed (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/image.liquid': '', - 'blocks/text.liquid': '', - 'blocks/slide.liquid': ` - {% schema %} - { - "name": "Slide", - "blocks": [ - { - "type": "text" - }, - { - "type": "image" - } - ] - } - {% endschema %} - `, - [`${path}/slideshow.liquid`]: ` - {% schema %} - { - "name": "Slideshow", - "blocks": [ - { - "type": "slide" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "slide", - "blocks": [ - { - "type": "image" - } - ] - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`should report errors on the correct file for nested blocks when they are not allowed (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/image.liquid': '', - 'blocks/group.liquid': '', - 'blocks/text.liquid': '', - 'blocks/slide.liquid': ` - {% schema %} - { - "name": "Slide", - "blocks": [ - { - "type": "text" - }, - { - "type": "image" - } - ] - } - {% endschema %} - `, - [`${path}/slideshow.liquid`]: ` - {% schema %} - { - "name": "Slideshow", - "blocks": [ - { - "type": "slide" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "slide", - "blocks": [ - { - "type": "group" - } - ] - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.have.lengthOf(1); - expect(offenses[0].uri).to.equal(`file:///${path}/slideshow.liquid`); - }); - - it(`should not report errors for local blocks in presets when no root-level blocks array exists (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/_card.liquid': ` - {% schema %} - { - "name": "Card", - "blocks": [ - { "type": "_title" }, - { "type": "_image" } - ] - } - {% endschema %} - `, - 'blocks/_title.liquid': '', - 'blocks/_image.liquid': '', - [`${path}/cards.liquid`]: ` - {% schema %} - { - "name": "Cards", - "presets": [ - { - "name": "Default", - "blocks": { - "card-1": { - "type": "_card", - "blocks": { - "title-1": { "type": "_title" }, - "image-1": { "type": "_image" } - } - } - } - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - }); - - it(`should still report errors for local blocks in presets when root-level blocks array contains only @app (${path} bucket)`, async () => { - const theme: MockTheme = { - 'blocks/_card.liquid': '', - [`${path}/cards.liquid`]: ` - {% schema %} - { - "name": "Cards", - "blocks": [ - { "type": "@app" } - ], - "presets": [ - { - "name": "Default", - "blocks": { - "card-1": { "type": "_card" } - } - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).not.to.be.empty; - }); - - it('should not crash or timeout with cyclical nested block relationships', async () => { - const theme: MockTheme = { - 'blocks/block-b.liquid': ` - {% schema %} - { - "name": "Block B", - "blocks": [ - { - "type": "block-c" - } - ] - } - {% endschema %} - `, - 'blocks/block-a.liquid': ` - {% schema %} - { - "name": "Block A", - "blocks": [ - { - "type": "block-b" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "block-b", - "blocks": [ - { - "type": "block-c" - } - ] - } - ] - } - ] - } - {% endschema %} - `, - 'blocks/block-c.liquid': ` - {% schema %} - { - "name": "Block C", - "blocks": [ - { - "type": "block-a" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "block-a", - "blocks": [ - { - "type": "block-b" - } - ] - } - ] - } - ] - } - {% endschema %} - `, - }; - - const timeout = new Promise((_, reject) => - setTimeout(() => reject(new Error('Test exceeded 500 ms')), 500), - ); - - const testPromise = (async () => { - const offenses = await check(theme, [ValidBlockTarget]); - expect(offenses).to.be.empty; - })(); - - await Promise.race([testPromise, timeout]); - }); }); }); }); diff --git a/packages/theme-check-common/src/checks/valid-content-for-argument-types/index.spec.ts b/packages/theme-check-common/src/checks/valid-content-for-argument-types/index.spec.ts deleted file mode 100644 index aeff25c3b..000000000 --- a/packages/theme-check-common/src/checks/valid-content-for-argument-types/index.spec.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, applySuggestions } from '../../test'; -import { ValidContentForArgumentTypes } from '.'; -import { BasicParamTypes } from '../../liquid-doc/utils'; - -describe('Module: ValidContentForParamTypes', () => { - describe('type validation', () => { - const typeTests = [ - { - type: 'string', - validValues: ["'hello'", "''", 'product'], - invalidValues: [ - { value: '123', actualType: BasicParamTypes.Number }, - { value: 'true', actualType: BasicParamTypes.Boolean }, - ], - }, - { - type: 'number', - validValues: ['0', '123', '-1', 'product'], - invalidValues: [ - { value: "'hello'", actualType: BasicParamTypes.String }, - { value: 'true', actualType: BasicParamTypes.Boolean }, - ], - }, - { - type: 'boolean', - validValues: ['true', 'false', 'nil', 'empty', 'product', '123', "'hello'"], - invalidValues: [], - }, - { - type: 'object', - validValues: ['product', '(1..3)'], - invalidValues: [ - { value: "'hello'", actualType: BasicParamTypes.String }, - { value: '123', actualType: BasicParamTypes.Number }, - { value: 'true', actualType: BasicParamTypes.Boolean }, - { value: 'empty', actualType: BasicParamTypes.Boolean }, - ], - }, - ]; - - for (const test of typeTests) { - describe(`${test.type} validation`, () => { - const makeBlock = (type: string) => ` - {% doc %} - @param {${type}} param - Description - {% enddoc %} -
    {{ param }}
    - `; - - test.validValues.forEach((value) => { - it(`should accept ${value} for ${test.type}`, async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: ${value} %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock(test.type), - }, - ); - expect(offenses).toHaveLength(0); - }); - }); - - test.invalidValues.forEach(({ value, actualType: expectedType }) => { - it(`should reject ${value} for ${test.type}`, async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: ${value} %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock(test.type), - }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - `Type mismatch for argument 'param': expected ${test.type}, got ${expectedType}`, - ); - }); - }); - }); - } - }); - - describe('edge cases', () => { - it('should handle mixed case type annotations', async () => { - const sourceCode = `{% content_for 'block', type: 'card', text: "hello", count: 5, flag: true, data: product %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {String} text - The text - @param {NUMBER} count - The count - @param {BOOLEAN} flag - The flag - @param {Object} data - The data - {% enddoc %} -
    {{ text }}{{ count }}{{ flag }}{{ data }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should ignore variable lookups', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: product_title %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {String} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not report when snippet has no doc comment', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: 123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': `

    This snippet has no doc comment

    `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not enforce unsupported types', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: 123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {Unsupported} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not report for unrecognized arguments', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: "hello", unrecognized: 123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {String} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - }); - - describe('suggestions', () => { - const makeBlock = (type: string) => ` - {% doc %} - @param {${type}} param - Description - {% enddoc %} -
    {{ param }}
    - `; - - it('should suggest replacing with default value for type or removing value', async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock('string'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].suggest).toHaveLength(2); - expect(offenses[0].suggest?.[0]?.message).toBe("Replace with default value '''' for string"); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% content_for 'block', type: 'card', param: '' %}`); - - const suggestions = applySuggestions(sourceCode, offenses[0]); - expect(suggestions?.[1]).toEqual(`{% content_for 'block', type: 'card', param: %}`); - }); - - it('should allow users to fix a single argument when multiple are provided`', async () => { - const sourceCode = `{% content_for 'block', type: 'card', title: 123, count: 5 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {string} title - The title - @param {number} count - The count - {% enddoc %} -
    {{ title }} {{ count }}
    - `, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Type mismatch for argument 'title': expected string, got number", - ); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% content_for 'block', type: 'card', title: '', count: 5 %}`); - }); - - it('should handle arguments with trailing commas', async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: 123, %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% content_for 'block', type: 'card', param: '', %}`); - }); - - it('should handle arguments with complex spacing', async () => { - const sourceCode = `{% content_for 'block', type: 'card', - title: 123, - count: 5 - %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': ` - {% doc %} - @param {string} title - The title - @param {number} count - The count - {% enddoc %} - `, - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% content_for 'block', type: 'card', - title: '', - count: 5 - %}`); - }); - - it('should handle argument with no space after colon', async () => { - const sourceCode = `{% content_for 'block', type: 'card', param:123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% content_for 'block', type: 'card', param:'' %}`); - }); - - it('should handle argument with multiple spaces after colon', async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% content_for 'block', type: 'card', param: '' %}`); - }); - - it('should handle argument with newlines', async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: - 123 - %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% content_for 'block', type: 'card', param: - '' - %}`); - }); - - it('should suggest removal and replacement if expected type has a default value', async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock('string'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].suggest).toHaveLength(2); - expect(offenses[0].suggest?.[0]?.message).toBe("Replace with default value '''' for string"); - expect(offenses[0].suggest?.[1]?.message).toBe('Remove value'); - }); - - it("should only suggest removal if expected type default value is ''", async () => { - const sourceCode = `{% content_for 'block', type: 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidContentForArgumentTypes, - sourceCode, - undefined, - {}, - { - 'blocks/card.liquid': makeBlock('object'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].suggest).toHaveLength(1); - expect(offenses[0].suggest?.[0]?.message).toBe('Remove value'); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-content-for-arguments/index.spec.ts b/packages/theme-check-common/src/checks/valid-content-for-arguments/index.spec.ts deleted file mode 100644 index acfbed531..000000000 --- a/packages/theme-check-common/src/checks/valid-content-for-arguments/index.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ValidContentForArguments } from '.'; -import { runLiquidCheck } from '../../test'; - -describe('Module: ValidContentForArguments', () => { - describe('{% content_for "unknown" %}', () => { - it('should not report offenses for strategies we do not know or support yet', async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "snippet", closest.product: product %}', - ); - expect(offenses).toHaveLength(0); - }); - }); - - describe('{% content_for "blocks" %}', () => { - it('should accept `closest.*` kwargs', async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "blocks", closest.product: product %}', - ); - expect(offenses).toHaveLength(0); - }); - - it('should not accept `context.*` kwargs', async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "blocks", context.product: product %}', - ); - expect(offenses).toHaveLength(1); - expect(offenses[0]!.message).to.equal( - `{% content_for "blocks" %} only accepts 'closest.*' arguments`, - ); - }); - - it('should report offenses for non-`closest.*` kwargs', async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "blocks", product: product %}', - ); - expect(offenses).toHaveLength(1); - expect(offenses[0]!.message).to.equal( - `{% content_for "blocks" %} only accepts 'closest.*' arguments`, - ); - }); - }); - - describe('{% content_for "block", type: "", id: "" %}', () => { - it('should accept valid arguments', async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "block", type: "text", id: "static-block" %}', - ); - expect(offenses).toHaveLength(0); - }); - - it(`should report an offense if 'type' is not a string`, async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "block", type: 10, id: "static-block" %}', - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).to.equal(`The 'type' argument should be a string`); - }); - - it(`should report an offense if 'id' is not a string`, async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "block", type: "foo", id: (0..10) %}', - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).to.equal(`The 'id' argument should be a string`); - }); - - it(`should accept static arguments`, async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "block", type: "foo", id: "id", product: product %}', - ); - expect(offenses).toHaveLength(0); - }); - - it(`should report an offense for reserved arguments`, async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "block", type: "foo", id: "id", settings: settings %}', - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).to.equal( - `{% content_for "block" %} doesn't support 'settings' because it's a reserved argument.`, - ); - }); - - it(`should report offenses for multiple reserved arguments`, async () => { - const offenses = await runLiquidCheck( - ValidContentForArguments, - '{% content_for "block", type: "foo", id: "id", settings: settings, section: section %}', - ); - const messages = offenses.map((o) => o.message); - - expect(messages).to.deep.equal([ - `{% content_for "block" %} doesn't support 'settings' because it's a reserved argument.`, - `{% content_for "block" %} doesn't support 'section' because it's a reserved argument.`, - ]); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-doc-param-types/index.spec.ts b/packages/theme-check-common/src/checks/valid-doc-param-types/index.spec.ts deleted file mode 100644 index 3068ae8c8..000000000 --- a/packages/theme-check-common/src/checks/valid-doc-param-types/index.spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { ValidDocParamTypes } from './index'; -import { runLiquidCheck, applySuggestions } from '../../test'; -import { BasicParamTypes } from '../../liquid-doc/utils'; - -describe('Module: ValidDocParamTypes', () => { - Object.values(BasicParamTypes).forEach((paramType) => { - it(`should not report an error when a valid basic parameter (${paramType}) type is used`, async () => { - const sourceCode = ` - {% doc %} - @param {${paramType}} param1 - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(ValidDocParamTypes, sourceCode); - - expect(offenses).to.be.empty; - }); - }); - - it(`should not report an error when a valid liquid object parameter (product) type is used`, async () => { - const sourceCode = ` - {% doc %} - @param {product} param1 - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(ValidDocParamTypes, sourceCode); - - expect(offenses).to.be.empty; - }); - - it(`should not report an error when a valid liquid object array parameter (product[]) type is used`, async () => { - const sourceCode = ` - {% doc %} - @param {product[]} param1 - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(ValidDocParamTypes, sourceCode); - - expect(offenses).to.be.empty; - }); - - it.each([ - "'heading' | 'small'", - '"Heading"|"small"', - "'heading'", - "'' | ' small '", - "'a|b' | 'a}b'", - `"it's" | 'say "hi"'`, - ])('accepts the string enum {%s}', async (type) => { - const source = `{% doc %}\n @param {${type}} [variant] - Text style\n{% enddoc %}`; - expect(await runLiquidCheck(ValidDocParamTypes, source)).toHaveLength(0); - }); - - it.each([ - "'heading' |", - "| 'heading'", - "'heading' || 'small'", - "'heading' 'small'", - "'heading' | small", - "'heading' | 'small", - "'heading' | number", - 'heading | small', - '1 | 2', - 'true | false', - "('heading' | 'small')", - "'heading'[]", - ])( - 'reports the complete invalid enum {%s} and preserves the optional parameter in its fix', - async (type) => { - const source = `{% doc %}\n @param { ${type} } [variant] - Text style\n{% enddoc %}`; - const offenses = await runLiquidCheck(ValidDocParamTypes, source); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe(`The parameter type ' ${type} ' is not supported.`); - expect(source.slice(offenses[0].start.index, offenses[0].end.index)).toBe(`{ ${type} }`); - expect(applySuggestions(source, offenses[0])).toEqual([ - '{% doc %}\n @param [variant] - Text style\n{% enddoc %}', - ]); - }, - ); - - it('should report an error with suggestions when an invalid parameter type is used', async () => { - const sourceCode = ` - {% doc %} - @param {invalidType} param1 - Example param - {% enddoc %} - `; - - const offenses = await runLiquidCheck(ValidDocParamTypes, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal("The parameter type 'invalidType' is not supported."); - expect(offenses[0].suggest).to.have.length(1); - expect(offenses[0]!.suggest![0].message).to.equal('Remove invalid parameter type'); - }); - - it('should apply suggestion when an invalid parameter type is used', async () => { - const sources = [ - `{% doc %} @param {invalidType} param1 - Example param {% enddoc %}`, - `{% doc %} @param { invalidType } param1 - Example param {% enddoc %}`, - ]; - - for (const source of sources) { - const offenses = await runLiquidCheck(ValidDocParamTypes, source); - - expect(offenses).to.have.length(1); - const suggestions = applySuggestions(source, offenses[0]); - - expect(suggestions).to.include(`{% doc %} @param param1 - Example param {% enddoc %}`); - } - }); - - it('validates named types, arrays, and enums together', async () => { - const source = `{% doc %} - @param {product} product - @param {string[]} labels - @param {'heading' | 'small'} [variant] - @param {unknown[]} unknown - @param {'heading' |} invalid_variant - {% enddoc %}`; - const offenses = await runLiquidCheck(ValidDocParamTypes, source); - expect(offenses.map(({ message }) => message)).toEqual([ - "The parameter type 'unknown[]' is not supported.", - "The parameter type ''heading' |' is not supported.", - ]); - }); - - it('removes the invalid type without changing braces in its description', async () => { - const source = `{% doc %}\n @param {'heading' |} [variant] - example {foo}\n{% enddoc %}`; - const offenses = await runLiquidCheck(ValidDocParamTypes, source); - expect(offenses).toHaveLength(1); - expect(applySuggestions(source, offenses[0])).toEqual([ - '{% doc %}\n @param [variant] - example {foo}\n{% enddoc %}', - ]); - }); - - describe('without a docset', () => { - const runWithoutDocset = (source: string) => - runLiquidCheck(ValidDocParamTypes, source, undefined, { themeDocset: undefined }); - - it('reports invalid string literal types', async () => { - const source = `{% doc %}\n @param {'heading' |} variant\n{% enddoc %}`; - const offenses = await runWithoutDocset(source); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe("The parameter type ''heading' |' is not supported."); - expect(applySuggestions(source, offenses[0])).toEqual([ - '{% doc %}\n @param variant\n{% enddoc %}', - ]); - }); - - it.each(["'heading' | 'small'", '"heading"'])('accepts the string enum {%s}', async (type) => { - const source = `{% doc %}\n @param {${type}} variant\n{% enddoc %}`; - expect(await runWithoutDocset(source)).toHaveLength(0); - }); - - it.each(['product', 'product[]', 'invalidType', 'unknown[]'])( - 'does not check the named type {%s}, which requires the docset', - async (type) => { - const source = `{% doc %}\n @param {${type}} variant\n{% enddoc %}`; - expect(await runWithoutDocset(source)).toHaveLength(0); - }, - ); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-html-translation/index.spec.ts b/packages/theme-check-common/src/checks/valid-html-translation/index.spec.ts deleted file mode 100644 index f48e108e6..000000000 --- a/packages/theme-check-common/src/checks/valid-html-translation/index.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ValidHTMLTranslation } from './index'; -import { highlightedOffenses, runJSONCheck } from '../../test'; -import { expect, describe, it } from 'vitest'; - -describe('Module: ValidHTMLTranslation', () => { - it('should not report valid HTML in translations', async () => { - const sourceCode = JSON.stringify({ - hello_html: '

    Hello, world

    ', - image_html: "", - line_break_html: '
    ', - self_closing_svg_html: '', - foo: 'bar', - }); - - const offenses = await runJSONCheck(ValidHTMLTranslation, sourceCode, 'locales/en.json'); - expect(offenses).toHaveLength(0); - }); - - it('should report invalid HTML in translations', async () => { - const sourceCode = JSON.stringify({ - hello_html: '

    Hello, world', - }); - - const offenses = await runJSONCheck(ValidHTMLTranslation, sourceCode, 'locales/en.json'); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).to.equal( - "LiquidHTMLParsingError: Attempting to end parsing before HtmlElement 'h1' was closed.", - ); - - const highlights = highlightedOffenses({ 'locales/en.json': sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).to.equal(`"

    Hello, world"`); - }); - - it('should report nested invalid HTML in translations', async () => { - const sourceCode = JSON.stringify({ - hello: { - world: { - today: { - good: { - day_html: '

    Hello, world', - }, - }, - }, - }, - }); - - const offenses = await runJSONCheck(ValidHTMLTranslation, sourceCode, 'locales/en.json'); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).to.equal( - "LiquidHTMLParsingError: Attempting to end parsing before HtmlElement 'h1' was closed.", - ); - - const highlights = highlightedOffenses({ 'locales/en.json': sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).to.equal(`"

    Hello, world"`); - }); - - it('should report pluralized key with invalid HTML', async () => { - const sourceCode = JSON.stringify({ - hello_html: { one: '

    Hello, world' }, - }); - - const offenses = await runJSONCheck(ValidHTMLTranslation, sourceCode, 'locales/en.json'); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).to.equal( - "LiquidHTMLParsingError: Attempting to end parsing before HtmlElement 'h1' was closed.", - ); - - const highlights = highlightedOffenses({ 'locales/en.json': sourceCode }, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).to.equal(`"

    Hello, world"`); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-json/index.spec.ts b/packages/theme-check-common/src/checks/valid-json/index.spec.ts index 0af297110..ab00a7582 100644 --- a/packages/theme-check-common/src/checks/valid-json/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-json/index.spec.ts @@ -89,14 +89,4 @@ describe('Module: ValidJSON', () => { expect(highlights).to.have.length(1); expect(highlights[0]).to.equal('10'); }); - - it('should not report any errors for valid JSON', async () => { - const validJson = `{ - "key1": "value1", - "key2": "value2" - }`; - - const offenses = await runJSONCheck(ValidJSON, validJson); - expect(offenses).to.be.empty; - }); }); diff --git a/packages/theme-check-common/src/checks/valid-local-blocks/index.spec.ts b/packages/theme-check-common/src/checks/valid-local-blocks/index.spec.ts deleted file mode 100644 index be49a36a5..000000000 --- a/packages/theme-check-common/src/checks/valid-local-blocks/index.spec.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { ValidLocalBlocks } from './index'; -import { check, MockTheme } from '../../test'; - -describe('ValidLocalBlocks with array-style blocks', () => { - it('should report errors when root level local blocks are defined in a block bucket', async () => { - const theme: MockTheme = { - 'blocks/local-blocks.liquid': ` - {% schema %} - { - "name": "Block name", - "blocks": [ - { - "type": "local_block", - "name": "Local Block" - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal('Local scoped blocks are not supported in theme blocks.'); - }); - - it('should report errors when sections use theme blocks together with locally scoped blocks in root level', async () => { - const theme: MockTheme = { - 'blocks/text.liquid': '', - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "text" - }, - { - "type": "local_block", - "name": "Local Block" - } - ] - } - {% endschema %} - `, - }; - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Sections cannot use theme blocks together with locally scoped blocks.', - ); - }); - - it('should report errors when sections use static theme blocks together with locally scoped blocks', async () => { - const theme: MockTheme = { - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "local_block", - "name": "Local Block" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "id": "static_block", - "type": "static_block", - "static": true - } - ] - } - ] - } - {% endschema %} - `, - }; - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - 'Sections cannot use static theme blocks together with locally scoped blocks.', - ); - }); - - it('should report errors with correct indices when sections use static theme blocks together with locally scoped blocks', async () => { - const theme: MockTheme = { - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "local_block", - "name": "Local Block" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "id": "static_block", - "type": "static_block", - "static": true - } - ] - } - ] - } - {% endschema %} - `, - }; - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.have.length(1); - - const content = theme['sections/local-blocks.liquid']; - const errorContent = content.slice(offenses[0].start.index, offenses[0].end.index); - expect(errorContent).to.equal('"static_block"'); - }); -}); - -describe('ValidLocalBlocks on edge cases', () => { - it('should not report errors for block setting types', async () => { - const theme: MockTheme = { - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "@app" - }, - { - "type": "link_list", - "name": "Link list", - "settings": [ - { - "type": "inline_richtext", - "id": "heading", - "default": "Heading" - }, - { - "type": "link_list", - "id": "menu", - "default": "Footer" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.be.empty; - }); - - it('should not report errors when @app is used alongside static blocks', async () => { - const theme: MockTheme = { - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "@app" - }, - { - "type": "static_block", - "static": true - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.be.empty; - }); - - it('should not report errors on themes that define local blocks at the root level and use them in presets', async () => { - const theme: MockTheme = { - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "local_block", - "name": "Local Block" - } - ], - "presets": [ - { - "name": "Default", - "blocks": [ - { - "type": "local_block" - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.be.empty; - }); - - it('should not report errors on older themes that define local blocks at the root level and use them in hash-style presets', async () => { - const theme: MockTheme = { - 'sections/local-blocks.liquid': ` - {% schema %} - { - "name": "Section name", - "blocks": [ - { - "type": "local_block", - "name": "Local Block" - } - ], - "presets": [ - { - "name": "Default", - "blocks": { - "local_block": { - "type": "local_block" - } - } - } - ] - } - {% endschema %} - `, - }; - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.be.empty; - }); - - it('should not report errors when preset blocks have a name property', async () => { - const theme: MockTheme = { - 'blocks/product-grid.liquid': ` - {% schema %} - { - "name": "Product grid", - "blocks": [{ "type": "product-card" }], - "presets": [ - { - "name": "Product grid", - "blocks": { - "product-grid-card": { - "type": "product-card", - "static": true, - "blocks": { - "card_gallery_eK4Hr7": { - "type": "card-gallery", - "name": "potato", - }, - "group_4RMFLV": { - "type": "group" - } - }, - "block_order": ["card_gallery_eK4Hr7", "group_4RMFLV"] - } - } - } - ] - } - {% endschema %} - - `, - }; - - const offenses = await check(theme, [ValidLocalBlocks]); - expect(offenses).to.be.empty; - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-render-snippet-argument-types/index.spec.ts b/packages/theme-check-common/src/checks/valid-render-snippet-argument-types/index.spec.ts deleted file mode 100644 index 6df33b4e3..000000000 --- a/packages/theme-check-common/src/checks/valid-render-snippet-argument-types/index.spec.ts +++ /dev/null @@ -1,468 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { runLiquidCheck, applySuggestions } from '../../test'; -import { ValidRenderSnippetArgumentTypes } from '.'; -import { BasicParamTypes } from '../../liquid-doc/utils'; - -describe('Module: ValidRenderSnippetParamTypes', () => { - describe('type validation', () => { - const typeTests = [ - { - type: 'string', - validValues: ["'hello'", "''", 'product'], - invalidValues: [ - { value: '123', actualType: BasicParamTypes.Number }, - { value: 'true', actualType: BasicParamTypes.Boolean }, - ], - }, - { - type: 'number', - validValues: ['0', '123', '-1', 'product'], - invalidValues: [ - { value: "'hello'", actualType: BasicParamTypes.String }, - { value: 'true', actualType: BasicParamTypes.Boolean }, - ], - }, - { - type: 'boolean', - validValues: ['true', 'false', 'nil', 'empty', 'product', '123', "'hello'"], - invalidValues: [], - }, - { - type: 'object', - validValues: ['product', '(1..3)'], - invalidValues: [ - { value: "'hello'", actualType: BasicParamTypes.String }, - { value: '123', actualType: BasicParamTypes.Number }, - { value: 'true', actualType: BasicParamTypes.Boolean }, - { value: 'empty', actualType: BasicParamTypes.Boolean }, - ], - }, - ]; - - for (const test of typeTests) { - describe(`${test.type} validation`, () => { - const makeSnippet = (type: string) => ` - {% doc %} - @param {${type}} param - Description - {% enddoc %} -
    {{ param }}
    - `; - - test.validValues.forEach((value) => { - it(`should accept ${value} for ${test.type}`, async () => { - const sourceCode = `{% render 'card', param: ${value} %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet(test.type), - }, - ); - expect(offenses).toHaveLength(0); - }); - }); - - test.invalidValues.forEach(({ value, actualType: expectedType }) => { - it(`should reject ${value} for ${test.type}`, async () => { - const sourceCode = `{% render 'card', param: ${value} %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet(test.type), - }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - `Type mismatch for argument 'param': expected ${test.type}, got ${expectedType}`, - ); - }); - }); - }); - } - }); - - describe('edge cases', () => { - it('should handle mixed case type annotations', async () => { - const sourceCode = `{% render 'card', text: "hello", count: 5, flag: true, data: product %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {String} text - The text - @param {NUMBER} count - The count - @param {BOOLEAN} flag - The flag - @param {Object} data - The data - {% enddoc %} -
    {{ text }}{{ count }}{{ flag }}{{ data }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should ignore variable lookups', async () => { - const sourceCode = `{% render 'card', title: product_title %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {String} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not report when snippet has no doc comment', async () => { - const sourceCode = `{% render 'card', title: 123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': `

    This snippet has no doc comment

    `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not enforce unsupported types', async () => { - const sourceCode = `{% render 'card', title: 123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {Unsupported} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should not report for unrecognized arguments', async () => { - const sourceCode = `{% render 'card', title: "hello", unrecognized: 123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {String} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(0); - }); - - it('should report when `with` alias is used', async () => { - const sourceCode = `{% render 'card' with 12 as title %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {String} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Type mismatch for argument 'title': expected string, got number", - ); - }); - - it('should report when `for` alias is used', async () => { - const sourceCode = `{% render 'card' for 123 as title %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {String} title - The title - {% enddoc %} -
    {{ title }}
    - `, - }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Type mismatch for argument 'title': expected string, got number", - ); - }); - }); - - describe('suggestions', () => { - const makeSnippet = (type: string) => ` - {% doc %} - @param {${type}} param - Description - {% enddoc %} -
    {{ param }}
    - `; - - it('should suggest replacing with default value for type or removing value', async () => { - const sourceCode = `{% render 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].suggest).toHaveLength(2); - expect(offenses[0].suggest?.[0]?.message).toBe("Replace with default value '''' for string"); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card', param: '' %}`); - - const suggestions = applySuggestions(sourceCode, offenses[0]); - expect(suggestions?.[1]).toEqual(`{% render 'card', param: %}`); - }); - - it('should allow users to fix a single argument when multiple are provided`', async () => { - const sourceCode = `{% render 'card', title: 123, count: 5 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title - @param {number} count - The count - {% enddoc %} -
    {{ title }} {{ count }}
    - `, - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Type mismatch for argument 'title': expected string, got number", - ); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card', title: '', count: 5 %}`); - }); - - it('should handle arguments with trailing commas', async () => { - const sourceCode = `{% render 'card', param: 123, %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card', param: '', %}`); - }); - - it('should handle arguments with complex spacing', async () => { - const sourceCode = `{% render 'card', - title: 123, - count: 5 - %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': ` - {% doc %} - @param {string} title - The title - @param {number} count - The count - {% enddoc %} - `, - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card', - title: '', - count: 5 - %}`); - }); - - it('should handle argument with no space after colon', async () => { - const sourceCode = `{% render 'card', param:123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card', param:'' %}`); - }); - - it('should handle argument with multiple spaces after colon', async () => { - const sourceCode = `{% render 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card', param: '' %}`); - }); - - it('should handle argument with newlines', async () => { - const sourceCode = `{% render 'card', param: - 123 - %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card', param: - '' - %}`); - }); - - it('should suggest removal and replacement if expected type has a default value', async () => { - const sourceCode = `{% render 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].suggest).toHaveLength(2); - expect(offenses[0].suggest?.[0]?.message).toBe("Replace with default value '''' for string"); - expect(offenses[0].suggest?.[1]?.message).toBe('Remove value'); - }); - - it("should only suggest removal if expected type default value is ''", async () => { - const sourceCode = `{% render 'card', param: 123 %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('object'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].suggest).toHaveLength(1); - expect(offenses[0].suggest?.[0]?.message).toBe('Remove value'); - }); - - it('should handle when aliases `with` syntax is used', async () => { - const sourceCode = `{% render 'card' with 123 as param %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Type mismatch for argument 'param': expected string, got number", - ); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card' with '' as param %}`); - }); - - it('should handle when aliases `for` syntax is used', async () => { - const sourceCode = `{% render 'card' for 123 as param %}`; - const offenses = await runLiquidCheck( - ValidRenderSnippetArgumentTypes, - sourceCode, - undefined, - {}, - { - 'snippets/card.liquid': makeSnippet('string'), - }, - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Type mismatch for argument 'param': expected string, got number", - ); - - const result = applySuggestions(sourceCode, offenses[0]); - expect(result?.[0]).toEqual(`{% render 'card' for '' as param %}`); - }); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-schema-name/index.spec.ts b/packages/theme-check-common/src/checks/valid-schema-name/index.spec.ts index bd7b9938f..01ca149a4 100644 --- a/packages/theme-check-common/src/checks/valid-schema-name/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-schema-name/index.spec.ts @@ -1,5 +1,5 @@ import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck, check } from '../../test'; +import { runLiquidCheck } from '../../test'; import { ValidSchemaName } from './index'; import { ThemeSchemaType } from '../..'; @@ -30,63 +30,4 @@ describe('Module: ValidSchemaName', () => { expect(offenses).toHaveLength(0); }); - - it('reports offense with schema name over 25 characters long', async () => { - const sourceCode = ` - {% schema %} - { - "name": "test-schema-name-that-is-too-long" - } - {% endschema %} - `; - - const offenses = await runLiquidCheck(ValidSchemaName, sourceCode, DEFAULT_FILE_NAME); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "Schema name 'test-schema-name-that-is-too-long' is too long (max 25 characters)", - ); - - const highlights = highlightedOffenses({ [DEFAULT_FILE_NAME]: sourceCode }, offenses); - expect(highlights).to.have.length(1); - expect(highlights[0]).toBe('"test-schema-name-that-is-too-long"'); - }); - - it('reports no offenses with schema name translation that exists and is not over 25 chars long', async () => { - const offenses = await check( - { - 'locales/en.default.schema.json': - '{ "default": { "my_translation_key": "My translation is good."}}', - 'code.liquid': ` - {% schema %} - { - "name": "t:default.my_translation_key" - } - {% endschema %}`, - }, - [ValidSchemaName], - ); - - expect(offenses).toHaveLength(0); - }); - - it('reports an offense with schema name translation that exists and is over 25 chars long', async () => { - const offenses = await check( - { - 'locales/en.default.schema.json': - '{ "my_translation_key": "My translation is tooooooooo long."}', - 'blocks/code.liquid': ` - {% schema %} - { - "name": "t:my_translation_key" - } - {% endschema %}`, - }, - [ValidSchemaName], - ); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "Schema name 'My translation is tooooooooo long.' from 'locales/en.default.schema.json' is too long (max 25 characters)", - ); - }); }); diff --git a/packages/theme-check-common/src/checks/valid-schema-translations/index.spec.ts b/packages/theme-check-common/src/checks/valid-schema-translations/index.spec.ts deleted file mode 100644 index bef08641c..000000000 --- a/packages/theme-check-common/src/checks/valid-schema-translations/index.spec.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { highlightedOffenses, check } from '../../test'; -import { ValidSchemaTranslations } from './index'; - -describe('Module: ValidSchemaTranslations', () => { - it('reports no offense when schema has no translation keys', async () => { - const offenses = await check( - { - 'locales/en.default.schema.json': '{}', - 'sections/file.liquid': ` - {% schema %} - { - "name": "My Section", - "settings": [ - { - "type": "text", - "id": "title", - "label": "Title" - } - ] - } - {% endschema %}`, - }, - [ValidSchemaTranslations], - ); - - expect(offenses).toHaveLength(0); - }); - - it('reports no offense when all translation keys exist', async () => { - const offenses = await check( - { - 'locales/en.default.schema.json': JSON.stringify({ - sections: { - header: { - name: 'Header', - settings: { - title: { - label: 'Title', - info: 'Enter a title', - }, - }, - }, - }, - }), - 'sections/file.liquid': ` - {% schema %} - { - "name": "t:sections.header.name", - "settings": [ - { - "type": "text", - "id": "title", - "label": "t:sections.header.settings.title.label", - "info": "t:sections.header.settings.title.info" - } - ] - } - {% endschema %}`, - }, - [ValidSchemaTranslations], - ); - - expect(offenses).toHaveLength(0); - }); - - it('reports an offense when a translation key is missing', async () => { - const themeFiles = { - 'locales/en.default.schema.json': JSON.stringify({ - sections: { - header: { - name: 'Header', - }, - }, - }), - 'sections/file.liquid': ` - {% schema %} - { - "name": "t:sections.header.missing_key" - } - {% endschema %}`, - }; - - const offenses = await check(themeFiles, [ValidSchemaTranslations]); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "'t:sections.header.missing_key' does not have a matching entry in 'locales/en.default.schema.json'", - ); - - const highlights = highlightedOffenses(themeFiles, offenses); - expect(highlights).toHaveLength(1); - expect(highlights[0]).toBe('"t:sections.header.missing_key"'); - }); - - it('reports multiple offenses when multiple translation keys are missing', async () => { - const offenses = await check( - { - 'locales/en.default.schema.json': JSON.stringify({ - sections: { - header: { - name: 'Header', - }, - }, - }), - 'sections/file.liquid': ` - {% schema %} - { - "name": "t:sections.header.name", - "settings": [ - { - "type": "text", - "id": "title", - "label": "t:sections.header.settings.title.label", - "info": "t:sections.header.settings.title.info" - } - ] - } - {% endschema %}`, - }, - [ValidSchemaTranslations], - ); - - expect(offenses).toHaveLength(2); - expect(offenses[0].message).toEqual( - "'t:sections.header.settings.title.label' does not have a matching entry in 'locales/en.default.schema.json'", - ); - expect(offenses[1].message).toEqual( - "'t:sections.header.settings.title.info' does not have a matching entry in 'locales/en.default.schema.json'", - ); - }); - - it('reports offense for missing translation in nested arrays', async () => { - const themeFiles = { - 'locales/en.default.schema.json': JSON.stringify({ - sections: { - header: { - name: 'Header', - }, - }, - }), - 'sections/file.liquid': ` - {% schema %} - { - "name": "t:sections.header.name", - "blocks": [ - { - "type": "text", - "name": "t:sections.header.blocks.text.name" - } - ] - } - {% endschema %}`, - }; - - const offenses = await check(themeFiles, [ValidSchemaTranslations]); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toEqual( - "'t:sections.header.blocks.text.name' does not have a matching entry in 'locales/en.default.schema.json'", - ); - }); - - it('reports no offense when schema is invalid JSON', async () => { - const offenses = await check( - { - 'locales/en.default.schema.json': '{}', - 'sections/file.liquid': ` - {% schema %} - { invalid json } - {% endschema %}`, - }, - [ValidSchemaTranslations], - ); - - expect(offenses).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-schema/index.spec.ts b/packages/theme-check-common/src/checks/valid-schema/index.spec.ts index 3984ceaba..2c695160b 100644 --- a/packages/theme-check-common/src/checks/valid-schema/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-schema/index.spec.ts @@ -159,16 +159,4 @@ describe('ValidSchema', () => { expect(offenses).to.have.length(0); }); - - it('should not complain when a schema is invalid outside of sections', async () => { - const sourceCode = ` - {% schema %} - ${JSON.stringify(INVALID_SECTION_SCHEMA, null, 2)} - {% endschema %} - `; - - const offenses = await runLiquidCheck(ValidSchema, sourceCode); - - expect(offenses).to.have.length(0); - }); }); diff --git a/packages/theme-check-common/src/checks/valid-scoped-css-class/index.spec.ts b/packages/theme-check-common/src/checks/valid-scoped-css-class/index.spec.ts index 58b033feb..ea7b7e872 100644 --- a/packages/theme-check-common/src/checks/valid-scoped-css-class/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-scoped-css-class/index.spec.ts @@ -1,723 +1,8 @@ import { expect, describe, it } from 'vitest'; import { ValidScopedCSSClass } from './index'; -import { check, runLiquidCheck } from '../../test'; +import { runLiquidCheck } from '../../test'; describe('Module: ValidScopedCSSClass', () => { - const noDeps = { - getReferences: async () => [] as any[], - getDependencies: async () => [] as any[], - }; - - describe('local stylesheet scope', () => { - it('reports no offense when a class is defined in the local stylesheet', async () => { - const sourceCode = ` - {% stylesheet %} - .my-class { color: red; } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - - it('does not report when a class is not defined anywhere in the theme', async () => { - const sourceCode = ` - {% stylesheet %} - .defined-class { color: red; } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - - it('reports an offense when a class exists in the theme but is out of scope', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .local-class { color: red; } - {% endstylesheet %} -
    Hello
    - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .other-class { color: blue; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(1); - expect(sectionOffenses[0].message).toBe( - "CSS class 'other-class' may be defined outside the scope of this file.", - ); - }); - - it('reports only out-of-scope theme classes, ignores unknown classes', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .a { color: red; } - .c { color: blue; } - {% endstylesheet %} -
    Hello
    - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .b { font-size: 14px; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(1); - expect(sectionOffenses[0].message).toBe( - "CSS class 'b' may be defined outside the scope of this file.", - ); - }); - - it('handles multiple stylesheet tags', async () => { - const sourceCode = ` - {% stylesheet %} - .from-first { color: red; } - {% endstylesheet %} - {% stylesheet %} - .from-second { color: blue; } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - - it('does not report when a class is in scope even if also defined out of scope', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .shared-class { color: red; } - {% endstylesheet %} -
    Hello
    - `, - 'snippets/unrelated.liquid': ` - {% stylesheet %} - .shared-class { color: blue; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(0); - }); - }); - - describe('dynamic class attributes', () => { - it('skips dynamic Liquid output in class attribute values', async () => { - const sourceCode = ` - {% stylesheet %} - .static { color: red; } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - - it('checks static parts and skips dynamic parts in mixed attributes', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .static { color: red; } - {% endstylesheet %} -
    Hello
    - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .out-of-scope { color: blue; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(1); - expect(sectionOffenses[0].message).toBe( - "CSS class 'out-of-scope' may be defined outside the scope of this file.", - ); - }); - - it('does not report partial tokens glued to Liquid nodes', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .static { color: red; } - {% endstylesheet %} -
    Hello
    - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .btn- { color: blue; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(0); - }); - }); - - describe('CSS class extraction edge cases', () => { - it('extracts classes from complex selectors', async () => { - const sourceCode = ` - {% stylesheet %} - .parent > .child { color: red; } - .a + .b ~ .c { color: blue; } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - - it('extracts classes from pseudo-class selectors', async () => { - const sourceCode = ` - {% stylesheet %} - .btn:hover { color: red; } - .link::before { content: ''; } - {% endstylesheet %} - - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - - it('extracts classes from media queries', async () => { - const sourceCode = ` - {% stylesheet %} - @media (min-width: 768px) { - .responsive { display: block; } - } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - - it('does not extract classes from CSS comments', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - /* .commented-out { color: red; } */ - .real { color: blue; } - {% endstylesheet %} -
    Hello
    - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .commented-out { color: green; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(1); - expect(sectionOffenses[0].message).toBe( - "CSS class 'commented-out' may be defined outside the scope of this file.", - ); - }); - }); - - describe('HTML element types', () => { - it('checks void elements', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .valid { color: red; } - {% endstylesheet %} - - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .invalid { color: blue; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(1); - expect(sectionOffenses[0].message).toBe( - "CSS class 'invalid' may be defined outside the scope of this file.", - ); - }); - - it('checks self-closing elements', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .valid { color: red; } - {% endstylesheet %} - - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .invalid { color: blue; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(1); - expect(sectionOffenses[0].message).toBe( - "CSS class 'invalid' may be defined outside the scope of this file.", - ); - }); - - it('checks SVG elements and their children', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .valid { color: red; } - {% endstylesheet %} - - - - - `, - 'snippets/other.liquid': ` - {% stylesheet %} - .out-of-scope { color: blue; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - noDeps, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(3); - expect(sectionOffenses.every((o) => o.message.includes('out-of-scope'))).toBe(true); - }); - }); - - describe('empty and whitespace class attributes', () => { - it('does not report for empty class attribute', async () => { - const sourceCode = ` - {% stylesheet %} - .a { color: red; } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - }); - - describe('ancestor stylesheet scope', () => { - it('considers classes from an ancestor file as in scope', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .parent-class { color: red; } - {% endstylesheet %} - {% render 'child' %} - `, - 'snippets/child.liquid': ` -
    Hello
    - `, - }, - [ValidScopedCSSClass], - { - getReferences: async (uri: string) => { - if (uri.endsWith('snippets/child.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/child.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - getDependencies: async (uri: string) => { - if (uri.endsWith('sections/section.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/child.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - }, - ); - - const snippetOffenses = offenses.filter((o) => o.uri.endsWith('snippets/child.liquid')); - expect(snippetOffenses).toHaveLength(0); - }); - - it('follows grandparent ancestors via direct references', async () => { - const offenses = await check( - { - 'layout/theme.liquid': ` - {% stylesheet %} - .layout-class { color: red; } - {% endstylesheet %} - {% sections 'main' %} - `, - 'sections/section.liquid': ` - {% render 'snippet' %} - `, - 'snippets/snippet.liquid': ` -
    Hello
    - `, - }, - [ValidScopedCSSClass], - { - getReferences: async (uri: string) => { - if (uri.endsWith('snippets/snippet.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/snippet.liquid' }, - type: 'direct' as const, - }, - ]; - } - if (uri.endsWith('sections/section.liquid')) { - return [ - { - source: { uri: 'file:///layout/theme.liquid' }, - target: { uri: 'file:///sections/section.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - getDependencies: async (uri: string) => { - if (uri.endsWith('layout/theme.liquid')) { - return [ - { - source: { uri: 'file:///layout/theme.liquid' }, - target: { uri: 'file:///sections/section.liquid' }, - type: 'direct' as const, - }, - ]; - } - if (uri.endsWith('sections/section.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/snippet.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - }, - ); - - const snippetOffenses = offenses.filter((o) => o.uri.endsWith('snippets/snippet.liquid')); - expect(snippetOffenses).toHaveLength(0); - }); - - it('does not consider indirect references as ancestors', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% stylesheet %} - .parent-class { color: red; } - {% endstylesheet %} - `, - 'snippets/child.liquid': ` - {% stylesheet %} - .local-class { color: blue; } - {% endstylesheet %} -
    Hello
    - `, - }, - [ValidScopedCSSClass], - { - getReferences: async (uri: string) => { - if (uri.endsWith('snippets/child.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/child.liquid' }, - type: 'indirect' as const, - }, - ]; - } - return []; - }, - getDependencies: async () => [], - }, - ); - - const snippetOffenses = offenses.filter((o) => o.uri.endsWith('snippets/child.liquid')); - expect(snippetOffenses).toHaveLength(1); - expect(snippetOffenses[0].message).toBe( - "CSS class 'parent-class' may be defined outside the scope of this file.", - ); - }); - }); - - describe('rendered snippet scope', () => { - it('considers classes from a rendered snippet as in scope', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% render 'styles' %} -
    Hello
    - `, - 'snippets/styles.liquid': ` - {% stylesheet %} - .snippet-class { color: red; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - { - getReferences: async () => [], - getDependencies: async (uri: string) => { - if (uri.endsWith('sections/section.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/styles.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - }, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(0); - }); - - it('considers classes from snippet descendants (recursive)', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% render 'outer' %} -
    Hello
    - `, - 'snippets/outer.liquid': ` - {% render 'inner' %} - `, - 'snippets/inner.liquid': ` - {% stylesheet %} - .deep-class { color: red; } - {% endstylesheet %} - `, - }, - [ValidScopedCSSClass], - { - getReferences: async () => [], - getDependencies: async (uri: string) => { - if (uri.endsWith('sections/section.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/outer.liquid' }, - type: 'direct' as const, - }, - ]; - } - if (uri.endsWith('snippets/outer.liquid')) { - return [ - { - source: { uri: 'file:///snippets/outer.liquid' }, - target: { uri: 'file:///snippets/inner.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - }, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(0); - }); - - it('considers classes from snippets rendered by ancestors', async () => { - const offenses = await check( - { - 'sections/section.liquid': ` - {% render 'styles' %} - {% render 'child' %} - `, - 'snippets/styles.liquid': ` - {% stylesheet %} - .style-class { color: red; } - {% endstylesheet %} - `, - 'snippets/child.liquid': ` -
    Hello
    - `, - }, - [ValidScopedCSSClass], - { - getReferences: async (uri: string) => { - if (uri.endsWith('snippets/child.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/child.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - getDependencies: async (uri: string) => { - if (uri.endsWith('sections/section.liquid')) { - return [ - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/styles.liquid' }, - type: 'direct' as const, - }, - { - source: { uri: 'file:///sections/section.liquid' }, - target: { uri: 'file:///snippets/child.liquid' }, - type: 'direct' as const, - }, - ]; - } - return []; - }, - }, - ); - - const childOffenses = offenses.filter((o) => o.uri.endsWith('snippets/child.liquid')); - expect(childOffenses).toHaveLength(0); - }); - }); - - describe('CSS asset file scope', () => { - it('considers classes from .css files in the assets folder', async () => { - const offenses = await check( - { - 'assets/theme.css': `.asset-class { color: red; }`, - 'sections/section.liquid': ` -
    Hello
    - `, - }, - [ValidScopedCSSClass], - { - getReferences: async () => [], - getDependencies: async () => [], - }, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(0); - }); - - it('considers classes from multiple .css asset files', async () => { - const offenses = await check( - { - 'assets/base.css': `.base-class { color: red; }`, - 'assets/components.css': `.component-class { color: blue; }`, - 'sections/section.liquid': ` -
    Hello
    - `, - }, - [ValidScopedCSSClass], - { - getReferences: async () => [], - getDependencies: async () => [], - }, - ); - - const sectionOffenses = offenses.filter((o) => o.uri.endsWith('sections/section.liquid')); - expect(sectionOffenses).toHaveLength(0); - }); - }); - describe('graceful degradation', () => { it('reports no offenses when getReferences is unavailable', async () => { const sourceCode = ` @@ -745,37 +30,4 @@ describe('Module: ValidScopedCSSClass', () => { expect(offenses).toHaveLength(0); }); }); - - describe('no classes defined anywhere', () => { - it('reports no offenses when no CSS classes are defined in scope', async () => { - const sourceCode = ` -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - }); - - describe('no class usage', () => { - it('reports no offenses when there are no class attributes', async () => { - const sourceCode = ` - {% stylesheet %} - .some-class { color: red; } - {% endstylesheet %} -
    Hello
    - `; - const offenses = await runLiquidCheck( - ValidScopedCSSClass, - sourceCode, - 'sections/section.liquid', - noDeps, - ); - expect(offenses).toHaveLength(0); - }); - }); }); diff --git a/packages/theme-check-common/src/checks/valid-settings-key/index.spec.ts b/packages/theme-check-common/src/checks/valid-settings-key/index.spec.ts index d40e35fbc..7ee3b991d 100644 --- a/packages/theme-check-common/src/checks/valid-settings-key/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-settings-key/index.spec.ts @@ -16,108 +16,6 @@ describe('Module: ValidSettingsKey', () => { ], }; - describe('default settings', () => { - it('does not report an error when default setting exists', async () => { - const theme = { - 'sections/example.liquid': toLiquidFile({ - ...schemaTemplate, - default: { - settings: { - 'existent-setting': 'value', - }, - }, - }), - }; - - const offenses = await check(theme, [ValidSettingsKey]); - expect(offenses).to.have.length(0); - }); - - it('reports an error when default setting does not exist', async () => { - const theme = { - 'sections/example.liquid': toLiquidFile({ - ...schemaTemplate, - default: { - settings: { - 'non-existent-setting': 'value', - }, - }, - }), - }; - - const offenses = await check(theme, [ValidSettingsKey]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `Setting 'non-existent-setting' does not exist in schema.`, - ); - }); - - it('does not report an error when missing default settings is referenced from a block schema', async () => { - const theme = { - 'blocks/example.liquid': toLiquidFile({ - ...schemaTemplate, - default: { - settings: { - 'non-existent-setting': 'value', - }, - }, - }), - }; - - const offenses = await check(theme, [ValidSettingsKey]); - expect(offenses).to.have.length(0); - }); - }); - - describe('presets settings', () => { - it('does not report an error when presets setting exists', async () => { - const theme = { - 'sections/example.liquid': toLiquidFile({ - ...schemaTemplate, - presets: [ - { - settings: { - 'existent-setting': 'value', - }, - }, - ], - }), - }; - - const offenses = await check(theme, [ValidSettingsKey]); - expect(offenses).to.have.length(0); - }); - - it('reports an error when presets setting does not exist', async () => { - const theme = { - 'sections/example.liquid': toLiquidFile({ - ...schemaTemplate, - presets: [ - { - settings: { - 'non-existent-setting-1': 'value', - }, - }, - { - settings: { - 'non-existent-setting-2': 'value', - }, - }, - ], - }), - }; - - const offenses = await check(theme, [ValidSettingsKey]); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal( - `Setting 'non-existent-setting-1' does not exist in schema.`, - ); - expect(offenses[1].message).to.equal( - `Setting 'non-existent-setting-2' does not exist in schema.`, - ); - }); - }); - const tests = [ { label: 'default', @@ -150,84 +48,6 @@ describe('Module: ValidSettingsKey', () => { 'blocks/referenced.liquid': toLiquidFile(schemaTemplate), }; - it(`does not report an error when ${label} block setting exists in referenced file`, async () => { - const fileToTest = { - 'sections/example.liquid': toLiquidFile({ - ...schemaTemplate, - ...blockTemplate([ - { - type: 'referenced', - settings: { - 'existent-setting': 'value', - }, - }, - ]), - }), - }; - - const offenses = await check( - { - ...referencedBlock, - ...fileToTest, - }, - [ValidSettingsKey], - ); - expect(offenses).to.have.length(0); - }); - - it('does not report an error when referenced file does not exist', async () => { - const fileToTest = { - 'sections/example.liquid': toLiquidFile({ - ...schemaTemplate, - ...blockTemplate([ - { - type: 'non-existent-file', - settings: { - 'non-existent-setting': 'value', - }, - }, - ]), - }), - }; - - const offenses = await check( - { - ...referencedBlock, - ...fileToTest, - }, - [ValidSettingsKey], - ); - expect(offenses).to.have.length(0); - }); - - it(`reports an error when ${label} block setting does not exist in referenced file`, async () => { - const fileToTest = { - 'sections/example.liquid': toLiquidFile({ - ...schemaTemplate, - ...blockTemplate([ - { - type: 'referenced', - settings: { - 'non-existent-setting': 'value', - }, - }, - ]), - }), - }; - - const offenses = await check( - { - ...referencedBlock, - ...fileToTest, - }, - [ValidSettingsKey], - ); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal( - `Setting 'non-existent-setting' does not exist in 'blocks/referenced.liquid'.`, - ); - }); - it(`reports an error when ${label} block setting does not exist in a referenced file that loads slowly`, async () => { const theme = { ...referencedBlock, @@ -250,88 +70,6 @@ describe('Module: ValidSettingsKey', () => { expect(offenses).to.have.length(1); }); }); - - describe('local blocks', () => { - const localBlocksTemplate = { - blocks: [ - { - type: 'local-block', - name: 'Local block', - settings: [ - { - id: 'local-setting', - type: 'text', - label: 'Local Setting', - }, - ], - }, - ], - }; - - it(`reports an error when ${label} block setting does not exist in existing local block`, async () => { - const fileToTest = { - 'sections/example.liquid': toLiquidFile({ - ...localBlocksTemplate, - ...blockTemplate([ - { - type: 'local-block', - settings: { - 'non-existent-setting-1': 'value', - 'non-existent-setting-2': 'value', - }, - }, - ]), - }), - }; - - const offenses = await check(fileToTest, [ValidSettingsKey]); - expect(offenses).to.have.length(2); - expect(offenses[0].message).to.equal( - `Setting 'non-existent-setting-1' does not exist in schema.`, - ); - expect(offenses[1].message).to.equal( - `Setting 'non-existent-setting-2' does not exist in schema.`, - ); - }); - - it(`does not report an error when ${label} block setting does not exist in non-existent local block`, async () => { - const fileToTest = { - 'sections/example.liquid': toLiquidFile({ - ...localBlocksTemplate, - ...blockTemplate([ - { - type: 'non-existent-local-block', - settings: { - 'non-existent-setting': 'value', - }, - }, - ]), - }), - }; - - const offenses = await check(fileToTest, [ValidSettingsKey]); - expect(offenses).to.have.length(0); - }); - - it(`does not report an error when ${label} block setting exists in local block`, async () => { - const fileToTest = { - 'sections/example.liquid': toLiquidFile({ - ...localBlocksTemplate, - ...blockTemplate([ - { - type: 'local-block', - settings: { - 'local-setting': 'value', - }, - }, - ]), - }), - }; - - const offenses = await check(fileToTest, [ValidSettingsKey]); - expect(offenses).to.have.length(0); - }); - }); }); }); }); diff --git a/packages/theme-check-common/src/checks/valid-standard-event-data/index.spec.ts b/packages/theme-check-common/src/checks/valid-standard-event-data/index.spec.ts deleted file mode 100644 index 29e87a99c..000000000 --- a/packages/theme-check-common/src/checks/valid-standard-event-data/index.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { highlightedOffenses, runLiquidCheck } from '../../test'; -import { ValidStandardEventData } from './index'; - -describe('Module: ValidStandardEventData', () => { - it('reports an offense when the context is not a supported value for products', async () => { - const sourceCode = `{{ product | standard_event_data: 'view', context: 'homepage' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported context 'homepage' for product. Valid values: page, search, collection, dialog, recommendation. The 'context' argument can also be omitted.", - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights[0]).to.eql("'homepage'"); - }); - - it('reports an offense when the context is not a supported value for carts', async () => { - const sourceCode = `{{ cart | standard_event_data: 'view', context: 'banner' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported context 'banner' for cart. Valid values: page, dialog. The 'context' argument can also be omitted.", - ); - }); - - it('reports an offense when the context is valid for products but the input is a cart', async () => { - const sourceCode = `{{ cart | standard_event_data: 'view', context: 'recommendation' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported context 'recommendation' for cart. Valid values: page, dialog. The 'context' argument can also be omitted.", - ); - }); - - it('does not report an offense on supported product context values', async () => { - const contexts = ['page', 'search', 'collection', 'dialog', 'recommendation']; - - for (const context of contexts) { - const sourceCode = `{{ product | standard_event_data: 'view', context: '${context}' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses, `expected '${context}' to be a valid product context`).toHaveLength(0); - } - }); - - it('does not report an offense on supported cart context values', async () => { - const contexts = ['page', 'dialog']; - - for (const context of contexts) { - const sourceCode = `{{ cart | standard_event_data: 'view', context: '${context}' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses, `expected '${context}' to be a valid cart context`).toHaveLength(0); - } - }); - - it('does not report an offense on collections, whose context is ignored', async () => { - const sourceCode = `{{ collection | standard_event_data: 'view', context: 'homepage' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('falls back to the union of contexts when the input is not a known global', async () => { - const sourceCode = `{{ line_item.product | standard_event_data: 'view', context: 'recommendation' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('falls back to the union of contexts when the filter input is chained', async () => { - const sourceCode = `{{ cart | default: other_cart | standard_event_data: 'view', context: 'recommendation' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('reports an offense when the context is outside the union for an unknown input', async () => { - const sourceCode = `{{ line_item.product | standard_event_data: 'view', context: 'homepage' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported context 'homepage'. Valid values: page, search, collection, dialog, recommendation. The 'context' argument can also be omitted.", - ); - }); - - it('does not report an offense when the context is a variable', async () => { - const sourceCode = `{{ product | standard_event_data: 'view', context: section.settings.context }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('reports an offense when the context is a non-string literal', async () => { - const sourceCode = `{{ product | standard_event_data: 'view', context: 123 }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported context for product. Valid values: page, search, collection, dialog, recommendation. The 'context' argument can also be omitted.", - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights[0]).to.eql('123'); - }); - - it('does not report an offense when the context argument is omitted', async () => { - const sourceCode = `{{ collection | standard_event_data: 'view' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('reports an offense when the event type is not supported', async () => { - const sourceCode = `{{ product | standard_event_data: 'click', context: 'page' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported event type 'click'. The only supported event type is 'view'.", - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights[0]).to.eql("'click'"); - }); - - it('reports an offense when the event type is not supported on a collection', async () => { - const sourceCode = `{{ collection | standard_event_data: 'click' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported event type 'click'. The only supported event type is 'view'.", - ); - }); - - it('does not report an offense when the event type is a variable', async () => { - const sourceCode = `{{ product | standard_event_data: event_type }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(0); - }); - - it('reports an offense when the event type is a non-string literal', async () => { - const sourceCode = `{{ product | standard_event_data: 123 }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported event type. The only supported event type is 'view'.", - ); - - const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses); - expect(highlights[0]).to.eql('123'); - }); - - it('reports an offense when the event type is a boolean literal', async () => { - const sourceCode = `{{ product | standard_event_data: true }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - "Unsupported event type. The only supported event type is 'view'.", - ); - }); - - it('reports both offenses when the event type and the context are invalid', async () => { - const sourceCode = `{{ product | standard_event_data: 'click', context: 'homepage' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(2); - }); - - it('does not report an offense on other filters', async () => { - const sourceCode = `{{ product | json }}{{ 'homepage' | append: 'view' }}`; - const offenses = await runLiquidCheck(ValidStandardEventData, sourceCode); - - expect(offenses).toHaveLength(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-visible-if/index.spec.ts b/packages/theme-check-common/src/checks/valid-visible-if/index.spec.ts deleted file mode 100644 index b529ee487..000000000 --- a/packages/theme-check-common/src/checks/valid-visible-if/index.spec.ts +++ /dev/null @@ -1,656 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { check } from '../../test'; -import { ValidVisibleIf, ValidVisibleIfSettingsSchema } from './index'; - -type ThemeSchema = { [key: string]: unknown; settings?: Record[] }; -type Theme = { [key: `${string}.json`]: ThemeSchema[]; [key: `${string}.liquid`]: ThemeSchema }; - -const toLiquid = (schema: unknown) => ` - {% schema %} - ${JSON.stringify(schema, undefined, 2)} - {% endschema %} -`; - -const makeTheme = (themeData: Record) => - Object.fromEntries( - Object.entries(themeData).map(([key, value]) => [ - key, - key.endsWith('.json') ? JSON.stringify(value, undefined, 2) : toLiquid(value), - ]), - ); - -const checkRule = (themeData: Theme) => - check(makeTheme(themeData), [ValidVisibleIf, ValidVisibleIfSettingsSchema]); - -const baseThemeData: Theme = { - 'sections/example.liquid': { - name: 'My Section', - settings: [{ id: 'some-section-setting' }], - }, - 'blocks/example.liquid': { - name: 'My Block', - settings: [{ id: 'some-block-setting' }], - }, - 'config/settings_schema.json': [ - { - name: 'theme_info', - theme_name: 'Test Theme', - theme_version: '0.1.0', - theme_author: 'Shopify', - }, - { - name: 't:some.namespace', - settings: [ - { type: 'header', content: 't:some.other.namespace' }, - { id: 'some-global-setting' }, - ], - }, - ], -}; - -describe('Module: ValidVisibleIf', () => { - it('reports no error for a valid reference to a block schema setting (simple lookup)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings.some-block-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports no error for a valid reference to a block schema setting (expression)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings.some-block-setting != "null" }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports no error for a valid reference to a global setting (simple lookup)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ settings.some-global-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports no error for a valid reference to a global setting (expression)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ "null" != settings.some-global-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports no error for a variable assigned in the Liquid file', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ effective_fill == "image" }}', - }); - - const theme = makeTheme(themeData); - theme['blocks/example.liquid'] = ` - {% liquid - assign effective_fill = 'image' - %} - ${theme['blocks/example.liquid']} - `; - - const offenses = await check(theme, [ValidVisibleIf, ValidVisibleIfSettingsSchema]); - expect(offenses).toEqual([]); - }); - - it('reports no error for a variable assigned after the schema tag', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ effective_fill == "image" }}', - }); - - const theme = makeTheme(themeData); - theme['blocks/example.liquid'] += ` - {% assign effective_fill = 'image' %} - `; - - const offenses = await check(theme, [ValidVisibleIf, ValidVisibleIfSettingsSchema]); - expect(offenses).toEqual([]); - }); - - it('reports no error for a valid reference to a section schema (simple lookup)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ section.settings.some-section-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports no error for a valid reference to a section schema (simple lookup) in a block schema', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - }); - - themeData['blocks/example.liquid'].settings!.push({ - visible_if: '{{ section.settings.some-section-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports no error for a valid reference to a section schema (expression)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ section.settings.some-section-setting != "null" }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports no error for a valid reference to a global setting (in theme settings)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['config/settings_schema.json'].at(-1)!.settings!.push({ - id: 'some-other-setting', - visible_if: '{{ settings.some-global-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toEqual([]); - }); - - it('reports an error for an invalid reference to a global variable (in block)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ some-nonexistent-whatever }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 49, - "index": 195, - "line": 10, - }, - "fix": undefined, - "message": "Invalid variable: "some-nonexistent-whatever" was not found.", - "severity": 0, - "start": { - "character": 24, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///blocks/example.liquid", - }, - ] - `); - }); - - it('reports an error for an invalid reference to a block schema setting (simple lookup)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings.some-nonexistent-block-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 69, - "index": 215, - "line": 10, - }, - "fix": undefined, - "message": "Invalid variable: "block.settings.some-nonexistent-block-setting" was not found.", - "severity": 0, - "start": { - "character": 24, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///blocks/example.liquid", - }, - ] - `); - }); - - it('reports an error for an invalid reference to a block schema setting (expression)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings.some-nonexistent-block-setting != "null" }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 69, - "index": 215, - "line": 10, - }, - "fix": undefined, - "message": "Invalid variable: "block.settings.some-nonexistent-block-setting" was not found.", - "severity": 0, - "start": { - "character": 24, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///blocks/example.liquid", - }, - ] - `); - }); - - it('reports an error for an invalid reference to a section schema (simple lookup)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ section.settings.some-nonexistent-section-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 73, - "index": 223, - "line": 10, - }, - "fix": undefined, - "message": "Invalid variable: "section.settings.some-nonexistent-section-setting" was not found.", - "severity": 0, - "start": { - "character": 24, - "index": 174, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///sections/example.liquid", - }, - ] - `); - }); - - it('reports an error for an invalid reference to a section schema (expression)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ section.settings.some-nonexistent-section-setting != "null" }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 73, - "index": 223, - "line": 10, - }, - "fix": undefined, - "message": "Invalid variable: "section.settings.some-nonexistent-section-setting" was not found.", - "severity": 0, - "start": { - "character": 24, - "index": 174, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///sections/example.liquid", - }, - ] - `); - }); - - it('reports an error for an invalid reference to a global setting (in theme settings)', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['config/settings_schema.json'].at(-1)!.settings!.push({ - id: 'some-other-setting', - visible_if: '{{ settings.some-non-existent-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 60, - "index": 427, - "line": 19, - }, - "fix": undefined, - "message": "Invalid variable: "settings.some-non-existent-setting" was not found.", - "severity": 0, - "start": { - "character": 26, - "index": 393, - "line": 19, - }, - "suggest": undefined, - "type": "JSON", - "uri": "file:///config/settings_schema.json", - }, - ] - `); - }); - - it('reports a detailed error when trying to use a block var in a section', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings.some-section-setting != "null" }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 59, - "index": 209, - "line": 10, - }, - "fix": undefined, - "message": "Invalid visible_if: can't refer to "block" when not in a block file.", - "severity": 0, - "start": { - "character": 24, - "index": 174, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///sections/example.liquid", - }, - ] - `); - }); - - it('reports a detailed error when trying to use a block var in theme settings', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['config/settings_schema.json'].at(-1)!.settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings.some-block-setting }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 59, - "index": 426, - "line": 19, - }, - "fix": undefined, - "message": "Invalid visible_if: can't refer to "block" when not in a block file.", - "severity": 0, - "start": { - "character": 26, - "index": 393, - "line": 19, - }, - "suggest": undefined, - "type": "JSON", - "uri": "file:///config/settings_schema.json", - }, - ] - `); - }); - - it('reports a detailed error when attempting to use a namespace as a var', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 38, - "index": 184, - "line": 10, - }, - "fix": undefined, - "message": "Invalid variable: "block.settings" refers to a namespace, but is being used here as a variable.", - "severity": 0, - "start": { - "character": 24, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///blocks/example.liquid", - }, - ] - `); - }); - - it('reports a detailed error when attempting to use a var as a namespace', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['blocks/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ block.settings.some-block-setting.another-thing }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 71, - "index": 217, - "line": 10, - }, - "fix": undefined, - "message": "Invalid variable: "block.settings.some-block-setting" refers to a variable, but is being used here as a namespace.", - "severity": 0, - "start": { - "character": 24, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///blocks/example.liquid", - }, - ] - `); - }); - - it('reports visible_if fields declared with incorrect or missing brackets', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{% section.settings.some-section-setting != "null" %}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 77, - "index": 227, - "line": 10, - }, - "fix": undefined, - "message": "Invalid visible_if expression. It should take the form "{{ }}".", - "severity": 0, - "start": { - "character": 20, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///sections/example.liquid", - }, - ] - `); - }); - - it('reports malformed visible_if fields', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ section.settings.some-section-setting !r=erf "null" }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 81, - "index": 231, - "line": 10, - }, - "fix": undefined, - "message": "Syntax error: cannot parse visible_if expression.", - "severity": 0, - "start": { - "character": 20, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///sections/example.liquid", - }, - ] - `); - }); - - it('does not reports when true or false is hardcoded', async () => { - for (const bool of ['true', 'false']) { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: `{{ ${bool} }}`, - }); - - const offenses = await checkRule(themeData); - expect(offenses).to.be.empty; - } - }); - - it('reports a warning for unparseable expressions', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: `{{ something $ unparseable }}`, - }); - - const offenses = await checkRule(themeData); - expect(offenses).to.be.lengthOf(1); - expect(offenses).to.containOffense('Syntax error: cannot parse visible_if expression.'); - }); - - it('reports when no variable lookup is found', async () => { - const themeData = structuredClone(baseThemeData); - - themeData['sections/example.liquid'].settings!.push({ - id: 'some-other-setting', - visible_if: '{{ "some random string" }}', - }); - - const offenses = await checkRule(themeData); - expect(offenses).toMatchInlineSnapshot(` - [ - { - "check": "ValidVisibleIf", - "end": { - "character": 50, - "index": 200, - "line": 10, - }, - "fix": undefined, - "message": "visible_if expression contains no references to any settings. This is likely an error.", - "severity": 0, - "start": { - "character": 20, - "index": 170, - "line": 10, - }, - "suggest": undefined, - "type": "LiquidHtml", - "uri": "file:///sections/example.liquid", - }, - ] - `); - }); -}); diff --git a/packages/theme-check-common/src/checks/variable-name/index.spec.ts b/packages/theme-check-common/src/checks/variable-name/index.spec.ts deleted file mode 100644 index 28efcabab..000000000 --- a/packages/theme-check-common/src/checks/variable-name/index.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect, describe, it } from 'vitest'; -import { VariableName } from './index'; -import { applySuggestions, runLiquidCheck } from '../../test'; - -describe('Module: VariableName', () => { - it('should report an error when a variable is using wrong naming format', async () => { - const sourceCode = `{% assign variableName = "value" %}`; - - const offenses = await runLiquidCheck(VariableName, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.equal("The variable 'variableName' uses wrong naming format"); - }); - - it('should not report an error when a variable is using correct format', async () => { - const sourceCode = `{% assign variable_name = "value" %}`; - - const offenses = await runLiquidCheck(VariableName, sourceCode); - - expect(offenses).to.be.empty; - }); - - it('should allow a single leading underscore', async () => { - const sourceCode = ` - {% assign _variable_name = "value" %} - {% capture _captured_value %}value{% endcapture %} - `; - - const offenses = await runLiquidCheck(VariableName, sourceCode); - - expect(offenses).to.be.empty; - }); - - it('should still enforce the naming format after a leading underscore', async () => { - const sourceCode = `{% assign _variableName = "value" %}`; - - const offenses = await runLiquidCheck(VariableName, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0]!.suggest![0].message).to.equal( - "Change variable '_variableName' to '_variable_name'", - ); - }); - - it('should reject more than one leading underscore', async () => { - const sourceCode = `{% assign __variable_name = "value" %}`; - - const offenses = await runLiquidCheck(VariableName, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0]!.suggest![0].message).to.equal( - "Change variable '__variable_name' to '_variable_name'", - ); - }); - - it('should provide a suggestion to change the variable naming', async () => { - const sourceCode = `{% assign variableName = "value" %}`; - - const offenses = await runLiquidCheck(VariableName, sourceCode); - - expect(offenses).to.have.length(1); - expect(offenses[0].suggest).to.have.length(1); - expect(offenses[0]!.suggest![0].message).to.equal( - "Change variable 'variableName' to 'variable_name'", - ); - }); - - it('should fix the code by changing the wrongly formatted variable', async () => { - const sourceCode = `{% assign variableName = "value" %}`; - - const offenses = await runLiquidCheck(VariableName, sourceCode); - const suggestions = applySuggestions(sourceCode, offenses[0]); - - const expectedFixedCode = `{% assign variable_name = "value" %}`; - - expect(suggestions).to.include(expectedFixedCode); - }); - - // It's impossible to make an idempotent rule that works for all cases. We - // have to accept whatever spacing the user has input as valid. - it('should not complain about numbers inside variable names', async () => { - const varNames = [ - `first_b_2_b_model`, - `first_b2_b_model`, - `first_b2b_model`, - `first_3_d_model`, - `first3_d_model`, - ]; - for (const varName of varNames) { - const sourceCode = `{% assign ${varName} = "value" %}`; - const offenses = await runLiquidCheck(VariableName, sourceCode); - expect(offenses).to.be.empty; - } - }); -}); diff --git a/packages/theme-check-common/src/disabled-checks/index.spec.ts b/packages/theme-check-common/src/disabled-checks/index.spec.ts deleted file mode 100644 index 7c7233dc2..000000000 --- a/packages/theme-check-common/src/disabled-checks/index.spec.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { check } from '../test'; -import { Offense } from '..'; -import { describe, it, expect } from 'vitest'; -import { LiquidFilter, RenderMarkup } from './test-checks'; -import { UndefinedObject } from '../checks/undefined-object'; -import { UniqueDocParamNames } from '../checks/unique-doc-param-names'; - -const commentTypes = [ - (text: string) => `{% # ${text} %}`, - (text: string) => `{% comment %}${text}{% endcomment %}`, -]; - -function expectLiquidFilterOffense(offenses: Offense[], file: string, assetName: string) { - const expectedStart = file.indexOf(`'${assetName}' | random_filter`) + `'${assetName}'`.length; - const expectedEnd = expectedStart + ' | random_filter'.length; - const foundOffenses = offenses.filter( - ({ message, start, end }) => - message === 'Liquid filter can not be used' && - start.index === expectedStart && - end.index === expectedEnd, - ); - expect(foundOffenses).to.have.length(1); -} - -function expectRenderMarkupOffense(offenses: Offense[], templateName: string) { - const foundOffenses = offenses.filter( - ({ message }) => message === `'${templateName}' can not be rendered`, - ); - expect(foundOffenses).to.have.length(1); -} - -describe('Module: DisabledChecks', () => { - const checks = [LiquidFilter, RenderMarkup]; - - describe(`Comment variant`, () => { - it('should disable checks for the entire document if comment is placed on the first line', async () => { - for (const buildComment of commentTypes) { - const file = `${buildComment('theme-check-disable LiquidFilter')} -{% comment %} - This is some comment about the file... - Adding a comment here should not mess with theme-check-disable -{% endcomment %} -{{ 'asset' | random_filter }} -{% render 'something' %}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(1); - expectRenderMarkupOffense(offenses, 'something.liquid'); - } - }); - - it('should disable all checks even if comment has additional spaces', async () => { - for (const buildComment of commentTypes) { - const file = `${buildComment(' theme-check-disable ')} -{{ 'asset' | random_filter }} -{% render 'something' %}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(0); - } - }); - - it('should disable all checks for a section of the template', async () => { - for (const buildComment of commentTypes) { - const file = `{{ 'asset-1' | random_filter }} -{% render 'something-1' %} -${buildComment('theme-check-disable')} -{{ 'asset-2' | random_filter }} -{% render 'something-2' %} -${buildComment('theme-check-enable')} -{{ 'asset-3' | random_filter }}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(3); - expectLiquidFilterOffense(offenses, file, 'asset-1'); - expectLiquidFilterOffense(offenses, file, 'asset-3'); - expectRenderMarkupOffense(offenses, 'something-1.liquid'); - } - }); - - it('should disable a specific check if check is included in the comment', async () => { - for (const buildComment of commentTypes) { - const file = `${buildComment('theme-check-disable LiquidFilter')} -{{ 'asset-1' | random_filter }} -{% render 'something' %} -${buildComment('theme-check-enable LiquidFilter')} -{{ 'asset-2' | random_filter }}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(2); - expectLiquidFilterOffense(offenses, file, 'asset-2'); - expectRenderMarkupOffense(offenses, 'something.liquid'); - } - }); - - it('should disable multiple checks if checks are separated by a comma (and maybe some spaces)', async () => { - for (const buildComment of commentTypes) { - const file = `${buildComment('theme-check-disable LiquidFilter, RenderMarkup')} -{{ 'asset-1' | random_filter }} -{% render 'something' %} -${buildComment('theme-check-enable LiquidFilter,RenderMarkup')} -{{ 'asset-2' | random_filter }}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(1); - expectLiquidFilterOffense(offenses, file, 'asset-2'); - } - }); - - it('should enable specific checks individually', async () => { - for (const buildComment of commentTypes) { - const file = `${buildComment('theme-check-disable LiquidFilter, RenderMarkup')} -{{ 'asset-1' | random_filter }} -{% render 'something-1' %} -${buildComment('theme-check-enable RenderMarkup')} -{{ 'asset-2' | random_filter }} -{% render 'something-2' %} -${buildComment('theme-check-enable LiquidFilter')} -{{ 'asset-3' | random_filter }} -{% render 'something-3' %}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(3); - expectLiquidFilterOffense(offenses, file, 'asset-3'); - expectRenderMarkupOffense(offenses, 'something-2.liquid'); - expectRenderMarkupOffense(offenses, 'something-3.liquid'); - } - }); - - describe('Mix of general and specific commands', () => { - it('should not reenable specific check when all checks have been disabled before', async () => { - for (const buildComment of commentTypes) { - const file = `${buildComment('theme-check-disable')} -{{ 'asset-1' | random_filter }} -{% render 'something-1' %} -${buildComment('theme-check-enable RenderMarkup')} -{{ 'asset-2' | random_filter }} -{% render 'something-2' %}`; - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(0); - } - }); - - it('should reenable all checks when specific ones have been disabled before', async () => { - for (const buildComment of commentTypes) { - const file = `${buildComment('theme-check-disable LiquidFilter, RenderMarkup')} -{{ 'asset-3' | random_filter }} -{% render 'something-3' %} -${buildComment('theme-check-enable')} -{{ 'asset-4' | random_filter }} -{% render 'something-4' %}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(2); - expectLiquidFilterOffense(offenses, file, 'asset-4'); - expectRenderMarkupOffense(offenses, 'something-4.liquid'); - } - }); - }); - - describe('disable next line', () => { - it('should disable the next line if there is one', async () => { - const file = `{% # theme-check-disable-next-line %} -{% render 'something' %} -{% render 'other-thing' %}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(1); - expectRenderMarkupOffense(offenses, 'other-thing.liquid'); - }); - - it("should not disable the liquid tag's children node if theme check is disabled", async () => { - const file = `{% # theme-check-disable-next-line %} -{% if condition %} - {% render 'something' %} -{% endif %} -{% render 'other-thing' %}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(2); - expectRenderMarkupOffense(offenses, 'something.liquid'); - expectRenderMarkupOffense(offenses, 'other-thing.liquid'); - }); - - it('should disable the next line inside a liquid tag if there is one', async () => { - const file = `{% liquid - # theme-check-disable-next-line - render 'something' - render 'other-thing' -%}`; - - const offenses = await check({ 'code.liquid': file }, checks); - expect(offenses).to.have.length(1); - expectRenderMarkupOffense(offenses, 'other-thing.liquid'); - }); - - it("should disable the parent node's next node if theme check is disabled as the last child node", async () => { - const file = `{% liquid - if condition - # theme-check-disable-next-line - elsif other_condition - endif -%}`; - - const offenses = await check({ 'code.liquid': file }, [UndefinedObject]); - expect(offenses).to.have.length(1); - expect(offenses).toContainEqual( - expect.objectContaining({ - message: "Unknown object 'condition' used.", - }), - ); - }); - - it('should not disable any checks if theme-check is disabled at the end', async () => { - const file = `{% liquid - echo hello - echo everyone - # theme-check-disable-next-line -%}`; - - const offenses = await check({ 'code.liquid': file }, [UndefinedObject]); - expect(offenses).to.have.length(2); - expect(offenses).toContainEqual( - expect.objectContaining({ - message: "Unknown object 'hello' used.", - }), - ); - expect(offenses).toContainEqual( - expect.objectContaining({ - message: "Unknown object 'everyone' used.", - }), - ); - }); - - it('should disable the next line if the content is an HTML tag with liquid', async () => { - const file = `{% # theme-check-disable-next-line %} -
    -
    `; - - const offenses = await check({ 'code.liquid': file }, [UndefinedObject]); - expect(offenses).to.have.length(1); - expect(offenses).toContainEqual( - expect.objectContaining({ - message: "Unknown object 'bar' used.", - }), - ); - }); - - it('should not disable the next line if the specified rule does not exist', async () => { - const file = `{% # theme-check-disable-next-line FAKE_RULE %} -
    `; - - const offenses = await check({ 'code.liquid': file }, [UndefinedObject]); - expect(offenses).to.have.length(1); - expect(offenses).toContainEqual( - expect.objectContaining({ - message: "Unknown object 'foo' used.", - }), - ); - }); - - it('should disable checks inside doc tags when disable-next-line is placed before the doc tag', async () => { - const file = `{% # theme-check-disable-next-line UniqueDocParamNames %} -{% doc %} - @param foo - first param - @param foo - duplicate param -{% enddoc %}`; - - const offenses = await check({ 'code.liquid': file }, [UniqueDocParamNames]); - expect(offenses).to.have.length(0); - }); - - it('should disable all checks inside doc tags when disable-next-line is used without specific check names', async () => { - const file = `{% # theme-check-disable-next-line %} -{% doc %} - @param bar - first param - @param bar - duplicate param -{% enddoc %}`; - - const offenses = await check({ 'code.liquid': file }, [UniqueDocParamNames]); - expect(offenses).to.have.length(0); - }); - - it('should still report offenses in doc tags when disable-next-line is not present', async () => { - const file = `{% doc %} - @param baz - first param - @param baz - duplicate param -{% enddoc %}`; - - const offenses = await check({ 'code.liquid': file }, [UniqueDocParamNames]); - expect(offenses).to.have.length(1); - expect(offenses).toContainEqual( - expect.objectContaining({ - message: "The parameter 'baz' is defined more than once.", - }), - ); - }); - }); - }); -}); diff --git a/packages/theme-check-common/src/disabled-checks/test-checks.ts b/packages/theme-check-common/src/disabled-checks/test-checks.ts deleted file mode 100644 index 7c3d65ecb..000000000 --- a/packages/theme-check-common/src/disabled-checks/test-checks.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { LiquidCheckDefinition, Severity, SourceCodeType } from '..'; -import { NodeTypes } from '@shopify/liquid-html-parser'; - -export const LiquidFilter: LiquidCheckDefinition = { - meta: { - code: 'LiquidFilter', - name: 'Complains about every LiquidFilter', - docs: { - description: 'Complains about every LiquidFilter', - recommended: true, - }, - type: SourceCodeType.LiquidHtml, - severity: Severity.ERROR, - schema: {}, - targets: [], - }, - - create(context) { - return { - LiquidFilter: async (node) => { - context.report({ - message: 'Liquid filter can not be used', - startIndex: node.position.start, - endIndex: node.position.end, - }); - }, - }; - }, -}; - -export const RenderMarkup: LiquidCheckDefinition = { - meta: { - code: 'RenderMarkup', - name: 'Complains about every RenderMarkup', - docs: { - description: 'Complains about every RenderMarkup', - recommended: true, - }, - type: SourceCodeType.LiquidHtml, - severity: Severity.ERROR, - schema: {}, - targets: [], - }, - - create(context) { - return { - async RenderMarkup(node) { - if ( - node.snippet.type === NodeTypes.VariableLookup || - node.snippet.type === NodeTypes.Range - ) { - return; - } - - context.report({ - message: `'${node.snippet.value}.liquid' can not be rendered`, - startIndex: node.snippet.position.start, - endIndex: node.snippet.position.end, - }); - }, - }; - }, -}; diff --git a/packages/theme-check-common/src/index.ts b/packages/theme-check-common/src/index.ts index 72853b9b1..e25f18c1b 100644 --- a/packages/theme-check-common/src/index.ts +++ b/packages/theme-check-common/src/index.ts @@ -37,6 +37,7 @@ import { ValidateJSON, } from './types'; import { getPosition } from './utils'; +import { getFileSize } from './utils/file-utils'; import { visitJSON, visitLiquid } from './visitors'; export * from './AbstractFileSystem'; @@ -92,6 +93,7 @@ export async function check( mode: config.context, fileExists: makeFileExists(fs), fileSize: makeFileSize(fs), + remoteFileSize: injectedDependencies.remoteFileSize ?? getFileSize, getDefaultLocale: makeGetDefaultLocale(fs, rootUri), getDefaultTranslations: makeGetDefaultTranslations(fs, theme, rootUri), getDefaultSchemaLocale: makeGetDefaultSchemaLocale(fs, rootUri), diff --git a/packages/theme-check-common/src/liquid-doc/enum-arguments.spec.ts b/packages/theme-check-common/src/liquid-doc/enum-arguments.spec.ts index fac1ff053..31672e1a6 100644 --- a/packages/theme-check-common/src/liquid-doc/enum-arguments.spec.ts +++ b/packages/theme-check-common/src/liquid-doc/enum-arguments.spec.ts @@ -6,7 +6,6 @@ import { ValidBlockArgumentTypes } from '../checks/valid-block-argument-types'; import { MissingRenderSnippetArguments } from '../checks/missing-render-snippet-arguments'; import { MissingContentForArguments } from '../checks/missing-content-for-arguments'; import { MissingBlockArguments } from '../checks/missing-block-arguments'; -import { ValidDocParamTypes } from '../checks/valid-doc-param-types'; import { applySuggestions, runLiquidCheck } from '../test'; const enumType = `'heading' | "small"`; @@ -54,66 +53,6 @@ const aliasCallers = ['with', 'for'].map((keyword) => ({ describe('LiquidDoc enum arguments', () => { for (const caller of [...namedCallers, ...aliasCallers]) { describe(caller.name, () => { - it.each(["'heading'", '"heading"', "'small'", '"small"'])( - 'accepts enum member %s', - async (value) => { - const offenses = await runLiquidCheck( - caller.check, - caller.source(value), - 'templates/index.liquid', - {}, - { [caller.file]: definition() }, - ); - expect(offenses).toHaveLength(0); - }, - ); - - it.each(["'Heading'", "'body'", '123', 'false', 'nil', 'null', 'empty', 'blank', '(1..3)'])( - 'rejects literal %s and highlights only the value', - async (value) => { - const source = caller.source(value); - const offenses = await runLiquidCheck( - caller.check, - source, - 'templates/index.liquid', - {}, - { [caller.file]: definition() }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain(enumType); - expect(offenses[0].message).toContain(`got ${value}`); - expect(source.slice(offenses[0].start.index, offenses[0].end.index)).toBe(value); - }, - ); - - it.each(['style', 'block.settings.style', "settings['style']"])( - 'leaves dynamic value %s unverified', - async (value) => { - const offenses = await runLiquidCheck( - caller.check, - caller.source(value), - 'templates/index.liquid', - {}, - { [caller.file]: definition() }, - ); - expect(offenses).toHaveLength(0); - }, - ); - - it.each(["'heading' |", "'heading' | number"])( - 'does not cascade errors from unsupported declaration %s', - async (type) => { - const offenses = await runLiquidCheck( - caller.check, - caller.source("'body'"), - 'templates/index.liquid', - {}, - { [caller.file]: definition(type) }, - ); - expect(offenses).toHaveLength(0); - }, - ); - it('offers each allowed member as a replacement and preserves its quoting', async () => { const source = caller.source("'body'"); const offenses = await runLiquidCheck( @@ -149,151 +88,7 @@ describe('LiquidDoc enum arguments', () => { }); } - for (const caller of namedCallers) { - it(`${caller.name} permits omission of an optional enum`, async () => { - const offenses = await runLiquidCheck( - caller.missingCheck, - caller.source(), - 'templates/index.liquid', - {}, - { [caller.file]: definition() }, - ); - expect(offenses).toHaveLength(0); - }); - - it(`${caller.name} reports omission of a required enum`, async () => { - const offenses = await runLiquidCheck( - caller.missingCheck, - caller.source(), - 'templates/index.liquid', - {}, - { [caller.file]: definition(enumType, false) }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("Missing required argument 'variant'"); - }); - } - - for (const caller of aliasCallers) { - it.each(['product', 'product[]', 'string[]'])( - `${caller.name} preserves checking of literal values against named declaration %s`, - async (type) => { - const offenses = await runLiquidCheck( - caller.check, - caller.source('123'), - 'templates/index.liquid', - {}, - { [caller.file]: definition(type) }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toBe( - `Type mismatch for argument 'variant': expected ${type}, got number`, - ); - }, - ); - - it('leaves dynamic alias values unverified for named declarations', async () => { - const offenses = await runLiquidCheck( - caller.check, - caller.source('product'), - 'templates/index.liquid', - {}, - { [caller.file]: definition('product') }, - ); - expect(offenses).toHaveLength(0); - }); - - it.each(['Product', 'Product[]'])( - `${caller.name} leaves uppercase declaration %s to ValidDocParamTypes`, - async (type) => { - const offenses = await runLiquidCheck( - caller.check, - caller.source('123'), - 'templates/index.liquid', - {}, - { [caller.file]: definition(type) }, - ); - expect(offenses).toHaveLength(0); - - const declarationOffenses = await runLiquidCheck( - ValidDocParamTypes, - definition(type), - caller.file, - ); - expect(declarationOffenses.map(({ message }) => message)).toEqual([ - `The parameter type '${type}' is not supported.`, - ]); - }, - ); - - it.each(['unknown', 'unknown[]'])( - `${caller.name} leaves named declaration %s to ValidDocParamTypes when the docset lacks it`, - async (type) => { - const offenses = await runLiquidCheck( - caller.check, - caller.source('123'), - 'templates/index.liquid', - {}, - { [caller.file]: definition(type) }, - ); - expect(offenses).toHaveLength(0); - }, - ); - - describe(`${caller.name} without a docset`, () => { - const runWithoutDocset = (value: string, type: string) => - runLiquidCheck( - caller.check, - caller.source(value), - 'templates/index.liquid', - { themeDocset: undefined }, - { [caller.file]: definition(type) }, - ); - - it.each(['product', 'Product', 'product[]', 'string[]', 'unknown'])( - 'checks literal values against named declaration %s', - async (type) => { - const offenses = await runWithoutDocset('123', type); - expect(offenses.map(({ message }) => message)).toEqual([ - `Type mismatch for argument 'variant': expected ${type.toLowerCase()}, got number`, - ]); - }, - ); - - it('checks literal values against enum declarations', async () => { - const offenses = await runWithoutDocset("'body'", enumType); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain(enumType); - }); - - it.each(["'heading' |", "'heading' | number", 'product['])( - 'does not cascade errors from unsupported declaration %s', - async (type) => { - expect(await runWithoutDocset("'body'", type)).toHaveLength(0); - }, - ); - - it('leaves dynamic values unverified', async () => { - expect(await runWithoutDocset('product', 'product')).toHaveLength(0); - }); - }); - } - for (const caller of namedCallers.filter((caller) => caller.name !== 'block')) { - it.each(['product', 'product[]', 'string[]', 'unknown'])( - `${caller.name} preserves existing skips for named declaration %s`, - async (type) => { - const offenses = await runLiquidCheck( - caller.check, - caller.source("'heading'"), - 'templates/index.liquid', - {}, - { [caller.file]: definition(type) }, - ); - expect(offenses).toHaveLength(0); - }, - ); - it.each([`"Heading" | 'small'`, `'' | 'heading'`, `"a'b" | 'small'`])( `${caller.name} inserts a legal enum member for a missing required argument (%s)`, async (type) => { @@ -327,20 +122,4 @@ describe('LiquidDoc enum arguments', () => { }, ); } - - it('rejects a block array literal for a string enum without trying to infer its primitive type', async () => { - const source = `{% block 'text', variant: ['heading', 'small'] %}Text{% endblock %}`; - const offenses = await runLiquidCheck( - ValidBlockArgumentTypes, - source, - 'templates/index.liquid', - {}, - { 'blocks/text.liquid': definition() }, - ); - expect(offenses).toHaveLength(1); - expect(offenses[0].message).toContain("got ['heading', 'small']"); - expect(source.slice(offenses[0].start.index, offenses[0].end.index)).toBe( - "['heading', 'small']", - ); - }); }); diff --git a/packages/theme-check-common/src/specs.spec.ts b/packages/theme-check-common/src/specs.spec.ts new file mode 100644 index 000000000..c819c078e --- /dev/null +++ b/packages/theme-check-common/src/specs.spec.ts @@ -0,0 +1,246 @@ +import { readdirSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parse } from 'yaml'; +import { + allChecks, + CheckDefinition, + ChecksSettings, + Mode, + Modes, + Offense, + Reference, + Severity, + ThemeDocset, +} from './index'; +import * as path from './path'; +import { applyFix, applySuggestions, autofix, check } from './test'; +import { MockTheme } from './test/MockTheme'; + +/** + * Runs the language-neutral check specs in specs/checks at the repo root. + * The format is described in specs/checks/README.md. + */ + +/** Without `check`, a spec tests a feature, and each case lists its `checks`. */ +interface Spec { + check?: string; + severity?: 'error' | 'warning' | 'info'; + cases: Case[]; +} + +interface Case { + name: string; + checks?: string[]; + files: Record; + docset?: string | Docset; + config?: ChecksSettings; + mode?: Mode; + references?: { source: string; target: string; type?: Reference['type'] }[]; + offenses?: SpecOffense[]; + autofix?: Record; +} + +interface SpecOffense { + check?: string; + file: string; + message: string; + start: [line: number, character: number]; + highlight: Text; + fix?: string; + suggest?: { message: string; result: string }[]; +} + +interface Docset { + filters?: any[]; + objects?: any[]; + tags?: any[]; + systemTranslations?: any; +} + +/** A file's content: a string, or parts to join, for files too large to write out. */ +type Text = string | (string | { repeat: string; times: number })[]; + +const specsDir = resolve(__dirname, '../../../specs/checks'); +const rootUri = path.normalize('file:/'); +const specced = new Set(); + +for (const specFile of readdirSync(specsDir).filter((f) => f.endsWith('.yml'))) { + const spec: Spec = parse(readFileSync(resolve(specsDir, specFile), 'utf8')); + const checkDefs = allChecks.filter((def) => def.meta.code === spec.check); + if (spec.check) specced.add(spec.check); + + describe(`specs/${specFile}`, () => { + if (spec.check) { + it(`has ${spec.check}'s severity`, () => { + expect(checkDefs).not.toHaveLength(0); + for (const def of checkDefs) { + expect(Severity[def.meta.severity].toLowerCase()).toBe(spec.severity); + } + }); + } + + for (const c of spec.cases) { + it(c.name, async () => { + // A file at a URL is a remote file: the check only ever sees its size. + const isRemote = (file: string) => /^https?:\/\//.test(file); + const entries = Object.entries(c.files).map(([file, text]) => [file, expand(text)]); + const files = Object.fromEntries(entries.filter(([file]) => !isRemote(file))); + const remoteSizes = new Map( + entries + .filter(([file]) => isRemote(file)) + .map(([url, text]) => [url, Buffer.byteLength(text)]), + ); + const defs = c.checks + ? allChecks.filter((def) => c.checks!.includes(def.meta.code)) + : checkDefs; + expect(defs, 'checks to run').not.toHaveLength(0); + for (const code of c.checks ?? []) { + expect( + defs.map((def) => def.meta.code), + 'checks', + ).toContain(code); + } + // An option the check doesn't have would be silently ignored. + for (const [code, options] of Object.entries(c.config ?? {})) { + const def = defs.find((def) => def.meta.code === code); + for (const key of Object.keys(options ?? {})) { + expect(Object.keys(def?.meta.schema ?? {}), `${code} options`).toContain(key); + } + } + // Which files render which, as production gets it from the theme graph. + const references: Reference[] = (c.references ?? []).map(({ source, target, type }) => { + expect(Object.keys(files), 'reference ends').toContain(source); + expect(Object.keys(files), 'reference ends').toContain(target); + expect(['direct', 'indirect', 'preset'], 'reference type').toContain(type ?? 'direct'); + return { + source: { uri: path.join(rootUri, source) }, + target: { uri: path.join(rootUri, target) }, + type: type ?? 'direct', + }; + }); + const deps = { + themeDocset: toDocset(c.docset), + getReferences: async (uri: string) => references.filter((r) => r.target.uri === uri), + getDependencies: async (uri: string) => references.filter((r) => r.source.uri === uri), + remoteFileSize: async (url: string) => remoteSizes.get(url) ?? 0, + }; + expect(Modes).toContain(c.mode ?? 'theme'); + const offenses = await check(files, defs, deps, c.config, c.mode); + const expected = c.offenses ?? []; + const actual = offenses.map((offense) => toSpecOffense(offense, files, defs, !!c.checks)); + // A highlight written as parts is compared as written, so a failure doesn't print it whole. + for (const offense of actual) { + const parts = expected.find( + (e) => typeof e.highlight !== 'string' && expand(e.highlight) === offense.highlight, + ); + if (parts) offense.highlight = parts.highlight; + else if (offense.highlight.length > 1000) { + offense.highlight = `${offense.highlight.slice(0, 100)}… (${offense.highlight.length} characters)`; + } + } + expect(actual.sort(byPosition)).toEqual(expected.sort(byPosition)); + // All the fixes, applied at once. The case states each file with several. + const fixedFiles = actual.filter((o) => o.fix !== undefined).map((o) => o.file); + const multiFix = [ + ...new Set(fixedFiles.filter((file, i) => fixedFiles.indexOf(file) !== i)), + ]; + const fixed = fixedFiles.length ? await autofix(files, offenses) : {}; + expect(Object.fromEntries(multiFix.map((file) => [file, fixed[file]])), 'autofix').toEqual( + c.autofix ?? {}, + ); + }); + } + }); +} + +it('has a spec for every check', () => { + expect(allChecks.map((def) => def.meta.code).filter((code) => !specced.has(code))).toEqual([]); +}); + +function expand(text: Text): string { + if (typeof text === 'string') return text; + return text + .map((part) => { + if (typeof part === 'string') return part; + if (typeof part.repeat !== 'string' || !Number.isInteger(part.times)) { + throw new Error(`A part is a string or { repeat, times }: ${JSON.stringify(part)}`); + } + return part.repeat.repeat(part.times); + }) + .join(''); +} + +function toDocset(docset: Case['docset']): ThemeDocset | undefined { + if (!docset) return undefined; + const data: Docset = + typeof docset === 'string' + ? parse(readFileSync(resolve(specsDir, 'docsets', `${docset}.yml`), 'utf8')) + : docset; + return { + filters: async () => data.filters ?? [], + objects: async () => data.objects ?? [], + liquidDrops: async () => data.objects ?? [], + tags: async () => data.tags ?? [], + systemTranslations: async () => data.systemTranslations ?? {}, + }; +} + +function toSpecOffense( + offense: Offense, + files: MockTheme, + defs: CheckDefinition[], + withCheck: boolean, +): SpecOffense { + const file = path.relative(offense.uri, rootUri); + const text = source(files, offense.uri); + // What a case doesn't state must be right too: the check's severity, and the positions. + const def = defs.find((def) => def.meta.code === offense.check); + expect(def, `offense of ${offense.check}`).toBeDefined(); + expect(offense.severity, 'offense severity').toBe(def!.meta.severity); + for (const [name, { index, line, character }] of Object.entries({ + start: offense.start, + end: offense.end, + })) { + expect(index, `offense ${name}`).toBeLessThanOrEqual(text.length); + expect([line, character], `offense ${name} line and character`).toEqual(position(text, index)); + } + expect(offense.end.index, 'offense end').toBeGreaterThanOrEqual(offense.start.index); + const result: SpecOffense = { + ...(withCheck && { check: offense.check }), + file, + message: offense.message, + start: [offense.start.line, offense.start.character], + highlight: text.slice(offense.start.index, offense.end.index), + }; + if (offense.fix) result.fix = applyFix(files, offense); + if (offense.suggest) { + const results = applySuggestions(files, offense)!; + result.suggest = offense.suggest.map(({ message }, i) => ({ message, result: results[i] })); + } + return result; +} + +/** The [line, character] of an index, both in UTF-16 code units. */ +function position(source: string, index: number): [line: number, character: number] { + const before = source.slice(0, index); + return [before.split('\n').length - 1, index - (before.lastIndexOf('\n') + 1)]; +} + +/** A file's source by URI: a case may spell a path another way, like `snippets\card.liquid`. */ +function source(files: MockTheme, uri: string): string { + const entry = Object.entries(files).find(([file]) => path.join(rootUri, file) === uri); + if (!entry) throw new Error(`An offense in a file that isn't in the case: ${uri}`); + return entry[1]; +} + +function byPosition(a: SpecOffense, b: SpecOffense) { + return ( + a.file.localeCompare(b.file) || + a.start[0] - b.start[0] || + a.start[1] - b.start[1] || + a.message.localeCompare(b.message) || + (a.check ?? '').localeCompare(b.check ?? '') || + JSON.stringify(a.highlight).localeCompare(JSON.stringify(b.highlight)) + ); +} diff --git a/packages/theme-check-common/src/test/block-fixtures.ts b/packages/theme-check-common/src/test/block-fixtures.ts deleted file mode 100644 index 7d398aa66..000000000 --- a/packages/theme-check-common/src/test/block-fixtures.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CheckDefinition, SourceCodeType } from '../types'; -import { runLiquidCheck } from './test-helper'; - -export const INVALID_SCHEMA_BLOCK = ['{% schema %}', '{ invalid', '{% endschema %}'].join('\n'); - -/** Source for a block with the given schema settings and optional LiquidDoc `@param` lines. */ -export function blockSource(settings: Record[], params: string[] = []): string { - return [ - ...(params.length > 0 ? [liquidDocBlock(params)] : []), - '{% schema %}', - JSON.stringify({ settings }), - '{% endschema %}', - ].join('\n'); -} - -/** Source for a schema-less block with only the given LiquidDoc `@param` lines. */ -export function liquidDocBlock(params: string[]): string { - return ['{% doc %}', ...params.map((param) => ` ${param}`), '{% enddoc %}'].join('\n'); -} - -/** - * Runs a check on `templates/test.liquid` with `block` as `blocks/card.liquid`. - * Pass `undefined` to leave the target block missing. - */ -export function runBlockCallCheck( - check: CheckDefinition, - template: string, - block: string | undefined, -) { - return runLiquidCheck( - check, - template, - 'templates/test.liquid', - {}, - block === undefined ? undefined : { 'blocks/card.liquid': block }, - ); -} diff --git a/packages/theme-check-common/src/types.ts b/packages/theme-check-common/src/types.ts index 1c9a10eac..e7a75989c 100644 --- a/packages/theme-check-common/src/types.ts +++ b/packages/theme-check-common/src/types.ts @@ -407,6 +407,12 @@ export interface Dependencies { * Returns an empty array if no dependencies found */ getDependencies?: (uri: string) => Promise; + + /** + * Get the size in bytes of a remote file, such as a script on a CDN. + * Returns 0 when it can't be found. Defaults to the Content-Length of a HEAD request. + */ + remoteFileSize?: (url: string) => Promise; } export type ValidateJSON = ( @@ -420,6 +426,7 @@ export interface AugmentedDependencies extends Dependencies { getBlockParameters: GetBlockParameters; fileExists: (uri: UriString) => Promise; fileSize: (uri: UriString) => Promise; + remoteFileSize: (url: string) => Promise; getDefaultLocale: () => Promise; getDefaultSchemaLocale: () => Promise; getDefaultTranslations(): Promise; diff --git a/packages/theme-check-common/src/utils/file-utils.ts b/packages/theme-check-common/src/utils/file-utils.ts index 6f10b12fd..0b5e1234f 100644 --- a/packages/theme-check-common/src/utils/file-utils.ts +++ b/packages/theme-check-common/src/utils/file-utils.ts @@ -36,8 +36,11 @@ export async function getFileSize(url: string): Promise { } } -export async function hasRemoteAssetSizeExceededThreshold(url: string, thresholdInBytes: number) { - const fileSize = await getFileSize(url); +export async function hasRemoteAssetSizeExceededThreshold< + T extends SourceCodeType, + S extends Schema, +>(context: Context, url: string, thresholdInBytes: number) { + const fileSize = await context.remoteFileSize(url); return fileSize > thresholdInBytes; } diff --git a/packages/theme-graph/fixtures/theme-app-extension/assets/.gitkeep b/packages/theme-graph/fixtures/theme-app-extension/assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/theme-graph/fixtures/theme-app-extension/blocks/app-block.liquid b/packages/theme-graph/fixtures/theme-app-extension/blocks/app-block.liquid deleted file mode 100644 index 1f8ad8e4e..000000000 --- a/packages/theme-graph/fixtures/theme-app-extension/blocks/app-block.liquid +++ /dev/null @@ -1,9 +0,0 @@ -{% render 'shared' %} - -{% schema %} -{ - "name": "App block", - "target": "section", - "settings": [] -} -{% endschema %} diff --git a/packages/theme-graph/fixtures/theme-app-extension/shopify.extension.toml b/packages/theme-graph/fixtures/theme-app-extension/shopify.extension.toml deleted file mode 100644 index b3639ea12..000000000 --- a/packages/theme-graph/fixtures/theme-app-extension/shopify.extension.toml +++ /dev/null @@ -1,2 +0,0 @@ -name = "Example theme app extension" -type = "theme" diff --git a/packages/theme-graph/fixtures/theme-app-extension/snippets/shared.liquid b/packages/theme-graph/fixtures/theme-app-extension/snippets/shared.liquid deleted file mode 100644 index c20f2d0e7..000000000 --- a/packages/theme-graph/fixtures/theme-app-extension/snippets/shared.liquid +++ /dev/null @@ -1 +0,0 @@ -
    Shared snippet
    diff --git a/packages/theme-graph/package.json b/packages/theme-graph/package.json index ae20ffec5..d26e0d4cb 100644 --- a/packages/theme-graph/package.json +++ b/packages/theme-graph/package.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@shopify/theme-check-node": "workspace:^3.31.0", - "@types/node": "^22.18.8" + "@types/node": "^22.18.8", + "yaml": "^2.8.3" } } diff --git a/packages/theme-graph/src/graph/build.spec.ts b/packages/theme-graph/src/graph/build.spec.ts index 77702a594..5c012402c 100644 --- a/packages/theme-graph/src/graph/build.spec.ts +++ b/packages/theme-graph/src/graph/build.spec.ts @@ -1,14 +1,10 @@ -import { path as pathUtils, SourceCodeType } from '@shopify/theme-check-common'; -import { assert, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { buildThemeGraph } from '../index'; -import { toSourceCode } from '../toSourceCode'; -import { Dependencies, JsonModuleKind, LiquidModuleKind, ModuleType, ThemeGraph } from '../types'; -import { getDependencies, skeleton, themeAppExtension } from './test-helpers'; +import { Dependencies, ThemeGraph } from '../types'; +import { getDependencies, skeleton } from './test-helpers'; describe('Module: index', () => { const rootUri = skeleton; - const p = (part: string) => pathUtils.join(rootUri, ...part.split('/')); - const loc = (part: string) => expect.objectContaining({ uri: p(part) }); let dependencies: Dependencies; beforeAll(async () => { @@ -16,29 +12,6 @@ describe('Module: index', () => { }, 15000); describe('Unit: buildThemeGraph', { timeout: 10000 }, () => { - it('build a graph of the theme', { timeout: 10000 }, async () => { - const graph = await buildThemeGraph(rootUri, dependencies); - expect(graph).toBeDefined(); - }); - - it.each([ - "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}", - "{% liquid\n block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000'\n endblock\n %}", - ])('skips app block dependencies while preserving theme blocks: %s', async (appBlock) => { - const graph = await buildThemeGraph(rootUri, { - ...dependencies, - getSourceCode: (uri) => - uri === p('layout/theme.liquid') - ? toSourceCode(uri, `${appBlock}{% block 'text' %}{% endblock %}`) - : dependencies.getSourceCode(uri), - }); - - expect( - graph.modules[p('layout/theme.liquid')].dependencies.map((ref) => ref.target.uri), - ).toEqual([p('blocks/text.liquid')]); - expect(Object.keys(graph.modules).some((uri) => uri.includes('shopify:'))).toBe(false); - }); - describe('with a valid theme graph', () => { let graph: ThemeGraph; @@ -46,252 +19,10 @@ describe('Module: index', () => { graph = await buildThemeGraph(rootUri, dependencies); }); - it('uses app blocks as entry points in app mode', async () => { - const appDependencies = await getDependencies(themeAppExtension); - const appGraph = await buildThemeGraph(themeAppExtension, { - ...appDependencies, - mode: 'app', - }); - const app = (part: string) => pathUtils.join(themeAppExtension, ...part.split('/')); - - expect(appGraph.entryPoints.map((x) => x.uri)).toEqual([app('blocks/app-block.liquid')]); - expect( - appGraph.modules[app('snippets/shared.liquid')].references.map((x) => x.source.uri), - ).toEqual([app('blocks/app-block.liquid')]); - }); - it('have a root URI', () => { expect(graph.rootUri).toBeDefined(); expect(graph.rootUri).toBe(rootUri); }); - - // We're using sections as entry points because the section rendering API can render - // any section without it needing a preset or default value in its schema. - it('infers entry points from templates, layouts, and section files', () => { - expect(graph.entryPoints).toHaveLength(4); - expect(graph.entryPoints.map((x) => x.uri)).toEqual( - expect.arrayContaining([ - p('templates/index.json'), - p('layout/theme.liquid'), - p('sections/custom-section.liquid'), - p('sections/header.liquid'), - ]), - ); - }); - - it("finds layout/theme.liquid's dependencies and references", () => { - const themeLayout = graph.modules[p('layout/theme.liquid')]; - assert(themeLayout); - - // outgoing links - const deps = themeLayout.dependencies; - assert(deps.map((x) => x.source.uri).every((x) => x === p('layout/theme.liquid'))); - expect(deps.map((x) => x.target.uri)).toEqual( - expect.arrayContaining([ - p('sections/header-group.json'), - p('snippets/layout-parent.liquid'), - p('blocks/layout-block.liquid'), - p('assets/theme.js'), - p('assets/theme.css'), - ]), - ); - - // ingoing links - const refs = themeLayout.references; - expect(refs).toHaveLength(1); - assert(refs.map((x) => x.target.uri).every((x) => x === p('layout/theme.liquid'))); - expect(refs.map((x) => x.source.uri)).toEqual( - expect.arrayContaining([p('templates/index.json')]), - ); - }); - - it('follows direct and transitive dependencies from layouts', () => { - expect( - graph.modules[p('snippets/layout-parent.liquid')].references.map((x) => x.source.uri), - ).toContain(p('layout/theme.liquid')); - expect( - graph.modules[p('snippets/layout-child.liquid')].references.map((x) => x.source.uri), - ).toContain(p('snippets/layout-parent.liquid')); - expect( - graph.modules[p('blocks/layout-block.liquid')].references.map((x) => x.source.uri), - ).toContain(p('layout/theme.liquid')); - expect( - graph.modules[p('snippets/block-child.liquid')].references.map((x) => x.source.uri), - ).toContain(p('blocks/layout-block.liquid')); - }); - - it("finds templates/index.json's dependencies and references", () => { - const indexTemplate = graph.modules[p('templates/index.json')]; - assert(indexTemplate); - assert(indexTemplate.type === ModuleType.Json); - assert(indexTemplate.kind === JsonModuleKind.Template); - - // outgoing links - const deps = indexTemplate.dependencies; - assert(deps.map((x) => x.source.uri).every((x) => x === p('templates/index.json'))); - expect(deps.map((x) => x.target.uri)).toEqual( - expect.arrayContaining([ - p('layout/theme.liquid'), - p('sections/custom-section.liquid'), - p('blocks/group.liquid'), - p('blocks/text.liquid'), - ]), - ); - - // ingoing links - const refs = indexTemplate.references; - expect(refs).toHaveLength(0); - }); - - it("finds sections/custom-section's dependencies and references", () => { - const customSection = graph.modules[p('sections/custom-section.liquid')]; - assert(customSection); - assert(customSection.type === ModuleType.Liquid); - assert(customSection.kind === LiquidModuleKind.Section); - - // outgoing links - const deps = customSection.dependencies; - assert(deps.map((x) => x.source.uri).every((x) => x === customSection.uri)); - expect(deps.map((x) => x.target.uri)).toEqual( - expect.arrayContaining([ - p('blocks/group.liquid'), - p('blocks/text.liquid'), - p('blocks/_private.liquid'), - ]), - ); - - // ingoing links - const refs = customSection.references; - assert(refs.map((x) => x.target.uri).every((x) => x === customSection.uri)); - expect(refs.map((x) => x.source.uri)).toEqual( - expect.arrayContaining([p('templates/index.json'), p('sections/header-group.json')]), - ); - expect(refs).toHaveLength(2); - }); - - it("finds blocks/group's dependencies and references", () => { - const groupBlock = graph.modules[p('blocks/group.liquid')]; - assert(groupBlock); - assert(groupBlock.type === ModuleType.Liquid); - assert(groupBlock.kind === LiquidModuleKind.Block); - - const deps = groupBlock.dependencies; - assert(deps.map((x) => x.source.uri).every((x) => x === groupBlock.uri)); - expect(deps).toEqual( - expect.arrayContaining([ - { - source: loc('blocks/group.liquid'), - target: loc('snippets/parent.liquid'), - type: 'direct', - }, // direct dep in snippet - { - source: loc('blocks/group.liquid'), - target: loc('blocks/text.liquid'), - type: 'indirect', - }, // indirect because of @theme - { - source: loc('blocks/group.liquid'), - target: loc('blocks/text.liquid'), - type: 'preset', - }, // direct dep in preset - ]), - ); - - const refs = groupBlock.references; - assert(refs.map((x) => x.target.uri).every((x) => x === groupBlock.uri)); - expect(refs.map((x) => x.source.uri)).toEqual( - expect.arrayContaining([ - p('templates/index.json'), - p('sections/custom-section.liquid'), // @theme ref - p('sections/header-group.json'), // custom-section > group - p('blocks/group.liquid'), // @theme ref - ]), - ); - - expect(refs).toContainEqual( - // Expecting the `@theme` reference in the custom-section schema to be indirect - expect.objectContaining({ - type: 'indirect', - source: { - uri: p('sections/custom-section.liquid'), - range: [expect.any(Number), expect.any(Number)], - }, - }), - ); - }); - - it("finds the snippets/parent's dependencies and references", async () => { - const parentSnippet = graph.modules[p('snippets/parent.liquid')]; - assert(parentSnippet); - assert(parentSnippet.type === ModuleType.Liquid); - assert(parentSnippet.kind === LiquidModuleKind.Snippet); - - // outgoing links - const deps = parentSnippet.dependencies; - assert(deps.map((x) => x.source.uri).every((x) => x === parentSnippet.uri)); - expect(deps.map((x) => x.target.uri)).toEqual( - expect.arrayContaining([ - p('snippets/child.liquid'), // {% render 'child' %} - p('assets/theme.js'), // - ]), - ); - - // ingoing links - const refs = parentSnippet.references; - assert(refs.map((x) => x.target.uri).every((x) => x === parentSnippet.uri)); - expect(refs.map((x) => x.source.uri)).toEqual( - expect.arrayContaining([ - p('blocks/group.liquid'), // {% render 'parent' %} - ]), - ); - - // {% render 'child', children: children %} dependency - const parentSource = await dependencies.getSourceCode(p('snippets/parent.liquid')); - assert(parentSource); - assert(parentSource.type === SourceCodeType.LiquidHtml); - expect(parentSnippet.dependencies.map((x) => x.source)).toContainEqual( - expect.objectContaining({ - uri: p('snippets/parent.liquid'), - range: [ - parentSource.source.indexOf('{% render "child"'), - parentSource.source.indexOf('{% render "child"') + - '{% render "child", children: children %}'.length, - ], - }), - ); - - // dependency - expect(parentSnippet.dependencies.map((x) => x.source)).toContainEqual( - expect.objectContaining({ - uri: p('snippets/parent.liquid'), - range: [ - parentSource.source.indexOf(' { - const staticBlock = graph.modules[p('blocks/_static.liquid')]; - assert(staticBlock); - assert(staticBlock.type === ModuleType.Liquid); - assert(staticBlock.kind === LiquidModuleKind.Block); - - // outgoing links - const deps = staticBlock.dependencies; - expect(deps).toEqual([]); - - // ingoing links - const refs = staticBlock.references; - assert(refs.map((x) => x.target.uri).every((x) => x === staticBlock.uri)); - expect(refs.map((x) => x.source.uri)).toEqual( - expect.arrayContaining([ - p('sections/header-group.json'), - p('blocks/render-static.liquid'), - ]), - ); - }); }); }); }); diff --git a/packages/theme-graph/src/graph/test-helpers.ts b/packages/theme-graph/src/graph/test-helpers.ts index bc88c0acf..40801fcf9 100644 --- a/packages/theme-graph/src/graph/test-helpers.ts +++ b/packages/theme-graph/src/graph/test-helpers.ts @@ -23,7 +23,6 @@ export function makeGetSourceCode(fs: AbstractFileSystem) { export const fixturesRoot = pathUtils.join(URI.file(__dirname), ...'../../fixtures'.split('/')); export const skeleton = pathUtils.join(fixturesRoot, 'skeleton'); -export const themeAppExtension = pathUtils.join(fixturesRoot, 'theme-app-extension'); export async function getDependencies(rootUri: string, fs: AbstractFileSystem = NodeFileSystem) { const getSourceCode = makeGetSourceCode(fs); diff --git a/packages/theme-graph/src/specs.spec.ts b/packages/theme-graph/src/specs.spec.ts new file mode 100644 index 000000000..3624f7840 --- /dev/null +++ b/packages/theme-graph/src/specs.spec.ts @@ -0,0 +1,133 @@ +import { Mode, Modes, path, Reference } from '@shopify/theme-check-common'; +import { MockFileSystem } from '@shopify/theme-check-common/src/test'; +import { readdirSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parse } from 'yaml'; +import { getDependencies } from './graph/test-helpers'; +import { buildThemeGraph } from './index'; +import { ThemeModule } from './types'; + +/** + * Runs the language-neutral graph specs in specs/graph at the repo root. + * The format is described in specs/graph/README.md. + */ + +interface Spec { + cases: Case[]; +} + +interface Case { + name: string; + mode?: Mode; + files: Record; + entryPoints?: string[]; + modules?: SpecModule[]; + edges?: SpecEdge[]; +} + +interface SpecModule { + file: string; + type: string; + kind: string; + exists?: false; +} + +type Position = [line: number, character: number]; + +interface SpecEdge { + source: string; + start?: Position; + text?: string; + target: string; + targetStart?: Position; + targetText?: string; + type?: 'indirect' | 'preset'; +} + +const specsDir = resolve(__dirname, '../../../specs/graph'); +const rootUri = path.normalize('file:/'); +const caseKeys = ['name', 'mode', 'files', 'entryPoints', 'modules', 'edges']; + +for (const specFile of readdirSync(specsDir).filter((f) => f.endsWith('.yml'))) { + const spec: Spec = parse(readFileSync(resolve(specsDir, specFile), 'utf8')); + + describe(`specs/${specFile}`, () => { + for (const c of spec.cases) { + it(c.name, async () => { + // A misspelled optional key would silently check nothing. + for (const key of Object.keys(c)) expect(caseKeys, 'case keys').toContain(key); + expect(Modes).toContain(c.mode ?? 'theme'); + const fs = new MockFileSystem(c.files, rootUri); + const deps = await getDependencies(rootUri, fs); + const graph = await buildThemeGraph(rootUri, { ...deps, mode: c.mode }); + const modules = Object.values(graph.modules); + // Each edge is a dependency of its source and a reference of its target. + for (const module of modules) { + for (const edge of module.dependencies) { + expect(edge.source.uri, 'dependency source').toBe(module.uri); + } + for (const edge of module.references) { + expect(edge.target.uri, 'reference target').toBe(module.uri); + } + } + const toSpecEdges = (edges: Reference[]) => + edges.map((edge) => toSpecEdge(edge, c.files)).sort(byEdge); + const edges = toSpecEdges(modules.flatMap((module) => module.dependencies)); + expect(edges).toEqual([...(c.edges ?? [])].sort(byEdge)); + expect(toSpecEdges(modules.flatMap((module) => module.references)), 'references').toEqual( + edges, + ); + if (c.entryPoints) { + expect(graph.entryPoints.map((module) => file(module.uri)).sort()).toEqual( + [...c.entryPoints].sort(), + ); + } + if (c.modules) { + expect(modules.map(toSpecModule).sort(byFile)).toEqual([...c.modules].sort(byFile)); + } + }); + } + }); +} + +function file(uri: string) { + return path.relative(uri, rootUri); +} + +/** The [line, character] of an offset, both in UTF-16 code units. */ +function position(source: string, offset: number): Position { + const before = source.slice(0, offset); + return [before.split('\n').length - 1, offset - (before.lastIndexOf('\n') + 1)]; +} + +function toSpecEdge({ source, target, type }: Reference, files: Case['files']): SpecEdge { + // Cut out at the range, so with start the text pins it even when the text repeats. + const at = (uri: string, [start, end]: [number, number]) => { + expect(end, 'edge end').toBeGreaterThanOrEqual(start); + return [position(files[file(uri)], start), files[file(uri)].slice(start, end)] as const; + }; + const [start, text] = source.range ? at(source.uri, source.range) : []; + const [targetStart, targetText] = target.range ? at(target.uri, target.range) : []; + return { + source: file(source.uri), + ...(start && { start, text }), + target: file(target.uri), + ...(targetStart && { targetStart, targetText }), + ...(type !== 'direct' && { type }), + }; +} + +function toSpecModule({ uri, type, kind, exists }: ThemeModule): SpecModule { + return { file: file(uri), type, kind, ...(exists === false && { exists }) }; +} + +function byEdge(a: SpecEdge, b: SpecEdge) { + const key = (e: SpecEdge) => + JSON.stringify([e.source, e.start, e.text, e.target, e.targetStart, e.targetText, e.type]); + return key(a).localeCompare(key(b)); +} + +function byFile(a: SpecModule, b: SpecModule) { + return a.file.localeCompare(b.file); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4cc91989b..7c45e32c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,6 +321,9 @@ importers: '@types/node': specifier: ^22.18.8 version: 22.18.8 + yaml: + specifier: ^2.8.3 + version: 2.8.3 packages/theme-language-server-browser: dependencies: diff --git a/specs/checks/README.md b/specs/checks/README.md new file mode 100644 index 000000000..96c444ad4 --- /dev/null +++ b/specs/checks/README.md @@ -0,0 +1,119 @@ +# Check specs + +Executable, language-neutral specs for theme-check checks: one `.yml` per check. Every +implementation runs them. In TypeScript that's `packages/theme-check-common/src/specs.spec.ts`, +which runs with the rest of the vitest suite and fails if a check has no spec. To change what a +check does, change its spec first. + +## Format + +```yaml +check: UnusedAssign # the check's code +severity: warning # its default severity +cases: + - name: reports a variable that is assigned but not used + files: # the theme, by path from its root + snippets/example.liquid: | + {% assign unused = 1 %} + docset: default # optional: docsets/default.yml. Without it, the run has no theme docset. + config: ... # optional: check options + mode: app # optional: theme or app. Without it, theme. + checks: [UnusedAssign, UndefinedObject] # optional: run these instead of the spec's check + references: ... # optional: which files render which + offenses: # every offense the check reports. Without it, none. + - check: UnusedAssign # the check that reported it, only when the case lists checks + file: snippets/example.liquid # the file the offense is in + message: The variable 'unused' is assigned but not used + start: [0, 0] # line and character, 0-based, in UTF-16 code units + highlight: "{% assign unused = 1 %}" # the text from start to the offense's end + fix: ... # the file after the fix, when the offense has one + suggest: # each suggestion, when the offense has any + - message: Remove the unused variable 'unused' + result: "\n" # the file after applying it +``` + +The docset is the Shopify reference data checks look things up in: `filters`, `objects`, `tags` +and `systemTranslations`. A case names a file in `docsets/`, or writes one out in the same shape: + +```yaml +docset: + tags: [{ name: custom_tag }] +``` + +`config` holds check options by check code, as in `.theme-check.yml`: + +```yaml +config: + LiquidComplexity: { maxComplexity: 5 } +``` + +`references` lists which files render which: what production gets from the theme graph. `type` +is `direct` (a `{% render %}`, the default), `indirect` or `preset`: + +```yaml +references: + - { source: sections/section.liquid, target: snippets/child.liquid } +``` + +A file at an `http://` or `https://` URL is a remote file, like a script on a CDN. It isn't part +of the theme: checks only see its size. + +```yaml +files: + templates/index.liquid: + https://cdn.example.com/app.js: console.log('hello world'); +``` + +A spec for a feature rather than a check, like `disabled-checks.yml`, has no `check` or +`severity`, and each of its cases lists its `checks`. + +A file too large to write out can be a list of parts, joined in order. A part is a string or +`{ repeat: , times: }`: + +```yaml +files: + config/markets.json: ['{"d":"', { repeat: a, times: 262137 }, '"}'] # 262,145 bytes +``` + +So can a highlight that's too large to write out: + +```yaml +offenses: + - file: config/markets.json + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: ['{"d":"', { repeat: a, times: 262137 }, '"}'] +``` + +A case states `autofix` for exactly the files with two or more fixes: each file after applying all +its fixes at once. Fixes that can't be applied together fail the case. + +```yaml +autofix: + file.liquid: "{{ 'hello' | upcase | downcase | reverse }}" +``` + +An offense's `highlight` is cut out of the file at the range the check reported, never searched +for, so `start` and `highlight` pin the range even when the text repeats. + +Offenses are compared as an unordered list. A case fails on a missing, extra or different +offense, and when the check throws. What it doesn't state must be right too: each offense's +severity is its check's, and its start and end are in order, in the file, at the line and +character of their index. + +## The run + +Each case runs only its spec's check, or its `checks`, in its `mode`, with its `config` and +defaults for the rest. An option or check that doesn't exist is an error, since it would otherwise +be ignored. Every dependency is either declared in the case or derived from `files`: + +- `fs` serves `files` plus an empty `.theme-check.yml`. +- Block and section schemas come from the `{% schema %}` of `blocks/*.liquid` and + `sections/*.liquid`. They're assumed valid. +- Doc definitions come from the `{% doc %}` of the file at that path. +- CSS classes come from the files. +- `getReferences` and `getDependencies` answer from `references`: the edges into the file, and + out of it. Both ends of an edge must be in `files`. +- A remote file's size is its length in UTF-8 bytes. Any other URL has size 0, as when the request + fails, so nothing reaches the network. +- A `docset` gets the same augmentations as in production (`AugmentedThemeDocset`). diff --git a/specs/checks/app-block-missing-schema.yml b/specs/checks/app-block-missing-schema.yml new file mode 100644 index 000000000..26978279f --- /dev/null +++ b/specs/checks/app-block-missing-schema.yml @@ -0,0 +1,98 @@ +check: AppBlockMissingSchema +severity: error +cases: + - name: should report an error when schema tag is missing on a theme app extension (blocks only) + files: + blocks/footer.liquid: | +
    + {% for block in section.blocks %} + {% case block.type %} + {% when 'link' %} + + + {% when 'custom-text' %} +
    + {{ block.settings.custom-text-field }} +
    + {% endcase %} + {% endfor %} +
    + offenses: + - file: blocks/footer.liquid + message: The schema does not exist + start: [0, 0] + highlight: "" + - name: should not report an error when schema tag is missing on a snippet in a theme app extension ) + files: + snippets/footer.liquid: | +
    + {% for block in section.blocks %} + {% case block.type %} + {% when 'link' %} + + + {% when 'custom-text' %} +
    + {{ block.settings.custom-text-field }} +
    + {% endcase %} + {% endfor %} +
    + - name: should not report when the schema is present on a theme app extension + files: + blocks/footer.liquid: |- +
    + {% for block in section.blocks %} + {% case block.type %} + {% when 'link' %} + + + {% when 'custom-text' %} +
    + {{ block.settings.custom-text-field }} +
    + {% endcase %} + {% endfor %} +
    + + {% schema %} + { + "name": "Footer", + "max_blocks": 8, + "blocks": [ + { + "type": "link", + "name": "Link", + "settings": [ + { + "id": "linkurl", + "type": "url", + "label": "URL link" + }, + { + "id": "linktext", + "type": "text", + "label": "Link text" + } + ] + }, + { + "type": "custom-text", + "name": "Custom Text", + "settings": [ + { + "id": "custom-text-field", + "type": "textarea", + "label": "Text" + } + ] + } + ] + } + {% endschema %} diff --git a/specs/checks/app-block-valid-tags.yml b/specs/checks/app-block-valid-tags.yml new file mode 100644 index 000000000..b91e377bd --- /dev/null +++ b/specs/checks/app-block-valid-tags.yml @@ -0,0 +1,81 @@ +check: AppBlockValidTags +severity: error +cases: + - name: should report an offense when the forbidden tag is used in blocks directory liquid file (1) + files: + blocks/app.liquid: | + {% include 'test' %} + {% schema %} + { } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "Theme app extension blocks cannot contain 'include' tags" + start: [0, 0] + highlight: "{% include 'test' %}" + - name: should report an offense when the forbidden tag is used in blocks directory liquid file (2) + files: + blocks/app.liquid: | + {% layout 'test' %} + {% schema %} + { } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "Theme app extension blocks cannot contain 'layout' tags" + start: [0, 0] + highlight: "{% layout 'test' %}" + - name: should report an offense when the forbidden tag is used in blocks directory liquid file (3) + files: + blocks/app.liquid: | + {% section 'test' %} + {% schema %} + { } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "Theme app extension blocks cannot contain 'section' tags" + start: [0, 0] + highlight: "{% section 'test' %}" + - name: should report an offense when the forbidden tag is used in blocks directory liquid file (4) + files: + blocks/app.liquid: | + {% sections 'test' %} + {% schema %} + { } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "Theme app extension blocks cannot contain 'sections' tags" + start: [0, 0] + highlight: "{% sections 'test' %}" + - name: should report an offense when the forbidden tag is used with an end tag in blocks directory liquid file (1) + files: + blocks/app.liquid: | + {% javascript %} + {% endjavascript %} + {% schema %} + { } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "Theme app extension blocks cannot contain 'javascript' tags" + start: [0, 0] + highlight: |- + {% javascript %} + {% endjavascript %} + - name: should report an offense when the forbidden tag is used with an end tag in blocks directory liquid file (2) + files: + blocks/app.liquid: | + {% stylesheet %} + {% endstylesheet %} + {% schema %} + { } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "Theme app extension blocks cannot contain 'stylesheet' tags" + start: [0, 0] + highlight: |- + {% stylesheet %} + {% endstylesheet %} diff --git a/specs/checks/asset-preload.yml b/specs/checks/asset-preload.yml new file mode 100644 index 000000000..86ba4b524 --- /dev/null +++ b/specs/checks/asset-preload.yml @@ -0,0 +1,72 @@ +check: AssetPreload +severity: warning +cases: + - name: no offense with link element + files: + file.liquid: | + + + - name: reports stylesheet preloading + files: + file.liquid: | + + offenses: + - file: file.liquid + message: For better performance, prefer using the preload argument of the stylesheet_tag filter + start: [0, 0] + highlight: '' + - name: reports image preloading + files: + file.liquid: | + + offenses: + - file: file.liquid + message: For better performance, prefer using the preload argument of the image_tag filter + start: [0, 0] + highlight: '' + - name: allows high-priority image preloading + files: + file.liquid: | + + - name: reports image preloading when attribute values contain Liquid + files: + file.liquid: | + + + offenses: + - file: file.liquid + message: For better performance, prefer using the preload argument of the image_tag filter + start: [0, 0] + highlight: "" + - file: file.liquid + message: For better performance, prefer using the preload argument of the image_tag filter + start: [1, 0] + highlight: "" + - name: reports general preloading + files: + file.liquid: | + + offenses: + - file: file.liquid + message: For better performance, prefer using the preload_tag filter + start: [0, 0] + highlight: '' + - name: should report offenses for manual preloading of assets + files: + file.liquid: | + + + + offenses: + - file: file.liquid + message: For better performance, prefer using the preload_tag filter + start: [0, 0] + highlight: "" + - file: file.liquid + message: For better performance, prefer using the preload argument of the stylesheet_tag filter + start: [1, 0] + highlight: "" + - file: file.liquid + message: For better performance, prefer using the preload argument of the image_tag filter + start: [2, 0] + highlight: "" diff --git a/specs/checks/asset-size-app-block-css.yml b/specs/checks/asset-size-app-block-css.yml new file mode 100644 index 000000000..db24002f1 --- /dev/null +++ b/specs/checks/asset-size-app-block-css.yml @@ -0,0 +1,54 @@ +check: AssetSizeAppBlockCSS +severity: error +cases: + - name: should not report any offenses if CSS is smaller than threshold + files: + assets/app.css: "* { color: green } " + blocks/app.liquid: | + {% schema %} + { + "stylesheet": "app.css" + } + {% endschema %} + - name: should report an offense if CSS is larger than threshold + files: + assets/app.css: "* { color: green } " + blocks/app.liquid: | + {% schema %} + { + "stylesheet": "app.css" + } + {% endschema %} + config: + AssetSizeAppBlockCSS: { thresholdInBytes: 1 } + offenses: + - file: blocks/app.liquid + message: "The file size for 'app.css' (19 B) exceeds the configured threshold (1 B)" + start: [2, 17] + highlight: app.css + - name: should report an offense if the CSS file does not exist + files: + blocks/app.liquid: | + {% schema %} + { + "stylesheet": "nonexistent.css" + } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "'nonexistent.css' does not exist." + start: [2, 17] + highlight: nonexistent.css + - name: should reports offense if the CSS file does not exist and the asset has a trailing comma + files: + blocks/app.liquid: | + {% schema %} + { + "stylesheet": "nonexistent.css", + } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "'nonexistent.css' does not exist." + start: [2, 17] + highlight: nonexistent.css diff --git a/specs/checks/asset-size-app-block-javascript.yml b/specs/checks/asset-size-app-block-javascript.yml new file mode 100644 index 000000000..aca75aa76 --- /dev/null +++ b/specs/checks/asset-size-app-block-javascript.yml @@ -0,0 +1,41 @@ +check: AssetSizeAppBlockJavaScript +severity: error +cases: + - name: should not report any offenses if JavaScript is smaller than threshold + files: + assets/app.js: 'console.log("Hello, world!");' + blocks/app.liquid: | + {% schema %} + { + "javascript": "app.js" + } + {% endschema %} + - name: should report an offense if JavaScript is larger than threshold + files: + assets/app.js: 'console.log("Hello, world!");' + blocks/app.liquid: | + {% schema %} + { + "javascript": "app.js" + } + {% endschema %} + config: + AssetSizeAppBlockJavaScript: { thresholdInBytes: 1 } + offenses: + - file: blocks/app.liquid + message: "The file size for 'app.js' (29 B) exceeds the configured threshold (1 B)" + start: [2, 17] + highlight: app.js + - name: should report an offense if the JavaScript file does not exist + files: + blocks/app.liquid: | + {% schema %} + { + "javascript": "nonexistent.js" + } + {% endschema %} + offenses: + - file: blocks/app.liquid + message: "'nonexistent.js' does not exist." + start: [2, 17] + highlight: nonexistent.js diff --git a/specs/checks/asset-size-css.yml b/specs/checks/asset-size-css.yml new file mode 100644 index 000000000..c282ddb44 --- /dev/null +++ b/specs/checks/asset-size-css.yml @@ -0,0 +1,84 @@ +check: AssetSizeCSS +severity: error +cases: + - name: should not report any offenses if CSS is smaller than threshold + files: + assets/theme.css: "* { color: green !important; }" + templates/index.liquid: | + + + + + + - name: should report an offense if CSS is larger than threshold + files: + assets/theme.css: "* { color: green !important; }" + templates/index.liquid: | + + + + + + config: + AssetSizeCSS: { thresholdInBytes: 1 } + offenses: + - file: templates/index.liquid + message: The CSS file size exceeds the threshold of 1 bytes + start: [2, 16] + highlight: "{{ 'theme.css' | asset_url }}" + - name: should report a warning when the CSS file size exceeds the threshold + files: + templates/index.liquid: | + + + + + + https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css: "* { color: green; }" + config: + AssetSizeCSS: { thresholdInBytes: 1 } + offenses: + - file: templates/index.liquid + message: The CSS file size exceeds the threshold of 1 bytes + start: [2, 16] + highlight: "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" + - name: should not report any offenses if CSS is smaller than threshold 2 + files: + assets/theme.css: 'console.log("hello world");' + templates/index.liquid: | + + + {{ 'theme.css' | asset_url | stylesheet_tag }} + {{ "https://example.com" | stylesheet_tag }} + + + https://example.com: "* { color: green; }" + - name: should report an offense if CSS is larger than threshold 2 + files: + assets/theme.css: 'console.log("hello world");' + templates/index.liquid: | + + + {{ 'theme.css' | asset_url | stylesheet_tag }} + {{ "https://example.com" | stylesheet_tag }} + + + https://example.com: "* { color: green; }" + config: + AssetSizeCSS: { thresholdInBytes: 2 } + offenses: + - file: templates/index.liquid + message: The CSS file size exceeds the threshold of 2 bytes + start: [2, 7] + highlight: "'theme.css' | asset_url | stylesheet_tag " + - file: templates/index.liquid + message: The CSS file size exceeds the threshold of 2 bytes + start: [3, 7] + highlight: '"https://example.com"' + - name: should not report any offenses if there is no stylesheet + files: + templates/index.liquid: | + + + + diff --git a/specs/checks/asset-size-javascript.yml b/specs/checks/asset-size-javascript.yml new file mode 100644 index 000000000..58c3fba87 --- /dev/null +++ b/specs/checks/asset-size-javascript.yml @@ -0,0 +1,92 @@ +check: AssetSizeJavaScript +severity: error +cases: + - name: "should not find file size for invalid URLs: https://{{ settings.url }}" + files: + templates/index.liquid: '' + - name: "should not find file size for invalid URLs: {{ 'this_file_does_not_exist.js' | asset_url }}" + files: + templates/index.liquid: "" + - name: "should not find file size for invalid URLs: {% if on_product %}https://hello.world{% else %}https://hi.world{% endif %}" + files: + templates/index.liquid: '' + - name: should report a warning when the JavaScript http request exceeds the threshold + files: + templates/index.liquid: | + + + + + + https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js: "console.log('hello world');" + config: + AssetSizeJavaScript: { thresholdInBytes: 1 } + offenses: + - file: templates/index.liquid + message: JavaScript on every page load exceeds compressed size threshold (1 Bytes), consider using the import on interaction pattern. + start: [2, 17] + highlight: "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" + - name: should not report any offenses if JavaScript is smaller than threshold + files: + assets/theme.js: "console.log('hello world'); console.log('Oh. Hi Mark!')" + templates/index.liquid: | + + + + + + - name: should report an offense if JavaScript is larger than threshold + files: + assets/theme.js: "console.log('hello world'); console.log('Oh. Hi Mark!')" + templates/index.liquid: | + + + + + + config: + AssetSizeJavaScript: { thresholdInBytes: 2 } + offenses: + - file: templates/index.liquid + message: JavaScript on every page load exceeds compressed size threshold (2 Bytes), consider using the import on interaction pattern. + start: [2, 17] + highlight: "{{ 'theme.js' | asset_url }}" + - name: should not report any offenses for inline JavaScript + files: + templates/index.liquid: | + + + + + + - name: should report an offense if JS is larger than threshold + files: + assets/theme.js: 'console.log("hello world");' + templates/index.liquid: | + + + {{ 'theme.js' | asset_url | script_tag }} + {{ "https://example.com" | script_tag }} + + + https://example.com: "console.log('hello world');" + config: + AssetSizeJavaScript: { thresholdInBytes: 2 } + offenses: + - file: templates/index.liquid + message: JavaScript on every page load exceeds compressed size threshold (2 Bytes), consider using the import on interaction pattern. + start: [2, 7] + highlight: "'theme.js' | asset_url | script_tag " + - file: templates/index.liquid + message: JavaScript on every page load exceeds compressed size threshold (2 Bytes), consider using the import on interaction pattern. + start: [3, 7] + highlight: '"https://example.com"' + - name: should not report any offenses if there is no javascript + files: + templates/index.liquid: | + + + + diff --git a/specs/checks/block-content-usage.yml b/specs/checks/block-content-usage.yml new file mode 100644 index 000000000..f6deea591 --- /dev/null +++ b/specs/checks/block-content-usage.yml @@ -0,0 +1,77 @@ +check: BlockContentUsage +severity: error +cases: + - name: 'reports the block.content lookup in "{{ block.content }}"' + files: + blocks/card.liquid: "{{ block.content }}" + offenses: + - file: blocks/card.liquid + message: "Use the implicit 'content' parameter directly instead of 'block.content'." + start: [0, 3] + highlight: block.content + - name: "reports the block.content lookup in \"{{ block['content'] | upcase }}\"" + files: + blocks/card.liquid: "{{ block['content'] | upcase }}" + offenses: + - file: blocks/card.liquid + message: "Use the implicit 'content' parameter directly instead of 'block.content'." + start: [0, 3] + highlight: "block['content']" + - name: 'reports the block.content lookup in "{{ block.content.size }}"' + files: + blocks/card.liquid: "{{ block.content.size }}" + offenses: + - file: blocks/card.liquid + message: "Use the implicit 'content' parameter directly instead of 'block.content'." + start: [0, 3] + highlight: block.content.size + - name: 'reports the block.content lookup in "{% if block.content != blank %}{{ content }}{% endif %}"' + files: + blocks/card.liquid: "{% if block.content != blank %}{{ content }}{% endif %}" + offenses: + - file: blocks/card.liquid + message: "Use the implicit 'content' parameter directly instead of 'block.content'." + start: [0, 6] + highlight: block.content + - name: "reports the block.content lookup in \"{% render 'card', body: block.content %}\"" + files: + blocks/card.liquid: "{% render 'card', body: block.content %}" + offenses: + - file: blocks/card.liquid + message: "Use the implicit 'content' parameter directly instead of 'block.content'." + start: [0, 24] + highlight: block.content + - name: 'reports the block.content lookup in "{% liquid\n echo block.content\n%}"' + files: + blocks/card.liquid: |- + {% liquid + echo block.content + %} + offenses: + - file: blocks/card.liquid + message: "Use the implicit 'content' parameter directly instead of 'block.content'." + start: [1, 7] + highlight: block.content + - name: accepts bare content and other block properties + files: + blocks/card.liquid: |- + {{ content }} + {{ block.id }} + {{ block.settings.content }} + {{ block.shopify_attributes }} + {{ product.content }} + - name: does not report the ordinary block object in sections/main.liquid + files: + sections/main.liquid: "{% for block in section.blocks %}{{ block.content }}{% endfor %}" + - name: does not report the ordinary block object in snippets/card.liquid + files: + snippets/card.liquid: "{% for block in section.blocks %}{{ block.content }}{% endfor %}" + - name: does not report the ordinary block object in templates/index.liquid + files: + templates/index.liquid: "{% for block in section.blocks %}{{ block.content }}{% endfor %}" + - name: does not report the ordinary block object in layout/theme.liquid + files: + layout/theme.liquid: "{% for block in section.blocks %}{{ block.content }}{% endfor %}" + - name: does not report block.content inside a tag that does not render Liquid + files: + blocks/card.liquid: '{% javascript %}console.log("{{ block.content }}");{% endjavascript %}' diff --git a/specs/checks/block-id-usage.yml b/specs/checks/block-id-usage.yml new file mode 100644 index 000000000..bbe44c4c3 --- /dev/null +++ b/specs/checks/block-id-usage.yml @@ -0,0 +1,50 @@ +check: BlockIdUsage +severity: warning +cases: + - name: reports offense with the use of block.id in an if liquid tag + files: + file.liquid: | + {% if block.id == '123' %} + No bueno + {% endif %} + offenses: + - file: file.liquid + message: The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID. + start: [0, 6] + highlight: "block.id == '123'" + - name: reports offense with the use of block.id in an elseif liquid tag + files: + file.liquid: | + {% if potato == '123' %} + No bueno + {% elsif block.id == '123' %} + No bueno + {% endif %} + offenses: + - file: file.liquid + message: The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID. + start: [2, 9] + highlight: "block.id == '123'" + - name: reports offense with the use of block.id in an unless liquid tag + files: + file.liquid: | + {% unless block.id == '123' %} + No bueno + {% endunless %} + offenses: + - file: file.liquid + message: The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID. + start: [0, 10] + highlight: "block.id == '123'" + - name: reports offense with the use of block.id in a case liquid tag + files: + file.liquid: | + {% case block.id %} + {% when '123' %} + No bueno + {% endcase %} + offenses: + - file: file.liquid + message: The ID is dynamically generated by Shopify and is subject to change. You should avoid relying on a literal value of this ID. + start: [0, 8] + highlight: block.id diff --git a/specs/checks/cdn-preconnect.yml b/specs/checks/cdn-preconnect.yml new file mode 100644 index 000000000..a06810b8f --- /dev/null +++ b/specs/checks/cdn-preconnect.yml @@ -0,0 +1,27 @@ +check: CdnPreconnect +severity: error +cases: + - name: should report preconnect to cdn.shopify.com + files: + file.liquid: | + + offenses: + - file: file.liquid + message: Preconnecting to cdn.shopify.com is unnecessary and can lead to worse performance + start: [0, 0] + highlight: '' + - name: should report crossorigin preconnect to cdn.shopify.com + files: + file.liquid: | + + offenses: + - file: file.liquid + message: Preconnecting to cdn.shopify.com is unnecessary and can lead to worse performance + start: [0, 0] + highlight: '' + - name: should not report other link types + files: + file.liquid: | + + + diff --git a/specs/checks/content-for-header-modification.yml b/specs/checks/content-for-header-modification.yml new file mode 100644 index 000000000..1f65ad9dd --- /dev/null +++ b/specs/checks/content-for-header-modification.yml @@ -0,0 +1,44 @@ +check: ContentForHeaderModification +severity: error +cases: + - name: reports a offense with the use of a filter + files: + file.liquid: "{{ content_for_header | split: ',' }}" + offenses: + - file: file.liquid + message: "Do not rely on the content of `content_for_header`" + start: [0, 0] + highlight: "{{ content_for_header | split: ',' }}" + - name: reports offense with the use of assign + files: + file.liquid: "{% assign x = content_for_header %}" + offenses: + - file: file.liquid + message: "Do not rely on the content of `content_for_header`" + start: [0, 0] + highlight: "{% assign x = content_for_header %}" + - name: reports offense with the use of capture + files: + file.liquid: | + {% capture x %} + {{ content_for_header }} + {% endcapture %} + offenses: + - file: file.liquid + message: "Do not rely on the content of `content_for_header`" + start: [1, 2] + highlight: "{{ content_for_header }}" + - name: reports offense with the use of echo + files: + file.liquid: | + {% liquid + echo content_for_header | split: ',' + %} + offenses: + - file: file.liquid + message: "Do not rely on the content of `content_for_header`" + start: [1, 2] + highlight: "echo content_for_header | split: ','" + - name: does not report an offense with normal use + files: + file.liquid: "{{ content_for_header }}" diff --git a/specs/checks/deprecate-bgsizes.yml b/specs/checks/deprecate-bgsizes.yml new file mode 100644 index 000000000..bef4b9836 --- /dev/null +++ b/specs/checks/deprecate-bgsizes.yml @@ -0,0 +1,30 @@ +check: DeprecateBgsizes +severity: warning +cases: + - name: should report offenses for deprecated attributes + files: + file.liquid: | +
    +
    + offenses: + - file: file.liquid + message: 'Use the native loading="lazy" attribute instead of lazysizes' + start: [0, 12] + highlight: lazyload + - file: file.liquid + message: Use the CSS imageset attribute instead of data-bgset + start: [0, 22] + highlight: 'data-bgset="image.jpg"' + - file: file.liquid + message: 'Use the native loading="lazy" attribute instead of lazysizes' + start: [1, 12] + highlight: lazyload + - file: file.liquid + message: Use the CSS imageset attribute instead of data-bgset + start: [1, 22] + highlight: 'data-bgset="image2.jpg"' + - name: should not report offenses for non-deprecated attributes + files: + file.liquid: | +
    +
    diff --git a/specs/checks/deprecate-lazysizes.yml b/specs/checks/deprecate-lazysizes.yml new file mode 100644 index 000000000..727e6d989 --- /dev/null +++ b/specs/checks/deprecate-lazysizes.yml @@ -0,0 +1,16 @@ +check: DeprecateLazysizes +severity: warning +cases: + - name: should report data-srcset and data-sizes attributes + files: + file.liquid: | + + offenses: + - file: file.liquid + message: 'Use the native loading="lazy" attribute instead of lazysizes' + start: [0, 0] + highlight: '' + - name: should not report srcset and sizes attributes + files: + file.liquid: | + diff --git a/specs/checks/deprecated-filter.yml b/specs/checks/deprecated-filter.yml new file mode 100644 index 000000000..ac5decdec --- /dev/null +++ b/specs/checks/deprecated-filter.yml @@ -0,0 +1,1464 @@ +check: DeprecatedFilter +severity: warning +cases: + - name: should report an offense when a deprecated filter is used + files: + file.liquid: | + {{ '#EA5AB9' | hex_to_rgba }} + {{ '#EA5AB9' | hex_to_rgba: 0.5 }} + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'hex_to_rgba', consider using 'color_to_rgb'." + start: [0, 13] + highlight: "| hex_to_rgba" + fix: | + {{ '#EA5AB9' | color_to_rgb }} + {{ '#EA5AB9' | hex_to_rgba: 0.5 }} + - file: file.liquid + message: "Deprecated filter 'hex_to_rgba', consider using 'color_to_rgb'." + start: [1, 13] + highlight: "| hex_to_rgba: 0.5" + fix: | + {{ '#EA5AB9' | hex_to_rgba }} + {{ '#EA5AB9' | color_to_rgb | color_modify: 'alpha', 0.5 }} + autofix: + file.liquid: | + {{ '#EA5AB9' | color_to_rgb }} + {{ '#EA5AB9' | color_to_rgb | color_modify: 'alpha', 0.5 }} + - name: should not report an offense when a non-deprecated filter is used + files: + file.liquid: | + {{ '#EA5AB9' | color_to_rgb }} + {{ '#EA5AB9' | color_to_rgb | color_modify: 'alpha', 0.5 }} + docset: default + - name: "should suggest a fix to replace the deprecated 'hex_to_rgba' filter" + files: + file.liquid: '{{ "#EA5AB9" | hex_to_rgba }}' + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'hex_to_rgba', consider using 'color_to_rgb'." + start: [0, 13] + highlight: "| hex_to_rgba" + fix: '{{ "#EA5AB9" | color_to_rgb }}' + - name: "should suggest a fix to replace the deprecated 'hex_to_rgba' filter when an alpha value is passed" + files: + file.liquid: '{{ "#EA5AB9" | hex_to_rgba: 0.5 }}' + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'hex_to_rgba', consider using 'color_to_rgb'." + start: [0, 13] + highlight: "| hex_to_rgba: 0.5" + fix: "{{ \"#EA5AB9\" | color_to_rgb | color_modify: 'alpha', 0.5 }}" + - name: "should suggest a fix to replace the deprecated 'img_url' filter" + files: + file.liquid: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [0, 26] + highlight: "| img_url: '200x', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | image_url: width: 400, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [1, 26] + highlight: "| img_url: '200x', scale: 2" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 400 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [2, 26] + highlight: "| img_url: '200x'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | image_url: width: 200 }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [3, 26] + highlight: "| img_url: '200x300'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | image_url: width: 200, height: 300 }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [4, 26] + highlight: "| img_url: 'x300'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | image_url: height: 300 }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [5, 26] + highlight: "| img_url" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | image_url: width: 100, height: 100 }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [7, 0] + highlight: " | img_url: '200x'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | image_url: width: 200 + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [10, 0] + highlight: |2- + | img_url: '200x', + format: format + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | image_url: width: 200, format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [14, 25] + highlight: "| img_url: '200x'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | image_url: width: 200}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [15, 26] + highlight: "| img_url: '200x'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | image_url: width: 200-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [16, 39] + highlight: "| img_url: '200x'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | image_url: width: 200 %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [18, 25] + highlight: "| img_url: '200x'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | image_url: width: 200 + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [20, 26] + highlight: "| img_url: 'master'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | image_url: width: 100 }} + {{ product.featured_image | img_url: '4000x', scale: 2 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [21, 26] + highlight: "| img_url: '4000x', scale: 2" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: '200x', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: '200x', scale: 2 }} + {{ product.featured_image | img_url: '200x' }} + {{ product.featured_image | img_url: '200x300' }} + {{ product.featured_image | img_url: 'x300' }} + {{ product.featured_image | img_url }} + {{ product.featured_image + | img_url: '200x' + }} + {{ product.featured_image + | img_url: '200x', + format: format + | image_tag + }} + {{product.featured_image | img_url: '200x'}} + {{-product.featured_image | img_url: '200x'-}} + {% assign url = product.featured_image | img_url: '200x' %} + {% assign url = + product.featured_image | img_url: '200x' + %} + {{ product.featured_image | img_url: 'master' }} + {{ product.featured_image | image_url: width: 5760 }} + + // not supported: + {{ product.featured_image | img_url: variable }} + {{ product.featured_image | img_url: '200x', scale: variable }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [24, 26] + highlight: "| img_url: variable" + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [25, 26] + highlight: "| img_url: '200x', scale: variable" + - name: "should suggest a fix to replace the deprecated 'img_url' filter with named sizes" + files: + file.liquid: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [0, 26] + highlight: "| img_url: 'pico', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | image_url: width: 32, height: 32, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [1, 26] + highlight: "| img_url: 'pico'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 16, height: 16 }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [3, 26] + highlight: "| img_url: 'icon', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | image_url: width: 64, height: 64, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [4, 26] + highlight: "| img_url: 'icon'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 32, height: 32 }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [6, 26] + highlight: "| img_url: 'thumb', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | image_url: width: 100, height: 100, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [7, 26] + highlight: "| img_url: 'thumb'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 50, height: 50 }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [9, 26] + highlight: "| img_url: 'small', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | image_url: width: 200, height: 200, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [10, 26] + highlight: "| img_url: 'small'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 100, height: 100 }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [12, 26] + highlight: "| img_url: 'compact', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | image_url: width: 320, height: 320, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [13, 26] + highlight: "| img_url: 'compact'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 160, height: 160 }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [15, 26] + highlight: "| img_url: 'medium', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | image_url: width: 480, height: 480, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [16, 26] + highlight: "| img_url: 'medium'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 240, height: 240 }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [18, 26] + highlight: "| img_url: 'large', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | image_url: width: 960, height: 960, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [19, 26] + highlight: "| img_url: 'large'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 480, height: 480 }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [21, 26] + highlight: "| img_url: 'grande', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | image_url: width: 1200, height: 1200, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [22, 26] + highlight: "| img_url: 'grande'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 600, height: 600 }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [24, 26] + highlight: "| img_url: 'original', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | image_url: width: 2048, height: 2048, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [25, 26] + highlight: "| img_url: 'original'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 1024, height: 1024 }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [27, 26] + highlight: "| img_url: 'master', scale: 2, crop: 'center'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | image_url: width: 100, crop: 'center' }} + {{ product.featured_image | img_url: 'master' }} + - file: file.liquid + message: "Deprecated filter 'img_url'." + start: [28, 26] + highlight: "| img_url: 'master'" + suggest: + - message: "Replace 'img_url' with 'image_url'." + result: | + {{ product.featured_image | img_url: 'pico', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'pico' }} + + {{ product.featured_image | img_url: 'icon', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'icon' }} + + {{ product.featured_image | img_url: 'thumb', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'thumb' }} + + {{ product.featured_image | img_url: 'small', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'small' }} + + {{ product.featured_image | img_url: 'compact', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'compact' }} + + {{ product.featured_image | img_url: 'medium', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'medium' }} + + {{ product.featured_image | img_url: 'large', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'large' }} + + {{ product.featured_image | img_url: 'grande', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'grande' }} + + {{ product.featured_image | img_url: 'original', scale: 2, crop: 'center' }} + {{ product.featured_image | img_url: 'original' }} + + {{ product.featured_image | img_url: 'master', scale: 2, crop: 'center' }} + {{ product.featured_image | image_url: width: 100 }} + - name: "should suggest a fix to replace the deprecated 'article_img_url' filter" + files: + file.liquid: "{{ article.image | article_img_url }}" + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'article_img_url'." + start: [0, 17] + highlight: "| article_img_url" + suggest: + - message: "Replace 'article_img_url' with 'image_url'." + result: "{{ article.image | image_url: width: 100, height: 100 }}" + - name: "should suggest a fix to replace the deprecated 'article_img_url' filter with named sizes" + files: + file.liquid: '{{ article.image | article_img_url: "grande" }}' + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'article_img_url'." + start: [0, 17] + highlight: '| article_img_url: "grande"' + suggest: + - message: "Replace 'article_img_url' with 'image_url'." + result: "{{ article.image | image_url: width: 600, height: 600 }}" + - name: "should suggest a fix to replace the deprecated 'collection_img_url' filter" + files: + file.liquid: "{{ collection.image | collection_img_url }}" + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'collection_img_url'." + start: [0, 20] + highlight: "| collection_img_url" + suggest: + - message: "Replace 'collection_img_url' with 'image_url'." + result: "{{ collection.image | image_url: width: 100, height: 100 }}" + - name: "should suggest a fix to replace the deprecated 'collection_img_url' filter with named sizes" + files: + file.liquid: '{{ collection.image | collection_img_url: "grande" }}' + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'collection_img_url'." + start: [0, 20] + highlight: '| collection_img_url: "grande"' + suggest: + - message: "Replace 'collection_img_url' with 'image_url'." + result: "{{ collection.image | image_url: width: 600, height: 600 }}" + - name: "should suggest a fix to replace the deprecated 'product_img_url' filter" + files: + file.liquid: "{{ p.featured_image | product_img_url }}" + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'product_img_url'." + start: [0, 20] + highlight: "| product_img_url" + suggest: + - message: "Replace 'product_img_url' with 'image_url'." + result: "{{ p.featured_image | image_url: width: 100, height: 100 }}" + - name: "should suggest a fix to replace the deprecated 'product_img_url' filter with named sizes" + files: + file.liquid: '{{ p.featured_image | product_img_url: "grande" }}' + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'product_img_url'." + start: [0, 20] + highlight: '| product_img_url: "grande"' + suggest: + - message: "Replace 'product_img_url' with 'image_url'." + result: "{{ p.featured_image | image_url: width: 600, height: 600 }}" + - name: "should suggest a fix to replace the deprecated 'img_tag' filter" + files: + file.liquid: | + {{ product | img_tag }} + {{ product | img_tag: 'image alt text' }} + {{ product | img_tag: 'image alt text', 'my-css-class' }} + {{ product | img_tag: 'image alt text', 'my-css-class', '200x300' }} + {{ product | img_tag: 'image alt text', 'my-css-class', 'grande' }} + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'img_tag'." + start: [0, 11] + highlight: "| img_tag" + suggest: + - message: "Replace 'img_tag' with 'image_tag'." + result: | + {{ product | image_url: width: 100 | image_tag }} + {{ product | img_tag: 'image alt text' }} + {{ product | img_tag: 'image alt text', 'my-css-class' }} + {{ product | img_tag: 'image alt text', 'my-css-class', '200x300' }} + {{ product | img_tag: 'image alt text', 'my-css-class', 'grande' }} + - file: file.liquid + message: "Deprecated filter 'img_tag'." + start: [1, 11] + highlight: "| img_tag: 'image alt text'" + suggest: + - message: "Replace 'img_tag' with 'image_tag'." + result: | + {{ product | img_tag }} + {{ product | image_url: width: 100 | image_tag: alt: 'image alt text' }} + {{ product | img_tag: 'image alt text', 'my-css-class' }} + {{ product | img_tag: 'image alt text', 'my-css-class', '200x300' }} + {{ product | img_tag: 'image alt text', 'my-css-class', 'grande' }} + - file: file.liquid + message: "Deprecated filter 'img_tag'." + start: [2, 11] + highlight: "| img_tag: 'image alt text', 'my-css-class'" + suggest: + - message: "Replace 'img_tag' with 'image_tag'." + result: | + {{ product | img_tag }} + {{ product | img_tag: 'image alt text' }} + {{ product | image_url: width: 100 | image_tag: alt: 'image alt text', class: 'my-css-class' }} + {{ product | img_tag: 'image alt text', 'my-css-class', '200x300' }} + {{ product | img_tag: 'image alt text', 'my-css-class', 'grande' }} + - file: file.liquid + message: "Deprecated filter 'img_tag'." + start: [3, 11] + highlight: "| img_tag: 'image alt text', 'my-css-class', '200x300'" + suggest: + - message: "Replace 'img_tag' with 'image_tag'." + result: | + {{ product | img_tag }} + {{ product | img_tag: 'image alt text' }} + {{ product | img_tag: 'image alt text', 'my-css-class' }} + {{ product | image_url: width: 200, height: 300 | image_tag: width: 200, height: 300, alt: 'image alt text', class: 'my-css-class' }} + {{ product | img_tag: 'image alt text', 'my-css-class', 'grande' }} + - file: file.liquid + message: "Deprecated filter 'img_tag'." + start: [4, 11] + highlight: "| img_tag: 'image alt text', 'my-css-class', 'grande'" + suggest: + - message: "Replace 'img_tag' with 'image_tag'." + result: | + {{ product | img_tag }} + {{ product | img_tag: 'image alt text' }} + {{ product | img_tag: 'image alt text', 'my-css-class' }} + {{ product | img_tag: 'image alt text', 'my-css-class', '200x300' }} + {{ product | image_url: width: 600, height: 600 | image_tag: width: 600, height: 600, alt: 'image alt text', class: 'my-css-class' }} + - name: reports from the pipe when a non-breaking space comes before it + files: + file.liquid: "{{ article.image\u00a0| article_img_url }}" + docset: default + offenses: + - file: file.liquid + message: "Deprecated filter 'article_img_url'." + start: [0, 17] + highlight: "| article_img_url" + suggest: + - message: "Replace 'article_img_url' with 'image_url'." + result: "{{ article.image\u00a0| image_url: width: 100, height: 100 }}" diff --git a/specs/checks/deprecated-fonts-on-sections-and-blocks.yml b/specs/checks/deprecated-fonts-on-sections-and-blocks.yml new file mode 100644 index 000000000..46002e7ad --- /dev/null +++ b/specs/checks/deprecated-fonts-on-sections-and-blocks.yml @@ -0,0 +1,495 @@ +check: DeprecatedFontsOnSectionsAndBlocks +severity: warning +cases: + - name: reports no warning when the setting has a non-deprecated font as default value + files: + sections/file.liquid: |2- + {% schema %} + { + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "alegreya_n4" + } + ] + } + {% endschema %} + - name: reports a warning when setting has a deprecated font as default value + files: + sections/file.liquid: |2- + {% schema %} + { + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "helvetica_n4" + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [7, 19] + highlight: '"helvetica_n4"' + - name: reports a warning when multiple settings have a deprecated font as default value + files: + sections/file.liquid: |2- + {% schema %} + { + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "helvetica_n4" + }, + { + "type": "font_picker", + "id": "footer_font", + "label": "Footer font", + "default": "helvetica_n4" + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [7, 19] + highlight: '"helvetica_n4"' + - file: sections/file.liquid + message: "setting 'footer_font' is using deprecated font 'helvetica_n4'" + start: [13, 19] + highlight: '"helvetica_n4"' + - name: does not report a warning when the local block setting has no deprecated font + files: + sections/file.liquid: |2- + {% schema %} + { + "blocks": [{ + "type": "my-local-block", + "name": "My Local Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "alegreya_n4" + } + ] + }] + } + {% endschema %} + - name: reports a warning when the local block setting has a deprecated font + files: + sections/file.liquid: |2- + {% schema %} + { + "blocks": [{ + "type": "my-local-block", + "name": "My Local Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "helvetica_n4" + } + ] + }] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [10, 21] + highlight: '"helvetica_n4"' + - name: reports no warning when the preset settings do not use deprecated fonts + files: + sections/file.liquid: |2- + {% schema %} + { + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "alegreya_n4" + } + ], + "presets": [ + { + "name": "Preset 1", + "settings": { + "heading_font": "alegreya_n4" + } + } + ] + } + {% endschema %} + - name: reports a warning when the preset setting has a deprecated font as default value + files: + sections/file.liquid: |2- + {% schema %} + { + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "alegreya_n4" + } + ], + "presets": [ + { + "name": "Preset 1", + "settings": { + "heading_font": "helvetica_n4" + } + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [14, 26] + highlight: '"helvetica_n4"' + - name: reports no warning when the preset block setting has no deprecated fonts (blocks as a hash) + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "presets": [ + { + "name": "Preset 1", + "blocks": { + "text-1": { + "type": "text", + "settings": { + "heading_font": "alegreya_n4" + } + } + } + } + ] + } + {% endschema %} + blocks/text.liquid: |- + {% schema %} + { + "name": "Text Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + } + {% endschema %} + - name: reports a warning when the preset block setting has a deprecated font (blocks as a hash) + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "presets": [ + { + "name": "Preset 1", + "blocks": { + "text-1": { + "type": "text", + "settings": { + "heading_font": "helvetica_n4" + } + } + } + } + ] + } + {% endschema %} + blocks/text.liquid: |- + {% schema %} + { + "name": "Text Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + } + {% endschema %} + offenses: + - file: sections/section-1.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [10, 30] + highlight: '"helvetica_n4"' + - name: reports a warning when the preset block setting has a deprecated font in a nested block (blocks as a hash) + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "presets": [ + { + "name": "Preset 1", + "blocks": { + "text-1": { + "type": "text", + "blocks": { + "text-2": { + "type": "text", + "settings": { "heading_font": "helvetica_n4" } + } + } + } + } + } + ] + } + {% endschema %} + blocks/text.liquid: |- + {% schema %} + { + "name": "Text Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + } + {% endschema %} + offenses: + - file: sections/section-1.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [12, 46] + highlight: '"helvetica_n4"' + - name: reports no warning when the preset block setting has no deprecated fonts (blocks as an array) + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "presets": [ + { + "name": "Preset 1", + "blocks": [ + { + "type": "text", + "settings": { "heading_font": "alegreya_n4" } + } + ] + } + ] + } + {% endschema %} + blocks/text.liquid: |- + {% schema %} + { + "name": "Text Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + } + {% endschema %} + - name: reports a warning when the preset block setting has a deprecated font (blocks as an array) + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "presets": [ + { + "name": "Preset 1", + "blocks": [ + { + "type": "text", + "settings": { "heading_font": "helvetica_n4" } + } + ] + } + ] + } + {% endschema %} + blocks/text.liquid: |- + {% schema %} + { + "name": "Text Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + } + {% endschema %} + offenses: + - file: sections/section-1.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [9, 42] + highlight: '"helvetica_n4"' + - name: reports a warning when the preset nested block setting has a deprecated fonts (blocks as an array) + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "presets": [ + { + "name": "Preset 1", + "blocks": [ + { + "type": "text", + "blocks": [ + { + "type": "text", + "settings": { "heading_font": "helvetica_n4" } + } + ] + } + ] + } + ] + } + {% endschema %} + blocks/text.liquid: |- + {% schema %} + { + "name": "Text Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + } + {% endschema %} + offenses: + - file: sections/section-1.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [12, 46] + highlight: '"helvetica_n4"' + - name: reports no warning when the section default setting has no deprecated fonts + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + "defaults": { + "settings": { + "heading_font": "alegreya_n4" + } + } + } + {% endschema %} + - name: reports a warning when the section default setting has a deprecated font + files: + sections/file.liquid: |- + {% schema %} + { + "name": "Section 1", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + "default": { + "settings": { + "heading_font": "helvetica_n4" + } + } + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [12, 24] + highlight: '"helvetica_n4"' + - name: reports no warning when the section default block setting has no deprecated fonts + files: + sections/section-1.liquid: |- + {% schema %} + { + "name": "Section 1", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ], + "blocks": [ + { + "type": "text" + } + ], + "defaults": { + "blocks": { + "type": "text", + "settings": { + "heading_font": "alegreya_n4" + } + } + } + } + {% endschema %} + - name: reports a warning when the section default block setting has a deprecated font + files: + sections/file.liquid: |- + {% schema %} + { + "name": "Section 1", + "blocks": [ + { + "type": "text", + "name": "Local Text Block", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font" + } + ] + } + ], + "default": { + "blocks": [{ + "type": "text", + "settings": { + "heading_font": "helvetica_n4" + } + }] + } + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "setting 'heading_font' is using deprecated font 'helvetica_n4'" + start: [20, 26] + highlight: '"helvetica_n4"' diff --git a/specs/checks/deprecated-fonts-on-settings-data.yml b/specs/checks/deprecated-fonts-on-settings-data.yml new file mode 100644 index 000000000..117c76146 --- /dev/null +++ b/specs/checks/deprecated-fonts-on-settings-data.yml @@ -0,0 +1,85 @@ +check: DeprecatedFontsOnSettingsData +severity: warning +cases: + - name: reports a warning when settings_data.json has a deprecated font in current settings + files: + config/settings_data.json: |- + { + "current": { + "heading_font": "helvetica_n4", + "body_font": "alegreya_n4" + } + } + offenses: + - file: config/settings_data.json + message: 'The font "helvetica_n4" is deprecated' + start: [2, 20] + highlight: '"helvetica_n4"' + - name: reports warnings for deprecated fonts in presets + files: + config/settings_data.json: |- + { + "current": {}, + "presets": { + "Default": { + "heading_font": "helvetica_n4", + "body_font": "agmena_n4" + } + } + } + offenses: + - file: config/settings_data.json + message: 'The font "helvetica_n4" is deprecated' + start: [4, 22] + highlight: '"helvetica_n4"' + - file: config/settings_data.json + message: 'The font "agmena_n4" is deprecated' + start: [5, 19] + highlight: '"agmena_n4"' + - name: reports no warning when settings_data.json has no deprecated fonts + files: + config/settings_data.json: |- + { + "current": { + "heading_font": "alegreya_n4", + "body_font": "bitter_n4" + } + } + - name: reports no warning for non-string values + files: + config/settings_data.json: |- + { + "current": { + "show_header": true, + "items_per_page": 12, + "colors_body_bg": "#ffffff" + } + } + - name: does not run check on files other than settings_data.json + files: + config/settings_schema.json: |- + { + "current": { + "heading_font": "helvetica_n4" + } + } + - name: reports warnings for deprecated fonts deeply nested in preset blocks + files: + config/settings_data.json: |- + { + "current": { + "blocks": { + "block-id-1": { + "type": "header", + "settings": { + "heading_font": "helvetica_n4" + } + } + } + } + } + offenses: + - file: config/settings_data.json + message: 'The font "helvetica_n4" is deprecated' + start: [6, 26] + highlight: '"helvetica_n4"' diff --git a/specs/checks/deprecated-fonts-on-settings-schema.yml b/specs/checks/deprecated-fonts-on-settings-schema.yml new file mode 100644 index 000000000..da4b4a074 --- /dev/null +++ b/specs/checks/deprecated-fonts-on-settings-schema.yml @@ -0,0 +1,72 @@ +check: DeprecatedFontsOnSettingsSchema +severity: warning +cases: + - name: reports a warning when settings_schema.json has a deprecated font + files: + config/settings_schema.json: |- + [ + { + "name": "Fonts", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "helvetica_n4" + } + ] + } + ] + offenses: + - file: config/settings_schema.json + message: 'The font "helvetica_n4" is deprecated' + start: [8, 23] + highlight: '"helvetica_n4"' + - name: reports no warning when settings_schema.json has non-deprecated fonts + files: + config/settings_schema.json: |- + [ + { + "name": "Fonts", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "alegreya_n4" + } + ] + } + ] + - name: reports no warning for non-font_picker settings + files: + config/settings_schema.json: |- + [ + { + "name": "Fonts", + "settings": [ + { + "type": "text", + "id": "heading_text", + "label": "Heading text", + "default": "helvetica_n4" + } + ] + } + ] + - name: does not run check on files other than settings_schema.json + files: + config/other_file.json: |- + [ + { + "name": "Fonts", + "settings": [ + { + "type": "font_picker", + "id": "heading_font", + "label": "Heading font", + "default": "helvetica_n4" + } + ] + } + ] diff --git a/specs/checks/deprecated-tag.yml b/specs/checks/deprecated-tag.yml new file mode 100644 index 000000000..7b8af7c48 --- /dev/null +++ b/specs/checks/deprecated-tag.yml @@ -0,0 +1,76 @@ +check: DeprecatedTag +severity: warning +cases: + - name: should report an offense when include tag is used + files: + file.liquid: | + {% include 'templates/foo.liquid' %} + offenses: + - file: file.liquid + message: "Use the 'render' tag instead of 'include'" + start: [0, 3] + highlight: include + suggest: + - message: "Replace 'include' with 'render'" + result: | + {% render 'templates/foo.liquid' %} + - name: should not report an offense when render tag is used + files: + file.liquid: | + {% render 'templates/foo.liquid' %} + - name: should suggest replacing include tag with render tag + files: + file.liquid: | + {% include 'foo.liquid' %} + {% assign greeting = "hello world" %} + {% include 'greeting.liquid' %} + offenses: + - file: file.liquid + message: "Use the 'render' tag instead of 'include'" + start: [0, 3] + highlight: include + suggest: + - message: "Replace 'include' with 'render'" + result: | + {% render 'foo.liquid' %} + {% assign greeting = "hello world" %} + {% include 'greeting.liquid' %} + - file: file.liquid + message: "Use the 'render' tag instead of 'include'" + start: [2, 3] + highlight: include + suggest: + - message: "Replace 'include' with 'render'" + result: | + {% include 'foo.liquid' %} + {% assign greeting = "hello world" %} + {% render 'greeting.liquid' %} + - name: should report multiple offenses when multiple include tags are used in the same line + files: + file.liquid: "{% include 'foo.liquid' %} Some text {% include 'bar.liquid' %}" + offenses: + - file: file.liquid + message: "Use the 'render' tag instead of 'include'" + start: [0, 3] + highlight: include + suggest: + - message: "Replace 'include' with 'render'" + result: "{% render 'foo.liquid' %} Some text {% include 'bar.liquid' %}" + - file: file.liquid + message: "Use the 'render' tag instead of 'include'" + start: [0, 40] + highlight: include + suggest: + - message: "Replace 'include' with 'render'" + result: "{% include 'foo.liquid' %} Some text {% render 'bar.liquid' %}" + - name: should report offenses when include tags are nested + files: + file.liquid: "{% if true %} {% include 'foo.liquid' %} {% endif %}" + offenses: + - file: file.liquid + message: "Use the 'render' tag instead of 'include'" + start: [0, 17] + highlight: include + suggest: + - message: "Replace 'include' with 'render'" + result: "{% if true %} {% render 'foo.liquid' %} {% endif %}" diff --git a/specs/checks/disabled-checks.yml b/specs/checks/disabled-checks.yml new file mode 100644 index 000000000..d34b7edba --- /dev/null +++ b/specs/checks/disabled-checks.yml @@ -0,0 +1,461 @@ +# How theme-check-disable comments turn checks off. Each case lists the checks it runs. +cases: + - name: should disable checks for the entire document if comment is placed on the first line, in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable UnknownFilter %} + {% comment %} + This is some comment about the file... + Adding a comment here should not mess with theme-check-disable + {% endcomment %} + {{ 'asset' | random_filter }} + {% render 'something' %} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/something.liquid' does not exist" + start: [6, 10] + highlight: "'something'" + - name: should disable checks for the entire document if comment is placed on the first line, in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% comment %}theme-check-disable UnknownFilter{% endcomment %} + {% comment %} + This is some comment about the file... + Adding a comment here should not mess with theme-check-disable + {% endcomment %} + {{ 'asset' | random_filter }} + {% render 'something' %} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/something.liquid' does not exist" + start: [6, 10] + highlight: "'something'" + - name: should disable all checks even if comment has additional spaces, in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable %} + {{ 'asset' | random_filter }} + {% render 'something' %} + - name: should disable all checks even if comment has additional spaces, in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% comment %} theme-check-disable {% endcomment %} + {{ 'asset' | random_filter }} + {% render 'something' %} + - name: should disable all checks for a section of the template, in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {{ 'asset-1' | random_filter }} + {% render 'something-1' %} + {% # theme-check-disable %} + {{ 'asset-2' | random_filter }} + {% render 'something-2' %} + {% # theme-check-enable %} + {{ 'asset-3' | random_filter }} + offenses: + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [0, 13] + highlight: "| random_filter" + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-1.liquid' does not exist" + start: [1, 10] + highlight: "'something-1'" + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [6, 13] + highlight: "| random_filter" + - name: should disable all checks for a section of the template, in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {{ 'asset-1' | random_filter }} + {% render 'something-1' %} + {% comment %}theme-check-disable{% endcomment %} + {{ 'asset-2' | random_filter }} + {% render 'something-2' %} + {% comment %}theme-check-enable{% endcomment %} + {{ 'asset-3' | random_filter }} + offenses: + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [0, 13] + highlight: "| random_filter" + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-1.liquid' does not exist" + start: [1, 10] + highlight: "'something-1'" + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [6, 13] + highlight: "| random_filter" + - name: should disable a specific check if check is included in the comment, in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable UnknownFilter %} + {{ 'asset-1' | random_filter }} + {% render 'something' %} + {% # theme-check-enable UnknownFilter %} + {{ 'asset-2' | random_filter }} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/something.liquid' does not exist" + start: [2, 10] + highlight: "'something'" + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [4, 13] + highlight: "| random_filter" + - name: should disable a specific check if check is included in the comment, in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% comment %}theme-check-disable UnknownFilter{% endcomment %} + {{ 'asset-1' | random_filter }} + {% render 'something' %} + {% comment %}theme-check-enable UnknownFilter{% endcomment %} + {{ 'asset-2' | random_filter }} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/something.liquid' does not exist" + start: [2, 10] + highlight: "'something'" + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [4, 13] + highlight: "| random_filter" + - name: should disable multiple checks if checks are separated by a comma (and maybe some spaces), in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable UnknownFilter, MissingTemplate %} + {{ 'asset-1' | random_filter }} + {% render 'something' %} + {% # theme-check-enable UnknownFilter,MissingTemplate %} + {{ 'asset-2' | random_filter }} + offenses: + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [4, 13] + highlight: "| random_filter" + - name: should disable multiple checks if checks are separated by a comma (and maybe some spaces), in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% comment %}theme-check-disable UnknownFilter, MissingTemplate{% endcomment %} + {{ 'asset-1' | random_filter }} + {% render 'something' %} + {% comment %}theme-check-enable UnknownFilter,MissingTemplate{% endcomment %} + {{ 'asset-2' | random_filter }} + offenses: + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [4, 13] + highlight: "| random_filter" + - name: should enable specific checks individually, in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable UnknownFilter, MissingTemplate %} + {{ 'asset-1' | random_filter }} + {% render 'something-1' %} + {% # theme-check-enable MissingTemplate %} + {{ 'asset-2' | random_filter }} + {% render 'something-2' %} + {% # theme-check-enable UnknownFilter %} + {{ 'asset-3' | random_filter }} + {% render 'something-3' %} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-2.liquid' does not exist" + start: [5, 10] + highlight: "'something-2'" + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [7, 13] + highlight: "| random_filter" + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-3.liquid' does not exist" + start: [8, 10] + highlight: "'something-3'" + - name: should enable specific checks individually, in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% comment %}theme-check-disable UnknownFilter, MissingTemplate{% endcomment %} + {{ 'asset-1' | random_filter }} + {% render 'something-1' %} + {% comment %}theme-check-enable MissingTemplate{% endcomment %} + {{ 'asset-2' | random_filter }} + {% render 'something-2' %} + {% comment %}theme-check-enable UnknownFilter{% endcomment %} + {{ 'asset-3' | random_filter }} + {% render 'something-3' %} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-2.liquid' does not exist" + start: [5, 10] + highlight: "'something-2'" + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [7, 13] + highlight: "| random_filter" + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-3.liquid' does not exist" + start: [8, 10] + highlight: "'something-3'" + - name: should not reenable specific check when all checks have been disabled before, in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable %} + {{ 'asset-1' | random_filter }} + {% render 'something-1' %} + {% # theme-check-enable MissingTemplate %} + {{ 'asset-2' | random_filter }} + {% render 'something-2' %} + - name: should not reenable specific check when all checks have been disabled before, in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% comment %}theme-check-disable{% endcomment %} + {{ 'asset-1' | random_filter }} + {% render 'something-1' %} + {% comment %}theme-check-enable MissingTemplate{% endcomment %} + {{ 'asset-2' | random_filter }} + {% render 'something-2' %} + - name: should reenable all checks when specific ones have been disabled before, in an inline comment + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable UnknownFilter, MissingTemplate %} + {{ 'asset-3' | random_filter }} + {% render 'something-3' %} + {% # theme-check-enable %} + {{ 'asset-4' | random_filter }} + {% render 'something-4' %} + offenses: + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [4, 13] + highlight: "| random_filter" + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-4.liquid' does not exist" + start: [5, 10] + highlight: "'something-4'" + - name: should reenable all checks when specific ones have been disabled before, in a comment tag + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% comment %}theme-check-disable UnknownFilter, MissingTemplate{% endcomment %} + {{ 'asset-3' | random_filter }} + {% render 'something-3' %} + {% comment %}theme-check-enable{% endcomment %} + {{ 'asset-4' | random_filter }} + {% render 'something-4' %} + offenses: + - check: UnknownFilter + file: code.liquid + message: "Unknown filter 'random_filter' used." + start: [4, 13] + highlight: "| random_filter" + - check: MissingTemplate + file: code.liquid + message: "'snippets/something-4.liquid' does not exist" + start: [5, 10] + highlight: "'something-4'" + - name: should disable the next line if there is one + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable-next-line %} + {% render 'something' %} + {% render 'other-thing' %} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/other-thing.liquid' does not exist" + start: [2, 10] + highlight: "'other-thing'" + - name: "should not disable the liquid tag's children node if theme check is disabled" + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% # theme-check-disable-next-line %} + {% if condition %} + {% render 'something' %} + {% endif %} + {% render 'other-thing' %} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/something.liquid' does not exist" + start: [2, 12] + highlight: "'something'" + - check: MissingTemplate + file: code.liquid + message: "'snippets/other-thing.liquid' does not exist" + start: [4, 10] + highlight: "'other-thing'" + - name: should disable the next line inside a liquid tag if there is one + checks: [UnknownFilter, MissingTemplate] + docset: default + files: + code.liquid: |- + {% liquid + # theme-check-disable-next-line + render 'something' + render 'other-thing' + %} + offenses: + - check: MissingTemplate + file: code.liquid + message: "'snippets/other-thing.liquid' does not exist" + start: [3, 9] + highlight: "'other-thing'" + - name: "should disable the parent node's next node if theme check is disabled as the last child node" + checks: [UndefinedObject] + docset: default + files: + code.liquid: |- + {% liquid + if condition + # theme-check-disable-next-line + elsif other_condition + endif + %} + offenses: + - check: UndefinedObject + file: code.liquid + message: "Unknown object 'condition' used." + start: [1, 5] + highlight: condition + - name: should not disable any checks if theme-check is disabled at the end + checks: [UndefinedObject] + docset: default + files: + code.liquid: |- + {% liquid + echo hello + echo everyone + # theme-check-disable-next-line + %} + offenses: + - check: UndefinedObject + file: code.liquid + message: "Unknown object 'hello' used." + start: [1, 7] + highlight: hello + - check: UndefinedObject + file: code.liquid + message: "Unknown object 'everyone' used." + start: [2, 7] + highlight: everyone + - name: should disable the next line if the content is an HTML tag with liquid + checks: [UndefinedObject] + docset: default + files: + code.liquid: |- + {% # theme-check-disable-next-line %} +
    +
    + offenses: + - check: UndefinedObject + file: code.liquid + message: "Unknown object 'bar' used." + start: [2, 15] + highlight: bar + - name: should not disable the next line if the specified rule does not exist + checks: [UndefinedObject] + docset: default + files: + code.liquid: |- + {% # theme-check-disable-next-line FAKE_RULE %} +
    + offenses: + - check: UndefinedObject + file: code.liquid + message: "Unknown object 'foo' used." + start: [1, 15] + highlight: foo + - name: should disable checks inside doc tags when disable-next-line is placed before the doc tag + checks: [UniqueDocParamNames] + docset: default + files: + code.liquid: |- + {% # theme-check-disable-next-line UniqueDocParamNames %} + {% doc %} + @param foo - first param + @param foo - duplicate param + {% enddoc %} + - name: should disable all checks inside doc tags when disable-next-line is used without specific check names + checks: [UniqueDocParamNames] + docset: default + files: + code.liquid: |- + {% # theme-check-disable-next-line %} + {% doc %} + @param bar - first param + @param bar - duplicate param + {% enddoc %} + - name: should still report offenses in doc tags when disable-next-line is not present + checks: [UniqueDocParamNames] + docset: default + files: + code.liquid: |- + {% doc %} + @param baz - first param + @param baz - duplicate param + {% enddoc %} + offenses: + - check: UniqueDocParamNames + file: code.liquid + message: "The parameter 'baz' is defined more than once." + start: [2, 9] + highlight: baz diff --git a/specs/checks/docsets/default.yml b/specs/checks/docsets/default.yml new file mode 100644 index 000000000..19a098cfa --- /dev/null +++ b/specs/checks/docsets/default.yml @@ -0,0 +1,51 @@ +# The docset most cases use: a few filters and objects, and no tags. +filters: + - name: item_count_for_variant + - name: link_to_type + - name: link_to_vendor + - name: append + - name: upcase + - name: downcase + - name: color_to_rgb + - name: hex_to_rgba + deprecated: true + deprecation_reason: '`hex_to_rgba` has been replaced by [`color_to_rgb`](/do...' + - name: currency_selector + deprecated: true + deprecation_reason: 'Deprecated without a direct replacement because the [cur...' + - name: article_img_url + deprecated: true + deprecation_reason: '`article_img_url` has been replaced by [`image_url`](/d...' + - name: collection_img_url + deprecated: true + deprecation_reason: '`collection_img_url` has been replaced by [`image_url`](...' + - name: img_tag + deprecated: true + deprecation_reason: '`img_tag` has been replaced by [`image_tag`](/docs/api/...' + - name: img_url + deprecated: true + deprecation_reason: '`img_url` has been replaced by [`image_url`](/docs/api/...' + - name: product_img_url + deprecated: true + deprecation_reason: '`product_img_url` has been replaced by [`image_url`](/d...' +objects: + - name: collections + - name: product + access: { global: false, parents: [], template: [product] } + - name: image + access: { global: false, parents: [], template: [] } + - name: section + access: { global: false, parents: [], template: [] } + - name: block + access: { global: false, parents: [], template: [] } + - name: app + access: { global: false, parents: [], template: [] } + - name: predictive_search + access: { global: false, parents: [], template: [] } + - name: recommendations + access: { global: false, parents: [], template: [] } + - name: comment + access: { global: false, parents: [], template: [] } +tags: [] +systemTranslations: + shopify.sentence.words_connector: ', ' diff --git a/specs/checks/duplicate-block-arguments.yml b/specs/checks/duplicate-block-arguments.yml new file mode 100644 index 000000000..32f1baee4 --- /dev/null +++ b/specs/checks/duplicate-block-arguments.yml @@ -0,0 +1,60 @@ +check: DuplicateBlockArguments +severity: warning +cases: + - name: reports duplicate arguments after interface sources are merged + files: + templates/test.liquid: "{% block 'card', heading: 'a', heading: 'b' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Duplicate argument 'heading' in block tag for 'card'." + start: [0, 31] + highlight: "heading: 'b'" + - name: does not report unique arguments + files: + templates/test.liquid: "{% block 'card', heading: 'a', content: body %}{% endblock %}" + - name: "warns that text body content takes precedence over content:" + files: + templates/test.liquid: "{% block 'card', content: body %}Body{% endblock %}" + offenses: + - file: templates/test.liquid + message: "The explicit 'content' argument has no effect because the inline block body takes precedence." + start: [0, 17] + highlight: "content: body" + - name: "warns that comment body content takes precedence over content:" + files: + templates/test.liquid: "{% block 'card', content: body %}{% comment %}x{% endcomment %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "The explicit 'content' argument has no effect because the inline block body takes precedence." + start: [0, 17] + highlight: "content: body" + - name: "does not let a whitespace-only body override content:" + files: + templates/test.liquid: |- + {% block 'card', content: body %} + + {% endblock %} + - name: "preserves the duplicate warning when body content also overrides content:" + files: + templates/test.liquid: "{% block 'card', content: first, content: second %}Body{% endblock %}" + offenses: + - file: templates/test.liquid + message: "The explicit 'content' argument has no effect because the inline block body takes precedence." + start: [0, 17] + highlight: "content: first" + - file: templates/test.liquid + message: "Duplicate argument 'content' in block tag for 'card'." + start: [0, 33] + highlight: "content: second" + - file: templates/test.liquid + message: "The explicit 'content' argument has no effect because the inline block body takes precedence." + start: [0, 33] + highlight: "content: second" + - name: reports duplicates without resolving the target block + files: + templates/test.liquid: "{% block 'missing', value: 'a', value: 'b' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Duplicate argument 'value' in block tag for 'missing'." + start: [0, 32] + highlight: "value: 'b'" diff --git a/specs/checks/duplicate-content-for-arguments.yml b/specs/checks/duplicate-content-for-arguments.yml new file mode 100644 index 000000000..660dd9779 --- /dev/null +++ b/specs/checks/duplicate-content-for-arguments.yml @@ -0,0 +1,86 @@ +check: DuplicateContentForArguments +severity: warning +cases: + - name: should report duplicate arguments in content_for tags + files: + file.liquid: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3' %} + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in content_for tag for static block 'fake-block'." + start: [0, 80] + highlight: "param1: 'value3'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2' %} + - name: should report multiple duplicate arguments in content_for tags + files: + file.liquid: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3', param2: 'value4', param1: 'value5' %} + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in content_for tag for static block 'fake-block'." + start: [0, 80] + highlight: "param1: 'value3'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param2: 'value4', param1: 'value5' %} + - file: file.liquid + message: "Duplicate argument 'param2' in content_for tag for static block 'fake-block'." + start: [0, 98] + highlight: "param2: 'value4'" + suggest: + - message: "Remove duplicate argument 'param2'" + result: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3', param1: 'value5' %} + - file: file.liquid + message: "Duplicate argument 'param1' in content_for tag for static block 'fake-block'." + start: [0, 116] + highlight: "param1: 'value5'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3', param2: 'value4' %} + - name: should correctly suggest fixing all duplicate arguments except for the first + files: + file.liquid: "{% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3', param1: 'value4' %}" + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in content_for tag for static block 'fake-block'." + start: [0, 80] + highlight: "param1: 'value3'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: "{% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value4' %}" + - file: file.liquid + message: "Duplicate argument 'param1' in content_for tag for static block 'fake-block'." + start: [0, 98] + highlight: "param1: 'value4'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: "{% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param1: 'value3' %}" + - name: should not report when there are no duplicate arguments + files: + file.liquid: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param3: 'value3' %} + - name: should not report for variable render tags where snippet name is a variable + files: + file.liquid: | + {% render my_variable, param1: 'value1', param1: 'value2' %} + - name: should handle remove duplicate param when there are multiple render tags + files: + file.liquid: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param3: 'value3' %} + {% content_for 'block', type: 'fake-block', param1: 'value4', param2: 'value5', param1: 'value6' %} + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in content_for tag for static block 'fake-block'." + start: [1, 80] + highlight: "param1: 'value6'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% content_for 'block', type: 'fake-block', param1: 'value1', param2: 'value2', param3: 'value3' %} + {% content_for 'block', type: 'fake-block', param1: 'value4', param2: 'value5' %} diff --git a/specs/checks/duplicate-render-snippet-arguments.yml b/specs/checks/duplicate-render-snippet-arguments.yml new file mode 100644 index 000000000..c1a85a2f1 --- /dev/null +++ b/specs/checks/duplicate-render-snippet-arguments.yml @@ -0,0 +1,110 @@ +check: DuplicateRenderSnippetArguments +severity: warning +cases: + - name: should report duplicate arguments in render tags + files: + file.liquid: | + {% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3' %} + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in render tag for snippet 'snippet'." + start: [0, 57] + highlight: "param1: 'value3'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% render 'snippet', param1: 'value1', param2: 'value2' %} + - name: should report multiple duplicate arguments in render tags + files: + file.liquid: | + {% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3', param2: 'value4', param1: 'value5' %} + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in render tag for snippet 'snippet'." + start: [0, 57] + highlight: "param1: 'value3'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% render 'snippet', param1: 'value1', param2: 'value2', param2: 'value4', param1: 'value5' %} + - file: file.liquid + message: "Duplicate argument 'param2' in render tag for snippet 'snippet'." + start: [0, 75] + highlight: "param2: 'value4'" + suggest: + - message: "Remove duplicate argument 'param2'" + result: | + {% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3', param1: 'value5' %} + - file: file.liquid + message: "Duplicate argument 'param1' in render tag for snippet 'snippet'." + start: [0, 93] + highlight: "param1: 'value5'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3', param2: 'value4' %} + - name: "should not report when render tag is using `with/for` alias syntax" + files: + file.liquid: | + {% render 'snippet' with 'string' as param1, param1: 'value1' %} + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in render tag for snippet 'snippet'." + start: [0, 45] + highlight: "param1: 'value1'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% render 'snippet' with 'string' as param1 %} + - name: should correctly suggest fixing all duplicate arguments except for the first + files: + file.liquid: "{% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3', param1: 'value4' %}" + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in render tag for snippet 'snippet'." + start: [0, 57] + highlight: "param1: 'value3'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: "{% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value4' %}" + - file: file.liquid + message: "Duplicate argument 'param1' in render tag for snippet 'snippet'." + start: [0, 75] + highlight: "param1: 'value4'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: "{% render 'snippet', param1: 'value1', param2: 'value2', param1: 'value3' %}" + - name: should suggest removing duplicates when with / for alias is used + files: + file.liquid: "{% render 'snippet' with 'string' as param2, param1: 'value1', param2: 'value2' %}" + offenses: + - file: file.liquid + message: "Duplicate argument 'param2' in render tag for snippet 'snippet'." + start: [0, 63] + highlight: "param2: 'value2'" + suggest: + - message: "Remove duplicate argument 'param2'" + result: "{% render 'snippet' with 'string' as param2, param1: 'value1' %}" + - name: should not report when there are no duplicate arguments + files: + file.liquid: | + {% render 'snippet', param1: 'value1', param2: 'value2', param3: 'value3' %} + - name: should not report for variable render tags where snippet name is a variable + files: + file.liquid: | + {% render my_variable, param1: 'value1', param1: 'value2' %} + - name: should handle remove duplicate param when there are multiple render tags + files: + file.liquid: | + {% render 'snippet', param1: 'value1', param2: 'value2', param3: 'value3' %} + {% render 'snippet', param1: 'value4', param2: 'value5', param1: 'value6' %} + offenses: + - file: file.liquid + message: "Duplicate argument 'param1' in render tag for snippet 'snippet'." + start: [1, 57] + highlight: "param1: 'value6'" + suggest: + - message: "Remove duplicate argument 'param1'" + result: | + {% render 'snippet', param1: 'value1', param2: 'value2', param3: 'value3' %} + {% render 'snippet', param1: 'value4', param2: 'value5' %} diff --git a/specs/checks/empty-block-content.yml b/specs/checks/empty-block-content.yml new file mode 100644 index 000000000..ecebae5a8 --- /dev/null +++ b/specs/checks/empty-block-content.yml @@ -0,0 +1,161 @@ +check: EmptyBlockContent +severity: warning +cases: + - name: should not report offenses when the blocks array is not empty in sections + files: + sections/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test", + "blocks": [ + { + "type": "test-block" + } + ] + } + {% endschema %} + - name: should report offenses when the blocks array is empty in sections + files: + sections/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test", + "blocks": [] + } + {% endschema %} + offenses: + - file: sections/test_block.liquid + message: "The 'content_for \"blocks\"' tag is present, but the blocks array is empty." + start: [1, 4] + highlight: "{% content_for 'blocks' %}" + - name: should report offenses when the blocks array is empty with the correct indices in sections + files: + sections/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test", + "blocks": [] + } + {% endschema %} + offenses: + - file: sections/test_block.liquid + message: "The 'content_for \"blocks\"' tag is present, but the blocks array is empty." + start: [1, 4] + highlight: "{% content_for 'blocks' %}" + - name: should report offenses when the blocks array has not been defined in sections + files: + sections/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test" + } + {% endschema %} + offenses: + - file: sections/test_block.liquid + message: "The 'content_for \"blocks\"' tag is present, but the blocks array is not defined." + start: [1, 4] + highlight: "{% content_for 'blocks' %}" + - name: "should not report offenses when 'content_for blocks' is not present in sections" + files: + sections/test_block.liquid: | + {% schema %} + { + "name": "Test", + "blocks": [ + { + "type": "test-block" + } + ] + } + {% endschema %} + - name: should not report offenses when the blocks array is not empty in blocks + files: + blocks/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test", + "blocks": [ + { + "type": "test-block" + } + ] + } + {% endschema %} + - name: should report offenses when the blocks array is empty in blocks + files: + blocks/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test", + "blocks": [] + } + {% endschema %} + offenses: + - file: blocks/test_block.liquid + message: "The 'content_for \"blocks\"' tag is present, but the blocks array is empty." + start: [1, 4] + highlight: "{% content_for 'blocks' %}" + - name: should report offenses when the blocks array is empty with the correct indices in blocks + files: + blocks/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test", + "blocks": [] + } + {% endschema %} + offenses: + - file: blocks/test_block.liquid + message: "The 'content_for \"blocks\"' tag is present, but the blocks array is empty." + start: [1, 4] + highlight: "{% content_for 'blocks' %}" + - name: should report offenses when the blocks array has not been defined in blocks + files: + blocks/test_block.liquid: | +
    + {% content_for 'blocks' %} +
    + {% schema %} + { + "name": "Test" + } + {% endschema %} + offenses: + - file: blocks/test_block.liquid + message: "The 'content_for \"blocks\"' tag is present, but the blocks array is not defined." + start: [1, 4] + highlight: "{% content_for 'blocks' %}" + - name: "should not report offenses when 'content_for blocks' is not present in blocks" + files: + blocks/test_block.liquid: | + {% schema %} + { + "name": "Test", + "blocks": [ + { + "type": "test-block" + } + ] + } + {% endschema %} diff --git a/specs/checks/excessive-settings-count.yml b/specs/checks/excessive-settings-count.yml new file mode 100644 index 000000000..2af756f9d --- /dev/null +++ b/specs/checks/excessive-settings-count.yml @@ -0,0 +1,293 @@ +check: ExcessiveSettingsCount +severity: warning +cases: + - name: does not report at the tolerated maximum + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "name": "Test", + "settings": [ + { "type": "text", "id": "setting_0", "label": "Setting 0" }, + { "type": "text", "id": "setting_1", "label": "Setting 1" }, + { "type": "text", "id": "setting_2", "label": "Setting 2" }, + { "type": "text", "id": "setting_3", "label": "Setting 3" }, + { "type": "text", "id": "setting_4", "label": "Setting 4" }, + { "type": "text", "id": "setting_5", "label": "Setting 5" }, + { "type": "text", "id": "setting_6", "label": "Setting 6" }, + { "type": "text", "id": "setting_7", "label": "Setting 7" }, + { "type": "text", "id": "setting_8", "label": "Setting 8" }, + { "type": "text", "id": "setting_9", "label": "Setting 9" }, + { "type": "text", "id": "setting_10", "label": "Setting 10" }, + { "type": "text", "id": "setting_11", "label": "Setting 11" }, + { "type": "text", "id": "setting_12", "label": "Setting 12" }, + { "type": "text", "id": "setting_13", "label": "Setting 13" }, + { "type": "text", "id": "setting_14", "label": "Setting 14" }, + { "type": "text", "id": "setting_15", "label": "Setting 15" }, + { "type": "text", "id": "setting_16", "label": "Setting 16" }, + { "type": "text", "id": "setting_17", "label": "Setting 17" }, + { "type": "text", "id": "setting_18", "label": "Setting 18" }, + { "type": "text", "id": "setting_19", "label": "Setting 19" }, + { "type": "text", "id": "setting_20", "label": "Setting 20" }, + { "type": "text", "id": "setting_21", "label": "Setting 21" }, + { "type": "text", "id": "setting_22", "label": "Setting 22" }, + { "type": "text", "id": "setting_23", "label": "Setting 23" }, + { "type": "text", "id": "setting_24", "label": "Setting 24" }, + { "type": "text", "id": "setting_25", "label": "Setting 25" }, + { "type": "text", "id": "setting_26", "label": "Setting 26" }, + { "type": "text", "id": "setting_27", "label": "Setting 27" }, + { "type": "text", "id": "setting_28", "label": "Setting 28" }, + { "type": "text", "id": "setting_29", "label": "Setting 29" }, + { "type": "text", "id": "setting_30", "label": "Setting 30" }, + { "type": "text", "id": "setting_31", "label": "Setting 31" }, + { "type": "text", "id": "setting_32", "label": "Setting 32" }, + { "type": "text", "id": "setting_33", "label": "Setting 33" }, + { "type": "text", "id": "setting_34", "label": "Setting 34" }, + { "type": "text", "id": "setting_35", "label": "Setting 35" }, + { "type": "text", "id": "setting_36", "label": "Setting 36" }, + { "type": "text", "id": "setting_37", "label": "Setting 37" }, + { "type": "text", "id": "setting_38", "label": "Setting 38" }, + { "type": "text", "id": "setting_39", "label": "Setting 39" } + ] + } + {% endschema %} + - name: reports when settings exceed the tolerated maximum + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "name": "Test", + "settings": [ + { "type": "text", "id": "setting_0", "label": "Setting 0" }, + { "type": "text", "id": "setting_1", "label": "Setting 1" }, + { "type": "text", "id": "setting_2", "label": "Setting 2" }, + { "type": "text", "id": "setting_3", "label": "Setting 3" }, + { "type": "text", "id": "setting_4", "label": "Setting 4" }, + { "type": "text", "id": "setting_5", "label": "Setting 5" }, + { "type": "text", "id": "setting_6", "label": "Setting 6" }, + { "type": "text", "id": "setting_7", "label": "Setting 7" }, + { "type": "text", "id": "setting_8", "label": "Setting 8" }, + { "type": "text", "id": "setting_9", "label": "Setting 9" }, + { "type": "text", "id": "setting_10", "label": "Setting 10" }, + { "type": "text", "id": "setting_11", "label": "Setting 11" }, + { "type": "text", "id": "setting_12", "label": "Setting 12" }, + { "type": "text", "id": "setting_13", "label": "Setting 13" }, + { "type": "text", "id": "setting_14", "label": "Setting 14" }, + { "type": "text", "id": "setting_15", "label": "Setting 15" }, + { "type": "text", "id": "setting_16", "label": "Setting 16" }, + { "type": "text", "id": "setting_17", "label": "Setting 17" }, + { "type": "text", "id": "setting_18", "label": "Setting 18" }, + { "type": "text", "id": "setting_19", "label": "Setting 19" }, + { "type": "text", "id": "setting_20", "label": "Setting 20" }, + { "type": "text", "id": "setting_21", "label": "Setting 21" }, + { "type": "text", "id": "setting_22", "label": "Setting 22" }, + { "type": "text", "id": "setting_23", "label": "Setting 23" }, + { "type": "text", "id": "setting_24", "label": "Setting 24" }, + { "type": "text", "id": "setting_25", "label": "Setting 25" }, + { "type": "text", "id": "setting_26", "label": "Setting 26" }, + { "type": "text", "id": "setting_27", "label": "Setting 27" }, + { "type": "text", "id": "setting_28", "label": "Setting 28" }, + { "type": "text", "id": "setting_29", "label": "Setting 29" }, + { "type": "text", "id": "setting_30", "label": "Setting 30" }, + { "type": "text", "id": "setting_31", "label": "Setting 31" }, + { "type": "text", "id": "setting_32", "label": "Setting 32" }, + { "type": "text", "id": "setting_33", "label": "Setting 33" }, + { "type": "text", "id": "setting_34", "label": "Setting 34" }, + { "type": "text", "id": "setting_35", "label": "Setting 35" }, + { "type": "text", "id": "setting_36", "label": "Setting 36" }, + { "type": "text", "id": "setting_37", "label": "Setting 37" }, + { "type": "text", "id": "setting_38", "label": "Setting 38" }, + { "type": "text", "id": "setting_39", "label": "Setting 39" }, + { "type": "text", "id": "setting_40", "label": "Setting 40" } + ] + } + {% endschema %} + offenses: + - file: sections/test.liquid + message: This schema declares 41 settings, which exceeds the maximum of 40. Consider splitting this section or block into smaller pieces, or grouping related options with a header. + start: [1, 0] + highlight: "{% schema %}" + - name: reports when count exceeds a custom maximum + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "name": "Test", + "settings": [ + { "type": "text", "id": "setting_0", "label": "Setting 0" }, + { "type": "text", "id": "setting_1", "label": "Setting 1" }, + { "type": "text", "id": "setting_2", "label": "Setting 2" } + ] + } + {% endschema %} + config: + ExcessiveSettingsCount: { maxSettings: 2 } + offenses: + - file: sections/test.liquid + message: This schema declares 3 settings, which exceeds the maximum of 2. Consider splitting this section or block into smaller pieces, or grouping related options with a header. + start: [1, 0] + highlight: "{% schema %}" + - name: does not report when count equals the configured maximum + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "name": "Test", + "settings": [ + { "type": "text", "id": "setting_0", "label": "Setting 0" }, + { "type": "text", "id": "setting_1", "label": "Setting 1" }, + { "type": "text", "id": "setting_2", "label": "Setting 2" }, + { "type": "text", "id": "setting_3", "label": "Setting 3" }, + { "type": "text", "id": "setting_4", "label": "Setting 4" }, + { "type": "text", "id": "setting_5", "label": "Setting 5" }, + { "type": "text", "id": "setting_6", "label": "Setting 6" }, + { "type": "text", "id": "setting_7", "label": "Setting 7" }, + { "type": "text", "id": "setting_8", "label": "Setting 8" }, + { "type": "text", "id": "setting_9", "label": "Setting 9" }, + { "type": "text", "id": "setting_10", "label": "Setting 10" }, + { "type": "text", "id": "setting_11", "label": "Setting 11" }, + { "type": "text", "id": "setting_12", "label": "Setting 12" }, + { "type": "text", "id": "setting_13", "label": "Setting 13" }, + { "type": "text", "id": "setting_14", "label": "Setting 14" }, + { "type": "text", "id": "setting_15", "label": "Setting 15" }, + { "type": "text", "id": "setting_16", "label": "Setting 16" }, + { "type": "text", "id": "setting_17", "label": "Setting 17" }, + { "type": "text", "id": "setting_18", "label": "Setting 18" }, + { "type": "text", "id": "setting_19", "label": "Setting 19" }, + { "type": "text", "id": "setting_20", "label": "Setting 20" }, + { "type": "text", "id": "setting_21", "label": "Setting 21" }, + { "type": "text", "id": "setting_22", "label": "Setting 22" }, + { "type": "text", "id": "setting_23", "label": "Setting 23" }, + { "type": "text", "id": "setting_24", "label": "Setting 24" }, + { "type": "text", "id": "setting_25", "label": "Setting 25" }, + { "type": "text", "id": "setting_26", "label": "Setting 26" }, + { "type": "text", "id": "setting_27", "label": "Setting 27" }, + { "type": "text", "id": "setting_28", "label": "Setting 28" }, + { "type": "text", "id": "setting_29", "label": "Setting 29" }, + { "type": "text", "id": "setting_30", "label": "Setting 30" }, + { "type": "text", "id": "setting_31", "label": "Setting 31" }, + { "type": "text", "id": "setting_32", "label": "Setting 32" }, + { "type": "text", "id": "setting_33", "label": "Setting 33" }, + { "type": "text", "id": "setting_34", "label": "Setting 34" }, + { "type": "text", "id": "setting_35", "label": "Setting 35" }, + { "type": "text", "id": "setting_36", "label": "Setting 36" }, + { "type": "text", "id": "setting_37", "label": "Setting 37" }, + { "type": "text", "id": "setting_38", "label": "Setting 38" }, + { "type": "text", "id": "setting_39", "label": "Setting 39" } + ] + } + {% endschema %} + config: + ExcessiveSettingsCount: { maxSettings: 40 } + - name: excludes header and paragraph entries that have no id + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "settings": [ + { "type": "header", "content": "Group" }, + { "type": "text", "id": "a", "label": "A" }, + { "type": "paragraph", "content": "Note" }, + { "type": "text", "id": "b", "label": "B" }, + { "type": "text", "id": "c", "label": "C" } + ] + } + {% endschema %} + config: + ExcessiveSettingsCount: { maxSettings: 3 } + - name: ignores settings nested in blocks + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "settings": [ + { "type": "text", "id": "setting_0", "label": "Setting 0" }, + { "type": "text", "id": "setting_1", "label": "Setting 1" }, + { "type": "text", "id": "setting_2", "label": "Setting 2" }, + { "type": "text", "id": "setting_3", "label": "Setting 3" }, + { "type": "text", "id": "setting_4", "label": "Setting 4" } + ], + "blocks": [ + { + "type": "child", + "settings": [ + { "type": "text", "id": "nested_0", "label": "N0" }, + { "type": "text", "id": "nested_1", "label": "N1" }, + { "type": "text", "id": "nested_2", "label": "N2" }, + { "type": "text", "id": "nested_3", "label": "N3" }, + { "type": "text", "id": "nested_4", "label": "N4" }, + { "type": "text", "id": "nested_5", "label": "N5" }, + { "type": "text", "id": "nested_6", "label": "N6" }, + { "type": "text", "id": "nested_7", "label": "N7" }, + { "type": "text", "id": "nested_8", "label": "N8" }, + { "type": "text", "id": "nested_9", "label": "N9" }, + { "type": "text", "id": "nested_10", "label": "N10" }, + { "type": "text", "id": "nested_11", "label": "N11" }, + { "type": "text", "id": "nested_12", "label": "N12" }, + { "type": "text", "id": "nested_13", "label": "N13" }, + { "type": "text", "id": "nested_14", "label": "N14" }, + { "type": "text", "id": "nested_15", "label": "N15" }, + { "type": "text", "id": "nested_16", "label": "N16" }, + { "type": "text", "id": "nested_17", "label": "N17" }, + { "type": "text", "id": "nested_18", "label": "N18" }, + { "type": "text", "id": "nested_19", "label": "N19" } + ] + } + ] + } + {% endschema %} + config: + ExcessiveSettingsCount: { maxSettings: 6 } + - name: reports above the threshold and not at the threshold (1) + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "name": "Test", + "settings": [ + { "type": "text", "id": "setting_0", "label": "Setting 0" }, + { "type": "text", "id": "setting_1", "label": "Setting 1" }, + { "type": "text", "id": "setting_2", "label": "Setting 2" }, + { "type": "text", "id": "setting_3", "label": "Setting 3" }, + { "type": "text", "id": "setting_4", "label": "Setting 4" }, + { "type": "text", "id": "setting_5", "label": "Setting 5" } + ] + } + {% endschema %} + config: + ExcessiveSettingsCount: { maxSettings: 5 } + offenses: + - file: sections/test.liquid + message: This schema declares 6 settings, which exceeds the maximum of 5. Consider splitting this section or block into smaller pieces, or grouping related options with a header. + start: [1, 0] + highlight: "{% schema %}" + - name: reports above the threshold and not at the threshold (2) + files: + sections/test.liquid: |- +
    section
    + {% schema %} + { + "name": "Test", + "settings": [ + { "type": "text", "id": "setting_0", "label": "Setting 0" }, + { "type": "text", "id": "setting_1", "label": "Setting 1" }, + { "type": "text", "id": "setting_2", "label": "Setting 2" }, + { "type": "text", "id": "setting_3", "label": "Setting 3" }, + { "type": "text", "id": "setting_4", "label": "Setting 4" } + ] + } + {% endschema %} + config: + ExcessiveSettingsCount: { maxSettings: 5 } + - name: does not report when schema is absent + files: + sections/test.liquid: "
    {{ product.title }}
    " + config: + ExcessiveSettingsCount: { maxSettings: 1 } diff --git a/specs/checks/hardcoded-routes.yml b/specs/checks/hardcoded-routes.yml new file mode 100644 index 000000000..5caeb36bd --- /dev/null +++ b/specs/checks/hardcoded-routes.yml @@ -0,0 +1,55 @@ +check: HardcodedRoutes +severity: warning +cases: + - name: should report offenses for hardcoded routes + files: + file.liquid: | + +
    + + + +
  • {{ 'Socks' | link_to: '/collections/socks' }}
  • +
  • {{ 'Home' | link_to: '/' }}
  • +
  • {{ 'Cart (' | append: cart.item_count | append: ')' | link_to: '/cart' }}
  • + offenses: + - file: file.liquid + message: Use routes object {{ routes.root_url }} instead of hardcoding / + start: [0, 9] + highlight: "/" + - file: file.liquid + message: Use routes object {{ routes.search_url }} instead of hardcoding /search + start: [1, 14] + highlight: "/search" + - file: file.liquid + message: Use routes object {{ routes.account_login_url }} instead of hardcoding /account/login + start: [2, 9] + highlight: "/account/login" + - file: file.liquid + message: Use routes object {{ routes.collections_url }} instead of hardcoding /collections + start: [3, 9] + highlight: "/collections" + - file: file.liquid + message: Use routes object {{ routes.cart_change_url }} instead of hardcoding /cart/change + start: [4, 9] + highlight: "/cart/change" + - file: file.liquid + message: Use routes object {{ routes.collections_url }} instead of hardcoding /collections + start: [5, 27] + highlight: "/collections" + - file: file.liquid + message: Use routes object {{ routes.root_url }} instead of hardcoding / + start: [6, 26] + highlight: "/" + - file: file.liquid + message: Use routes object {{ routes.cart_url }} instead of hardcoding /cart + start: [7, 68] + highlight: "/cart" + - name: should not report offenses for route objects + files: + file.liquid: | +
    + + + +
  • {{ 'About' | link_to: '/pages/about' }}
  • diff --git a/specs/checks/img-width-and-height.yml b/specs/checks/img-width-and-height.yml new file mode 100644 index 000000000..ed3322946 --- /dev/null +++ b/specs/checks/img-width-and-height.yml @@ -0,0 +1,58 @@ +check: ImgWidthAndHeight +severity: error +cases: + - name: should report missing attributes on multiple img tags with mixed attributes + files: + file.liquid: | + + + + + offenses: + - file: file.liquid + message: Missing height attribute on img tag + start: [1, 0] + highlight: '' + - file: file.liquid + message: Missing width attribute on img tag + start: [2, 0] + highlight: '' + - file: file.liquid + message: Missing width and height attributes on img tag + start: [3, 0] + highlight: '' + - name: should report missing width and height attributes on img tags + files: + file.liquid: | + content + + + offenses: + - file: file.liquid + message: Missing width and height attributes on img tag + start: [2, 0] + highlight: '' + - name: should report missing height attribute on img tags + files: + file.liquid: | + + + offenses: + - file: file.liquid + message: Missing height attribute on img tag + start: [1, 0] + highlight: '' + - name: should report missing width attribute on img tags + files: + file.liquid: | + + + offenses: + - file: file.liquid + message: Missing width attribute on img tag + start: [1, 0] + highlight: '' + - name: should not report missing attributes on img tags if they are not missing + files: + file.liquid: | + diff --git a/specs/checks/javascript-once-per-file.yml b/specs/checks/javascript-once-per-file.yml new file mode 100644 index 000000000..51167425c --- /dev/null +++ b/specs/checks/javascript-once-per-file.yml @@ -0,0 +1,18 @@ +check: JavascriptOncePerFile +severity: error +cases: + - name: reports second and subsequent javascript tags in a file + files: + sections/test.liquid: |- + {% javascript %}{% endjavascript %} + {% javascript %}{% endjavascript %} + {% javascript %}{% endjavascript %} + offenses: + - file: sections/test.liquid + message: "{% javascript %} can only appear once per file." + start: [1, 0] + highlight: "{% javascript %}" + - file: sections/test.liquid + message: "{% javascript %} can only appear once per file." + start: [2, 0] + highlight: "{% javascript %}" diff --git a/specs/checks/javascript-tag-in-wrong-file.yml b/specs/checks/javascript-tag-in-wrong-file.yml new file mode 100644 index 000000000..fdf810fea --- /dev/null +++ b/specs/checks/javascript-tag-in-wrong-file.yml @@ -0,0 +1,20 @@ +check: JavascriptTagInWrongFile +severity: error +cases: + - name: allows javascript tags in sections/test.liquid + files: + sections/test.liquid: "{% javascript %}{% endjavascript %}" + - name: allows javascript tags in blocks/test.liquid + files: + blocks/test.liquid: "{% javascript %}{% endjavascript %}" + - name: allows javascript tags in snippets/test.liquid + files: + snippets/test.liquid: "{% javascript %}{% endjavascript %}" + - name: reports javascript tags in templates/index.liquid + files: + templates/index.liquid: "{% javascript %}{% endjavascript %}" + offenses: + - file: templates/index.liquid + message: "{% javascript %} is only valid in section, block, or snippet files." + start: [0, 0] + highlight: "{% javascript %}" diff --git a/specs/checks/json-missing-block.yml b/specs/checks/json-missing-block.yml new file mode 100644 index 000000000..41d84f09a --- /dev/null +++ b/specs/checks/json-missing-block.yml @@ -0,0 +1,358 @@ +check: JSONMissingBlock +severity: error +cases: + - name: should report an offense when a block does not exist + files: + templates/product.failing.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "text_tqQTNE": { + "type": "missing_block" + } + }, + "block_order": ["text_tqQTNE"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "missing_block" + } + ] + } + {% endschema %} + offenses: + - file: templates/product.failing.json + message: "Theme block 'blocks/missing_block.liquid' does not exist." + start: [6, 26] + highlight: '"missing_block"' + - name: should not report an offense when block exists + files: + templates/product.valid.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "text_block": { + "type": "text" + } + }, + "block_order": ["text_block"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + blocks/text.liquid: "" + - name: should report an offense when a nested block does not exist + files: + templates/product.nested.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "parent_block": { + "type": "text", + "blocks": { + "child_block": { + "type": "missing_nested" + } + } + } + }, + "block_order": ["parent_block"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + blocks/text.liquid: | + {% schema %} + { + "name": "Text Block", + "blocks": [ + { + "type": "missing_nested" + } + ] + } + {% endschema %} + offenses: + - file: templates/product.nested.json + message: "Theme block 'blocks/missing_nested.liquid' does not exist." + start: [9, 30] + highlight: '"missing_nested"' + - name: should report an offense when block exists but is not in the section liquid schema + files: + templates/product.valid.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "text_block": { + "type": "text" + } + }, + "block_order": ["text_block"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "image" + } + ] + } + {% endschema %} + blocks/text.liquid: "" + blocks/image.liquid: "" + offenses: + - file: templates/product.valid.json + message: "Block type 'text' is not allowed in 'sections/custom-section.liquid'." + start: [6, 26] + highlight: '"text"' + - name: should report an offense when a nested block exists but is not in the block liquid schema + files: + templates/product.nested.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "parent_block": { + "type": "text", + "blocks": { + "child_block": { + "type": "missing_nested" + } + } + } + }, + "block_order": ["parent_block"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + blocks/text.liquid: | + {% schema %} + { + "name": "Text Block", + "blocks": [ + { + "type": "image" + } + ] + } + {% endschema %} + blocks/image.liquid: "" + blocks/missing_nested.liquid: "" + offenses: + - file: templates/product.nested.json + message: "Block type 'missing_nested' is not allowed in 'blocks/text.liquid'." + start: [9, 30] + highlight: '"missing_nested"' + - name: should report an offense when a nested private block exists but is not in the block liquid schema + files: + templates/product.nested.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "parent_block": { + "type": "text", + "blocks": { + "child_block": { + "type": "_private_block" + } + } + } + }, + "block_order": ["parent_block"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + blocks/text.liquid: | + {% schema %} + { + "name": "Text Block", + "blocks": [ + { + "type": "@theme" + } + ] + } + {% endschema %} + blocks/image.liquid: "" + blocks/_private_block.liquid: "" + offenses: + - file: templates/product.nested.json + message: "Block type '_private_block' is not allowed in 'blocks/text.liquid'." + start: [9, 30] + highlight: '"_private_block"' + - name: should not report an offense when a static block exists and is not in the block liquid schema + files: + templates/product.static.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "child_block": { + "type": "text" + }, + "static_block": { + "type": "static_block", + "static": true + } + }, + "block_order": ["child_block"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + blocks/text.liquid: | + {% schema %} + { + "name": "Text Block", + "blocks": [ + { + "type": "@theme" + } + ] + } + {% endschema %} + blocks/static_block.liquid: | + {% schema %} + { + "name": "Static Block", + } + {% endschema %} + - name: should ignore non-template JSON files + files: + config/index.json: |- + { + "blocks": { + "text_block": { + "type": "nonexistent" + } + } + } + - name: should ignore templates with local blocks + files: + templates/product.nested.json: |- + { + "sections": { + "custom-section": { + "type": "custom-section", + "blocks": { + "parent_block": { + "type": "text", + "blocks": { + "child_block": { + "type": "_private_block" + } + } + } + }, + "block_order": ["parent_block"] + } + }, + "order": ["custom-section"] + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "Custom Section", + "blocks": [ + { + "type": "text", + "name": "Text Block" + } + ] + } + {% endschema %} + blocks/text.liquid: | + {% schema %} + { + "name": "Text Block", + "blocks": [ + { + "type": "image", + "name": "Image Block" + } + ] + } + {% endschema %} + blocks/image.liquid: "" + blocks/_private_block.liquid: "" diff --git a/specs/checks/json-missing-section.yml b/specs/checks/json-missing-section.yml new file mode 100644 index 000000000..f186f4318 --- /dev/null +++ b/specs/checks/json-missing-section.yml @@ -0,0 +1,173 @@ +check: JSONMissingSection +severity: error +cases: + - name: JSON templates > should report an offense when a section file does not exist + files: + templates/index.json: |- + { + "sections": { + "hero_AAAAAA": { + "type": "featured-collection" + } + }, + "order": ["hero_AAAAAA"] + } + offenses: + - file: templates/index.json + message: "Section type 'featured-collection' does not refer to an existing section file" + start: [3, 22] + highlight: '"featured-collection"' + - name: JSON templates > should not report an offense when the section file exists + files: + templates/index.json: |- + { + "sections": { + "hero_AAAAAA": { + "type": "featured-collection" + } + }, + "order": ["hero_AAAAAA"] + } + sections/featured-collection.liquid: "" + - name: should report an offense for each missing section + files: + templates/index.json: |- + { + "sections": { + "hero_AAAAAA": { + "type": "missing-one" + }, + "main": { + "type": "existing-section" + }, + "footer_BBBBBB": { + "type": "missing-two" + } + }, + "order": ["hero_AAAAAA", "main", "footer_BBBBBB"] + } + sections/existing-section.liquid: "" + offenses: + - file: templates/index.json + message: "Section type 'missing-one' does not refer to an existing section file" + start: [3, 22] + highlight: '"missing-one"' + - file: templates/index.json + message: "Section type 'missing-two' does not refer to an existing section file" + start: [9, 22] + highlight: '"missing-two"' + - name: should not report an offense for section types with platform defaults + files: + templates/index.json: |- + { + "sections": { + "apps_section": { + "type": "apps" + }, + "blocks_section": { + "type": "_blocks" + } + }, + "order": ["apps_section", "blocks_section"] + } + - name: should not report an offense when a section entry has no type + files: + templates/index.json: |- + { + "sections": { + "inherited_section": { + "settings": {} + } + }, + "order": ["inherited_section"] + } + - name: Section groups > should report an offense when a section file does not exist + files: + sections/header-group.json: |- + { + "type": "header", + "name": "Header group", + "sections": { + "announcement": { + "type": "missing-announcement-bar" + } + }, + "order": ["announcement"] + } + offenses: + - file: sections/header-group.json + message: "Section type 'missing-announcement-bar' does not refer to an existing section file" + start: [5, 22] + highlight: '"missing-announcement-bar"' + - name: Section groups > should not report an offense when the section file exists + files: + sections/header-group.json: |- + { + "type": "header", + "name": "Header group", + "sections": { + "announcement": { + "type": "announcement-bar" + } + }, + "order": ["announcement"] + } + sections/announcement-bar.liquid: "" + - name: should not report an offense for the section group type itself + files: + sections/header-group.json: |- + { + "type": "header", + "name": "Header group", + "sections": {}, + "order": [] + } + - name: should ignore JSON files outside templates/ and sections/ + files: + config/settings_data.json: |- + { + "sections": { + "main": { + "type": "nonexistent" + } + } + } + locales/en.default.json: |- + { + "sections": { + "main": { "type": "nonexistent" } + } + } + - name: should not report an offense when sections is not an object + files: + templates/index.json: |- + { + "sections": "not-an-object", + "order": [] + } + - name: should report each duplicate section key at its own location + files: + templates/index.json: |- + { + "sections": { + "main": { + "type": "missing-one" + }, + "main": { + "type": "missing-two" + } + }, + "order": ["main"] + } + offenses: + - file: templates/index.json + message: "Section type 'missing-one' does not refer to an existing section file" + start: [3, 22] + highlight: '"missing-one"' + - file: templates/index.json + message: "Section type 'missing-two' does not refer to an existing section file" + start: [6, 22] + highlight: '"missing-two"' + - name: should not report an offense when the JSON is invalid + files: + templates/index.json: '{ "sections": { "main": { "type": ' diff --git a/specs/checks/json-syntax-error.yml b/specs/checks/json-syntax-error.yml new file mode 100644 index 000000000..f8528c12a --- /dev/null +++ b/specs/checks/json-syntax-error.yml @@ -0,0 +1,57 @@ +check: JSONSyntaxError +severity: error +cases: + - name: should report an error for invalid JSON (0) + files: + file.json: |- + { + "key1": "value1", + "key2": "value2",, + } + offenses: + - file: file.json + message: Property name expected + start: [2, 23] + highlight: "," + - file: file.json + message: Expecting a value + start: [2, 23] + highlight: "," + - name: should report an error for invalid JSON (1) + files: + file.json: | + { + "key1": "value1", + "key2": "value2" + offenses: + - file: file.json + message: Expecting a closing brace (}) + start: [3, 0] + highlight: "" + - name: should report an error for invalid JSON (2) + files: + file.json: |- + { + 'key1': "value1", + "key2": "value2" + } + offenses: + - file: file.json + message: Invalid symbol + start: [1, 6] + highlight: "'key1'" + - file: file.json + message: Property name expected + start: [1, 12] + highlight: ":" + - file: file.json + message: Expecting a value + start: [1, 22] + highlight: "," + - name: should not report any errors for valid JSON + files: + file.json: |- + { + "key1": "value1", + "key2": "value2" + } diff --git a/specs/checks/liquid-complexity.yml b/specs/checks/liquid-complexity.yml new file mode 100644 index 000000000..2497c33af --- /dev/null +++ b/specs/checks/liquid-complexity.yml @@ -0,0 +1,189 @@ +check: LiquidComplexity +severity: warning +cases: + - name: does not report when complexity equals the default maximum of 120 + files: + snippets/test.liquid: [{ repeat: "{% if enabled %}{% endif %}\n", times: 119 }] + - name: reports when complexity exceeds the default maximum of 120 + files: + snippets/test.liquid: [{ repeat: "{% if enabled %}{% endif %}\n", times: 120 }] + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 121, which exceeds the maximum of 120. The decision at line 120 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [119, 0] + highlight: "{% if enabled %}" + - name: counts nested logical expressions, with a maximum of 5 + config: + LiquidComplexity: { maxComplexity: 5 } + files: + snippets/test.liquid: |- + {% if available and visible %} + Featured product + {% elsif featured or highlighted %} + Highlighted product + {% endif %} + - name: counts nested logical expressions, with a maximum of 4 + config: + LiquidComplexity: { maxComplexity: 4 } + files: + snippets/test.liquid: |- + {% if available and visible %} + Featured product + {% elsif featured or highlighted %} + Highlighted product + {% endif %} + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 5, which exceeds the maximum of 4. The decision at line 1 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [0, 6] + highlight: "available and visible " + - name: counts logical expressions in unless conditions, with a maximum of 3 + config: + LiquidComplexity: { maxComplexity: 3 } + files: + snippets/test.liquid: |- + {% unless hidden or archived %} + Visible product + {% endunless %} + - name: counts logical expressions in unless conditions, with a maximum of 2 + config: + LiquidComplexity: { maxComplexity: 2 } + files: + snippets/test.liquid: |- + {% unless hidden or archived %} + Visible product + {% endunless %} + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 3, which exceeds the maximum of 2. The decision at line 1 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [0, 10] + highlight: "hidden or archived " + - name: does not count logical expressions in variable output or assign tags + config: + LiquidComplexity: { maxComplexity: 1 } + files: + snippets/test.liquid: |- + {% assign visible = product.available and customer %} + {{ product.available or customer }} + - name: counts decision points inside liquid tags, with a maximum of 3 + config: + LiquidComplexity: { maxComplexity: 3 } + files: + snippets/test.liquid: |- + {% liquid + if enabled + echo 'Enabled' + elsif archived + echo 'Archived' + endif + %} + - name: counts decision points inside liquid tags, with a maximum of 2 + config: + LiquidComplexity: { maxComplexity: 2 } + files: + snippets/test.liquid: |- + {% liquid + if enabled + echo 'Enabled' + elsif archived + echo 'Archived' + endif + %} + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 3, which exceeds the maximum of 2. The decision at line 4 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [3, 8] + highlight: elsif archived + - name: counts each nested condition as another decision point, with a maximum of 9 + config: + LiquidComplexity: { maxComplexity: 9 } + files: + snippets/test.liquid: |- + {% if product.available and product.published %} + {% unless customer.banned or customer.guest %} + {% if customer.tags contains 'vip' and settings.vip_enabled %} + VIP offer + {% elsif product.tags contains 'sale' or product.compare_at_price > product.price %} + Sale offer + {% endif %} + {% endunless %} + {% endif %} + - name: counts each nested condition as another decision point, with a maximum of 8 + config: + LiquidComplexity: { maxComplexity: 8 } + files: + snippets/test.liquid: |- + {% if product.available and product.published %} + {% unless customer.banned or customer.guest %} + {% if customer.tags contains 'vip' and settings.vip_enabled %} + VIP offer + {% elsif product.tags contains 'sale' or product.compare_at_price > product.price %} + Sale offer + {% endif %} + {% endunless %} + {% endif %} + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 9, which exceeds the maximum of 8. The decision at line 1 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [0, 6] + highlight: "product.available and product.published " + - name: resets complexity for each file + config: + LiquidComplexity: { maxComplexity: 2 } + files: + snippets/first.liquid: '{% if enabled %}{% endif %}' + snippets/second.liquid: '{% if enabled %}{% endif %}' + - name: tolerates parse errors + config: + LiquidComplexity: { maxComplexity: 1 } + files: + snippets/test.liquid: '{% if enabled %}' + - name: reports plain files when maxComplexity is 0 + config: + LiquidComplexity: { maxComplexity: 0 } + files: + snippets/test.liquid: Hello + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 1, which exceeds the maximum of 0. The decision at line 1 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [0, 0] + highlight: Hello + - name: counts case consistently with if and elsif, with a maximum of 4 + config: + LiquidComplexity: { maxComplexity: 4 } + files: + snippets/test.liquid: |- + {% case status %} + {% when 'active' %} + Active + {% when 'draft' %} + Draft + {% endcase %} + - name: counts case consistently with if and elsif, with a maximum of 3 + config: + LiquidComplexity: { maxComplexity: 3 } + files: + snippets/test.liquid: |- + {% case status %} + {% when 'active' %} + Active + {% when 'draft' %} + Draft + {% endcase %} + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 4, which exceeds the maximum of 3. The decision at line 4 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [3, 6] + highlight: "{% when 'draft' %}" + - name: reports the range of the decision point that first exceeds the maximum + config: + LiquidComplexity: { maxComplexity: 2 } + files: + snippets/test.liquid: |- + {% if first %}{% endif %} + {% if second %}{% endif %} + offenses: + - file: snippets/test.liquid + message: Liquid complexity is 3, which exceeds the maximum of 2. The decision at line 2 pushed it over the limit. Consider simplifying conditional logic, or moving self-contained decision logic into snippets with the render tag. + start: [1, 0] + highlight: "{% if second %}" diff --git a/specs/checks/liquid-doc-enum-arguments.yml b/specs/checks/liquid-doc-enum-arguments.yml new file mode 100644 index 000000000..e73f93b8f --- /dev/null +++ b/specs/checks/liquid-doc-enum-arguments.yml @@ -0,0 +1,2492 @@ +cases: + - name: "render > accepts enum member 'heading'" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'heading' %}" + docset: default + - name: 'render > accepts enum member "heading"' + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: \"heading\" %}" + docset: default + - name: "render > accepts enum member 'small'" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'small' %}" + docset: default + - name: 'render > accepts enum member "small"' + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: \"small\" %}" + docset: default + - name: "render > rejects literal 'Heading' and highlights only the value" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'Heading' %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'Heading'" + start: [0, 27] + highlight: "'Heading'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: "render > rejects literal 'body' and highlights only the value" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'body' %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 27] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > rejects literal 123 and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 123 %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 123" + start: [0, 27] + highlight: "123" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > rejects literal false and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: false %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got false" + start: [0, 27] + highlight: "false" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > rejects literal nil and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: nil %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got nil" + start: [0, 27] + highlight: nil + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > rejects literal null and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: null %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got null" + start: [0, 27] + highlight: "null" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > rejects literal empty and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: empty %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got empty" + start: [0, 27] + highlight: empty + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > rejects literal blank and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: blank %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got blank" + start: [0, 27] + highlight: blank + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > rejects literal (1..3) and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: (1..3) %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got (1..3)" + start: [0, 27] + highlight: "(1..3)" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > leaves dynamic value style unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: style %}" + docset: default + - name: render > leaves dynamic value block.settings.style unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: block.settings.style %}" + docset: default + - name: "render > leaves dynamic value settings['style'] unverified" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: settings['style'] %}" + docset: default + - name: "render > does not cascade errors from unsupported declaration 'heading' |" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' |} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'body' %}" + docset: default + - name: "render > does not cascade errors from unsupported declaration 'heading' | number" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | number} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'body' %}" + docset: default + - name: render > offers each allowed member as a replacement and preserves its quoting (1) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'body' %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 27] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% render 'text', variant: \"small\" %}" + - name: render > offers each allowed member as a replacement and preserves its quoting (2) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'heading' %}" + docset: default + - name: render > offers each allowed member as a replacement and preserves its quoting (3) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: \"small\" %}" + docset: default + - name: "content_for > accepts enum member 'heading'" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - name: 'content_for > accepts enum member "heading"' + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: \"heading\" %}" + - name: "content_for > accepts enum member 'small'" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'small' %}" + - name: 'content_for > accepts enum member "small"' + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: "content_for > rejects literal 'Heading' and highlights only the value" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'Heading' %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'Heading'" + start: [0, 59] + highlight: "'Heading'" + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: "content_for > rejects literal 'body' and highlights only the value" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'body' %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 59] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > rejects literal 123 and highlights only the value + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 123 %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 123" + start: [0, 59] + highlight: "123" + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > rejects literal false and highlights only the value + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: false %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got false" + start: [0, 59] + highlight: "false" + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > rejects literal nil and highlights only the value + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: nil %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got nil" + start: [0, 59] + highlight: nil + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > rejects literal null and highlights only the value + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: null %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got null" + start: [0, 59] + highlight: "null" + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > rejects literal empty and highlights only the value + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: empty %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got empty" + start: [0, 59] + highlight: empty + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > rejects literal blank and highlights only the value + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: blank %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got blank" + start: [0, 59] + highlight: blank + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > rejects literal (1..3) and highlights only the value + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: (1..3) %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got (1..3)" + start: [0, 59] + highlight: "(1..3)" + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > leaves dynamic value style unverified + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: style %}" + - name: content_for > leaves dynamic value block.settings.style unverified + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: block.settings.style %}" + - name: "content_for > leaves dynamic value settings['style'] unverified" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: settings['style'] %}" + - name: "content_for > does not cascade errors from unsupported declaration 'heading' |" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' |} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'body' %}" + - name: "content_for > does not cascade errors from unsupported declaration 'heading' | number" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | number} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'body' %}" + - name: content_for > offers each allowed member as a replacement and preserves its quoting (1) + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'body' %}" + offenses: + - check: ValidContentForArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 59] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - message: 'Replace with "small"' + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: content_for > offers each allowed member as a replacement and preserves its quoting (2) + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - name: content_for > offers each allowed member as a replacement and preserves its quoting (3) + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: \"small\" %}" + - name: "block > accepts enum member 'heading'" + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - name: 'block > accepts enum member "heading"' + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: \"heading\" %}Text{% endblock %}" + - name: "block > accepts enum member 'small'" + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'small' %}Text{% endblock %}" + - name: 'block > accepts enum member "small"' + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: "block > rejects literal 'Heading' and highlights only the value" + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'Heading' %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'Heading'" + start: [0, 26] + highlight: "'Heading'" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: "block > rejects literal 'body' and highlights only the value" + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'body' %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 26] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > rejects literal 123 and highlights only the value + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 123 %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 123" + start: [0, 26] + highlight: "123" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > rejects literal false and highlights only the value + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: false %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got false" + start: [0, 26] + highlight: "false" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > rejects literal nil and highlights only the value + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: nil %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got nil" + start: [0, 26] + highlight: nil + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > rejects literal null and highlights only the value + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: null %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got null" + start: [0, 26] + highlight: "null" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > rejects literal empty and highlights only the value + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: empty %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got empty" + start: [0, 26] + highlight: empty + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > rejects literal blank and highlights only the value + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: blank %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got blank" + start: [0, 26] + highlight: blank + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > rejects literal (1..3) and highlights only the value + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: (1..3) %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got (1..3)" + start: [0, 26] + highlight: "(1..3)" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > leaves dynamic value style unverified + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: style %}Text{% endblock %}" + - name: block > leaves dynamic value block.settings.style unverified + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: block.settings.style %}Text{% endblock %}" + - name: "block > leaves dynamic value settings['style'] unverified" + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: settings['style'] %}Text{% endblock %}" + - name: "block > does not cascade errors from unsupported declaration 'heading' |" + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' |} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'body' %}Text{% endblock %}" + - name: "block > does not cascade errors from unsupported declaration 'heading' | number" + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | number} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'body' %}Text{% endblock %}" + - name: block > offers each allowed member as a replacement and preserves its quoting (1) + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'body' %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 26] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: block > offers each allowed member as a replacement and preserves its quoting (2) + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - name: block > offers each allowed member as a replacement and preserves its quoting (3) + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: \"small\" %}Text{% endblock %}" + - name: "render with alias > accepts enum member 'heading'" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'heading' as variant %}" + docset: default + - name: 'render with alias > accepts enum member "heading"' + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with \"heading\" as variant %}" + docset: default + - name: "render with alias > accepts enum member 'small'" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'small' as variant %}" + docset: default + - name: 'render with alias > accepts enum member "small"' + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with \"small\" as variant %}" + docset: default + - name: "render with alias > rejects literal 'Heading' and highlights only the value" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'Heading' as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'Heading'" + start: [0, 22] + highlight: "'Heading'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: "render with alias > rejects literal 'body' and highlights only the value" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 22] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > rejects literal 123 and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 123" + start: [0, 22] + highlight: "123" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > rejects literal false and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with false as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got false" + start: [0, 22] + highlight: "false" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > rejects literal nil and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with nil as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got nil" + start: [0, 22] + highlight: nil + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > rejects literal null and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with null as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got null" + start: [0, 22] + highlight: "null" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > rejects literal empty and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with empty as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got empty" + start: [0, 22] + highlight: empty + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > rejects literal blank and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with blank as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got blank" + start: [0, 22] + highlight: blank + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > rejects literal (1..3) and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with (1..3) as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got (1..3)" + start: [0, 22] + highlight: "(1..3)" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > leaves dynamic value style unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with style as variant %}" + docset: default + - name: render with alias > leaves dynamic value block.settings.style unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with block.settings.style as variant %}" + docset: default + - name: "render with alias > leaves dynamic value settings['style'] unverified" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with settings['style'] as variant %}" + docset: default + - name: "render with alias > does not cascade errors from unsupported declaration 'heading' |" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' |} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + docset: default + - name: "render with alias > does not cascade errors from unsupported declaration 'heading' | number" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | number} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + docset: default + - name: render with alias > offers each allowed member as a replacement and preserves its quoting (1) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 22] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: render with alias > offers each allowed member as a replacement and preserves its quoting (2) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'heading' as variant %}" + docset: default + - name: render with alias > offers each allowed member as a replacement and preserves its quoting (3) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with \"small\" as variant %}" + docset: default + - name: "render for alias > accepts enum member 'heading'" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'heading' as variant %}" + docset: default + - name: 'render for alias > accepts enum member "heading"' + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for \"heading\" as variant %}" + docset: default + - name: "render for alias > accepts enum member 'small'" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'small' as variant %}" + docset: default + - name: 'render for alias > accepts enum member "small"' + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for \"small\" as variant %}" + docset: default + - name: "render for alias > rejects literal 'Heading' and highlights only the value" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'Heading' as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'Heading'" + start: [0, 21] + highlight: "'Heading'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: "render for alias > rejects literal 'body' and highlights only the value" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 21] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > rejects literal 123 and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 123" + start: [0, 21] + highlight: "123" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > rejects literal false and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for false as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got false" + start: [0, 21] + highlight: "false" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > rejects literal nil and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for nil as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got nil" + start: [0, 21] + highlight: nil + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > rejects literal null and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for null as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got null" + start: [0, 21] + highlight: "null" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > rejects literal empty and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for empty as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got empty" + start: [0, 21] + highlight: empty + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > rejects literal blank and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for blank as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got blank" + start: [0, 21] + highlight: blank + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > rejects literal (1..3) and highlights only the value + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for (1..3) as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got (1..3)" + start: [0, 21] + highlight: "(1..3)" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > leaves dynamic value style unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for style as variant %}" + docset: default + - name: render for alias > leaves dynamic value block.settings.style unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for block.settings.style as variant %}" + docset: default + - name: "render for alias > leaves dynamic value settings['style'] unverified" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for settings['style'] as variant %}" + docset: default + - name: "render for alias > does not cascade errors from unsupported declaration 'heading' |" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' |} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + docset: default + - name: "render for alias > does not cascade errors from unsupported declaration 'heading' | number" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | number} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + docset: default + - name: render for alias > offers each allowed member as a replacement and preserves its quoting (1) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 21] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: render for alias > offers each allowed member as a replacement and preserves its quoting (2) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'heading' as variant %}" + docset: default + - name: render for alias > offers each allowed member as a replacement and preserves its quoting (3) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for \"small\" as variant %}" + docset: default + - name: render permits omission of an optional enum + checks: [MissingRenderSnippetArguments] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' %}" + - name: render reports omission of a required enum + checks: [MissingRenderSnippetArguments] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' %}" + offenses: + - check: MissingRenderSnippetArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in render tag for snippet 'text'." + start: [0, 10] + highlight: "'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% render 'text', variant: 'heading' %}" + - name: content_for permits omission of an optional enum + checks: [MissingContentForArguments] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text' %}" + - name: content_for reports omission of a required enum + checks: [MissingContentForArguments] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text' %}" + offenses: + - check: MissingContentForArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in content_for tag for static block 'text'." + start: [0, 15] + highlight: "'block', type: 'text', id: 'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - name: block permits omission of an optional enum + checks: [MissingBlockArguments] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text' %}Text{% endblock %}" + - name: block reports omission of a required enum + checks: [MissingBlockArguments] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text' %}Text{% endblock %}" + offenses: + - check: MissingBlockArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in block tag for 'text'." + start: [0, 0] + highlight: "{% block 'text' %}" + - name: render with alias preserves checking of literal values against named declaration product + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product, got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: "render with alias preserves checking of literal values against named declaration product[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product[], got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: "render with alias preserves checking of literal values against named declaration string[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {string[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected string[], got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: leaves dynamic alias values unverified for named declarations (1) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with product as variant %}" + docset: default + - name: render with alias leaves uppercase declaration Product to ValidDocParamTypes (1) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + - name: render with alias leaves uppercase declaration Product to ValidDocParamTypes (2) + checks: [ValidDocParamTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product} [variant] - Text style + {% enddoc %} + {{ variant }} + docset: default + offenses: + - check: ValidDocParamTypes + file: snippets/text.liquid + message: "The parameter type 'Product' is not supported." + start: [1, 11] + highlight: "{Product}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + {{ variant }} + - name: "render with alias leaves uppercase declaration Product[] to ValidDocParamTypes (1)" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + - name: "render with alias leaves uppercase declaration Product[] to ValidDocParamTypes (2)" + checks: [ValidDocParamTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + docset: default + offenses: + - check: ValidDocParamTypes + file: snippets/text.liquid + message: "The parameter type 'Product[]' is not supported." + start: [1, 11] + highlight: "{Product[]}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + {{ variant }} + - name: render with alias leaves named declaration unknown to ValidDocParamTypes when the docset lacks it + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {unknown} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + - name: "render with alias leaves named declaration unknown[] to ValidDocParamTypes when the docset lacks it" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {unknown[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + docset: default + - name: render with alias without a docset > checks literal values against named declaration product + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product, got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: render with alias without a docset > checks literal values against named declaration Product + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product, got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: "render with alias without a docset > checks literal values against named declaration product[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product[], got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: "render with alias without a docset > checks literal values against named declaration string[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {string[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected string[], got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: render with alias without a docset > checks literal values against named declaration unknown + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {unknown} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected unknown, got number" + start: [0, 22] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' with as variant %}" + - name: render with alias without a docset > checks literal values against enum declarations + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 22] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' with 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' with \"small\" as variant %}" + - name: "render with alias without a docset > does not cascade errors from unsupported declaration 'heading' |" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' |} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + - name: "render with alias without a docset > does not cascade errors from unsupported declaration 'heading' | number" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | number} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + - name: "render with alias without a docset > does not cascade errors from unsupported declaration product[" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product[} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with 'body' as variant %}" + - name: render with alias without a docset > leaves dynamic values unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' with product as variant %}" + - name: render for alias preserves checking of literal values against named declaration product + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product, got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: "render for alias preserves checking of literal values against named declaration product[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product[], got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: "render for alias preserves checking of literal values against named declaration string[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {string[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected string[], got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: leaves dynamic alias values unverified for named declarations (2) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for product as variant %}" + docset: default + - name: render for alias leaves uppercase declaration Product to ValidDocParamTypes (1) + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + - name: render for alias leaves uppercase declaration Product to ValidDocParamTypes (2) + checks: [ValidDocParamTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product} [variant] - Text style + {% enddoc %} + {{ variant }} + docset: default + offenses: + - check: ValidDocParamTypes + file: snippets/text.liquid + message: "The parameter type 'Product' is not supported." + start: [1, 11] + highlight: "{Product}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + {{ variant }} + - name: "render for alias leaves uppercase declaration Product[] to ValidDocParamTypes (1)" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + - name: "render for alias leaves uppercase declaration Product[] to ValidDocParamTypes (2)" + checks: [ValidDocParamTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + docset: default + offenses: + - check: ValidDocParamTypes + file: snippets/text.liquid + message: "The parameter type 'Product[]' is not supported." + start: [1, 11] + highlight: "{Product[]}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + {{ variant }} + - name: render for alias leaves named declaration unknown to ValidDocParamTypes when the docset lacks it + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {unknown} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + - name: "render for alias leaves named declaration unknown[] to ValidDocParamTypes when the docset lacks it" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {unknown[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + docset: default + - name: render for alias without a docset > checks literal values against named declaration product + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product, got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: render for alias without a docset > checks literal values against named declaration Product + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {Product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product, got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: "render for alias without a docset > checks literal values against named declaration product[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected product[], got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: "render for alias without a docset > checks literal values against named declaration string[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {string[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected string[], got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: render for alias without a docset > checks literal values against named declaration unknown + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {unknown} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 123 as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Type mismatch for argument 'variant': expected unknown, got number" + start: [0, 21] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'text' for as variant %}" + - name: render for alias without a docset > checks literal values against enum declarations + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + offenses: + - check: ValidRenderSnippetArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got 'body'" + start: [0, 21] + highlight: "'body'" + suggest: + - message: "Replace with 'heading'" + result: "{% render 'text' for 'heading' as variant %}" + - message: 'Replace with "small"' + result: "{% render 'text' for \"small\" as variant %}" + - name: "render for alias without a docset > does not cascade errors from unsupported declaration 'heading' |" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' |} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + - name: "render for alias without a docset > does not cascade errors from unsupported declaration 'heading' | number" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'heading' | number} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + - name: "render for alias without a docset > does not cascade errors from unsupported declaration product[" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product[} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for 'body' as variant %}" + - name: render for alias without a docset > leaves dynamic values unverified + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' for product as variant %}" + - name: render preserves existing skips for named declaration product + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'heading' %}" + docset: default + - name: "render preserves existing skips for named declaration product[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'heading' %}" + docset: default + - name: "render preserves existing skips for named declaration string[]" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {string[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'heading' %}" + docset: default + - name: render preserves existing skips for named declaration unknown + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {unknown} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: 'heading' %}" + docset: default + - name: "render inserts a legal enum member for a missing required argument (\"Heading\" | 'small') (1)" + checks: [MissingRenderSnippetArguments] + files: + snippets/text.liquid: |- + {% doc %} + @param {"Heading" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' %}" + offenses: + - check: MissingRenderSnippetArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in render tag for snippet 'text'." + start: [0, 10] + highlight: "'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% render 'text', variant: \"Heading\" %}" + - name: "render inserts a legal enum member for a missing required argument (\"Heading\" | 'small') (2)" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {"Heading" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: \"Heading\" %}" + docset: default + - name: "render inserts a legal enum member for a missing required argument ('' | 'heading') (1)" + checks: [MissingRenderSnippetArguments] + files: + snippets/text.liquid: |- + {% doc %} + @param {'' | 'heading'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' %}" + offenses: + - check: MissingRenderSnippetArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in render tag for snippet 'text'." + start: [0, 10] + highlight: "'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% render 'text', variant: '' %}" + - name: "render inserts a legal enum member for a missing required argument ('' | 'heading') (2)" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {'' | 'heading'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: '' %}" + docset: default + - name: "render inserts a legal enum member for a missing required argument (\"a'b\" | 'small') (1)" + checks: [MissingRenderSnippetArguments] + files: + snippets/text.liquid: |- + {% doc %} + @param {"a'b" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text' %}" + offenses: + - check: MissingRenderSnippetArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in render tag for snippet 'text'." + start: [0, 10] + highlight: "'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% render 'text', variant: \"a'b\" %}" + - name: "render inserts a legal enum member for a missing required argument (\"a'b\" | 'small') (2)" + checks: [ValidRenderSnippetArgumentTypes] + files: + snippets/text.liquid: |- + {% doc %} + @param {"a'b" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% render 'text', variant: \"a'b\" %}" + docset: default + - name: content_for preserves existing skips for named declaration product + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {product} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - name: "content_for preserves existing skips for named declaration product[]" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {product[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - name: "content_for preserves existing skips for named declaration string[]" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {string[]} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - name: content_for preserves existing skips for named declaration unknown + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {unknown} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: 'heading' %}" + - name: "content_for inserts a legal enum member for a missing required argument (\"Heading\" | 'small') (1)" + checks: [MissingContentForArguments] + files: + blocks/text.liquid: |- + {% doc %} + @param {"Heading" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text' %}" + offenses: + - check: MissingContentForArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in content_for tag for static block 'text'." + start: [0, 15] + highlight: "'block', type: 'text', id: 'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"Heading\" %}" + - name: "content_for inserts a legal enum member for a missing required argument (\"Heading\" | 'small') (2)" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {"Heading" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: \"Heading\" %}" + - name: "content_for inserts a legal enum member for a missing required argument ('' | 'heading') (1)" + checks: [MissingContentForArguments] + files: + blocks/text.liquid: |- + {% doc %} + @param {'' | 'heading'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text' %}" + offenses: + - check: MissingContentForArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in content_for tag for static block 'text'." + start: [0, 15] + highlight: "'block', type: 'text', id: 'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: '' %}" + - name: "content_for inserts a legal enum member for a missing required argument ('' | 'heading') (2)" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'' | 'heading'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: '' %}" + - name: "content_for inserts a legal enum member for a missing required argument (\"a'b\" | 'small') (1)" + checks: [MissingContentForArguments] + files: + blocks/text.liquid: |- + {% doc %} + @param {"a'b" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text' %}" + offenses: + - check: MissingContentForArguments + file: templates/index.liquid + message: "Missing required argument 'variant' in content_for tag for static block 'text'." + start: [0, 15] + highlight: "'block', type: 'text', id: 'text' " + suggest: + - message: "Add required argument 'variant'" + result: "{% content_for 'block', type: 'text', id: 'text', variant: \"a'b\" %}" + - name: "content_for inserts a legal enum member for a missing required argument (\"a'b\" | 'small') (2)" + checks: [ValidContentForArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {"a'b" | 'small'} variant - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% content_for 'block', type: 'text', id: 'text', variant: \"a'b\" %}" + - name: rejects a block array literal for a string enum without trying to infer its primitive type + checks: [ValidBlockArgumentTypes] + files: + blocks/text.liquid: |- + {% doc %} + @param {'heading' | "small"} [variant] - Text style + {% enddoc %} + {{ variant }} + templates/index.liquid: "{% block 'text', variant: ['heading', 'small'] %}Text{% endblock %}" + offenses: + - check: ValidBlockArgumentTypes + file: templates/index.liquid + message: "Invalid value for argument 'variant': expected 'heading' | \"small\", got ['heading', 'small']" + start: [0, 26] + highlight: "['heading', 'small']" + suggest: + - message: "Replace with 'heading'" + result: "{% block 'text', variant: 'heading' %}Text{% endblock %}" + - message: 'Replace with "small"' + result: "{% block 'text', variant: \"small\" %}Text{% endblock %}" diff --git a/specs/checks/liquid-free-settings.yml b/specs/checks/liquid-free-settings.yml new file mode 100644 index 000000000..65fb3efb5 --- /dev/null +++ b/specs/checks/liquid-free-settings.yml @@ -0,0 +1,311 @@ +check: LiquidFreeSettings +severity: warning +cases: + - name: should not report errors for valid settings without liquid logic in sections bucket + files: + sections/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "text_value", + "type": "text", + "label": "Text Value", + "default": "Some text without liquid logic." + } + ] + } + {% endschema %} + - name: should not report errors for valid settings with dynamic source liquid logic in sections bucket + files: + sections/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "text_value", + "type": "text", + "label": "Text Value", + "default": "Title {{ product.title }}" + } + ] + } + {% endschema %} + - name: should not report errors for valid settings with liquid type field in sections bucket + files: + sections/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "text_value", + "type": "liquid", + "label": "Text Value", + "default": "{% render 'block' %}" + } + ] + } + {% endschema %} + - name: should report an error when settings value contains Liquid logic in sections bucket + files: + sections/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "input_with_logic", + "type": "text", + "label": "Input with Logic", + "default": "Hello {% if user %} User {% endif %}!" + } + ] + } + {% endschema %} + offenses: + - file: sections/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [8, 17] + highlight: '"Hello {% if user %} User {% endif %}!"' + - name: should report an error with correct indices when settings value contains Liquid logic in sections bucket + files: + sections/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "input_with_logic", + "type": "text", + "label": "Input with Logic", + "default": "Hello {% if user %} User {% endif %}!" + } + ] + } + {% endschema %} + offenses: + - file: sections/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [8, 17] + highlight: '"Hello {% if user %} User {% endif %}!"' + - name: should report an error when settings value contains Liquid logic in object format in sections bucket + files: + sections/test-section.liquid: |2 + {% schema %} + { + "name": "test", + "settings": { + "text_block": { + "type": "text", + "id": "text_block", + "label": "Text Block", + "default": "Hello {% if user %} User {% endif %}!" + } + } + } + {% endschema %} + offenses: + - file: sections/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [8, 19] + highlight: '"Hello {% if user %} User {% endif %}!"' + - name: should report errors when preset block values contain Liquid logic in sections bucket + files: + sections/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "presets": [ + { + "name": "Default", + "block": [ + { + "type": "text", + "settings": { + "text": "Hello World!{% random liquid logic %}" + } + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [10, 20] + highlight: '"Hello World!{% random liquid logic %}"' + - name: should not report errors when settings value is available_if and it contains liquid logic (1) + files: + sections/test-section.liquid: |2 + {% schema %} + { + "name": "test", + "settings": { + "text_block": { + "type": "text", + "id": "text_block", + "label": "Text Block", + "default": "Hello World!", + "visible_if": "{% if user %} true {% endif %}" + } + } + } + {% endschema %} + - name: should not report errors for valid settings without liquid logic in blocks bucket + files: + blocks/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "text_value", + "type": "text", + "label": "Text Value", + "default": "Some text without liquid logic." + } + ] + } + {% endschema %} + - name: should not report errors for valid settings with dynamic source liquid logic in blocks bucket + files: + blocks/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "text_value", + "type": "text", + "label": "Text Value", + "default": "Title {{ product.title }}" + } + ] + } + {% endschema %} + - name: should not report errors for valid settings with liquid type field in blocks bucket + files: + blocks/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "text_value", + "type": "liquid", + "label": "Text Value", + "default": "{% render 'block' %}" + } + ] + } + {% endschema %} + - name: should report an error when settings value contains Liquid logic in blocks bucket + files: + blocks/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "input_with_logic", + "type": "text", + "label": "Input with Logic", + "default": "Hello {% if user %} User {% endif %}!" + } + ] + } + {% endschema %} + offenses: + - file: blocks/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [8, 17] + highlight: '"Hello {% if user %} User {% endif %}!"' + - name: should report an error with correct indices when settings value contains Liquid logic in blocks bucket + files: + blocks/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "settings": [ + { + "id": "input_with_logic", + "type": "text", + "label": "Input with Logic", + "default": "Hello {% if user %} User {% endif %}!" + } + ] + } + {% endschema %} + offenses: + - file: blocks/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [8, 17] + highlight: '"Hello {% if user %} User {% endif %}!"' + - name: should report an error when settings value contains Liquid logic in object format in blocks bucket + files: + blocks/test-section.liquid: |2 + {% schema %} + { + "name": "test", + "settings": { + "text_block": { + "type": "text", + "id": "text_block", + "label": "Text Block", + "default": "Hello {% if user %} User {% endif %}!" + } + } + } + {% endschema %} + offenses: + - file: blocks/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [8, 19] + highlight: '"Hello {% if user %} User {% endif %}!"' + - name: should report errors when preset block values contain Liquid logic in blocks bucket + files: + blocks/test-section.liquid: | + {% schema %} + { + "name": "Section name", + "presets": [ + { + "name": "Default", + "block": [ + { + "type": "text", + "settings": { + "text": "Hello World!{% random liquid logic %}" + } + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/test-section.liquid + message: Settings values cannot contain liquid logic. + start: [10, 20] + highlight: '"Hello World!{% random liquid logic %}"' + - name: should not report errors when settings value is available_if and it contains liquid logic (2) + files: + blocks/test-section.liquid: |2 + {% schema %} + { + "name": "test", + "settings": { + "text_block": { + "type": "text", + "id": "text_block", + "label": "Text Block", + "default": "Hello World!", + "visible_if": "{% if user %} true {% endif %}" + } + } + } + {% endschema %} diff --git a/specs/checks/liquid-html-syntax-error.yml b/specs/checks/liquid-html-syntax-error.yml new file mode 100644 index 000000000..248eed2ca --- /dev/null +++ b/specs/checks/liquid-html-syntax-error.yml @@ -0,0 +1,1714 @@ +check: LiquidHTMLSyntaxError +severity: error +cases: + - name: should detect and fix double pipes in variable output + files: + file.liquid: "{{ 'hello' | upcase | | downcase }}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [0, 20] + highlight: "| | " + fix: "{{ 'hello' | upcase | downcase }}" + - name: should detect and fix double pipes with extra whitespace + files: + file.liquid: "{{ 'hello' | upcase | | downcase }}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [0, 20] + highlight: "| | " + fix: "{{ 'hello' | upcase | downcase }}" + - name: should detect and fix double pipes in assign tag + files: + file.liquid: "{% assign result = 'hello' | upcase | | downcase %}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [0, 36] + highlight: "| | " + fix: "{% assign result = 'hello' | upcase | downcase %}" + - name: should detect and fix double pipes in echo tag + files: + file.liquid: "{% echo 'hello' | upcase | | downcase %}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [0, 25] + highlight: "| | " + fix: "{% echo 'hello' | upcase | downcase %}" + - name: should handle multiple double pipes in one expression + files: + file.liquid: "{{ 'hello' | upcase | | downcase | | reverse }}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [0, 20] + highlight: "| | " + fix: "{{ 'hello' | upcase | downcase | | reverse }}" + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [0, 33] + highlight: "| | " + fix: "{{ 'hello' | upcase | | downcase | reverse }}" + autofix: + file.liquid: "{{ 'hello' | upcase | downcase | reverse }}" + - name: should detect and fix trailing pipe in variable output + files: + file.liquid: "{{ 'hello' | upcase | }}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove the trailing `|` character.' + start: [0, 19] + highlight: " |" + fix: "{{ 'hello' | upcase }}" + - name: should detect and fix trailing pipe in assign tag + files: + file.liquid: "{% assign result = 'hello' | upcase | %}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove the trailing `|` character.' + start: [0, 35] + highlight: " |" + fix: "{% assign result = 'hello' | upcase %}" + - name: should detect and fix trailing pipe in echo tag + files: + file.liquid: "{% echo 'hello' | upcase | %}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove the trailing `|` character.' + start: [0, 24] + highlight: " |" + fix: "{% echo 'hello' | upcase %}" + - name: should detect and fix multiple trailing pipes + files: + file.liquid: "{{ 'hello' | upcase | | }}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove the trailing `|` character.' + start: [0, 19] + highlight: " | |" + fix: "{{ 'hello' | upcase }}" + - name: should handle mixed valid and invalid pipes + files: + file.liquid: "{{ 'hello' | upcase | | downcase | reverse | }}" + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [0, 20] + highlight: "| | " + fix: "{{ 'hello' | upcase | downcase | reverse | }}" + - file: file.liquid + message: 'Syntax is not supported. Remove the trailing `|` character.' + start: [0, 42] + highlight: " |" + fix: "{{ 'hello' | upcase | | downcase | reverse }}" + autofix: + file.liquid: "{{ 'hello' | upcase | downcase | reverse }}" + - name: should not interfere with valid filter parameter syntax + files: + file.liquid: "{{ 'hello' | append: 'world' | upcase }}" + docset: default + - name: should handle liquid tag with mixed pipe issues + files: + file.liquid: |- + {% liquid + assign foo = 'test' | upcase | | downcase | + echo bar | reverse + %} + docset: default + offenses: + - file: file.liquid + message: 'Syntax is not supported. Remove extra `|` character(s).' + start: [1, 37] + highlight: "| | " + fix: |- + {% liquid + assign foo = 'test' | upcase | downcase | + echo bar | reverse + %} + - file: file.liquid + message: 'Syntax is not supported. Remove the trailing `|` character.' + start: [1, 49] + highlight: " |" + fix: |- + {% liquid + assign foo = 'test' | upcase | | downcase + echo bar | reverse + %} + autofix: + file.liquid: |- + {% liquid + assign foo = 'test' | upcase | downcase + echo bar | reverse + %} + - name: should not report on valid filter chains + files: + file.liquid: "{{ 'hello' | upcase | append: 'world' | downcase }}" + docset: default + - name: should not report on simple filters + files: + file.liquid: "{{ 'hello' | upcase }}" + docset: default + - name: should not report on filters with arguments + files: + file.liquid: "{{ product.title | append: ' - ' | append: shop.name }}" + docset: default + - name: should not report on valid assign with filters + files: + file.liquid: '{% assign title = product.title | upcase | truncate: 50 %}' + docset: default + - name: should not report on valid echo with filters + files: + file.liquid: '{% echo product.title | upcase | truncate: 50 %}' + docset: default + - name: should handle pipes in string literals correctly + files: + file.liquid: "{{ 'hello | world' | upcase }}" + docset: default + - name: should not report on conditional expressions with pipes + files: + file.liquid: "{{ product.title | default: 'No title' }}" + docset: default + - name: should not report an offense for valid boolean expressions (1) + files: + file.liquid: "{% if 1 > 2 %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (2) + files: + file.liquid: "{% if variable == 5 %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (3) + files: + file.liquid: "{% if 'abc' contains 'a' %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (4) + files: + file.liquid: "{% if product.title != '' %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (5) + files: + file.liquid: "{% if 1 and 2 %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (6) + files: + file.liquid: "{% if true or false %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (7) + files: + file.liquid: "{% if 10 > 5 and user.active %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (8) + files: + file.liquid: "{% if price >= 100 or discount %}hello{% endif %}" + docset: default + - name: should not report an offense for valid boolean expressions (9) + files: + file.liquid: "{% if user.name contains 'admin' or user.role == 'owner' %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (1) + files: + file.liquid: "{% if variable %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (2) + files: + file.liquid: "{% if user.active %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (3) + files: + file.liquid: "{% if true %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (4) + files: + file.liquid: "{% if false %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (5) + files: + file.liquid: "{% if 1 %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (6) + files: + file.liquid: "{% if 0 %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (7) + files: + file.liquid: "{% if 'string' %}hello{% endif %}" + docset: default + - name: should not report an offense for valid single values (8) + files: + file.liquid: "{% if contains %}hello{% endif %}" + docset: default + - name: should report an offense when parser stops at numbers + files: + file.liquid: "{% if 7 1 > 100 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value '7', and will ignore: '1 > 100'" + start: [0, 6] + highlight: "7 1 > 100" + fix: "{% if 7 %}hello{% endif %}" + - name: should report an offense when parser stops at strings + files: + file.liquid: "{% if 'hello' 1 > 100 %}world{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value ''hello'', and will ignore: '1 > 100'" + start: [0, 6] + highlight: "'hello' 1 > 100" + fix: "{% if 'hello' %}world{% endif %}" + - name: should report an offense when parser stops at liquid literals (1) + files: + file.liquid: "{% if true 1 > 0 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value 'true', and will ignore: '1 > 0'" + start: [0, 6] + highlight: true 1 > 0 + fix: "{% if true %}hello{% endif %}" + - name: should report an offense when parser stops at liquid literals (2) + files: + file.liquid: "{% if false 1 > 0 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value 'false', and will ignore: '1 > 0'" + start: [0, 6] + highlight: false 1 > 0 + fix: "{% if false %}hello{% endif %}" + - name: should report an offense when parser stops at liquid literals (3) + files: + file.liquid: "{% if nil 6 > 5 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value 'nil', and will ignore: '6 > 5'" + start: [0, 6] + highlight: nil 6 > 5 + fix: "{% if nil %}hello{% endif %}" + - name: should report an offense when parser stops at liquid literals (4) + files: + file.liquid: "{% if empty 123 456 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value 'empty', and will ignore: '123 456'" + start: [0, 6] + highlight: empty 123 456 + fix: "{% if empty %}hello{% endif %}" + - name: should report an offense when parser stops at liquid literals (5) + files: + file.liquid: "{% if blank 789 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value 'blank', and will ignore: '789'" + start: [0, 6] + highlight: blank 789 + fix: "{% if blank %}hello{% endif %}" + - name: should report offenses in different liquid tag types (1) + files: + file.liquid: "{% if 7 1 > 100 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value '7', and will ignore: '1 > 100'" + start: [0, 6] + highlight: "7 1 > 100" + fix: "{% if 7 %}hello{% endif %}" + - name: should report offenses in different liquid tag types (2) + files: + file.liquid: "{% unless 'test' 42 > 0 %}hello{% endunless %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value ''test'', and will ignore: '42 > 0'" + start: [0, 10] + highlight: "'test' 42 > 0" + fix: "{% unless 'test' %}hello{% endunless %}" + - name: should report offenses in different liquid tag types (3) + files: + file.liquid: "{% if false %}no{% elsif 7 1 > 100 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value '7', and will ignore: '1 > 100'" + start: [0, 25] + highlight: "7 1 > 100" + fix: "{% if false %}no{% elsif 7 %}hello{% endif %}" + - name: should report an offense for malformed expression starting with invalid token + files: + file.liquid: "{% if > 2 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '>'. Use a variable or value instead" + start: [0, 6] + highlight: "> 2" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for bare operators with no operands (1) + files: + file.liquid: "{% if > %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '>'. Use a variable or value instead" + start: [0, 6] + highlight: ">" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for bare operators with no operands (2) + files: + file.liquid: "{% if == %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '=='. Use a variable or value instead" + start: [0, 6] + highlight: "==" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for bare operators with no operands (3) + files: + file.liquid: "{% if < %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '<'. Use a variable or value instead" + start: [0, 6] + highlight: "<" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for bare operators with no operands (4) + files: + file.liquid: "{% if != %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '!='. Use a variable or value instead" + start: [0, 6] + highlight: "!=" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for bare operators with no operands (5) + files: + file.liquid: "{% if >= %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '>='. Use a variable or value instead" + start: [0, 6] + highlight: ">=" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for bare operators with no operands (6) + files: + file.liquid: "{% if <= %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '<='. Use a variable or value instead" + start: [0, 6] + highlight: "<=" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for other invalid starting characters (1) + files: + file.liquid: "{% if @ %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '@'. Use a variable or value instead" + start: [0, 6] + highlight: "@" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for other invalid starting characters (2) + files: + file.liquid: "{% if # %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '#'. Use a variable or value instead" + start: [0, 6] + highlight: "#" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for other invalid starting characters (3) + files: + file.liquid: "{% if $ %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '$'. Use a variable or value instead" + start: [0, 6] + highlight: "$" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for other invalid starting characters (4) + files: + file.liquid: "{% if & %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '&'. Use a variable or value instead" + start: [0, 6] + highlight: "&" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for malformed expressions in complex expressions (1) + files: + file.liquid: "{% if > 5 and true %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '>'. Use a variable or value instead" + start: [0, 6] + highlight: "> 5 and true" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for malformed expressions in complex expressions (2) + files: + file.liquid: "{% if == 2 or false %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '=='. Use a variable or value instead" + start: [0, 6] + highlight: "== 2 or false" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for malformed expressions in complex expressions (3) + files: + file.liquid: "{% if < 10 and variable %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional cannot start with '<'. Use a variable or value instead" + start: [0, 6] + highlight: "< 10 and variable" + fix: "{% if false %}hello{% endif %}" + - name: should report an offense for trailing tokens after comparison + files: + file.liquid: "{% if 1 == 2 foobar %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after '1 == 2' will be ignored" + start: [0, 6] + highlight: "1 == 2 foobar" + fix: "{% if 1 == 2 %}hello{% endif %}" + - name: should report an offense for malformed comparisons like missing quotes (1) + files: + file.liquid: "{% if 'wat' == 'squat > 2 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after ''wat' == 'squat' will be ignored" + start: [0, 6] + highlight: "'wat' == 'squat > 2" + fix: "{% if 'wat' == 'squat %}hello{% endif %}" + - name: should report an offense for malformed comparisons like missing quotes (2) + files: + file.liquid: "{% if 'wat' == 'squat' > 2 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after ''wat' == 'squat'' will be ignored" + start: [0, 6] + highlight: "'wat' == 'squat' > 2" + fix: "{% if 'wat' == 'squat' %}hello{% endif %}" + - name: should report an offense for malformed comparisons like missing quotes (3) + files: + file.liquid: "{% if price == 'test' != 5 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after 'price == 'test'' will be ignored" + start: [0, 6] + highlight: "price == 'test' != 5" + fix: "{% if price == 'test' %}hello{% endif %}" + - name: should report an offense for multiple trailing tokens + files: + file.liquid: "{% if 10 > 4 baz qux %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after '10 > 4' will be ignored" + start: [0, 6] + highlight: "10 > 4 baz qux" + fix: "{% if 10 > 4 %}hello{% endif %}" + - name: should report an offense for trailing junk with different operators (1) + files: + file.liquid: "{% if 'abc' contains 'a' noise %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after ''abc' contains 'a'' will be ignored" + start: [0, 6] + highlight: "'abc' contains 'a' noise" + fix: "{% if 'abc' contains 'a' %}hello{% endif %}" + - name: should report an offense for trailing junk with different operators (2) + files: + file.liquid: "{% if price <= 50 extra %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after 'price <= 50' will be ignored" + start: [0, 6] + highlight: "price <= 50 extra" + fix: "{% if price <= 50 %}hello{% endif %}" + - name: should report an offense for trailing junk with different operators (3) + files: + file.liquid: "{% if count != 0 junk %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after 'count != 0' will be ignored" + start: [0, 6] + highlight: "count != 0 junk" + fix: "{% if count != 0 %}hello{% endif %}" + - name: should not report an offense for valid logical continuations (1) + files: + file.liquid: "{% if 1 > 0 and 2 < 3 %}hello{% endif %}" + docset: default + - name: should not report an offense for valid logical continuations (2) + files: + file.liquid: "{% if x == 5 or y != 10 %}hello{% endif %}" + docset: default + - name: should not report an offense for valid logical continuations (3) + files: + file.liquid: "{% if price >= 100 and discount %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (1) + files: + file.liquid: "{% if true and variable %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (2) + files: + file.liquid: "{% if false or variable %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (3) + files: + file.liquid: "{% if 1 and user.active %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (4) + files: + file.liquid: "{% if 0 or fallback %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (5) + files: + file.liquid: "{% if 'string' and condition %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (6) + files: + file.liquid: "{% if 'value' or default %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (7) + files: + file.liquid: "{% if 42 and check %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (8) + files: + file.liquid: "{% if 3.14 or backup %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (9) + files: + file.liquid: "{% if nil and something %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (10) + files: + file.liquid: "{% if empty or alternative %}hello{% endif %}" + docset: default + - name: should not report an offense for truthy values followed by logical operators (11) + files: + file.liquid: "{% if blank and other %}hello{% endif %}" + docset: default + - name: should not report an offense for complex expressions with truthy values and logical operators (1) + files: + file.liquid: "{% if true and variable > 5 %}hello{% endif %}" + docset: default + - name: should not report an offense for complex expressions with truthy values and logical operators (2) + files: + file.liquid: "{% if 'test' or user.name == 'admin' %}hello{% endif %}" + docset: default + - name: should not report an offense for complex expressions with truthy values and logical operators (3) + files: + file.liquid: "{% if 42 and price <= 100 %}hello{% endif %}" + docset: default + - name: should not report an offense for complex expressions with truthy values and logical operators (4) + files: + file.liquid: "{% if false or count != 0 %}hello{% endif %}" + docset: default + - name: should not report an offense for complex expressions with truthy values and logical operators (5) + files: + file.liquid: "{% if empty and product.title contains 'sale' %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators errors after values (Liquid catches these) (1) + files: + file.liquid: "{% if my_var word > 5 %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators errors after values (Liquid catches these) (2) + files: + file.liquid: "{% if jake johnson > 5 %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators errors after values (Liquid catches these) (3) + files: + file.liquid: "{% if 'test' invalid > thing %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators errors after values (Liquid catches these) (4) + files: + file.liquid: "{% if user.name custom 'admin' %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators after variables (1) + files: + file.liquid: "{% if variable unknown > 5 %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators after variables (2) + files: + file.liquid: "{% if user.role badop 'admin' %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators after variables (3) + files: + file.liquid: "{% if price fake 100 %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators after variables (4) + files: + file.liquid: '{% if "str" blue == something %}hello{% endif %}' + docset: default + - name: should not report an offense for unknown operators after variables (5) + files: + file.liquid: "{% if red blue > something %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators in complex expressions (1) + files: + file.liquid: "{% if user.active and name fake 'test' %}hello{% endif %}" + docset: default + - name: should not report an offense for unknown operators in complex expressions (2) + files: + file.liquid: "{% unless 'test' some > thing %}hello{% endunless %}" + docset: default + - name: should not report an offense for pipe filter expressions + files: + file.liquid: "{% if wat | something == something %}hello{% endif %}" + docset: default + - name: should report an offense for misspelled logical operators (1) + files: + file.liquid: '{% if "wat" == "squat" adn "wat" == "squat" %}hello{% endif %}' + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after '\"wat\" == \"squat\"' will be ignored" + start: [0, 6] + highlight: '"wat" == "squat" adn "wat" == "squat"' + fix: '{% if "wat" == "squat" %}hello{% endif %}' + - name: should report an offense for misspelled logical operators (2) + files: + file.liquid: "{% if variable > 5 andd other < 10 %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Conditional is invalid. Anything after 'variable > 5' will be ignored" + start: [0, 6] + highlight: variable > 5 andd other < 10 + fix: "{% if variable > 5 %}hello{% endif %}" + - name: should report special message for JavaScript-style operators after literal values (1) + files: + file.liquid: "{% if true && false %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value 'true', and will ignore: '&& false'. Use 'and'/'or' instead of '&&'/'||' for multiple conditions" + start: [0, 6] + highlight: "true && false" + fix: "{% if true %}hello{% endif %}" + - name: should report special message for JavaScript-style operators after literal values (2) + files: + file.liquid: "{% if false || true %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value 'false', and will ignore: '|| true'. Use 'and'/'or' instead of '&&'/'||' for multiple conditions" + start: [0, 6] + highlight: "false || true" + fix: "{% if false %}hello{% endif %}" + - name: should report special message for JavaScript-style operators after literal values (3) + files: + file.liquid: '{% if "hello" && world %}hello{% endif %}' + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value '\"hello\"', and will ignore: '&& world'. Use 'and'/'or' instead of '&&'/'||' for multiple conditions" + start: [0, 6] + highlight: '"hello" && world' + fix: '{% if "hello" %}hello{% endif %}' + - name: should report special message for JavaScript-style operators after literal values (4) + files: + file.liquid: "{% if 42 || something %}hello{% endif %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported: Expression stops at truthy value '42', and will ignore: '|| something'. Use 'and'/'or' instead of '&&'/'||' for multiple conditions" + start: [0, 6] + highlight: "42 || something" + fix: "{% if 42 %}hello{% endif %}" + - name: should NOT report an offense for valid logical operators (1) + files: + file.liquid: "{% if price > 100 and discount < 50 %}hello{% endif %}" + docset: default + - name: should NOT report an offense for valid logical operators (2) + files: + file.liquid: "{% if user.active or user.premium %}hello{% endif %}" + docset: default + - name: should NOT report an offense for valid logical operators (3) + files: + file.liquid: "{% if x == 1 and y == 2 or z == 3 %}hello{% endif %}" + docset: default + - name: should not report on filters with alphanumeric trailing characters + files: + file.liquid: "{{ 'HELLO' | append123: 'world' }}" + docset: default + - name: should not report on filters with underscore trailing characters + files: + file.liquid: "{{ 'HELLO' | append_test }}" + docset: default + - name: should not report on valid filter names + files: + file.liquid: "{{ 'hello' | append: 'world' }}" + docset: default + - name: should not report on unknown filters (handled by UnknownFilter check) + files: + file.liquid: "{{ 'hello' | completely_unknown_filter: 'world' }}" + docset: default + - name: "should detect and fix append with trailing @ character" + files: + file.liquid: "{{ 'HELLO' | append@ }}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [0, 19] + highlight: "@" + fix: "{{ 'HELLO' | append }}" + - name: should detect and fix filter with space followed by characters + files: + file.liquid: "{{ 'HELLO' | append me: 'world' }}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters ' me' that should be removed." + start: [0, 19] + highlight: " me" + fix: "{{ 'HELLO' | append: 'world' }}" + - name: should handle normal filter followed by filter with special characters + files: + file.liquid: "{{ 'test' | downcase | append@: 'world' }}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [0, 29] + highlight: "@" + fix: "{{ 'test' | downcase | append: 'world' }}" + - name: should handle filter with special characters followed by normal filter + files: + file.liquid: "{{ 'test' | append!: 'hello' | upcase }}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '!' that should be removed." + start: [0, 18] + highlight: "!" + fix: "{{ 'test' | append: 'hello' | upcase }}" + - name: should handle two filters with special characters in a row + files: + file.liquid: "{{ 'test' | append@: 'hello' | upcase# }}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [0, 18] + highlight: "@" + fix: "{{ 'test' | append: 'hello' | upcase# }}" + - file: file.liquid + message: "Syntax is not supported Filter 'upcase' has trailing characters '#' that should be removed." + start: [0, 37] + highlight: "#" + fix: "{{ 'test' | append@: 'hello' | upcase }}" + autofix: + file.liquid: "{{ 'test' | append: 'hello' | upcase }}" + - name: should handle filter with space and characters followed by normal filter + files: + file.liquid: "{{ 'test' | append xyz: 'hello' | upcase }}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters ' xyz' that should be removed." + start: [0, 18] + highlight: " xyz" + fix: "{{ 'test' | append: 'hello' | upcase }}" + - name: "should detect and fix assign with trailing @ character" + files: + file.liquid: "{% assign foo = 'HELLO' | append@ %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [0, 32] + highlight: "@" + fix: "{% assign foo = 'HELLO' | append %}" + - name: should detect and fix assign with space followed by characters + files: + file.liquid: "{% assign bar = 'HELLO' | append me: 'world' %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters ' me' that should be removed." + start: [0, 32] + highlight: " me" + fix: "{% assign bar = 'HELLO' | append: 'world' %}" + - name: should handle assign with multiple filters with special characters + files: + file.liquid: "{% assign baz = 'test' | append@: 'hello' | upcase# %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [0, 31] + highlight: "@" + fix: "{% assign baz = 'test' | append: 'hello' | upcase# %}" + - file: file.liquid + message: "Syntax is not supported Filter 'upcase' has trailing characters '#' that should be removed." + start: [0, 50] + highlight: "#" + fix: "{% assign baz = 'test' | append@: 'hello' | upcase %}" + autofix: + file.liquid: "{% assign baz = 'test' | append: 'hello' | upcase %}" + - name: should not report on valid assign filters + files: + file.liquid: "{% assign valid = 'hello' | append: 'world' | upcase %}" + docset: default + - name: "should detect and fix echo with trailing @ character" + files: + file.liquid: "{% echo 'HELLO' | append@ %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [0, 24] + highlight: "@" + fix: "{% echo 'HELLO' | append %}" + - name: should detect and fix echo with space followed by characters + files: + file.liquid: "{% echo 'HELLO' | append me: 'world' %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters ' me' that should be removed." + start: [0, 24] + highlight: " me" + fix: "{% echo 'HELLO' | append: 'world' %}" + - name: should handle echo with multiple filters with special characters + files: + file.liquid: "{% echo 'test' | append@: 'hello' | upcase# %}" + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [0, 23] + highlight: "@" + fix: "{% echo 'test' | append: 'hello' | upcase# %}" + - file: file.liquid + message: "Syntax is not supported Filter 'upcase' has trailing characters '#' that should be removed." + start: [0, 42] + highlight: "#" + fix: "{% echo 'test' | append@: 'hello' | upcase %}" + autofix: + file.liquid: "{% echo 'test' | append: 'hello' | upcase %}" + - name: should not report on valid echo filters + files: + file.liquid: "{% echo 'hello' | append: 'world' | upcase %}" + docset: default + - name: should detect and fix assign in liquid tag with trailing characters + files: + file.liquid: |- + {% liquid + assign foo = 'test' | append@: 'hello' + %} + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [1, 36] + highlight: "@" + fix: |- + {% liquid + assign foo = 'test' | append: 'hello' + %} + - name: should detect and fix echo in liquid tag with trailing characters + files: + file.liquid: |- + {% liquid + echo 'test' | append@: 'hello' + %} + docset: default + offenses: + - file: file.liquid + message: "Syntax is not supported Filter 'append' has trailing characters '@' that should be removed." + start: [1, 28] + highlight: "@" + fix: |- + {% liquid + echo 'test' | append: 'hello' + %} + - name: should not report when args are valid (1) + files: + file.liquid: "{% for i in array reversed %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + - name: should not report when args are valid (2) + files: + file.liquid: "{% for i in array reversed offset: 1 %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + - name: should not report when args are valid (3) + files: + file.liquid: "{% tablerow x in array limit: 10 %}{% endtablerow %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + - name: should not report when args are valid (4) + files: + file.liquid: "{% tablerow x in array cols: 2 limit: 10 %}{% endtablerow %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + - name: should report when invalid args are found (1) + files: + file.liquid: "{% for i in array limit1: 10 %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `for in `. Invalid/Unknown arguments: limit1: 10" + start: [0, 18] + highlight: "limit1: 10" + fix: "{% for i in array %}{% endfor %}" + - name: should report when invalid args are found (2) + files: + file.liquid: "{% for i in array !! range: (1..2) %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `for in `. Invalid/Unknown arguments: !!, range: (1..2)" + start: [0, 18] + highlight: "!! range: (1..2)" + fix: "{% for i in array %}{% endfor %}" + - name: should report when invalid args are found (3) + files: + file.liquid: "{% tablerow i in (1..10) limit1: 10 %}{{ i }}{% endtablerow %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `tablerow in `. Invalid/Unknown arguments: limit1: 10" + start: [0, 25] + highlight: "limit1: 10" + fix: "{% tablerow i in (1..10) %}{{ i }}{% endtablerow %}" + - name: should report when positional args are found after named/invalid args (1) + files: + file.liquid: "{% for i in array limit: 10 reversed %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `for in `. Invalid/Unknown arguments: reversed" + start: [0, 18] + highlight: "limit: 10 reversed" + fix: "{% for i in array limit: 10 %}{% endfor %}" + - name: should report when positional args are found after named/invalid args (2) + files: + file.liquid: "{% for i in array reversed limit: 10 invalid %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `for in `. Invalid/Unknown arguments: invalid" + start: [0, 18] + highlight: "reversed limit: 10 invalid" + fix: "{% for i in array reversed limit: 10 %}{% endfor %}" + - name: should report when positional args are found after named/invalid args (3) + files: + file.liquid: "{% for i in array !! reversed limit: 10 invalid %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `for in `. Invalid/Unknown arguments: !!, reversed, invalid" + start: [0, 18] + highlight: "!! reversed limit: 10 invalid" + fix: "{% for i in array limit: 10 %}{% endfor %}" + - name: should report when positional args are found after named/invalid args (4) + files: + file.liquid: "{% for i in array reversed: (1..2) %}{% endfor %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `for in `. Invalid/Unknown arguments: reversed: (1..2)" + start: [0, 18] + highlight: "reversed: (1..2)" + fix: "{% for i in array %}{% endfor %}" + - name: detectInvalidLoopArguments > should report when there are multiple instances of the error + files: + file.liquid: "{% for i in array reversed limit: 10 invalid %}{% endfor %} {% tablerow x in array cols: 2 limit: 10 invalid %}{% endtablerow %}" + docset: {"filters":[],"tags":[{"name":"for","parameters":[{"name":"reversed","positional":true,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]},{"name":"tablerow","parameters":[{"name":"cols","positional":false,"description":"...","required":false,"types":[]},{"name":"limit","positional":false,"description":"...","required":false,"types":[]},{"name":"offset","positional":false,"description":"...","required":false,"types":[]}]}]} + offenses: + - file: file.liquid + message: "Arguments must be provided in the format `for in `. Invalid/Unknown arguments: invalid" + start: [0, 18] + highlight: "reversed limit: 10 invalid" + fix: "{% for i in array reversed limit: 10 %}{% endfor %} {% tablerow x in array cols: 2 limit: 10 invalid %}{% endtablerow %}" + - file: file.liquid + message: "Arguments must be provided in the format `tablerow in `. Invalid/Unknown arguments: invalid" + start: [0, 83] + highlight: "cols: 2 limit: 10 invalid" + fix: "{% for i in array reversed limit: 10 invalid %}{% endfor %} {% tablerow x in array cols: 2 limit: 10 %}{% endtablerow %}" + autofix: + file.liquid: "{% for i in array reversed limit: 10 %}{% endfor %} {% tablerow x in array cols: 2 limit: 10 %}{% endtablerow %}" + - name: should not report when echo value is valid (1) + files: + file.liquid: "{% echo '123' %}" + docset: default + - name: should not report when echo value is valid (2) + files: + file.liquid: "{% echo '123' | upcase %}" + docset: default + - name: should not report when echo value is valid (3) + files: + file.liquid: "{{ '123' }}" + docset: default + - name: should not report when echo value is valid (4) + files: + file.liquid: "{{ '123' | upcase }}" + docset: default + - name: should not report when echo value is valid (5) + files: + file.liquid: "{{ }}" + docset: default + - name: should not report when echo value is valid (6) + files: + file.liquid: "{{ echo }}" + docset: default + - name: should not report when echo value is valid (7) + files: + file.liquid: "{% liquid echo %}" + docset: default + - name: detectInvalidEchoValue > should not report when there are no filters provided + files: + file.liquid: "{% echo '123' %}" + docset: default + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (1) + files: + file.liquid: "{% echo '123' 555 text %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 13] + highlight: " 555 text" + fix: "{% echo '123' %}" + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (2) + files: + file.liquid: '{% echo "123" 555 text %}' + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 13] + highlight: " 555 text" + fix: '{% echo "123" %}' + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (3) + files: + file.liquid: "{% echo 123 555 text %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 11] + highlight: " 555 text" + fix: "{% echo 123 %}" + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (4) + files: + file.liquid: "{% echo true 555 text %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 12] + highlight: " 555 text" + fix: "{% echo true %}" + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (5) + files: + file.liquid: "{{ '123' 555 text }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 8] + highlight: " 555 text" + fix: "{{ '123' }}" + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (6) + files: + file.liquid: '{{ "123" 555 text }}' + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 8] + highlight: " 555 text" + fix: '{{ "123" }}' + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (7) + files: + file.liquid: "{{ 123 555 text }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 6] + highlight: " 555 text" + fix: "{{ 123 }}" + - name: detectInvalidEchoValue > should report when there are multiple values (no filters) (8) + files: + file.liquid: "{{ true 555 text }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 7] + highlight: " 555 text" + fix: "{{ true }}" + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (1) + files: + file.liquid: "{% echo '123' 555 text | upcase %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 13] + highlight: " 555 text" + fix: "{% echo '123' | upcase %}" + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (2) + files: + file.liquid: '{% echo "123" 555 text | upcase %}' + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 13] + highlight: " 555 text" + fix: '{% echo "123" | upcase %}' + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (3) + files: + file.liquid: "{% echo 123 555 text | default: 0 %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 11] + highlight: " 555 text" + fix: "{% echo 123 | default: 0 %}" + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (4) + files: + file.liquid: "{% echo true 555 text | fake-filter: 'yes' %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 12] + highlight: " 555 text" + fix: "{% echo true | fake-filter: 'yes' %}" + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (5) + files: + file.liquid: "{{ '123' 555 text | upcase }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 8] + highlight: " 555 text" + fix: "{{ '123' | upcase }}" + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (6) + files: + file.liquid: '{{ "123" 555 text | default: 0 }}' + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 8] + highlight: " 555 text" + fix: '{{ "123" | default: 0 }}' + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (7) + files: + file.liquid: "{{ 123 555 text | fake-filter: 'yes' }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 6] + highlight: " 555 text" + fix: "{{ 123 | fake-filter: 'yes' }}" + - name: detectInvalidEchoValue > should report when there are multiple values (with filters) (8) + files: + file.liquid: "{{ true 555 text | fake-filter: 'yes' }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 7] + highlight: " 555 text" + fix: "{{ true | fake-filter: 'yes' }}" + - name: detectInvalidEchoValue > should report when there are multiple instances of the error + files: + file.liquid: "{% echo zero %} {% echo one two %} {% echo one two %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 27] + highlight: " two" + fix: "{% echo zero %} {% echo one %} {% echo one two %}" + - file: file.liquid + message: Syntax is not supported + start: [0, 46] + highlight: " two" + fix: "{% echo zero %} {% echo one two %} {% echo one %}" + autofix: + file.liquid: "{% echo zero %} {% echo one %} {% echo one %}" + - name: should report when there is no value (1) + files: + file.liquid: "{% echo | upcase %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 8] + highlight: "| upcase" + fix: "{% echo blank %}" + - name: should report when there is no value (2) + files: + file.liquid: "{{ | upcase }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 3] + highlight: "| upcase" + fix: "{{ blank }}" + - name: should report when there is no value (3) + files: + file.liquid: "{% liquid echo | upcase %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 15] + highlight: "| upcase" + fix: "{% liquid echo blank %}" + - name: should report unclosed Liquid tags + files: + file.liquid: | + {% capture some_variable %} + Hello, world! + docset: default + offenses: + - file: file.liquid + message: "Attempting to end parsing before LiquidTag 'capture' was closed" + start: [0, 0] + highlight: "{% capture some_variable %}" + - name: should report unclosed HTML tags + files: + file.liquid: | + + Hello, world! + docset: default + offenses: + - file: file.liquid + message: "Attempting to end parsing before HtmlElement 'a' was closed" + start: [0, 0] + highlight: '' + - name: should report closing the wrong node (html/html) + files: + file.liquid: | + + Hello, world! + + docset: default + offenses: + - file: file.liquid + message: "Attempting to close HtmlElement 'b' before HtmlElement 'a' was closed" + start: [2, 0] + highlight: "" + - name: should report closing the wrong node (html/liquid) + files: + file.liquid: | + + Hello, world! + {% endif %} + docset: default + offenses: + - file: file.liquid + message: "Attempting to close LiquidTag 'endif' before HtmlElement 'a' was closed" + start: [0, 0] + highlight: '' + - name: should report unexpected tokens (1) + files: + file.liquid: | + {% if cond }} + docset: default + offenses: + - file: file.liquid + message: Expected LiquidTagClose but got EndOfInput + start: [0, 13] + highlight: "\n" + - name: should report unexpected tokens (2) + files: + file.liquid: | + + docset: default + offenses: + - file: file.liquid + message: Expected HtmlTagClose but got EndOfInput + start: [0, 20] + highlight: "\n" + - name: should report unexpected tokens (3) + files: + file.liquid: | + + docset: default + offenses: + - file: file.liquid + message: Expected LiquidTagClose but got EndOfInput + start: [0, 21] + highlight: "\n" + - name: should not report syntax error in valid Liquid code + files: + file.liquid: | + {% if some_variable %} + Hello, world! + {% endif %} + {% schema %} + { + "settings": [{ "visible_if": "{{ foo.bar == true }}" }] + } + {% endschema %} + docset: default + - name: should not report canonical inline app block paths + files: + file.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}" + docset: default + - name: should highligh the error (1) + files: + file.liquid: "
    " + docset: default + offenses: + - file: file.liquid + message: "Attempting to close HtmlElement 'b' before HtmlElement 'a' was closed" + start: [0, 8] + highlight: "" + - name: should highligh the error (2) + files: + file.liquid: "" + docset: default + offenses: + - file: file.liquid + message: "Attempting to close LiquidTag 'endif' before HtmlElement 'a' was closed" + start: [0, 5] + highlight: "" + - name: should highligh the error (3) + files: + file.liquid: '' + docset: default + offenses: + - file: file.liquid + message: Expected HtmlTagClose but got EndOfInput + start: [0, 13] + highlight: ">" + - name: detectMultipleAssignValues > should not report when there are no trailing values (1) + files: + file.liquid: "{% assign foo = '123' %}" + docset: default + - name: detectMultipleAssignValues > should not report when there are no trailing values (2) + files: + file.liquid: "{% assign foo = '123' | upcase %}" + docset: default + - name: detectMultipleAssignValues > should report when there are multiple values (no filters) (1) + files: + file.liquid: "{% assign foo = '123' 555 text %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 21] + highlight: " 555 text" + fix: "{% assign foo = '123' %}" + - name: detectMultipleAssignValues > should report when there are multiple values (no filters) (2) + files: + file.liquid: '{% assign foo = "123" 555 text %}' + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 21] + highlight: " 555 text" + fix: '{% assign foo = "123" %}' + - name: detectMultipleAssignValues > should report when there are multiple values (no filters) (3) + files: + file.liquid: "{% assign foo = 123 555 text %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 19] + highlight: " 555 text" + fix: "{% assign foo = 123 %}" + - name: detectMultipleAssignValues > should report when there are multiple values (no filters) (4) + files: + file.liquid: "{% assign foo = true 555 text %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 20] + highlight: " 555 text" + fix: "{% assign foo = true %}" + - name: detectMultipleAssignValues > should not report when there are no filters provided + files: + file.liquid: "{% assign foo = '123' %}" + docset: default + - name: detectMultipleAssignValues > should report when there are multiple instances of the error + files: + file.liquid: "{% assign foo = blank %} {% assign foo = '123' 555 text %} {% assign foo = '123' 555 text %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 46] + highlight: " 555 text" + fix: "{% assign foo = blank %} {% assign foo = '123' %} {% assign foo = '123' 555 text %}" + - file: file.liquid + message: Syntax is not supported + start: [0, 80] + highlight: " 555 text" + fix: "{% assign foo = blank %} {% assign foo = '123' 555 text %} {% assign foo = '123' %}" + autofix: + file.liquid: "{% assign foo = blank %} {% assign foo = '123' %} {% assign foo = '123' %}" + - name: detectMultipleAssignValues > should report when there are multiple values (with filters) (1) + files: + file.liquid: "{% assign foo = '123' 555 text | upcase %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 21] + highlight: " 555 text" + fix: "{% assign foo = '123' | upcase %}" + - name: detectMultipleAssignValues > should report when there are multiple values (with filters) (2) + files: + file.liquid: '{% assign foo = "123" 555 text | upcase %}' + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 21] + highlight: " 555 text" + fix: '{% assign foo = "123" | upcase %}' + - name: detectMultipleAssignValues > should report when there are multiple values (with filters) (3) + files: + file.liquid: "{% assign foo = 123 555 text | default: 0 %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 19] + highlight: " 555 text" + fix: "{% assign foo = 123 | default: 0 %}" + - name: detectMultipleAssignValues > should report when there are multiple values (with filters) (4) + files: + file.liquid: "{% assign foo = true 555 text | fake-filter: 'yes' %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 20] + highlight: " 555 text" + fix: "{% assign foo = true | fake-filter: 'yes' %}" + - name: should not report when range is valid (1) + files: + file.liquid: "{% for i in (1..10) %}{% endfor %}" + docset: default + - name: should not report when range is valid (2) + files: + file.liquid: "{% tablerow x in (a..b) %}{% endtablerow %}" + docset: default + - name: "should report when there is an extra `.` character in range (1)" + files: + file.liquid: "{% for i in (1...10) %}{% endfor %}" + docset: default + offenses: + - file: file.liquid + message: "Ranges must be in the format `(..)`. The start and end of the range must be whole numbers or variables." + start: [0, 12] + highlight: "(1...10)" + fix: "{% for i in (1..10) %}{% endfor %}" + - name: "should report when there is an extra `.` character in range (2)" + files: + file.liquid: "{% tablerow x in (a...b) %}{% endtablerow %}" + docset: default + offenses: + - file: file.liquid + message: "Ranges must be in the format `(..)`. The start and end of the range must be whole numbers or variables." + start: [0, 17] + highlight: "(a...b)" + fix: "{% tablerow x in (a..b) %}{% endtablerow %}" + - name: "should report when there is an extra `.` character in range (3)" + files: + file.liquid: "{% tablerow x in (a .. b) %}{% endtablerow %}" + docset: default + offenses: + - file: file.liquid + message: "Ranges must be in the format `(..)`. The start and end of the range must be whole numbers or variables." + start: [0, 17] + highlight: "(a .. b)" + fix: "{% tablerow x in (a..b) %}{% endtablerow %}" + - name: "should report when there is an extra `.` character in range (4)" + files: + file.liquid: "{% tablerow x in ( a..b ) %}{% endtablerow %}" + docset: default + offenses: + - file: file.liquid + message: "Ranges must be in the format `(..)`. The start and end of the range must be whole numbers or variables." + start: [0, 17] + highlight: "( a..b )" + fix: "{% tablerow x in (a..b) %}{% endtablerow %}" + - name: should report when the range contains a real number + files: + file.liquid: "{% for i in (-2.9..2.9) %}{% endfor %}" + docset: default + offenses: + - file: file.liquid + message: "Ranges must be in the format `(..)`. The start and end of the range must be whole numbers or variables." + start: [0, 12] + highlight: "(-2.9..2.9)" + fix: "{% for i in (-2..2) %}{% endfor %}" + - name: should report when the range contains a nested variable + files: + file.liquid: "{% for i in (some.var..some.other.var) %}{% endfor %}" + docset: default + - name: detectInvalidLoopRange > should report when there are multiple instances of the error + files: + file.liquid: "{% for i in (1...10) %}{% endfor %} {% tablerow x in (a...b) %}{% endtablerow %}" + docset: default + offenses: + - file: file.liquid + message: "Ranges must be in the format `(..)`. The start and end of the range must be whole numbers or variables." + start: [0, 12] + highlight: "(1...10)" + fix: "{% for i in (1..10) %}{% endfor %} {% tablerow x in (a...b) %}{% endtablerow %}" + - file: file.liquid + message: "Ranges must be in the format `(..)`. The start and end of the range must be whole numbers or variables." + start: [0, 53] + highlight: "(a...b)" + fix: "{% for i in (1...10) %}{% endfor %} {% tablerow x in (a..b) %}{% endtablerow %}" + autofix: + file.liquid: "{% for i in (1..10) %}{% endfor %} {% tablerow x in (a..b) %}{% endtablerow %}" + - name: should not report no unsupported parenthesis in the markup (1) + files: + file.liquid: "{% if condition %}{% endif %}" + docset: default + - name: should not report no unsupported parenthesis in the markup (2) + files: + file.liquid: "{% if range == (1..2) %}{% endif %}" + docset: default + - name: should report when there are unsupported parenthesis in the markup (1) + files: + file.liquid: "{% if (this and that) or other %}{% endif %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 6] + highlight: "(this and that) or other" + fix: "{% if this and that or other %}{% endif %}" + - name: should report when there are unsupported parenthesis in the markup (2) + files: + file.liquid: "{% if (a > b) and (c == d) %}{% endif %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 6] + highlight: "(a > b) and (c == d)" + fix: "{% if a > b and c == d %}{% endif %}" + - name: detectConditionalNodeUnsupportedParenthesis > should report when there are multiple instances of the error + files: + file.liquid: "{% if (a or c) %}{% endif %} {% if (a == b) %}{% endif %} {% if condition %}{% endif %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 6] + highlight: "(a or c)" + fix: "{% if a or c %}{% endif %} {% if (a == b) %}{% endif %} {% if condition %}{% endif %}" + - file: file.liquid + message: Syntax is not supported + start: [0, 35] + highlight: "(a == b)" + fix: "{% if (a or c) %}{% endif %} {% if a == b %}{% endif %} {% if condition %}{% endif %}" + autofix: + file.liquid: "{% if a or c %}{% endif %} {% if a == b %}{% endif %} {% if condition %}{% endif %}" + - name: detectTrailingAssignValue > should not report when there are no trailing values (1) + files: + file.liquid: "{% assign foo = '123' %}" + docset: default + - name: detectTrailingAssignValue > should not report when there are no trailing values (2) + files: + file.liquid: "{% assign foo = '123' | upcase %}" + docset: default + - name: should not report when the boolean expression is in a schema tag + files: + file.liquid: |- + {% schema %} + { + "visible_if": "{{ this > that }}" + } + {% endschema %} + docset: default + - name: should report all use of boolean expressions (1) + files: + file.liquid: "{% assign foo = something == else %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 16] + highlight: "something == else" + fix: "{% assign foo = something %}" + - name: should report all use of boolean expressions (2) + files: + file.liquid: "{% echo foo != bar %}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 8] + highlight: "foo != bar" + fix: "{% echo foo %}" + - name: should report all use of boolean expressions (3) + files: + file.liquid: "{{ this > that }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 3] + highlight: this > that + fix: "{{ this }}" + - name: should report all use of boolean expressions (4) + files: + file.liquid: "{{ bool and cond }}" + docset: default + offenses: + - file: file.liquid + message: Syntax is not supported + start: [0, 3] + highlight: bool and cond + fix: "{{ bool }}" diff --git a/specs/checks/liquid-nesting-depth.yml b/specs/checks/liquid-nesting-depth.yml new file mode 100644 index 000000000..6cac5fb0a --- /dev/null +++ b/specs/checks/liquid-nesting-depth.yml @@ -0,0 +1,170 @@ +check: LiquidNestingDepth +severity: warning +cases: + - name: does not report when nesting equals the tolerated depth + files: + snippets/test.liquid: |- + {% if condition_0 %} + {% if condition_1 %} + {% if condition_2 %} + {% if condition_3 %} + {% if condition_4 %} + {% if condition_5 %} + {% if condition_6 %} + {% if condition_7 %} + {% if condition_8 %} + {% if condition_9 %} + Nested content + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + - name: reports when nesting exceeds the tolerated depth + files: + snippets/test.liquid: |- + {% if condition_0 %} + {% if condition_1 %} + {% if condition_2 %} + {% if condition_3 %} + {% if condition_4 %} + {% if condition_5 %} + {% if condition_6 %} + {% if condition_7 %} + {% if condition_8 %} + {% if condition_9 %} + {% if condition_10 %} + Nested content + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + offenses: + - file: snippets/test.liquid + message: This Liquid block is nested 11 levels deep, which exceeds the maximum allowed depth of 10. + start: [10, 0] + highlight: "{% if condition_10 %}" + - name: does not report a plain Liquid template + files: + snippets/test.liquid: "
    {{ product.title }}
    " + config: + LiquidNestingDepth: { maxDepth: 1 } + - name: reports the first tag that exceeds a custom threshold + files: + snippets/test.liquid: |- + {% if product.available %} + {% for variant in product.variants %} + {% unless hidden %} + Available variant + {% endunless %} + {% endfor %} + {% endif %} + config: + LiquidNestingDepth: { maxDepth: 2 } + offenses: + - file: snippets/test.liquid + message: This Liquid block is nested 3 levels deep, which exceeds the maximum allowed depth of 2. + start: [2, 10] + highlight: "{% unless hidden %}" + - name: reports each nested control-flow tag deeper than the threshold + files: + snippets/test.liquid: |- + {% if condition_0 %} + {% if condition_1 %} + {% if condition_2 %} + {% if condition_3 %} + {% if condition_4 %} + Nested content + {% endif %} + {% endif %} + {% endif %} + {% endif %} + {% endif %} + config: + LiquidNestingDepth: { maxDepth: 3 } + offenses: + - file: snippets/test.liquid + message: This Liquid block is nested 4 levels deep, which exceeds the maximum allowed depth of 3. + start: [3, 0] + highlight: "{% if condition_3 %}" + - file: snippets/test.liquid + message: This Liquid block is nested 5 levels deep, which exceeds the maximum allowed depth of 3. + start: [4, 0] + highlight: "{% if condition_4 %}" + - name: does not count elsif and else branches as additional nesting + files: + snippets/test.liquid: |- + {% if product.available %} + Available + {% elsif product.tags contains 'coming-soon' %} + Coming soon + {% else %} + Unavailable + {% endif %} + config: + LiquidNestingDepth: { maxDepth: 1 } + - name: counts tablerow as a nesting tag + files: + snippets/test.liquid: |- + {% if section.blocks.size > 0 %} + {% tablerow block in section.blocks %} + {% unless block.settings.hidden %} + {{ block.settings.title }} + {% endunless %} + {% endtablerow %} + {% endif %} + config: + LiquidNestingDepth: { maxDepth: 2 } + offenses: + - file: snippets/test.liquid + message: This Liquid block is nested 3 levels deep, which exceeds the maximum allowed depth of 2. + start: [2, 10] + highlight: "{% unless block.settings.hidden %}" + - name: counts nested case blocks consistently with if, unless, and for blocks + files: + snippets/test.liquid: |- + {% case section.settings.layout %} + {% when 'grid' %} + {% for product in collection.products %} + {% if product.available %} + {% unless product.tags contains 'hidden' %} + {{ product.title }} + {% endunless %} + {% endif %} + {% endfor %} + {% endcase %} + config: + LiquidNestingDepth: { maxDepth: 3 } + offenses: + - file: snippets/test.liquid + message: This Liquid block is nested 4 levels deep, which exceeds the maximum allowed depth of 3. + start: [4, 14] + highlight: "{% unless product.tags contains 'hidden' %}" + - name: does not count HTML nesting as Liquid nesting depth + files: + snippets/test.liquid: |- +
    +
    +
    +
    + {% if product.available %} + Available + {% endif %} +
    +
    +
    +
    + config: + LiquidNestingDepth: { maxDepth: 1 } diff --git a/specs/checks/liquid-syntax-error.yml b/specs/checks/liquid-syntax-error.yml new file mode 100644 index 000000000..7685f9c3a --- /dev/null +++ b/specs/checks/liquid-syntax-error.yml @@ -0,0 +1,1400 @@ +check: LiquidSyntaxError +severity: error +cases: + - name: accepts section keyword arguments + files: + snippets/test.liquid: "{% section 'header', color: 'red' %}" + + # The parser (LiquidHTMLSyntaxError) catches this one, not this check. + - name: does not report bare bracket section keyword argument values + files: + snippets/test.liquid: "{% section 'header', foo: [0] %}" + + - name: knows tags from the docset inside liquid blocks + docset: + tags: [{ name: custom_tag }] + files: + snippets/test.liquid: |- + {% liquid + custom_tag + %} + + - name: reports unknown tags inside liquid blocks + files: + snippets/test.liquid: |- + {% liquid + hello world + %} + offenses: + - file: snippets/test.liquid + message: Unknown tag 'hello' + start: [1, 2] + highlight: hello world + + - name: reports each unknown tag where it is, even when the text repeats + files: + snippets/test.liquid: |- + {% liquid + echo 'hello world' + hello world + hello world + %} + offenses: + - file: snippets/test.liquid + message: Unknown tag 'hello' + start: [2, 2] + highlight: hello world + - file: snippets/test.liquid + message: Unknown tag 'hello' + start: [3, 2] + highlight: hello world + - name: reports parameter offenses for plain arguments + checks: &block-checks + - LiquidSyntaxError + - MissingBlockArguments + - UnrecognizedBlockArguments + - ValidBlockArgumentTypes + - DuplicateBlockArguments + docset: default + files: + templates/test.liquid: "{% block 'card', unknown: true, count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: &card |- + {% doc %} + @param {string} heading - Heading + @param {string} tracking_id - Tracking id + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"},{"id":"count","type":"number"}]} + {% endschema %} + offenses: + - check: DuplicateBlockArguments + file: templates/test.liquid + message: "Duplicate argument 'count' in block tag for 'card'." + start: [0, 47] + highlight: "count: 'more'" + - check: MissingBlockArguments + file: templates/test.liquid + message: "Missing required argument 'heading' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card', unknown: true, count: 'many', count: 'more' %}" + - check: MissingBlockArguments + file: templates/test.liquid + message: "Missing required argument 'tracking_id' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card', unknown: true, count: 'many', count: 'more' %}" + - check: UnrecognizedBlockArguments + file: templates/test.liquid + message: "Unknown argument 'unknown' in block tag for 'card'." + start: [0, 17] + highlight: "unknown: true" + - check: ValidBlockArgumentTypes + file: templates/test.liquid + message: "Type mismatch for argument 'count': expected number, got string" + start: [0, 39] + highlight: "'many'" + - check: ValidBlockArgumentTypes + file: templates/test.liquid + message: "Type mismatch for argument 'count': expected number, got string" + start: [0, 54] + highlight: "'more'" + - name: accepts block.name with a string value + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.name: 'Card', heading: 'Hello', count: 1, tracking_id: 'id' %}{% endblock %}" + blocks/card.liquid: *card + - name: "reports only one syntax error for block.settings.heading: 'Hello'" + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.settings.heading: 'Hello', count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'heading:' instead of 'block.settings.heading:' when calling a block." + start: [0, 17] + highlight: "block.settings.heading: 'Hello'" + - name: 'reports only one syntax error for block.settings.count: 1' + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.settings.count: 1, count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'count:' instead of 'block.settings.count:' when calling a block." + start: [0, 17] + highlight: "block.settings.count: 1" + - name: 'reports only one syntax error for block.content: body' + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.content: body, count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "block.content: body" + - name: "reports only one syntax error for block.settings.heading.label: 'Hello'" + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.settings.heading.label: 'Hello', count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "block.settings.heading.label: 'Hello'" + - name: "reports only one syntax error for heading.label: 'Hello'" + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', heading.label: 'Hello', count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "heading.label: 'Hello'" + - name: 'reports only one syntax error for block.name: value' + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.name: value, count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Syntax error in 'block' tag" + start: [0, 0] + highlight: "{% block 'card', block.name: value, count: 'many', count: 'more' %}{% endblock %}" + - name: "reports only one syntax error for block.name: value, heading.label: 'Hello'" + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.name: value, heading.label: 'Hello', count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 36] + highlight: "heading.label: 'Hello'" + - name: 'reports only one syntax error for block.name: value, block.settings.count: 1' + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.name: value, block.settings.count: 1, count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'count:' instead of 'block.settings.count:' when calling a block." + start: [0, 36] + highlight: "block.settings.count: 1" + - name: "reports only one syntax error for block.settings.heading: 'Hello', block.content: body" + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.settings.heading: 'Hello', block.content: body, count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'heading:' instead of 'block.settings.heading:' when calling a block." + start: [0, 17] + highlight: "block.settings.heading: 'Hello'" + - name: "reports only one syntax error for block.content: body, block.settings.heading: 'Hello'" + checks: *block-checks + docset: default + files: + templates/test.liquid: "{% block 'card', block.content: body, block.settings.heading: 'Hello', count: 'many', count: 'more' %}{% endblock %}" + blocks/card.liquid: *card + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "block.content: body" + - name: does not report unknown tags like {% foobar %} + files: + snippets/test.liquid: "{% foobar %}" + - name: reports misplaced branch keyword else + files: + snippets/test.liquid: "{% else %}" + offenses: + - file: snippets/test.liquid + message: "Unknown tag 'else'" + start: [0, 0] + highlight: "{% else %}" + - name: reports misplaced branch keyword when + files: + snippets/test.liquid: "{% when x %}" + offenses: + - file: snippets/test.liquid + message: "Unknown tag 'when'" + start: [0, 0] + highlight: "{% when x %}" + - name: reports misplaced branch keyword when inside if + files: + snippets/test.liquid: "{% if true %}{% when x %}{% endif %}" + offenses: + - file: snippets/test.liquid + message: "Unknown tag 'when'" + start: [0, 13] + highlight: "{% when x %}" + - name: reports misplaced branch keyword when inside for + files: + snippets/test.liquid: "{% for x in xs %}{% when x %}{% endfor %}" + offenses: + - file: snippets/test.liquid + message: "Unknown tag 'when'" + start: [0, 17] + highlight: "{% when x %}" + - name: reports misplaced branch keyword when inside liquid + files: + snippets/test.liquid: |- + {% liquid + when x + %} + offenses: + - file: snippets/test.liquid + message: "Unknown tag 'when'" + start: [1, 2] + highlight: when x + - name: reports misplaced branch keyword elsif inside liquid + files: + snippets/test.liquid: |- + {% liquid + elsif x + %} + offenses: + - file: snippets/test.liquid + message: "Unknown tag 'elsif'" + start: [1, 2] + highlight: elsif x + - name: does not report valid else branches in {% if true %}{% else %}{% endif %} + files: + snippets/test.liquid: "{% if true %}{% else %}{% endif %}" + - name: does not report valid else branches in {% case x %}{% when x %}{% else %}{% endcase %} + files: + snippets/test.liquid: "{% case x %}{% when x %}{% else %}{% endcase %}" + - name: does not report valid else branches in {% for x in xs %}{% else %}{% endfor %} + files: + snippets/test.liquid: "{% for x in xs %}{% else %}{% endfor %}" + - name: reports no-space multiline comments without prefixed continuation lines + files: + snippets/test.liquid: |- + {% #hello + world %} + offenses: + - file: snippets/test.liquid + message: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character" + start: [0, 0] + highlight: |- + {% #hello + world %} + - name: accepts no-space multiline comments with prefixed continuation lines + files: + snippets/test.liquid: |- + {% #hello + #world %} + - name: "accepts Ruby's blank-first-line single-continuation shape" + files: + snippets/test.liquid: |- + {% # + world %} + - name: "produces no diagnostics for {% assign x = 1 %}" + files: + snippets/test.liquid: "{% assign x = 1 %}" + - name: "produces no diagnostics for {% assign x = 'hello' %}" + files: + snippets/test.liquid: "{% assign x = 'hello' %}" + - name: "produces no diagnostics for {% assign x = foo.bar %}" + files: + snippets/test.liquid: "{% assign x = foo.bar %}" + - name: "produces no diagnostics for {% assign x = foo | filter %}" + files: + snippets/test.liquid: "{% assign x = foo | filter %}" + - name: "produces no diagnostics for {% assign x = %} (empty RHS)" + files: + snippets/test.liquid: "{% assign x = %}" + - name: "produces no diagnostics for {% assign x extra = x %}" + files: + snippets/test.liquid: "{% assign x extra = x %}" + - name: 'matches Ruby Liquid parity for unclosed assign quote prefix in {% assign "x = y %}' + files: + snippets/test.liquid: '{% assign "x = y %}' + - name: "matches Ruby Liquid parity for unclosed assign quote prefix in {% assign 'x = y %}" + files: + snippets/test.liquid: "{% assign 'x = y %}" + - name: "reports LiquidSyntaxError for {% assign = 1 %} (missing LHS)" + files: + snippets/test.liquid: "{% assign = 1 %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign = 1 %}" + - name: "reports LiquidSyntaxError for {% assign x.y = 1 %} (dotted LHS)" + files: + snippets/test.liquid: "{% assign x.y = 1 %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign x.y = 1 %}" + - name: "reports LiquidSyntaxError for {% assign !x = 1 %} (garbage before target)" + files: + snippets/test.liquid: "{% assign !x = 1 %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign !x = 1 %}" + - name: "reports diagnostics for {% assign x 42 = x %}" + files: + snippets/test.liquid: "{% assign x 42 = x %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign x 42 = x %}" + - name: "reports diagnostics for {% assign x 'hi' = x %}" + files: + snippets/test.liquid: "{% assign x 'hi' = x %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign x 'hi' = x %}" + - name: "reports diagnostics for {% assign x ? = x %}" + files: + snippets/test.liquid: "{% assign x ? = x %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign x ? = x %}" + - name: "reports LiquidSyntaxError for {% assign x = foo | %} (empty filter)" + files: + snippets/test.liquid: "{% assign x = foo | %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign x = foo | %}" + - name: produces no diagnostics for valid capture tags inside liquid blocks + files: + snippets/test.liquid: |- + {% liquid + capture x + endcapture + %} + - name: reports LiquidSyntaxError for skipped capture markup inside liquid blocks + files: + snippets/test.liquid: |- + {% liquid + capture @x + endcapture + %} + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'capture' tag" + start: [1, 2] + highlight: |- + capture @x + endcapture + - name: produces no diagnostics for {% include 42 %} + files: + snippets/test.liquid: "{% include 42 %}" + - name: produces no diagnostics for {% include 3.14 %} + files: + snippets/test.liquid: "{% include 3.14 %}" + - name: produces no diagnostics for {% include -5 %} + files: + snippets/test.liquid: "{% include -5 %}" + - name: produces no diagnostics for {% include true %} + files: + snippets/test.liquid: "{% include true %}" + - name: produces no diagnostics for {% include false %} + files: + snippets/test.liquid: "{% include false %}" + - name: produces no diagnostics for {% include nil %} + files: + snippets/test.liquid: "{% include nil %}" + - name: produces no diagnostics for {% include blank %} + files: + snippets/test.liquid: "{% include blank %}" + - name: produces no diagnostics for {% include empty %} + files: + snippets/test.liquid: "{% include empty %}" + - name: produces no diagnostics for {% include (1..5) %} + files: + snippets/test.liquid: "{% include (1..5) %}" + - name: "reports LiquidSyntaxError for {% include ? %}" + files: + snippets/test.liquid: "{% include ? %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'include' tag" + start: [0, 0] + highlight: "{% include ? %}" + - name: reports LiquidSyntaxError for bare bracket access in include bindings + files: + snippets/test.liquid: "{% include 42 for [0] %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed in strict2 mode + start: [0, 0] + highlight: "{% include 42 for [0] %}" + - name: reports LiquidSyntaxError for multipart include named arguments + files: + snippets/test.liquid: "{% include 42, key.secondkey: value %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'include' tag" + start: [0, 0] + highlight: "{% include 42, key.secondkey: value %}" + - name: produces no diagnostics for {% echo %} + files: + snippets/test.liquid: "{% echo %}" + - name: produces no diagnostics for {% echo %} + files: + snippets/test.liquid: "{% echo %}" + - name: produces no diagnostics for {{ }} + files: + snippets/test.liquid: "{{ }}" + - name: produces no diagnostics for {{ }} + files: + snippets/test.liquid: "{{ }}" + - name: reports LiquidSyntaxError for garbage-only variable output + files: + snippets/test.liquid: "{{ @ }}" + offenses: + - file: snippets/test.liquid + message: Syntax error in variable output + start: [0, 0] + highlight: "{{ @ }}" + - name: "produces no diagnostics for {{ product.title | append: }}" + files: + snippets/test.liquid: "{{ product.title | append: }}" + - name: "produces no diagnostics for {% echo product.title | append: %}" + files: + snippets/test.liquid: "{% echo product.title | append: %}" + - name: "produces no diagnostics for {% assign title = product.title | append: %}" + files: + snippets/test.liquid: "{% assign title = product.title | append: %}" + - name: "reports diagnostics for malformed prefixes in {{ x y | append: }}" + files: + snippets/test.liquid: "{{ x y | append: }}" + offenses: + - file: snippets/test.liquid + message: Syntax error in variable output + start: [0, 0] + highlight: "{{ x y | append: }}" + - name: "reports diagnostics for malformed prefixes in {% echo x y | append: %}" + files: + snippets/test.liquid: "{% echo x y | append: %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'echo' tag" + start: [0, 0] + highlight: "{% echo x y | append: %}" + - name: "reports diagnostics for malformed prefixes in {% assign a = x y | append: %}" + files: + snippets/test.liquid: "{% assign a = x y | append: %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign a = x y | append: %}" + - name: "reports diagnostics for {{ product.title | }}" + files: + snippets/test.liquid: "{{ product.title | }}" + offenses: + - file: snippets/test.liquid + message: Syntax error in variable output + start: [0, 0] + highlight: "{{ product.title | }}" + - name: "reports diagnostics for {% echo product.title | %}" + files: + snippets/test.liquid: "{% echo product.title | %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'echo' tag" + start: [0, 0] + highlight: "{% echo product.title | %}" + - name: reports LiquidSyntaxError for bare bracket tablerow named argument values + files: + snippets/test.liquid: "{% tablerow product in collection.products cols: [0] %}{% endtablerow %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed + start: [0, 0] + highlight: "{% tablerow product in collection.products cols: [0] %}{% endtablerow %}" + - name: uses injected tag docs to allow known loop arguments + files: + snippets/test.liquid: "{% tablerow product in collection.products cols: 3 limit: 6 %}{% endtablerow %}" + - name: produces no diagnostics for {% cycle x, %} + files: + snippets/test.liquid: "{% cycle x, %}" + - name: produces no diagnostics for {% tablerow product in collection.products, %}{% endtablerow %} + files: + snippets/test.liquid: "{% tablerow product in collection.products, %}{% endtablerow %}" + - name: produces no diagnostics for {% paginate collection.products by 12, %}{% endpaginate %} + files: + snippets/test.liquid: "{% paginate collection.products by 12, %}{% endpaginate %}" + - name: reports diagnostics for {% cycle x,, %} + files: + snippets/test.liquid: "{% cycle x,, %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'cycle' tag" + start: [0, 0] + highlight: "{% cycle x,, %}" + - name: "reports diagnostics for {% cycle x @, %}" + files: + snippets/test.liquid: "{% cycle x @, %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'cycle' tag" + start: [0, 0] + highlight: "{% cycle x @, %}" + - name: reports diagnostics for {% tablerow product in collection.products,, %}{% endtablerow %} + files: + snippets/test.liquid: "{% tablerow product in collection.products,, %}{% endtablerow %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'tablerow' tag" + start: [0, 0] + highlight: "{% tablerow product in collection.products,, %}{% endtablerow %}" + - name: "reports diagnostics for {% tablerow product in collection.products @, %}{% endtablerow %}" + files: + snippets/test.liquid: "{% tablerow product in collection.products @, %}{% endtablerow %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'tablerow' tag" + start: [0, 0] + highlight: "{% tablerow product in collection.products @, %}{% endtablerow %}" + - name: reports diagnostics for {% paginate collection.products by 12,, %}{% endpaginate %} + files: + snippets/test.liquid: "{% paginate collection.products by 12,, %}{% endpaginate %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'paginate' tag" + start: [0, 0] + highlight: "{% paginate collection.products by 12,, %}" + - name: "reports diagnostics for {% paginate collection.products by 12 @, %}{% endpaginate %}" + files: + snippets/test.liquid: "{% paginate collection.products by 12 @, %}{% endpaginate %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'paginate' tag" + start: [0, 0] + highlight: "{% paginate collection.products by 12 @, %}" + - name: emits a single LiquidSyntaxError on the same line + files: + snippets/test.liquid: "{% assign x = foo | %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign x = foo | %}" + - name: keeps one offense per line when errors are on different lines + files: + snippets/test.liquid: |- + {% assign x = foo | %} + {% assign = 1 %} + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [0, 0] + highlight: "{% assign x = foo | %}" + - file: snippets/test.liquid + message: "Syntax error in 'assign' tag" + start: [1, 0] + highlight: "{% assign = 1 %}" + - name: "reports LiquidSyntaxError for {% case [0] %} (bare bracket in case expression)" + files: + snippets/test.liquid: "{% case [0] %}{% when 1 %}{% endcase %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed + start: [0, 0] + highlight: "{% case [0] %}{% when 1 %}{% endcase %}" + - name: "reports LiquidSyntaxError for {% when [0] %} (bare bracket in when value)" + files: + snippets/test.liquid: "{% case x %}{% when [0] %}{% endcase %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed + start: [0, 12] + highlight: "{% when [0] %}" + - name: "reports LiquidSyntaxError for {% capture [0] %} (bare bracket in capture tag)" + files: + snippets/test.liquid: "{% capture [0] %}{% endcapture %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed + start: [0, 0] + highlight: "{% capture [0] %}{% endcapture %}" + - name: "reports LiquidSyntaxError for {% decrement [0] %} (bare bracket in decrement tag)" + files: + snippets/test.liquid: "{% decrement [0] %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed + start: [0, 0] + highlight: "{% decrement [0] %}" + - name: "reports LiquidSyntaxError for {{ [0] }} (bare bracket in variable output)" + files: + snippets/test.liquid: "{{ [0] }}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed in strict2 mode + start: [0, 0] + highlight: "{{ [0] }}" + - name: produces no diagnostics for valid unless conditions (1) + files: + snippets/test.liquid: "{% unless product.available %}Sold out{% endunless %}" + - name: produces no diagnostics for valid unless conditions (2) + files: + snippets/test.liquid: "{% unless product.available or customer.tags contains 'vip' %}Hidden{% endunless %}" + - name: produces no diagnostics for valid unless conditions (3) + files: + snippets/test.liquid: "{% unless items[0] %}Empty{% endunless %}" + - name: produces no diagnostics for valid unless conditions (4) + files: + snippets/test.liquid: |- + {% unless + product.available + %}Sold out{% endunless %} + - name: produces no diagnostics for valid unless conditions (5) + files: + snippets/test.liquid: |- + {% liquid + unless product.available + echo 'Sold out' + endunless + %} + - name: reports LiquidSyntaxError for skipped unless garbage (1) + files: + snippets/test.liquid: "{% unless product.available @ %}Sold out{% endunless %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'unless' tag" + start: [0, 0] + highlight: "{% unless product.available @ %}Sold out{% endunless %}" + - name: reports LiquidSyntaxError for skipped unless garbage (2) + files: + snippets/test.liquid: "{% unless product.available @ and customer %}Hidden{% endunless %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'unless' tag" + start: [0, 0] + highlight: "{% unless product.available @ and customer %}Hidden{% endunless %}" + - name: reports LiquidSyntaxError for skipped unless garbage (3) + files: + snippets/test.liquid: |- + {% unless + product.available + @ + %}Sold out{% endunless %} + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'unless' tag" + start: [0, 0] + highlight: |- + {% unless + product.available + @ + %}Sold out{% endunless %} + - name: reports LiquidSyntaxError for bare unless bracket access + files: + snippets/test.liquid: "{% unless [0] %}Hidden{% endunless %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed in strict2 mode + start: [0, 0] + highlight: "{% unless [0] %}Hidden{% endunless %}" + - name: reports LiquidSyntaxError for whitespace-separated unless quote prefixes (1) + files: + snippets/test.liquid: "{% unless ' product.available %}Sold out{% endunless %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'unless' tag" + start: [0, 0] + highlight: "{% unless ' product.available %}Sold out{% endunless %}" + - name: reports LiquidSyntaxError for whitespace-separated unless quote prefixes (2) + files: + snippets/test.liquid: '{% unless " product.available %}Sold out{% endunless %}' + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'unless' tag" + start: [0, 0] + highlight: '{% unless " product.available %}Sold out{% endunless %}' + - name: "reports LiquidSyntaxError for adjacent unless quote prefixes in {% unless 'product.available %}Sold out{% endunless %}" + files: + snippets/test.liquid: "{% unless 'product.available %}Sold out{% endunless %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'unless' tag" + start: [0, 0] + highlight: "{% unless 'product.available %}Sold out{% endunless %}" + - name: 'reports LiquidSyntaxError for adjacent unless quote prefixes in {% unless "product.available %}Sold out{% endunless %}' + files: + snippets/test.liquid: '{% unless "product.available %}Sold out{% endunless %}' + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'unless' tag" + start: [0, 0] + highlight: '{% unless "product.available %}Sold out{% endunless %}' + - name: "produces no diagnostics for closed string markup in {% if product.title == 'Hat' %}Hat{% endif %}" + files: + snippets/test.liquid: "{% if product.title == 'Hat' %}Hat{% endif %}" + - name: "produces no diagnostics for closed string markup in {% case product.type %}{% when 'shirt' %}Shirt{% endcase %}" + files: + snippets/test.liquid: "{% case product.type %}{% when 'shirt' %}Shirt{% endcase %}" + - name: "produces no diagnostics for closed string markup in {{ 'hello' }}" + files: + snippets/test.liquid: "{{ 'hello' }}" + - name: "reports LiquidSyntaxError for unclosed string markup in {% if ' product.available %}Available{% endif %}" + files: + snippets/test.liquid: "{% if ' product.available %}Available{% endif %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'if' tag" + start: [0, 0] + highlight: "{% if ' product.available %}Available{% endif %}" + - name: "reports LiquidSyntaxError for unclosed string markup in {% case product.type %}{% when ' shirt %}Shirt{% endcase %}" + files: + snippets/test.liquid: "{% case product.type %}{% when ' shirt %}Shirt{% endcase %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'when' tag" + start: [0, 23] + highlight: "{% when ' shirt %}" + - name: "reports LiquidSyntaxError for unclosed string markup in {{ 'hello }}" + files: + snippets/test.liquid: "{{ 'hello }}" + offenses: + - file: snippets/test.liquid + message: Syntax error in variable output + start: [0, 0] + highlight: "{{ 'hello }}" + - name: "produces no diagnostics for {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}" + - name: produces no diagnostics for an app block with only whitespace inside + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}\n {% endblock %}" + - name: "produces no diagnostics for {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.name: 'Example' %}{% endblock %}" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.name: 'Example' %}{% endblock %}" + - name: "reports {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', heading: 'Hello' %}{% endblock %}" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', heading: 'Hello' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - app blocks do not accept arguments" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', heading: 'Hello' %}{% endblock %}" + - name: "reports {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.settings.heading: 'Hello' %}{% endblock %}" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.settings.heading: 'Hello' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - app blocks do not accept arguments" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.settings.heading: 'Hello' %}{% endblock %}" + - name: "reports {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}Hello{% endblock %}" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}Hello{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - app blocks do not accept content" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}Hello{% endblock %}" + - name: "reports {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{{ product.title }}{% endblock %}" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{{ product.title }}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - app blocks do not accept content" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{{ product.title }}{% endblock %}" + - name: "rejects a non-string block.name on an app path: 1" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.name: 1 %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Syntax error in 'block' tag" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.name: 1 %}{% endblock %}" + - name: "rejects a non-string block.name on an app path: variable" + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.name: variable %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Syntax error in 'block' tag" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', block.name: variable %}{% endblock %}" + - name: reports malformed app block paths + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/snippets/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Syntax error in 'block' tag" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/snippets/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}" + - name: "reports only the app syntax error across block checks: {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', heading: 'First', heading: 'Second' %}{% endblock %}" + checks: [LiquidSyntaxError, MissingBlockArguments, UnrecognizedBlockArguments, ValidBlockArgumentTypes, DuplicateBlockArguments] + docset: default + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', heading: 'First', heading: 'Second' %}{% endblock %}" + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - app blocks do not accept arguments" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', heading: 'First', heading: 'Second' %}{% endblock %}" + - name: "reports only the app syntax error across block checks: {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', content: 'Explicit' %}Inline{% endblock %}" + checks: [LiquidSyntaxError, MissingBlockArguments, UnrecognizedBlockArguments, ValidBlockArgumentTypes, DuplicateBlockArguments] + docset: default + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', content: 'Explicit' %}Inline{% endblock %}" + offenses: + - check: LiquidSyntaxError + file: templates/test.liquid + message: "Liquid syntax error: in 'block' - app blocks do not accept arguments" + start: [0, 0] + highlight: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000', content: 'Explicit' %}Inline{% endblock %}" + - name: produces no block parameter diagnostics for app block paths + checks: [LiquidSyntaxError, MissingBlockArguments, UnrecognizedBlockArguments, ValidBlockArgumentTypes, DuplicateBlockArguments] + docset: default + files: + templates/test.liquid: "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}" + - name: tells authors to pass heading as a plain argument + files: + templates/test.liquid: "{% block 'card', block.settings.heading: 'Heading' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'heading:' instead of 'block.settings.heading:' when calling a block." + start: [0, 17] + highlight: "block.settings.heading: 'Heading'" + - name: tells authors to pass image_url as a plain argument + files: + templates/test.liquid: "{% block 'card', block.settings.image_url: product.featured_image %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'image_url:' instead of 'block.settings.image_url:' when calling a block." + start: [0, 17] + highlight: "block.settings.image_url: product.featured_image" + - name: "rejects the unsupported dotted argument block.content: body" + files: + templates/test.liquid: "{% block 'card', block.content: body %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "block.content: body" + - name: "rejects the unsupported dotted argument block.settings.heading.label: 'Heading'" + files: + templates/test.liquid: "{% block 'card', block.settings.heading.label: 'Heading' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "block.settings.heading.label: 'Heading'" + - name: "rejects the unsupported dotted argument block.unknown: 'value'" + files: + templates/test.liquid: "{% block 'card', block.unknown: 'value' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "block.unknown: 'value'" + - name: "rejects the unsupported dotted argument heading.label: 'Heading'" + files: + templates/test.liquid: "{% block 'card', heading.label: 'Heading' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "heading.label: 'Heading'" + - name: reports only the dotted argument in a body-form block + files: + templates/test.liquid: |- + {% block 'card', block.settings.heading: 'Heading' %} + Child content + {% endblock %} + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'heading:' instead of 'block.settings.heading:' when calling a block." + start: [0, 17] + highlight: "block.settings.heading: 'Heading'" + - name: "reports only the first of several dotted arguments in block.settings.heading: 'Heading', block.content: body, heading.label: 'Label'" + files: + templates/test.liquid: "{% block 'card', title: 'Title', block.settings.heading: 'Heading', block.content: body, heading.label: 'Label' %}Child{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'heading:' instead of 'block.settings.heading:' when calling a block." + start: [0, 33] + highlight: "block.settings.heading: 'Heading'" + - name: "reports only the first of several dotted arguments in block.content: body, block.settings.heading: 'Heading', block.settings.count: 1" + files: + templates/test.liquid: "{% block 'card', title: 'Title', block.content: body, block.settings.heading: 'Heading', block.settings.count: 1 %}Child{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 33] + highlight: "block.content: body" + - name: "reports only the first of several dotted arguments in block.settings.heading: 'Heading', block.settings.count: 1" + files: + templates/test.liquid: "{% block 'card', title: 'Title', block.settings.heading: 'Heading', block.settings.count: 1 %}Child{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Liquid syntax error: in 'block' - Use 'heading:' instead of 'block.settings.heading:' when calling a block." + start: [0, 33] + highlight: "block.settings.heading: 'Heading'" + - name: accepts block.name with a string value alongside plain arguments + files: + templates/test.liquid: "{% block 'card', block.name: 'Card', heading: 'Heading' %}{% endblock %}" + - name: "reports other malformed block tags on the whole tag in {% block 'card', block.settings %}{% endblock %}" + files: + templates/test.liquid: "{% block 'card', block.settings %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Syntax error in 'block' tag" + start: [0, 0] + highlight: "{% block 'card', block.settings %}{% endblock %}" + - name: "reports other malformed block tags on the whole tag in {% block 'card', block.name: value %}Child content{% endblock %}" + files: + templates/test.liquid: "{% block 'card', block.name: value %}Child content{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Syntax error in 'block' tag" + start: [0, 0] + highlight: "{% block 'card', block.name: value %}Child content{% endblock %}" + - name: "reports LiquidSyntaxError for unclosed 'block' tags inside liquid blocks" + files: + snippets/test.liquid: |- + {% liquid + block 'hero' + %} + offenses: + - file: snippets/test.liquid + message: "Unclosed block tag 'block' in {% liquid %} block" + start: [0, 0] + highlight: |- + {% liquid + block 'hero' + %} + - name: "reports LiquidSyntaxError for unclosed 'partial' tags inside liquid blocks" + files: + snippets/test.liquid: |- + {% liquid + partial 'product-card' + %} + offenses: + - file: snippets/test.liquid + message: "Unclosed block tag 'partial' in {% liquid %} block" + start: [0, 0] + highlight: |- + {% liquid + partial 'product-card' + %} + - name: "reports LiquidSyntaxError for standalone end'block' tags" + files: + snippets/test.liquid: "{% endblock %}" + offenses: + - file: snippets/test.liquid + message: "Attempting to close LiquidTag 'block' before it was opened without a matching 'block'" + start: [0, 0] + highlight: "{% endblock %" + - name: "reports LiquidSyntaxError for standalone end'partial' tags" + files: + snippets/test.liquid: "{% endpartial %}" + offenses: + - file: snippets/test.liquid + message: "Attempting to close LiquidTag 'partial' before it was opened without a matching 'partial'" + start: [0, 0] + highlight: "{% endpartial %" + - name: produces no diagnostics for doc closing markup + files: + snippets/test.liquid: "{% doc %}content{% enddoc foo %}" + - name: reports later unclosed doc blocks after accepted doc closing markup + files: + snippets/test.liquid: "{% doc %}content{% enddoc foo %}{% doc %}" + offenses: + - file: snippets/test.liquid + message: "Attempting to end parsing before LiquidRawTag 'doc' was closed" + start: [0, 0] + highlight: "{% doc %}" + - name: accepts later balanced doc blocks after accepted doc closing markup + files: + snippets/test.liquid: "{% doc %}content{% enddoc foo %}{% doc %}later{% enddoc %}" + - name: produces no diagnostics for whitespace-control doc closing markup + files: + snippets/test.liquid: "{% doc %}content{%- enddoc foo -%}" + - name: reports nested doc tags with whitespace-control delimiters + files: + snippets/test.liquid: "{% doc %}{%- doc -%}{% enddoc %}" + offenses: + - file: snippets/test.liquid + message: Nested doc tags are not allowed + start: [0, 0] + highlight: "{% doc %}" + - name: does not treat similarly-prefixed doc tag names as nested doc tags + files: + snippets/test.liquid: "{% doc %}{% docx %}{% enddoc %}" + - name: reports nested comment blocks masked by a later independent comment block + files: + snippets/test.liquid: "{% comment %}{% comment %}inner{% endcomment %}{% comment %}second{% endcomment %}" + offenses: + - file: snippets/test.liquid + message: "Liquid syntax error: 'comment' tag was never closed" + start: [0, 0] + highlight: "{% comment %}" + - name: reports nested raw blocks masked by a later independent raw block + files: + snippets/test.liquid: "{% comment %}{% raw %}inner{% endcomment %}{% raw %}second{% endraw %}" + offenses: + - file: snippets/test.liquid + message: "Attempting to end parsing before LiquidRawTag 'comment' was closed" + start: [0, 0] + highlight: "{% comment %}" + - name: accepts nested comment and raw blocks closed immediately after the parsed comment closer (1) + files: + snippets/test.liquid: "{% comment %}{% comment %}inner{% endcomment %}{% endcomment %}" + - name: accepts nested comment and raw blocks closed immediately after the parsed comment closer (2) + files: + snippets/test.liquid: "{% comment %}{% raw %}{% endcomment %}{% endraw %}{% endcomment %}" + - name: produces no diagnostics for Liquid syntax inside a valid raw body + files: + snippets/test.liquid: "{% raw %}{% if broken %}{% endraw %}" + - name: produces no diagnostics for raw closing markup + files: + snippets/test.liquid: "{% raw %}c{% endraw foo %}" + - name: reports later unclosed raw blocks after accepted raw closing markup + files: + snippets/test.liquid: "{% raw %}c{% endraw foo %}{% raw %}" + offenses: + - file: snippets/test.liquid + message: "Attempting to end parsing before LiquidRawTag 'raw' was closed" + start: [0, 0] + highlight: "{% raw %}" + - name: accepts later balanced raw blocks after accepted raw closing markup + files: + snippets/test.liquid: "{% raw %}c{% endraw foo %}{% raw %}later{% endraw %}" + - name: produces no diagnostics for whitespace-control raw closing markup + files: + snippets/test.liquid: "{% raw %}c{%- endraw foo -%}" + - name: reports parser errors for similarly-prefixed raw closing tags + files: + snippets/test.liquid: "{% raw %}c{% endrawx foo %}" + offenses: + - file: snippets/test.liquid + message: "Attempting to end parsing before LiquidRawTag 'raw' was closed" + start: [0, 0] + highlight: "{% raw %}" + - name: reports LiquidSyntaxError for raw opening markup + files: + snippets/test.liquid: "{% raw foo %}{% endraw %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'raw' tag" + start: [0, 7] + highlight: foo + - name: reports LiquidSyntaxError for skipped raw opening characters + files: + snippets/test.liquid: "{% raw @#$ %}{% endraw %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'raw' tag" + start: [0, 7] + highlight: "@#$" + - name: reports LiquidSyntaxError for quoted raw opening markup + files: + snippets/test.liquid: "{% raw 'hello' %}{% endraw %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'raw' tag" + start: [0, 7] + highlight: "'hello'" + - name: reports LiquidSyntaxError for unclosed quoted raw opening markup + files: + snippets/test.liquid: "{% raw 'hello %}{% endraw %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'raw' tag" + start: [0, 7] + highlight: "'hello" + - name: reports LiquidSyntaxError for unclosed raw tags inside liquid blocks + files: + snippets/test.liquid: |- + {% liquid + raw + %} + offenses: + - file: snippets/test.liquid + message: "Unclosed raw tag 'raw' in {% liquid %} block" + start: [0, 0] + highlight: |- + {% liquid + raw + %} + - name: does not report raw parser errors when source text merely mentions raw + files: + snippets/test.liquid: This text mentions raw and endraw without Liquid tags. + - name: produces no diagnostics for empty javascript markup in {% javascript %}{% endjavascript %} + files: + sections/test.liquid: "{% javascript %}{% endjavascript %}" + - name: produces no diagnostics for empty javascript markup in {% javascript %}{% endjavascript %} + files: + sections/test.liquid: "{% javascript %}{% endjavascript %}" + - name: produces no diagnostics for empty javascript markup in {%- javascript -%}{%- endjavascript -%} + files: + sections/test.liquid: "{%- javascript -%}{%- endjavascript -%}" + - name: "reports LiquidSyntaxError for invalid javascript opening markup: {% javascript foo %}{% endjavascript %}" + files: + sections/test.liquid: "{% javascript foo %}{% endjavascript %}" + offenses: + - file: sections/test.liquid + message: "Syntax Error in 'javascript' - Valid syntax: javascript" + start: [0, 0] + highlight: "{% javascript foo %}" + - name: "reports LiquidSyntaxError for invalid javascript opening markup: {% javascript @#$ %}{% endjavascript %}" + files: + sections/test.liquid: "{% javascript @#$ %}{% endjavascript %}" + offenses: + - file: sections/test.liquid + message: "Syntax Error in 'javascript' - Valid syntax: javascript" + start: [0, 0] + highlight: "{% javascript @#$ %}" + - name: does not treat similarly-prefixed javascript tags as javascript openings + files: + snippets/test.liquid: "{% javascriptx foo %}" + - name: produces no diagnostics for javascript closing markup + files: + sections/test.liquid: "{% javascript %}console.log('x');{% endjavascript foo %}" + - name: reports parser errors for similarly-prefixed javascript closing tags + files: + snippets/test.liquid: "{% javascript %}console.log('x');{% endjavascriptx foo %}" + offenses: + - file: snippets/test.liquid + message: "'javascript' tag was never closed" + start: [0, 0] + highlight: "{% javascript %}" + - name: produces no diagnostics for empty style markup in {% style %}{% endstyle %} + files: + snippets/test.liquid: "{% style %}{% endstyle %}" + - name: produces no diagnostics for empty style markup in {% style %}{% endstyle %} + files: + snippets/test.liquid: "{% style %}{% endstyle %}" + - name: reports LiquidSyntaxError for style arguments + files: + snippets/test.liquid: "{% style extra %}{% endstyle %}" + offenses: + - file: snippets/test.liquid + message: "'style' tag does not accept any arguments in \"extra\"" + start: [0, 0] + highlight: "{% style extra %}" + - name: produces no diagnostics for valid stylesheet markup in {% stylesheet %}{% endstylesheet %} + files: + sections/test.liquid: "{% stylesheet %}{% endstylesheet %}" + - name: produces no diagnostics for valid stylesheet markup in {% stylesheet %}{% endstylesheet %} + files: + sections/test.liquid: "{% stylesheet %}{% endstylesheet %}" + - name: "produces no diagnostics for valid stylesheet markup in {% stylesheet 'scss' %}{% endstylesheet %}" + files: + sections/test.liquid: "{% stylesheet 'scss' %}{% endstylesheet %}" + - name: reports LiquidSyntaxError for invalid stylesheet markup in {% stylesheet scss %}{% endstylesheet %} + files: + sections/test.liquid: "{% stylesheet scss %}{% endstylesheet %}" + offenses: + - file: sections/test.liquid + message: "'stylesheet' tag can only accept the string argument 'scss' in \"scss\"" + start: [0, 0] + highlight: "{% stylesheet scss %}" + - name: reports LiquidSyntaxError for invalid stylesheet markup in {% stylesheet scssx %}{% endstylesheet %} + files: + sections/test.liquid: "{% stylesheet scssx %}{% endstylesheet %}" + offenses: + - file: sections/test.liquid + message: "'stylesheet' tag can only accept the string argument 'scss' in \"scssx\"" + start: [0, 0] + highlight: "{% stylesheet scssx %}" + - name: reports LiquidSyntaxError for invalid stylesheet markup in {% stylesheet scss extra %}{% endstylesheet %} + files: + sections/test.liquid: "{% stylesheet scss extra %}{% endstylesheet %}" + offenses: + - file: sections/test.liquid + message: "'stylesheet' tag can only accept the string argument 'scss' in \"scss extra\"" + start: [0, 0] + highlight: "{% stylesheet scss extra %}" + - name: "reports LiquidSyntaxError for invalid stylesheet markup in {% stylesheet scss? %}{% endstylesheet %}" + files: + sections/test.liquid: "{% stylesheet scss? %}{% endstylesheet %}" + offenses: + - file: sections/test.liquid + message: "'stylesheet' tag can only accept the string argument 'scss' in \"scss?\"" + start: [0, 0] + highlight: "{% stylesheet scss? %}" + - name: "reports LiquidSyntaxError for Ruby-stricter comment raw bodies: {% comment %}{% comment %}inner{% endcomment %}" + files: + snippets/test.liquid: "{% comment %}{% comment %}inner{% endcomment %}" + offenses: + - file: snippets/test.liquid + message: "Liquid syntax error: 'comment' tag was never closed" + start: [0, 0] + highlight: "{% comment %}" + - name: "reports LiquidSyntaxError for Ruby-stricter comment raw bodies: {%- comment -%}{%- comment -%}inner{%- endcomment -%}" + files: + snippets/test.liquid: "{%- comment -%}{%- comment -%}inner{%- endcomment -%}" + offenses: + - file: snippets/test.liquid + message: "Liquid syntax error: 'comment' tag was never closed" + start: [0, 0] + highlight: "{%- comment -%}" + - name: "reports LiquidSyntaxError for Ruby-stricter comment raw bodies: {% comment %}{% raw %}hello{% endcomment %}" + files: + snippets/test.liquid: "{% comment %}{% raw %}hello{% endcomment %}" + offenses: + - file: snippets/test.liquid + message: "Attempting to end parsing before LiquidRawTag 'comment' was closed" + start: [0, 0] + highlight: "{% comment %}" + - name: "reports LiquidSyntaxError for Ruby-stricter comment raw bodies: {% comment %}this is {{{ not }}} valid{{ liquid{% endcomment %}" + files: + snippets/test.liquid: "{% comment %}this is {{{ not }}} valid{{ liquid{% endcomment %}" + offenses: + - file: snippets/test.liquid + message: "Liquid syntax error: 'comment' tag was never closed" + start: [0, 13] + highlight: this is {{{ not }}} valid{{ liquid + - name: "reports LiquidSyntaxError for Ruby-stricter comment raw bodies: {% comment %}this is {% if liquid{% endcomment %}" + files: + snippets/test.liquid: "{% comment %}this is {% if liquid{% endcomment %}" + offenses: + - file: snippets/test.liquid + message: "Liquid syntax error: 'comment' tag was never closed" + start: [0, 13] + highlight: this is {% if liquid + - name: produces no diagnostics for inert closed Liquid tags inside comment bodies + files: + snippets/test.liquid: "{% comment %}{% if %}{% endcomment %}" + - name: produces no diagnostics for closed raw tags inside comment bodies + files: + snippets/test.liquid: "{% comment %}{% raw %}hello{% endraw %}{% endcomment %}" + - name: does not treat similarly-prefixed names as comment or raw tags + files: + snippets/test.liquid: "{% comment %}{% commentx %}{% rawx %}{% endcomment %}" + - name: does not report parser errors when source text merely mentions comment tags + files: + snippets/test.liquid: This text mentions comment and endcomment without Liquid tags. + - name: "produces no diagnostics for Ruby-accepted inert comment raw bodies: {% comment %}{% comment %}inner{% endcomment %}{% endcomment %}" + files: + snippets/test.liquid: "{% comment %}{% comment %}inner{% endcomment %}{% endcomment %}" + - name: "produces no diagnostics for Ruby-accepted inert comment raw bodies: {% comment %}{% comment %}{% comment %}deep{% endcomment %}{% endcomment %}{% endcomment %}" + files: + snippets/test.liquid: "{% comment %}{% comment %}{% comment %}deep{% endcomment %}{% endcomment %}{% endcomment %}" + - name: "produces no diagnostics for Ruby-accepted inert comment raw bodies: {% comment %}{% raw %}{% endcomment %}{% endraw %}{% endcomment %}" + files: + snippets/test.liquid: "{% comment %}{% raw %}{% endcomment %}{% endraw %}{% endcomment %}" + - name: "produces no diagnostics for Ruby-accepted inert comment raw bodies: {%- comment -%}{%- raw -%}{%- endcomment -%}{%- endraw -%}{%- endcomment -%}" + files: + snippets/test.liquid: "{%- comment -%}{%- raw -%}{%- endcomment -%}{%- endraw -%}{%- endcomment -%}" + - name: produces no diagnostics for valid Liquid + files: + snippets/test.liquid: "{% if true %}hello{% endif %}" + - name: produces no diagnostics for Liquid output tags + files: + snippets/test.liquid: "{{ shop.name }}" + - name: produces no diagnostics for valid render markup + files: + snippets/test.liquid: "{% render 'snippet' %}" + - name: produces no diagnostics for valid render markup inside liquid blocks + files: + snippets/test.liquid: |- + {% liquid + render 'snippet', product: product + %} + - name: reports LiquidSyntaxError for skipped quote prefixes + files: + snippets/test.liquid: "{% render \"'snippet' %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'render' tag" + start: [0, 0] + highlight: "{% render \"'snippet' %}" + - name: reports LiquidSyntaxError for malformed string markup + files: + snippets/test.liquid: "{% render %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'render' tag" + start: [0, 0] + highlight: "{% render %}" + - name: reports LiquidSyntaxError for malformed string markup inside liquid blocks + files: + snippets/test.liquid: |- + {% liquid + render + %} + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'render' tag" + start: [1, 2] + highlight: render + - name: reports LiquidSyntaxError for bare bracket access in render snippet lookups + files: + snippets/test.liquid: "{% render [0] %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed in strict2 mode + start: [0, 0] + highlight: "{% render [0] %}" + - name: reports LiquidSyntaxError for bare bracket access in render binding values + files: + snippets/test.liquid: "{% render 'snippet' for [0] %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed in strict2 mode + start: [0, 0] + highlight: "{% render 'snippet' for [0] %}" + - name: reports LiquidSyntaxError for bare bracket access in render keyword values + files: + snippets/test.liquid: "{% render 'snippet', x: [0] %}" + offenses: + - file: snippets/test.liquid + message: Bare bracket access is not allowed in strict2 mode + start: [0, 0] + highlight: "{% render 'snippet', x: [0] %}" + - name: reports LiquidSyntaxError for skipped trailing garbage in render markup + files: + snippets/test.liquid: "{% render 'snippet' ? %}" + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'render' tag" + start: [0, 0] + highlight: "{% render 'snippet' ? %}" + - name: reports LiquidSyntaxError for skipped trailing garbage inside liquid blocks + files: + snippets/test.liquid: |- + {% liquid + render 'snippet' ? + %} + offenses: + - file: snippets/test.liquid + message: "Syntax error in 'render' tag" + start: [1, 2] + highlight: "render 'snippet' ?" diff --git a/specs/checks/matching-translations.yml b/specs/checks/matching-translations.yml new file mode 100644 index 000000000..a11010b0b --- /dev/null +++ b/specs/checks/matching-translations.yml @@ -0,0 +1,819 @@ +check: MatchingTranslations +severity: error +cases: + - name: should report offenses when the translation file is missing a key (1) + files: + locales/en.default.json: |- + { + "hello": "Hello", + "world": "World" + } + locales/pt-BR.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.json + message: "The translation for 'world' is missing" + start: [0, 0] + highlight: |- + { + "hello": "Olá" + } + fix: |- + { + "hello": "Olá", + "world": "TODO" + } + - name: should report offenses when the translation file is missing a key (2) + files: + locales/en.default.schema.json: |- + { + "hello": "Hello", + "world": "World" + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.schema.json + message: "The translation for 'world' is missing" + start: [0, 0] + highlight: |- + { + "hello": "Olá" + } + fix: |- + { + "hello": "Olá", + "world": "TODO" + } + - name: should report offenses when the default translation is missing a key (1) + files: + locales/en.default.json: |- + { + "hello": "Hello" + } + locales/pt-BR.json: |- + { + "hello": "Olá", + "world": "Mundo" + } + offenses: + - file: locales/pt-BR.json + message: "A default translation for 'world' does not exist" + start: [2, 2] + highlight: '"world": "Mundo"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": "Olá" + } + - name: should report offenses when the default translation is missing a key (2) + files: + locales/en.default.schema.json: |- + { + "hello": "Hello" + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá", + "world": "Mundo" + } + offenses: + - file: locales/pt-BR.schema.json + message: "A default translation for 'world' does not exist" + start: [2, 2] + highlight: '"world": "Mundo"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": "Olá" + } + - name: should report offenses when nested translation keys do not exist (1) + files: + locales/en.default.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.json: |- + { + "hello": {} + } + offenses: + - file: locales/pt-BR.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": {}' + fix: |- + { + "hello": { + "world": "TODO" + } + } + - name: should report offenses when nested translation keys do not exist (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.schema.json: |- + { + "hello": {} + } + offenses: + - file: locales/pt-BR.schema.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": {}' + fix: |- + { + "hello": { + "world": "TODO" + } + } + - name: should report offenses when translation shapes do not match (1) + files: + locales/en.default.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.json + message: "A default translation for 'hello' does not exist" + start: [1, 2] + highlight: '"hello": "Olá"' + suggest: + - message: Delete unneeded translation key + result: "{}" + - file: locales/pt-BR.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": "Olá"' + fix: |- + { + "hello": { + "world": "TODO" + } + } + - name: should report offenses when translation shapes do not match (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.schema.json + message: "A default translation for 'hello' does not exist" + start: [1, 2] + highlight: '"hello": "Olá"' + suggest: + - message: Delete unneeded translation key + result: "{}" + - file: locales/pt-BR.schema.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": "Olá"' + fix: |- + { + "hello": { + "world": "TODO" + } + } + - name: should report offenses when nested translation keys do not match (1) + files: + locales/en.default.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/fr.json: |- + { + "hello": { + "monde": "Bonjour, monde" + } + } + locales/es-ES.json: |- + { + "hello": { + "world": "Hello, world!", + "mundo": { + "hola": "¡Hola, mundo!" + } + } + } + offenses: + - file: locales/fr.json + message: "A default translation for 'hello.monde' does not exist" + start: [2, 4] + highlight: '"monde": "Bonjour, monde"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": {} + } + - file: locales/fr.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: |- + "hello": { + "monde": "Bonjour, monde" + } + fix: |- + { + "hello": { + "monde": "Bonjour, monde", + "world": "TODO" + } + } + - file: locales/es-ES.json + message: "A default translation for 'hello.mundo.hola' does not exist" + start: [4, 6] + highlight: '"hola": "¡Hola, mundo!"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": { + "world": "Hello, world!", + "mundo": {} + } + } + - name: should report offenses when nested translation keys do not match (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/fr.schema.json: |- + { + "hello": { + "monde": "Bonjour, monde" + } + } + locales/es-ES.schema.json: |- + { + "hello": { + "world": "Hello, world!", + "mundo": { + "hola": "¡Hola, mundo!" + } + } + } + offenses: + - file: locales/fr.schema.json + message: "A default translation for 'hello.monde' does not exist" + start: [2, 4] + highlight: '"monde": "Bonjour, monde"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": {} + } + - file: locales/fr.schema.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: |- + "hello": { + "monde": "Bonjour, monde" + } + fix: |- + { + "hello": { + "monde": "Bonjour, monde", + "world": "TODO" + } + } + - file: locales/es-ES.schema.json + message: "A default translation for 'hello.mundo.hola' does not exist" + start: [4, 6] + highlight: '"hola": "¡Hola, mundo!"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": { + "world": "Hello, world!", + "mundo": {} + } + } + - name: should not report offenses when default translations do not exist (1) + files: + locales/en.json: |- + { + "hello": "Hello" + } + locales/pt-BR.json: |- + { + "hello": "Olá" + } + - name: should not report offenses when default translations do not exist (2) + files: + locales/en.schema.json: |- + { + "hello": "Hello" + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá" + } + - name: should not report offenses when translations match (1) + files: + locales/en.default.json: |- + { + "hello": "Hello", + "world": "World" + } + locales/pt-BR.json: |- + { + "hello": "Olá", + "world": "Mundo" + } + - name: should not report offenses when translations match (2) + files: + locales/en.default.schema.json: |- + { + "hello": "Hello", + "world": "World" + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá", + "world": "Mundo" + } + - name: should not report offenses when nested translations match (1) + files: + locales/en.default.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.json: |- + { + "hello": { + "world": "Olá, mundo!" + } + } + locales/fr.json: |- + { + "hello": { + "world": "Bonjour, monde" + } + } + - name: should not report offenses when nested translations match (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.schema.json: |- + { + "hello": { + "world": "Olá, mundo!" + } + } + locales/fr.schema.json: |- + { + "hello": { + "world": "Bonjour, monde" + } + } + - name: should not report offenses and ignore pluralization (1) + files: + locales/en.default.json: |- + { + "hello": { + "one": "Hello, you", + "other": "Hello, y'all" + } + } + locales/pt-BR.json: |- + { + "hello": { + "zero": "Estou sozinho :(", + "few": "Olá, galerinha :)" + } + } + - name: should not report offenses and ignore pluralization (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "one": "Hello, you", + "other": "Hello, y'all" + } + } + locales/pt-BR.schema.json: |- + { + "hello": { + "zero": "Estou sozinho :(", + "few": "Olá, galerinha :)" + } + } + - name: should not report offenses and ignore keys provided by Shopify (1) + files: + locales/en.default.json: |- + { + "hello": "Hello", + "shopify": { + "checkout": { + "general": { + "page_title": "Checkout" + } + } + } + } + locales/pt-BR.json: |- + { + "hello": "Olá", + "shopify": { + "sentence": { + "words_connector": "hello world" + } + } + } + - name: should not report offenses and ignore keys provided by Shopify (2) + files: + locales/en.default.schema.json: |- + { + "hello": "Hello", + "shopify": { + "checkout": { + "general": { + "page_title": "Checkout" + } + } + } + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá", + "shopify": { + "sentence": { + "words_connector": "hello world" + } + } + } + - name: should not report offenses and ignore keys provided by customer accounts (1) + files: + locales/en.default.json: |- + { + "hello": "Hello", + "customer_accounts": { + "order": { + "title": "Order" + } + } + } + locales/pt-BR.json: |- + { + "hello": "Olá", + "customer_accounts": { + "profile": { + "title": "Perfil" + } + } + } + - name: should not report offenses and ignore keys provided by customer accounts (2) + files: + locales/en.default.schema.json: |- + { + "hello": "Hello", + "customer_accounts": { + "order": { + "title": "Order" + } + } + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá", + "customer_accounts": { + "profile": { + "title": "Perfil" + } + } + } + - name: 'should not report offenses and ignore "*.schema.json" files' + files: + locales/en.default.json: |- + { + "hello": "Hello" + } + locales/pt-BR.schema.json: |- + {} + - name: should highlight the proper element when the translation file is missing a key (1) + files: + locales/en.default.json: |- + { + "hello": "Hello", + "world": "World" + } + locales/pt-BR.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.json + message: "The translation for 'world' is missing" + start: [0, 0] + highlight: |- + { + "hello": "Olá" + } + fix: |- + { + "hello": "Olá", + "world": "TODO" + } + - name: should highlight the proper element when the translation file is missing a key (2) + files: + locales/en.default.schema.json: |- + { + "hello": "Hello", + "world": "World" + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.schema.json + message: "The translation for 'world' is missing" + start: [0, 0] + highlight: |- + { + "hello": "Olá" + } + fix: |- + { + "hello": "Olá", + "world": "TODO" + } + - name: should highlight the proper element when the default translation is missing a key (1) + files: + locales/en.default.json: |- + { + "hello": "Hello" + } + locales/pt-BR.json: |- + { + "hello": "Olá", + "world": "Mundo" + } + offenses: + - file: locales/pt-BR.json + message: "A default translation for 'world' does not exist" + start: [2, 2] + highlight: '"world": "Mundo"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": "Olá" + } + - name: should highlight the proper element when the default translation is missing a key (2) + files: + locales/en.default.schema.json: |- + { + "hello": "Hello" + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá", + "world": "Mundo" + } + offenses: + - file: locales/pt-BR.schema.json + message: "A default translation for 'world' does not exist" + start: [2, 2] + highlight: '"world": "Mundo"' + suggest: + - message: Delete unneeded translation key + result: |- + { + "hello": "Olá" + } + - name: should highlight the proper element when nested translation keys do not exist (1) + files: + locales/en.default.json: |- + { + "hello": { + "world": "Hello, world!" + }, + "welcome": "Welcome" + } + locales/pt-BR.json: |- + { + "hello": {}, + "welcome": "Bem-vinda" + } + offenses: + - file: locales/pt-BR.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": {}' + fix: |- + { + "hello": { + "world": "TODO" + }, + "welcome": "Bem-vinda" + } + - name: should highlight the proper element when nested translation keys do not exist (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "world": "Hello, world!" + }, + "welcome": "Welcome" + } + locales/pt-BR.schema.json: |- + { + "hello": {}, + "welcome": "Bem-vinda" + } + offenses: + - file: locales/pt-BR.schema.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": {}' + fix: |- + { + "hello": { + "world": "TODO" + }, + "welcome": "Bem-vinda" + } + - name: should highlight the proper element when nested translation keys do not exist and there is a sibling node (1) + files: + locales/en.default.json: |- + { + "hello": { + "shopify": "Shopify!", + "world": "Hello, world!" + }, + "welcome": "Welcome" + } + locales/pt-BR.json: |- + { + "hello": { + "shopify": "Shopify!" + }, + "welcome": "Bem-vinda" + } + offenses: + - file: locales/pt-BR.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: |- + "hello": { + "shopify": "Shopify!" + } + fix: |- + { + "hello": { + "shopify": "Shopify!", + "world": "TODO" + }, + "welcome": "Bem-vinda" + } + - name: should highlight the proper element when nested translation keys do not exist and there is a sibling node (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "shopify": "Shopify!", + "world": "Hello, world!" + }, + "welcome": "Welcome" + } + locales/pt-BR.schema.json: |- + { + "hello": { + "shopify": "Shopify!" + }, + "welcome": "Bem-vinda" + } + offenses: + - file: locales/pt-BR.schema.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: |- + "hello": { + "shopify": "Shopify!" + } + fix: |- + { + "hello": { + "shopify": "Shopify!", + "world": "TODO" + }, + "welcome": "Bem-vinda" + } + - name: should highlight the proper element when translation shapes do not match (1) + files: + locales/en.default.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.json + message: "A default translation for 'hello' does not exist" + start: [1, 2] + highlight: '"hello": "Olá"' + suggest: + - message: Delete unneeded translation key + result: "{}" + - file: locales/pt-BR.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": "Olá"' + fix: |- + { + "hello": { + "world": "TODO" + } + } + - name: should highlight the proper element when translation shapes do not match (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.schema.json: |- + { + "hello": "Olá" + } + offenses: + - file: locales/pt-BR.schema.json + message: "A default translation for 'hello' does not exist" + start: [1, 2] + highlight: '"hello": "Olá"' + suggest: + - message: Delete unneeded translation key + result: "{}" + - file: locales/pt-BR.schema.json + message: "The translation for 'hello.world' is missing" + start: [1, 2] + highlight: '"hello": "Olá"' + fix: |- + { + "hello": { + "world": "TODO" + } + } + - name: should not highlight anything if the file is unparseable (1) + files: + locales/en.default.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.json: '{"hello": }' + - name: should not highlight anything if the file is unparseable (2) + files: + locales/en.default.schema.json: |- + { + "hello": { + "world": "Hello, world!" + } + } + locales/pt-BR.schema.json: '{"hello": }' diff --git a/specs/checks/max-file-size.yml b/specs/checks/max-file-size.yml new file mode 100644 index 000000000..80e468126 --- /dev/null +++ b/specs/checks/max-file-size.yml @@ -0,0 +1,191 @@ +check: MaxFileSize +severity: error +cases: + - name: accepts assets at the exact byte limit + files: + assets/theme.css: [{ repeat: a, times: 20971520 }] + - name: accepts blocks at the exact byte limit + files: + blocks/product-card.liquid: [{ repeat: a, times: 262144 }] + - name: accepts config/settings_data.json at the exact byte limit + files: + config/settings_data.json: ['{"d":"', { repeat: a, times: 1572856 }, '"}'] + - name: accepts config/settings_schema.json at the exact byte limit + files: + config/settings_schema.json: ['[{"d":"', { repeat: a, times: 524278 }, '"}]'] + - name: accepts config at the exact byte limit + files: + config/markets.json: ['{"d":"', { repeat: a, times: 262136 }, '"}'] + - name: accepts layout at the exact byte limit + files: + layout/theme.liquid: + [ + '{{ content_for_header }}{{ content_for_layout }}', + { repeat: a, times: 262057 }, + ] + - name: accepts snippets at the exact byte limit + files: + snippets/card.liquid: [{ repeat: a, times: 262144 }] + - name: accepts templates/*.json at the exact byte limit + files: + templates/product.json: ['{"d":"', { repeat: a, times: 524280 }, '"}'] + - name: accepts templates at the exact byte limit + files: + templates/product.liquid: [{ repeat: a, times: 262144 }] + - name: accepts locales at the exact byte limit + files: + locales/en.default.json: ['{"d":"', { repeat: a, times: 1572856 }, '"}'] + - name: accepts sections/*.json at the exact byte limit + files: + sections/product.json: ['{"d":"', { repeat: a, times: 524280 }, '"}'] + - name: accepts sections at the exact byte limit + files: + sections/product.liquid: [{ repeat: a, times: 262144 }] + - name: reports assets one byte over the limit + files: + assets/theme.css: [{ repeat: a, times: 20971521 }] + offenses: + - file: assets/theme.css + message: Theme file is too large. It is 20971521 bytes but the limit is 20 MB. + start: [0, 0] + highlight: [{ repeat: a, times: 20971521 }] + - name: reports blocks one byte over the limit + files: + blocks/product-card.liquid: [{ repeat: a, times: 262145 }] + offenses: + - file: blocks/product-card.liquid + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: [{ repeat: a, times: 262145 }] + - name: reports config/settings_data.json one byte over the limit + files: + config/settings_data.json: ['{"d":"', { repeat: a, times: 1572857 }, '"}'] + offenses: + - file: config/settings_data.json + message: Theme file is too large. It is 1572865 bytes but the limit is 1536 KB. + start: [0, 0] + highlight: ['{"d":"', { repeat: a, times: 1572857 }, '"}'] + - name: reports config/settings_schema.json one byte over the limit + files: + config/settings_schema.json: ['[{"d":"', { repeat: a, times: 524279 }, '"}]'] + offenses: + - file: config/settings_schema.json + message: Theme file is too large. It is 524289 bytes but the limit is 512 KB. + start: [0, 0] + highlight: ['[{"d":"', { repeat: a, times: 524279 }, '"}]'] + - name: reports config one byte over the limit + files: + config/markets.json: ['{"d":"', { repeat: a, times: 262137 }, '"}'] + offenses: + - file: config/markets.json + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: ['{"d":"', { repeat: a, times: 262137 }, '"}'] + - name: reports layout one byte over the limit + files: + layout/theme.liquid: + [ + '{{ content_for_header }}{{ content_for_layout }}', + { repeat: a, times: 262058 }, + ] + offenses: + - file: layout/theme.liquid + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: ['{{ content_for_header }}{{ content_for_layout }}', { repeat: a, times: 262058 }] + - name: reports snippets one byte over the limit + files: + snippets/card.liquid: [{ repeat: a, times: 262145 }] + offenses: + - file: snippets/card.liquid + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: [{ repeat: a, times: 262145 }] + - name: reports templates/*.json one byte over the limit + files: + templates/product.json: ['{"d":"', { repeat: a, times: 524281 }, '"}'] + offenses: + - file: templates/product.json + message: Theme file is too large. It is 524289 bytes but the limit is 512 KB. + start: [0, 0] + highlight: ['{"d":"', { repeat: a, times: 524281 }, '"}'] + - name: reports templates one byte over the limit + files: + templates/product.liquid: [{ repeat: a, times: 262145 }] + offenses: + - file: templates/product.liquid + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: [{ repeat: a, times: 262145 }] + - name: reports locales one byte over the limit + files: + locales/en.default.json: ['{"d":"', { repeat: a, times: 1572857 }, '"}'] + offenses: + - file: locales/en.default.json + message: Theme file is too large. It is 1572865 bytes but the limit is 1536 KB. + start: [0, 0] + highlight: ['{"d":"', { repeat: a, times: 1572857 }, '"}'] + - name: reports sections/*.json one byte over the limit + files: + sections/product.json: ['{"d":"', { repeat: a, times: 524281 }, '"}'] + offenses: + - file: sections/product.json + message: Theme file is too large. It is 524289 bytes but the limit is 512 KB. + start: [0, 0] + highlight: ['{"d":"', { repeat: a, times: 524281 }, '"}'] + - name: reports sections one byte over the limit + files: + sections/product.liquid: [{ repeat: a, times: 262145 }] + offenses: + - file: sections/product.liquid + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: [{ repeat: a, times: 262145 }] + - name: reports oversized files under a VFS-prefixed theme path + files: + workspace/themes/123/templates/index.liquid: [{ repeat: a, times: 262145 }] + offenses: + - file: workspace/themes/123/templates/index.liquid + message: Theme file is too large. It is 262145 bytes but the limit is 256 KB. + start: [0, 0] + highlight: [{ repeat: a, times: 262145 }] + - name: excludes schema block bytes for template-table files + files: + sections/schema-test.liquid: + [ + { repeat: a, times: 262144 }, + '{% schema %}{"name":"Large schema","settings":[{"type":"textarea","id":"copy","label":"Copy","default":"', + { repeat: b, times: 1024 }, + '"}]}{% endschema %}', + ] + - name: excludes whitespace-controlled schema block bytes for template-table files + files: + sections/schema-whitespace-control-test.liquid: + [ + { repeat: a, times: 262144 }, + '{%- schema -%}{"name":"Large schema","settings":[{"type":"textarea","id":"copy","label":"Copy","default":"', + { repeat: b, times: 1024 }, + '"}]}{%- endschema -%}', + ] + - name: ignores oversized draft file _drafts/bfcm/templates/product.json + files: + _drafts/bfcm/templates/product.json: ['{"d":"', { repeat: a, times: 20971513 }, '"}'] + - name: ignores oversized draft file _drafts/bfcm/sections/product.json + files: + _drafts/bfcm/sections/product.json: ['{"d":"', { repeat: a, times: 20971513 }, '"}'] + - name: measures UTF-8 bytes instead of JavaScript string length + files: + snippets/multibyte.liquid: [{ repeat: é, times: 131073 }] + offenses: + - file: snippets/multibyte.liquid + message: Theme file is too large. It is 262146 bytes but the limit is 256 KB. + start: [0, 0] + highlight: [{ repeat: "é", times: 131073 }] + - name: reports oversized files even when the Liquid parser cannot build an AST + files: + snippets/broken.liquid: [{ repeat: a, times: 262145 }, '{% raw %}'] + offenses: + - file: snippets/broken.liquid + message: Theme file is too large. It is 262154 bytes but the limit is 256 KB. + start: [0, 0] + highlight: [{ repeat: a, times: 262145 }, '{% raw %}'] diff --git a/specs/checks/missing-asset.yml b/specs/checks/missing-asset.yml new file mode 100644 index 000000000..ad4ef49ba --- /dev/null +++ b/specs/checks/missing-asset.yml @@ -0,0 +1,48 @@ +check: MissingAsset +severity: error +cases: + - name: should report the missing asset when assigned to a variable + files: + snippets/snippet.liquid: | + {% assign logo_url = 'logo.png' | asset_url %} + Logo + snippets/existing.liquid: "" + offenses: + - file: snippets/snippet.liquid + message: "'assets/logo.png' does not exist" + start: [0, 21] + highlight: "'logo.png'" + - name: should report the missing asset when defined inline + files: + snippets/snippet.liquid: "" + snippets/existing.liquid: "" + offenses: + - file: snippets/snippet.liquid + message: "'assets/styles.css' does not exist" + start: [0, 32] + highlight: "'styles.css'" + - name: should report the missing asset when multiple filters applied + files: + snippets/snippet.liquid: "" + snippets/existing.liquid: "" + offenses: + - file: snippets/snippet.liquid + message: "'assets/styles.css' does not exist" + start: [0, 32] + highlight: "'styles.css'" + - name: should report no offenses when an asset file exists + files: + snippets/snippet.liquid: "" + assets/styles.css: "" + - name: should not report for compiled .js.liquid files + files: + snippets/snippet.liquid: "{{ 'foo.js' | asset_url }} " + assets/foo.js.liquid: 'console.log("{{ "hi" }}");' + - name: should not report for compiled .css.liquid files + files: + snippets/snippet.liquid: "{{ 'foo.css' | asset_url }} " + assets/foo.css.liquid: 'body { color: {{ "blue" }}; }' + - name: should not report for compiled .scss.liquid files + files: + snippets/snippet.liquid: "{{ 'foo.scss.css' | asset_url }} " + assets/foo.scss.liquid: 'html { & body { color: {{ "blue" }}; } }' diff --git a/specs/checks/missing-block-arguments.yml b/specs/checks/missing-block-arguments.yml new file mode 100644 index 000000000..d4d516b0a --- /dev/null +++ b/specs/checks/missing-block-arguments.yml @@ -0,0 +1,219 @@ +check: MissingBlockArguments +severity: warning +cases: + - name: does not require schema settings with implicit platform defaults + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"title","type":"text"},{"id":"enabled","type":"checkbox"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + - name: does not require schema settings with explicit schema defaults + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"columns","type":"range","min":1,"max":4,"step":1,"default":2},{"id":"heading_font","type":"font_picker","default":"assistant_n4"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + - name: does not require schema settings with settings that forbid defaults + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"product","type":"product"},{"id":"image","type":"image_picker"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + - name: requires a LiquidDoc echo of a schema setting with an implicit platform default + files: + blocks/card.liquid: |- + {% doc %} + @param {boolean} enabled - Enabled + {% enddoc %} + {% schema %} + {"settings":[{"id":"enabled","type":"checkbox"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Missing required argument 'enabled' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card' %}" + - name: requires a LiquidDoc echo of a schema setting with an explicit schema default + files: + blocks/card.liquid: |- + {% doc %} + @param {number} columns - Columns + {% enddoc %} + {% schema %} + {"settings":[{"id":"columns","type":"range","min":1,"max":4,"step":1,"default":2}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Missing required argument 'columns' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card' %}" + - name: requires a LiquidDoc echo of a schema setting with a setting that forbids defaults + files: + blocks/card.liquid: |- + {% doc %} + @param {image} image - Image + {% enddoc %} + {% schema %} + {"settings":[{"id":"image","type":"image_picker"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Missing required argument 'image' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card' %}" + - name: keeps optional LiquidDoc echoes of schema settings optional + files: + blocks/card.liquid: |- + {% doc %} + @param {string} [title] - Title + {% enddoc %} + {% schema %} + {"settings":[{"id":"title","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + - name: applies requiredness to LiquidDoc-only developer parameters + files: + blocks/card.liquid: |- + {% doc %} + @param {string} developer_id - Developer-only id + {% enddoc %} + {% schema %} + {"settings":[{"id":"title","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Missing required argument 'developer_id' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card' %}" + - name: requires LiquidDoc-only parameters from a schema-less block + files: + blocks/card.liquid: |- + {% doc %} + @param {string} tracking_id - Tracking id + {% enddoc %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Missing required argument 'tracking_id' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card' %}" + - name: accepts an explicit argument for required built-in content + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[]} + {% endschema %} + templates/test.liquid: "{% block 'card', content: body %}{% endblock %}" + - name: accepts an explicit argument for required schema-backed content + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[{"id":"content","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', content: body %}{% endblock %}" + - name: treats text in the invocation body as required content + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}Body{% endblock %}" + - name: treats a comment in the invocation body as required content + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% comment %}Hidden{% endcomment %}{% endblock %}" + - name: treats an assignment in the invocation body as required content + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% assign value = 'hidden' %}{% endblock %}" + - name: accepts a body for required schema-backed content + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[{"id":"content","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}Body{% endblock %}" + - name: does not treat a whitespace-only body as required content + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[]} + {% endschema %} + templates/test.liquid: |- + {% block 'card' %} + + {% endblock %} + offenses: + - file: templates/test.liquid + message: "Missing required argument 'content' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card' %}" + - name: requires schema-backed content when LiquidDoc declares it required + files: + blocks/card.liquid: |- + {% doc %} + @param {string} content - Body + {% enddoc %} + {% schema %} + {"settings":[{"id":"content","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Missing required argument 'content' in block tag for 'card'." + start: [0, 0] + highlight: "{% block 'card' %}" + - name: keeps schema-backed content optional when LiquidDoc declares it optional + files: + blocks/card.liquid: |- + {% doc %} + @param {string} [content] - Body + {% enddoc %} + {% schema %} + {"settings":[{"id":"content","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" + - name: does not report missing arguments for an unreadable missing target + files: + templates/test.liquid: "{% block 'card' %}{% endblock %}" + - name: does not report missing arguments for an unreadable invalid target schema + files: + blocks/card.liquid: |- + {% schema %} + { invalid + {% endschema %} + templates/test.liquid: "{% block 'card' %}{% endblock %}" diff --git a/specs/checks/missing-content-for-arguments.yml b/specs/checks/missing-content-for-arguments.yml new file mode 100644 index 000000000..f0e3c8e8d --- /dev/null +++ b/specs/checks/missing-content-for-arguments.yml @@ -0,0 +1,124 @@ +check: MissingContentForArguments +severity: warning +cases: + - name: should report missing required arguments + files: + blocks/card.liquid: | + {% doc %} + @param {string} required_string - A required string + @param {number} required_number - A required number + @param {boolean} required_boolean - A required boolean + @param {object} required_object - A required object + @param {string} [optional_string] - An optional string + @param {number} [optional_number] - An optional number + @param {object} [optional_object] - An optional object + @param {boolean} [optional_boolean] - An optional boolean + {% enddoc %} + file.liquid: "{% content_for 'block', type: 'card' %}" + offenses: + - file: file.liquid + message: "Missing required argument 'required_string' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_string'" + result: "{% content_for 'block', type: 'card', required_string: '' %}" + - file: file.liquid + message: "Missing required argument 'required_number' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_number'" + result: "{% content_for 'block', type: 'card', required_number: 0 %}" + - file: file.liquid + message: "Missing required argument 'required_boolean' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_boolean'" + result: "{% content_for 'block', type: 'card', required_boolean: false %}" + - file: file.liquid + message: "Missing required argument 'required_object' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_object'" + result: "{% content_for 'block', type: 'card', required_object: %}" + - name: should suggest adding missing required arguments when none already exist + files: + blocks/card.liquid: | + {% doc %} + @param {string} required_string - A required string + @param {number} required_number - A required number + @param {boolean} required_boolean - A required boolean + @param {object} required_object - A required object + @param {string} [optional_string] - An optional string + @param {number} [optional_number] - An optional number + @param {object} [optional_object] - An optional object + @param {boolean} [optional_boolean] - An optional boolean + {% enddoc %} + file.liquid: "{% content_for 'block', type: 'card' %}" + offenses: + - file: file.liquid + message: "Missing required argument 'required_string' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_string'" + result: "{% content_for 'block', type: 'card', required_string: '' %}" + - file: file.liquid + message: "Missing required argument 'required_number' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_number'" + result: "{% content_for 'block', type: 'card', required_number: 0 %}" + - file: file.liquid + message: "Missing required argument 'required_boolean' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_boolean'" + result: "{% content_for 'block', type: 'card', required_boolean: false %}" + - file: file.liquid + message: "Missing required argument 'required_object' in content_for tag for static block 'card'." + start: [0, 15] + highlight: "'block', type: 'card' " + suggest: + - message: "Add required argument 'required_object'" + result: "{% content_for 'block', type: 'card', required_object: %}" + - name: should handle mixed case type annotations + files: + blocks/card.liquid: | + {% doc %} + @param {String} text - The text + @param {NUMBER} count - The count + @param {BOOLEAN} flag - The flag + @param {Object} data - The data + {% enddoc %} +
    {{ text }}{{ count }}{{ flag }}{{ data }}
    + file.liquid: "{% content_for 'block', type: 'card', text: \"hello\", count: 5, flag: true, data: product %}" + - name: should not report when block has no doc comment + files: + blocks/card.liquid: "

    This snippet has no doc comment

    " + file.liquid: "{% content_for 'block', type: 'card', title: 'My Card' %}" + - name: should not report when LiquidDoc definition has no defined params + files: + blocks/card.liquid: | + {% doc %} + @description this is a description + @example this is an example + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% content_for 'block', type: 'card', title: 'My Card' %}" + - name: should not report when block name is a VariableLookup + files: + blocks/card.liquid: | + {% doc %} + @param {string} title - The title of the card + @param {string} description - The description of the card + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% assign block_name = 'card' %}{% content_for 'block', type: block_name, title: 'My Card' %}" diff --git a/specs/checks/missing-render-snippet-arguments.yml b/specs/checks/missing-render-snippet-arguments.yml new file mode 100644 index 000000000..190b67e9a --- /dev/null +++ b/specs/checks/missing-render-snippet-arguments.yml @@ -0,0 +1,192 @@ +check: MissingRenderSnippetArguments +severity: warning +cases: + - name: should report missing required arguments + files: + snippets/card.liquid: | + {% doc %} + @param {string} required_string - A required string + @param {number} required_number - A required number + @param {boolean} required_boolean - A required boolean + @param {object} required_object - A required object + @param {string} [optional_string] - An optional string + @param {number} [optional_number] - An optional number + @param {object} [optional_object] - An optional object + @param {boolean} [optional_boolean] - An optional boolean + {% enddoc %} + file.liquid: "{% render 'card' %}" + offenses: + - file: file.liquid + message: "Missing required argument 'required_string' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_string'" + result: "{% render 'card', required_string: '' %}" + - file: file.liquid + message: "Missing required argument 'required_number' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_number'" + result: "{% render 'card', required_number: 0 %}" + - file: file.liquid + message: "Missing required argument 'required_boolean' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_boolean'" + result: "{% render 'card', required_boolean: false %}" + - file: file.liquid + message: "Missing required argument 'required_object' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_object'" + result: "{% render 'card', required_object: %}" + - name: should suggest adding missing required arguments when none already exist + files: + snippets/card.liquid: | + {% doc %} + @param {string} required_string - A required string + @param {number} required_number - A required number + @param {boolean} required_boolean - A required boolean + @param {object} required_object - A required object + @param {string} [optional_string] - An optional string + @param {number} [optional_number] - An optional number + @param {object} [optional_object] - An optional object + @param {boolean} [optional_boolean] - An optional boolean + {% enddoc %} + file.liquid: "{% render 'card' %}" + offenses: + - file: file.liquid + message: "Missing required argument 'required_string' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_string'" + result: "{% render 'card', required_string: '' %}" + - file: file.liquid + message: "Missing required argument 'required_number' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_number'" + result: "{% render 'card', required_number: 0 %}" + - file: file.liquid + message: "Missing required argument 'required_boolean' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_boolean'" + result: "{% render 'card', required_boolean: false %}" + - file: file.liquid + message: "Missing required argument 'required_object' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' " + suggest: + - message: "Add required argument 'required_object'" + result: "{% render 'card', required_object: %}" + - name: should suggest adding missing argument after provided alias variable (1) + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + @param {string} description - The description of the card + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% render 'card' with 'my-card' as title %}" + offenses: + - file: file.liquid + message: "Missing required argument 'description' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' with 'my-card' as title " + suggest: + - message: "Add required argument 'description'" + result: "{% render 'card' with 'my-card' as title, description: '' %}" + - name: should suggest adding missing argument after provided alias variable (2) + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + @param {string} description - The description of the card + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% render 'card' for titles as title %}" + offenses: + - file: file.liquid + message: "Missing required argument 'description' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' for titles as title " + suggest: + - message: "Add required argument 'description'" + result: "{% render 'card' for titles as title, description: '' %}" + - name: should handle mixed case type annotations + files: + snippets/card.liquid: | + {% doc %} + @param {String} text - The text + @param {NUMBER} count - The count + @param {BOOLEAN} flag - The flag + @param {Object} data - The data + {% enddoc %} +
    {{ text }}{{ count }}{{ flag }}{{ data }}
    + file.liquid: "{% render 'card', text: \"hello\", count: 5, flag: true, data: product %}" + - name: should not report when snippet has no doc comment + files: + snippets/card.liquid: "

    This snippet has no doc comment

    " + file.liquid: "{% render 'card', title: 'My Card' %}" + - name: should not report when LiquidDoc definition has no defined params + files: + snippets/card.liquid: | + {% doc %} + @description this is a description + @example this is an example + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% render 'card', title: 'My Card' %}" + - name: should not report when snippet name is a VariableLookup + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + @param {string} description - The description of the card + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% assign snippet_name = 'card' %}{% render snippet_name, title: 'My Card' %}" + - name: 'should report when "with/for" alias syntax is used (1)' + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card' with 'my-card' as unknown %}" + offenses: + - file: file.liquid + message: "Missing required argument 'title' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' with 'my-card' as unknown " + suggest: + - message: "Add required argument 'title'" + result: "{% render 'card' with 'my-card' as unknown, title: '' %}" + - name: 'should report when "with/for" alias syntax is used (2)' + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card' for array as unknown %}" + offenses: + - file: file.liquid + message: "Missing required argument 'title' in render tag for snippet 'card'." + start: [0, 10] + highlight: "'card' for array as unknown " + suggest: + - message: "Add required argument 'title'" + result: "{% render 'card' for array as unknown, title: '' %}" diff --git a/specs/checks/missing-template.yml b/specs/checks/missing-template.yml new file mode 100644 index 000000000..dd69ae499 --- /dev/null +++ b/specs/checks/missing-template.yml @@ -0,0 +1,63 @@ +check: MissingTemplate +severity: error +cases: + - name: should report missing template errors (1) + files: + snippets/snippet.liquid: | + {% render 'existing' %} + {% render 'missing' with foo as arg %} + {% render myvariable %} + snippets/existing.liquid: "" + offenses: + - file: snippets/snippet.liquid + message: "'snippets/missing.liquid' does not exist" + start: [1, 10] + highlight: "'missing'" + - name: should report missing template errors (2) + files: + snippets/snippet.liquid: "{% include 'missing' %}" + snippets/existing.liquid: "" + offenses: + - file: snippets/snippet.liquid + message: "'snippets/missing.liquid' does not exist" + start: [0, 11] + highlight: "'missing'" + - name: should report missing template errors (3) + files: + sections/section.liquid: "{% section 'missing' %}" + sections/existing.liquid: "" + offenses: + - file: sections/section.liquid + message: "'sections/missing.liquid' does not exist" + start: [0, 11] + highlight: "'missing'" + - name: ignores missing files if ignored by ignore_missing pattern (1) + files: + file.liquid: '{% render "ignored" %}' + config: + MissingTemplate: { ignoreMissing: ["snippets/ignored.liquid"] } + - name: ignores missing files if ignored by ignore_missing pattern (2) + files: + file.liquid: '{% render "ignored" %}' + config: + MissingTemplate: { ignoreMissing: ["snippets/*.liquid"] } + - name: ignores missing files if ignored by ignore_missing pattern (3) + files: + file.liquid: '{% render "ignored" %}' + config: + MissingTemplate: { ignoreMissing: ["*/ignored.liquid"] } + - name: ignores missing files if ignored by ignore_missing pattern (4) + files: + file.liquid: '{% section "ignored" %}' + config: + MissingTemplate: { ignoreMissing: ["sections/ignored.liquid"] } + - name: ignores missing files if ignored by ignore_missing pattern (5) + files: + file.liquid: '{% section "ignored" %}' + config: + MissingTemplate: { ignoreMissing: ["sections/ignored.liquid"] } + - name: ignores missing files if ignored by ignore_missing pattern (6) + files: + file.liquid: '{% section "ignored" %}' + config: + MissingTemplate: { ignoreMissing: ["*/ignored.liquid"] } diff --git a/specs/checks/orphaned-snippet.yml b/specs/checks/orphaned-snippet.yml new file mode 100644 index 000000000..e3a72bd92 --- /dev/null +++ b/specs/checks/orphaned-snippet.yml @@ -0,0 +1,20 @@ +check: OrphanedSnippet +severity: warning +cases: + - name: should report a warning + files: + snippets/orphaned.liquid: '
    Orphaned content
    ' + offenses: + - file: snippets/orphaned.liquid + message: This snippet is not referenced by any other files + start: [0, 0] + highlight: "<" + - name: should not report a warning + files: + snippets/product-card.liquid: '
    Referenced snippet
    ' + templates/product.liquid: "{% render 'product-card' %}" + references: + - { source: templates/product.liquid, target: snippets/product-card.liquid } + - name: should not report warnings for templates + files: + templates/index.liquid: "{% section 'header' %}" diff --git a/specs/checks/pagination-size.yml b/specs/checks/pagination-size.yml new file mode 100644 index 000000000..e00cf3e06 --- /dev/null +++ b/specs/checks/pagination-size.yml @@ -0,0 +1,109 @@ +check: PaginationSize +severity: warning +cases: + - name: should report an offense when paginate size is less than minSize + files: + file.liquid: | + {% paginate collection.products by 0 %} + {% endpaginate %} + offenses: + - file: file.liquid + message: Pagination size must be a positive integer between 1 and 250. + start: [0, 35] + highlight: "0" + - name: should report an offense when paginate size is greater than maxSize + files: + file.liquid: | + {% paginate collection.products by 251 %} + {% endpaginate %} + offenses: + - file: file.liquid + message: Pagination size must be a positive integer between 1 and 250. + start: [0, 35] + highlight: "251" + - name: should not report an offense when paginate size is within the minSize and maxSize range + files: + file.liquid: | + {% paginate collection.products by 25 %} + {% endpaginate %} + - name: should not report an offense when paginate size is a section setting with a valid default value + files: + file.liquid: | + {% paginate collection.products by section.settings.products_per_page %} + {% endpaginate %} + {% schema %} + { + "name": "test", + "settings": [ + { + "type": "number", + "id": "products_per_page", + "label": "Products per Page", + "default": 12 + } + ] + } + {% endschema %} + - name: should report an offense when paginate size is a section setting with an invalid default value + files: + file.liquid: | + {% paginate collection.products by section.settings.products_per_page %} + {% endpaginate %} + {% schema %} + { + "name": "test", + "settings": [ + { + "type": "number", + "id": "products_per_page", + "label": "Products per Page", + "default": 251 + } + ] + } + {% endschema %} + offenses: + - file: file.liquid + message: "This setting's default value should be between 1 and 250 but is currently 251." + start: [0, 35] + highlight: section.settings.products_per_page + - name: should report an offense when paginate size is a section setting with a missing default value + files: + file.liquid: | + {% paginate collection.products by section.settings.products_per_page %} + {% endpaginate %} + {% schema %} + { + "name": "test", + "settings": [ + { + "type": "number", + "id": "products_per_page", + "label": "Products per Page" + } + ] + } + {% endschema %} + offenses: + - file: file.liquid + message: Default pagination size should be defined in the section settings. + start: [0, 35] + highlight: section.settings.products_per_page + - name: should not report an offense when settings is not an array + files: + file.liquid: | + {% paginate collection.products by section.settings.products_per_page %} + {% endpaginate %} + {% schema %} + { + "name": "test", + "settings": { + "products_per_page": { + "type": "number", + "id": "products_per_page", + "label": "Products per Page", + "default": 51 + } + } + } + {% endschema %} diff --git a/specs/checks/parser-blocking-script.yml b/specs/checks/parser-blocking-script.yml new file mode 100644 index 000000000..ab8d16286 --- /dev/null +++ b/specs/checks/parser-blocking-script.yml @@ -0,0 +1,114 @@ +check: ParserBlockingScript +severity: error +cases: + - name: should report the correct offense for the Liquid filter + files: + code.liquid: "{{ 'asset' | script_tag }}" + offenses: + - file: code.liquid + message: The script_tag filter is parser-blocking. Use a " + - message: Use an HTML script tag with the async attribute instead + result: "" + - name: should report the correct offense when using the script tag + files: + code.liquid: | + + offenses: + - file: code.liquid + message: "Avoid parser blocking scripts by adding `defer` or `async` on this tag" + start: [0, 0] + highlight: '' + suggest: + - message: Use an HTML script tag with the defer attribute instead + result: | + + - message: Use an HTML script tag with the async attribute instead + result: | + + - name: should suggest to replace a LiquidVariableOutput with a script tag that has the expression as URL + files: + code.liquid: "{{ 'asset' | script_tag }}" + offenses: + - file: code.liquid + message: The script_tag filter is parser-blocking. Use a " + - message: Use an HTML script tag with the async attribute instead + result: "" + - name: should suggest to replace a LiquidVariableOutput with a script tag that has the expression and previous filters as URL + files: + code.liquid: "{{ 'asset' | asset_url | script_tag }}" + offenses: + - file: code.liquid + message: The script_tag filter is parser-blocking. Use a " + - message: Use an HTML script tag with the async attribute instead + result: "" + - name: should not suggest anything if the script_tag appears in an echo tag + files: + code.liquid: "{% echo 'asset' | asset_url | script_tag %}" + offenses: + - file: code.liquid + message: The script_tag filter is parser-blocking. Use a ' + offenses: + - file: code.liquid + message: "Avoid parser blocking scripts by adding `defer` or `async` on this tag" + start: [0, 0] + highlight: '' + suggest: + - message: Use an HTML script tag with the defer attribute instead + result: '' + - message: Use an HTML script tag with the async attribute instead + result: '' + - name: should not report any offense when async is set on a script tag + files: + code.liquid: | + + - name: should not report any offense when defer is set on a script tag + files: + code.liquid: | + + - name: should not report any offense when async and defer are set on a script tag + files: + code.liquid: | + + - name: should not report any offense when using scripts of type module + files: + code.liquid: | + + diff --git a/specs/checks/remote-asset.yml b/specs/checks/remote-asset.yml new file mode 100644 index 000000000..58c828571 --- /dev/null +++ b/specs/checks/remote-asset.yml @@ -0,0 +1,218 @@ +check: RemoteAsset +severity: warning +cases: + - name: should report an offense when asset_url or img_url filters are not used + files: + file.liquid: | + + + + {{ url | img_tag }} + offenses: + - file: file.liquid + message: Use one of the asset_url filters to serve assets for better performance. + start: [0, 5] + highlight: "src=\"{{ 'image.png' }}\"" + - file: file.liquid + message: Use one of the asset_url filters to serve assets for better performance. + start: [1, 6] + highlight: "href=\"{{ 'style.css' }}\"" + - file: file.liquid + message: Use one of the asset_url filters to serve assets for better performance. + start: [2, 8] + highlight: "src=\"{{ 'script.js' }}\"" + - file: file.liquid + message: Use one of the asset_url filters to serve assets for better performance. + start: [3, 3] + highlight: "url | img_tag" + - name: should not report an offense when asset_url or img_url filters are used + files: + file.liquid: | + + + + {{ url | img_tag | asset_url }}, + {{ 'bootstrap.min.css' | asset_url | stylesheet_tag }} + {{ product | image_url: width: 450 | img_tag }} + - name: should not report an offense for links, videos, iframes + files: + file.liquid: | + +
    + + - name: should not report an offense when asset_url or img_url filters are used with other filters + files: + file.liquid: | + + + + - name: should not report an offense for non-asset elements + files: + file.liquid: | +
    + Link + - name: should not report an offense when asset_url filters are used with deprecated assets + files: + file.liquid: | + {{ product.featured_image | product_img_url }} + {{ article.image | article_img_url }} + {{ collection.image | collection_img_url }} + {{ 'image.png' | img_url }} + - name: should not report an offense when asset_url filters are used with non deprecated assets + files: + file.liquid: | + {{ 'option_selection.js' | shopify_asset_url }} + {{ 'lightbox.js' | global_asset_url | script_tag }} + {{ 'disclaimer.pdf' | file_url }} + {{ 'potions-header.png' | file_img_url: 'large' }} + {{ 'cart.js' | asset_url }} + {{ 'red-and-black-bramble-berries.jpg' | asset_img_url }} + - name: should report the correct message and index for a single offense + files: + file.liquid: "" + offenses: + - file: file.liquid + message: Use one of the asset_url filters to serve assets for better performance. + start: [0, 5] + highlight: "src=\"{{ 'image.png' }}\"" + - name: should not report an offense when asset_url filters are used with known assets + files: + file.liquid: |- + + ' + offenses: + - file: file.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [0, 8] + highlight: 'src="https://example.com/jquery.js"' + - name: should report an offense for remote stylesheets + files: + file.liquid: | + + + + offenses: + - file: file.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [0, 6] + highlight: 'href="https://example.com/bootstrap.css"' + - file: file.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [1, 6] + highlight: "href=\"{{ \"https://example.com/bootstrap.css\" | replace: 'bootstrap', 'tailwind' }}\"" + - file: file.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [2, 6] + highlight: "href=“https://thisisbad.com/styles.css“" + - name: should report an offense when a non shopify cdn is used as a liquid filter and when a asset_url filter is not used. + files: + file.liquid: | + {{ "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" | stylesheet_tag }} + offenses: + - file: file.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [0, 3] + highlight: '"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"' + - file: file.liquid + message: Use one of the asset_url filters to serve assets for better performance. + start: [0, 3] + highlight: '"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" | stylesheet_tag' + - name: should not report an offense for variable-based URLs + files: + file.liquid: | + + + + + - name: should not report an offense for hash URLs + files: + file.liquid: | + + + + - name: should not report an offense for base64 encoded data URIs + files: + file.liquid: | + + - name: should not report an offense for various base64 data URI image formats + files: + file.liquid: | + + + + + - name: should still report an offense for remote images alongside base64 images + files: + file.liquid: | + + + offenses: + - file: file.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [1, 5] + highlight: 'src="https://example.com/remote-image.png"' + - name: should not report an offense for data URIs used with img_tag filter + files: + file.liquid: | + {{ 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' | img_tag }} + - name: should still report an offense for string literals without asset_url filter + files: + file.liquid: | + + offenses: + - file: file.liquid + message: Use one of the asset_url filters to serve assets for better performance. + start: [0, 5] + highlight: "src=\"{{ 'image.png' }}\"" + - name: should not report an offence if url is a shopify CDN + files: + file.liquid: | + + + + - name: should report an offense if url is not listed in allowedDomains + files: + layout/theme.liquid: | + + config: + RemoteAsset: { allowedDomains: ["someotherdomain.com"] } + offenses: + - file: layout/theme.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [0, 8] + highlight: 'src="https://domain.com"' + - name: should report an offense if the url in the config is malformed/missing protocol + files: + layout/theme.liquid: | + + + config: + RemoteAsset: { allowedDomains: ["www.domain.com","domain.com"] } + offenses: + - file: layout/theme.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [0, 8] + highlight: 'src="https://domain.com"' + - file: layout/theme.liquid + message: Asset should be served by the Shopify CDN for better performance. + start: [1, 8] + highlight: 'src="https://www.domain.com"' + - name: should not report an offense if url is listed in allowedDomains + files: + layout/theme.liquid: | + + config: + RemoteAsset: { allowedDomains: ["https://domain.com","http://domain.com","https://www.domain.com"] } diff --git a/specs/checks/required-layout-theme-object.yml b/specs/checks/required-layout-theme-object.yml new file mode 100644 index 000000000..72030711e --- /dev/null +++ b/specs/checks/required-layout-theme-object.yml @@ -0,0 +1,116 @@ +check: RequiredLayoutThemeObject +severity: error +cases: + - name: should report an error if content_for_header is missing + files: + layout/theme.liquid: | + + + + + + {{ content_for_layout }} + + + offenses: + - file: layout/theme.liquid + message: "The required object '{{ content_for_header }}' is missing in layout/theme.liquid" + start: [1, 2] + highlight: |- + + + + fix: | + + + + {{ content_for_header }} + + {{ content_for_layout }} + + + - name: should report an error if content_for_layout is missing + files: + layout/theme.liquid: | + + + {{ content_for_header }} + + + + + + offenses: + - file: layout/theme.liquid + message: "The required object '{{ content_for_layout }}' is missing in layout/theme.liquid" + start: [4, 2] + highlight: |- + + + + fix: | + + + {{ content_for_header }} + + + + {{ content_for_layout }} + + - name: should not report an error if both content_for_header and content_for_layout are present + files: + layout/theme.liquid: | + + + {{ content_for_header }} + + + {{ content_for_layout }} + + + - name: should not report an error or fix if head or body tags are missing + files: + layout/theme.liquid: | + + + {{ content_for_header }} + {{ content_for_layout }} + + - name: should not report an error for non-layout/theme.liquid files + files: + snippets/example.liquid: "{% echo 'hello world' %}" + - name: "should report for any layout/*.liquid file, not only layout/theme.liquid" + files: + layout/checkout.liquid: | + + + + + + {{ content_for_layout }} + + + offenses: + - file: layout/checkout.liquid + message: "The required object '{{ content_for_header }}' is missing in layout/theme.liquid" + start: [1, 2] + highlight: |- + + + + fix: | + + + + {{ content_for_header }} + + {{ content_for_layout }} + + + - name: should not report an error if the file is unparseable + files: + snippets/example.liquid: | + + {{ content_for_header }} + {{ content_for_layout }}{% echo }} + diff --git a/specs/checks/reserved-doc-param-names.yml b/specs/checks/reserved-doc-param-names.yml new file mode 100644 index 000000000..6d0b5f1ad --- /dev/null +++ b/specs/checks/reserved-doc-param-names.yml @@ -0,0 +1,93 @@ +check: ReservedDocParamNames +severity: error +cases: + - name: reports Liquid literal parameter names in blocks/file.liquid + files: + blocks/file.liquid: | + {% doc %} + @param nil - Example param + @param null - Example param + @param true - Example param + @param false - Example param + @param blank - Example param + @param empty - Example param + {% enddoc %} + offenses: + - file: blocks/file.liquid + message: "The parameter name 'nil' is reserved because Liquid parses it as a literal." + start: [1, 9] + highlight: nil + - file: blocks/file.liquid + message: "The parameter name 'null' is reserved because Liquid parses it as a literal." + start: [2, 9] + highlight: "null" + - file: blocks/file.liquid + message: "The parameter name 'true' is reserved because Liquid parses it as a literal." + start: [3, 9] + highlight: "true" + - file: blocks/file.liquid + message: "The parameter name 'false' is reserved because Liquid parses it as a literal." + start: [4, 9] + highlight: "false" + - file: blocks/file.liquid + message: "The parameter name 'blank' is reserved because Liquid parses it as a literal." + start: [5, 9] + highlight: blank + - file: blocks/file.liquid + message: "The parameter name 'empty' is reserved because Liquid parses it as a literal." + start: [6, 9] + highlight: empty + - name: reports Liquid literal parameter names in snippets/file.liquid + files: + snippets/file.liquid: | + {% doc %} + @param nil - Example param + @param null - Example param + @param true - Example param + @param false - Example param + @param blank - Example param + @param empty - Example param + {% enddoc %} + offenses: + - file: snippets/file.liquid + message: "The parameter name 'nil' is reserved because Liquid parses it as a literal." + start: [1, 9] + highlight: nil + - file: snippets/file.liquid + message: "The parameter name 'null' is reserved because Liquid parses it as a literal." + start: [2, 9] + highlight: "null" + - file: snippets/file.liquid + message: "The parameter name 'true' is reserved because Liquid parses it as a literal." + start: [3, 9] + highlight: "true" + - file: snippets/file.liquid + message: "The parameter name 'false' is reserved because Liquid parses it as a literal." + start: [4, 9] + highlight: "false" + - file: snippets/file.liquid + message: "The parameter name 'blank' is reserved because Liquid parses it as a literal." + start: [5, 9] + highlight: blank + - file: snippets/file.liquid + message: "The parameter name 'empty' is reserved because Liquid parses it as a literal." + start: [6, 9] + highlight: empty + - name: allows non-literal parameter names in blocks/file.liquid + files: + blocks/file.liquid: | + {% doc %} + @param param1 - Example param + @param class - Example param + @param attributes - Example param + @param id - Example param + {% enddoc %} + - name: allows non-literal parameter names in snippets/file.liquid + files: + snippets/file.liquid: | + {% doc %} + @param param1 - Example param + @param class - Example param + @param attributes - Example param + @param id - Example param + {% enddoc %} diff --git a/specs/checks/schema-once-per-file.yml b/specs/checks/schema-once-per-file.yml new file mode 100644 index 000000000..e4765c71e --- /dev/null +++ b/specs/checks/schema-once-per-file.yml @@ -0,0 +1,18 @@ +check: SchemaOncePerFile +severity: error +cases: + - name: reports second and subsequent schema tags in a file + files: + sections/test.liquid: |- + {% schema %}{% endschema %} + {% schema %}{% endschema %} + {% schema %}{% endschema %} + offenses: + - file: sections/test.liquid + message: "{% schema %} can only appear once per file." + start: [1, 0] + highlight: "{% schema %}" + - file: sections/test.liquid + message: "{% schema %} can only appear once per file." + start: [2, 0] + highlight: "{% schema %}" diff --git a/specs/checks/schema-presets-block-order.yml b/specs/checks/schema-presets-block-order.yml new file mode 100644 index 000000000..25ead2bdc --- /dev/null +++ b/specs/checks/schema-presets-block-order.yml @@ -0,0 +1,297 @@ +check: SchemaPresetsBlockOrder +severity: warning +cases: + - name: reports no warning when the preset blocks are in the block_order + files: + sections/file.liquid: |2- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text" + }, + "block-2": { + "type": "icon" + } + }, + "block_order": ["block-1", "block-2"] + } + ] + } + {% endschema %} + - name: reports a warning when not all preset blocks are in the block_order + files: + sections/file.liquid: |- + {% schema %} + { + "name": "Test section", + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text" + }, + "block-2": { + "type": "icon" + } + }, + "block_order": ["block-1"] + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "block 'block-2' is missing from the block_order" + start: [14, 21] + highlight: '["block-1"]' + - name: reports no warning when the preset blocks has static blocks that are not in the block_order + files: + sections/file.liquid: |- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text" + }, + "block-2": { + "type": "icon", + "static": true + } + }, + "block_order": ["block-1"] + } + ] + } + {% endschema %} + - name: reports a warning when preset blocks has static blocks that are in the block_order + files: + sections/file.liquid: |- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text" + }, + "block-2": { + "type": "icon", + "static": true + } + }, + "block_order": ["block-1", "block-2"] + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "static block 'block-2' cannot be in the block_order" + start: [14, 21] + highlight: '["block-1", "block-2"]' + - name: reports no warning when the nested preset blocks are in the block_order + files: + sections/file.liquid: |- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text", + "blocks": { + "nested-1": { + "type": "nested" + }, + "nested-2": { + "type": "nested" + } + }, + "block_order": ["nested-1", "nested-2"] + }, + "block-2": { + "type": "icon" + } + }, + "block_order": ["block-1", "block-2"] + } + ] + } + {% endschema %} + - name: reports a warning when the nested preset blocks are not in the block_order + files: + sections/file.liquid: |- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text", + "blocks": { + "nested-1": { + "type": "nested" + }, + "nested-2": { + "type": "nested" + } + }, + "block_order": ["nested-1"] + }, + "block-2": { + "type": "icon" + } + }, + "block_order": ["block-1", "block-2"] + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "block 'nested-2' is missing from the block_order" + start: [16, 25] + highlight: '["nested-1"]' + - name: reports no warning when the nested preset blocks has static blocks that are not in the block_order + files: + sections/file.liquid: |- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text", + "blocks": { + "nested-1": { + "type": "nested", + "static": true + }, + "nested-2": { + "type": "nested" + } + }, + "block_order": ["nested-2"] + }, + "block-2": { + "type": "icon" + } + }, + "block_order": ["block-1", "block-2"] + } + ] + } + {% endschema %} + - name: reports a warning when the nested preset blocks has static blocks that are in the block_order + files: + sections/file.liquid: |- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text", + "blocks": { + "nested-1": { + "type": "nested", + "static": true + }, + "nested-2": { + "type": "nested" + } + }, + "block_order": ["nested-1", "nested-2"] + }, + "block-2": { + "type": "icon" + } + }, + "block_order": ["block-1", "block-2"] + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "static block 'nested-1' cannot be in the block_order" + start: [17, 25] + highlight: '["nested-1", "nested-2"]' + - name: reports a warning when there should be a block_order but it is missing + files: + sections/file.liquid: |- + {% schema %} + { + "name": "Test section", + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text" + } + } + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: block_order is missing + start: [11, 4] + highlight: "}" + - name: reports no warning we have all static blocks and no block_order + files: + sections/file.liquid: |2- + {% schema %} + { + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text", + "static": true + } + } + } + ] + } + {% endschema %} + - name: "reports a warning when a block in 'block_order' is not in 'blocks'" + files: + sections/file.liquid: |- + {% schema %} + { + "name": "Test section", + "presets": [ + { + "name": "Preset 1", + "blocks": { + "block-1": { + "type": "text" + } + }, + "block_order": ["block-1", "block-2"] + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "block 'block-2' is missing from 'blocks'" + start: [11, 33] + highlight: '"block-2"' diff --git a/specs/checks/schema-presets-static-blocks.yml b/specs/checks/schema-presets-static-blocks.yml new file mode 100644 index 000000000..f39102b38 --- /dev/null +++ b/specs/checks/schema-presets-static-blocks.yml @@ -0,0 +1,130 @@ +check: SchemaPresetsStaticBlocks +severity: error +cases: + - name: 'reports no errors when there are {% content_for "block" ... %} for each static block in the preset blocks array' + files: + sections/file.liquid: |- + {% content_for "block" type:"text" id: "block-1" %} + {% content_for "block" type:"icon" id: "block-2" %} + {% schema %} + { + "name": "Test section", + "blocks": [{"type": "@theme"}], + "presets": [ + { + "name": "Preset with two static blocks", + "blocks": [ + { + "type": "text", + "static": true, + "id": "block-1" + }, + { + "type": "icon", + "static": true, + "id": "block-2" + } + ] + } + ] + } + {% endschema %} + - name: 'reports an error when there are {% content_for "block" ... %} missing for static blocks in the preset blocks array' + files: + sections/file.liquid: |- + {% content_for "block" type:"text" id:"block-1" %} + {% comment %} here we are missing the other content_for block for block-2 {% endcomment %} + {% schema %} + { + "name": "Test section", + "blocks": [{"type": "@theme"}], + "presets": [ + { + "name": "Preset with two static blocks", + "blocks": [ + { + "type": "text", + "static": true, + "id": "block-1" + }, + { + "type": "icon", + "static": true, + "id": "block-2" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: 'Static block block-2 is missing a corresponding content_for "block" tag.' + start: [15, 8] + highlight: |- + { + "type": "icon", + "static": true, + "id": "block-2" + } + - name: 'reports no errors when there are {% content_for "block" ... %} for each static block in the preset blocks hash' + files: + sections/file.liquid: |- + {% content_for "block" type:"text" id: "block-1" %} + {% content_for "block" type:"icon" id: "block-2" %} + {% schema %} + { + "name": "Test section", + "blocks": [{"type": "@theme"}], + "presets": [ + { + "name": "Preset with two static blocks", + "blocks": { + "block-1": { + "type": "text", + "static": true + }, + "block-2": { + "type": "icon", + "static": true + } + } + } + ] + } + {% endschema %} + - name: 'reports an error when there are {% content_for "block" ... %} missing for static blocks in the preset blocks hash' + files: + sections/file.liquid: |- + {% content_for "block" type:"text" id:"block-1" %} + {% comment %} here we are missing the other content_for block for block-2 {% endcomment %} + {% schema %} + { + "name": "Test section", + "blocks": [{"type": "@theme"}], + "presets": [ + { + "name": "Preset with two static blocks", + "blocks": { + "block-1": { + "type": "text", + "static": true + }, + "block-2": { + "type": "icon", + "static": true + } + } + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: 'Static block block-2 is missing a corresponding content_for "block" tag.' + start: [14, 19] + highlight: |- + { + "type": "icon", + "static": true + } diff --git a/specs/checks/schema-section-or-block-only.yml b/specs/checks/schema-section-or-block-only.yml new file mode 100644 index 000000000..2b078a786 --- /dev/null +++ b/specs/checks/schema-section-or-block-only.yml @@ -0,0 +1,25 @@ +check: SchemaSectionOrBlockOnly +severity: error +cases: + - name: allows schema tags in sections/test.liquid + files: + sections/test.liquid: "{% schema %}{% endschema %}" + - name: allows schema tags in blocks/test.liquid + files: + blocks/test.liquid: "{% schema %}{% endschema %}" + - name: reports schema tags in templates/index.liquid + files: + templates/index.liquid: "{% schema %}{% endschema %}" + offenses: + - file: templates/index.liquid + message: "{% schema %} is only valid in section or block files." + start: [0, 0] + highlight: "{% schema %}" + - name: reports schema tags in snippets/test.liquid + files: + snippets/test.liquid: "{% schema %}{% endschema %}" + offenses: + - file: snippets/test.liquid + message: "{% schema %} is only valid in section or block files." + start: [0, 0] + highlight: "{% schema %}" diff --git a/specs/checks/static-stylesheet-and-javascript-tags.yml b/specs/checks/static-stylesheet-and-javascript-tags.yml new file mode 100644 index 000000000..0f43909a4 --- /dev/null +++ b/specs/checks/static-stylesheet-and-javascript-tags.yml @@ -0,0 +1,167 @@ +check: StaticStylesheetAndJavascriptTags +severity: error +cases: + - name: should not report errors for static CSS content in stylesheet tags + files: + templates/test.liquid: | + {% stylesheet %} + .button { + background-color: blue; + color: white; + padding: 10px 20px; + } + {% endstylesheet %} + - name: should not report errors for static JavaScript content in javascript tags + files: + templates/test.liquid: | + {% javascript %} + function greet(name) { + console.log('Hello, ' + name + '!'); + } + greet('World'); + {% endjavascript %} + - name: should report error for liquid variable in stylesheet tag + files: + templates/test.liquid: | + {% stylesheet %} + .button { + background-color: {{ settings.button_color }}; + color: white; + } + {% endstylesheet %} + offenses: + - file: templates/test.liquid + message: Liquid variable found in CSS block. {% stylesheet %} tags should only contain static CSS code. + start: [2, 22] + highlight: "{{ settings.button_color }}" + - name: should report error for liquid variable in javascript tag + files: + templates/test.liquid: | + {% javascript %} + const apiUrl = '{{ shop.url }}'; + fetch(apiUrl); + {% endjavascript %} + offenses: + - file: templates/test.liquid + message: Liquid variable found in JavaScript block. {% javascript %} tags should only contain static JavaScript code. + start: [1, 18] + highlight: "{{ shop.url }}" + - name: should report error for liquid tag in stylesheet tag + files: + templates/test.liquid: | + {% stylesheet %} + .button { + {% if settings.enable_hover %} + background-color: blue; + {% endif %} + } + {% endstylesheet %} + offenses: + - file: templates/test.liquid + message: Liquid tag found in CSS block. {% stylesheet %} tags should only contain static CSS code. + start: [2, 4] + highlight: |- + {% if settings.enable_hover %} + background-color: blue; + {% endif %} + - name: should report error for liquid tag in javascript tag + files: + templates/test.liquid: | + {% javascript %} + {% if settings.enable_analytics %} + console.log('Analytics enabled'); + {% endif %} + {% endjavascript %} + offenses: + - file: templates/test.liquid + message: Liquid tag found in JavaScript block. {% javascript %} tags should only contain static JavaScript code. + start: [1, 2] + highlight: |- + {% if settings.enable_analytics %} + console.log('Analytics enabled'); + {% endif %} + - name: should report multiple errors for multiple liquid nodes + files: + templates/test.liquid: | + {% stylesheet %} + .button { + background-color: {{ settings.button_color }}; + {% if settings.show_border %} + border: 1px solid {{ settings.border_color }}; + {% endif %} + } + {% endstylesheet %} + offenses: + - file: templates/test.liquid + message: Liquid variable found in CSS block. {% stylesheet %} tags should only contain static CSS code. + start: [2, 22] + highlight: "{{ settings.button_color }}" + - file: templates/test.liquid + message: Liquid tag found in CSS block. {% stylesheet %} tags should only contain static CSS code. + start: [3, 4] + highlight: |- + {% if settings.show_border %} + border: 1px solid {{ settings.border_color }}; + {% endif %} + - name: should not report errors for other raw tags like schema + files: + sections/test.liquid: | + {% schema %} + { + "name": "Section", + "settings": [ + { + "id": "color", + "type": "color", + "default": "{{ settings.default_color }}" + } + ] + } + {% endschema %} + - name: should not report errors for liquid in regular content outside raw tags + files: + templates/test.liquid: | +
    + {% if product.available %} + + {% endif %} +
    + + {% stylesheet %} + .static-style { + margin: 10px; + } + {% endstylesheet %} + - name: should report nested raw tags inside stylesheet tags + files: + templates/test.liquid: | + {% stylesheet %} + /* This should trigger an error */ + {% raw %} + .nested { color: red; } + {% endraw %} + {% endstylesheet %} + offenses: + - file: templates/test.liquid + message: Liquid tag found in CSS block. {% stylesheet %} tags should only contain static CSS code. + start: [2, 2] + highlight: |- + {% raw %} + .nested { color: red; } + {% endraw %} + - name: should not report offenses for empty stylesheet and javascript blocks + files: + templates/test.liquid: | + {% stylesheet %} + {% endstylesheet %} + + {% javascript %} + {% endjavascript %} + - name: should not report errors for liquid in style tags (different from stylesheet) + files: + templates/test.liquid: | + {% style %} + .button { + color: {{ settings.color }}; + } + {% endstyle %} diff --git a/specs/checks/stylesheet-once-per-file.yml b/specs/checks/stylesheet-once-per-file.yml new file mode 100644 index 000000000..fe8e9090f --- /dev/null +++ b/specs/checks/stylesheet-once-per-file.yml @@ -0,0 +1,18 @@ +check: StylesheetOncePerFile +severity: error +cases: + - name: reports second and subsequent stylesheet tags in a file + files: + sections/test.liquid: |- + {% stylesheet %}{% endstylesheet %} + {% stylesheet %}{% endstylesheet %} + {% stylesheet %}{% endstylesheet %} + offenses: + - file: sections/test.liquid + message: "{% stylesheet %} can only appear once per file." + start: [1, 0] + highlight: "{% stylesheet %}" + - file: sections/test.liquid + message: "{% stylesheet %} can only appear once per file." + start: [2, 0] + highlight: "{% stylesheet %}" diff --git a/specs/checks/stylesheet-tag-in-wrong-file.yml b/specs/checks/stylesheet-tag-in-wrong-file.yml new file mode 100644 index 000000000..02f0a3df0 --- /dev/null +++ b/specs/checks/stylesheet-tag-in-wrong-file.yml @@ -0,0 +1,20 @@ +check: StylesheetTagInWrongFile +severity: error +cases: + - name: allows stylesheet tags in sections/test.liquid + files: + sections/test.liquid: "{% stylesheet %}{% endstylesheet %}" + - name: allows stylesheet tags in blocks/test.liquid + files: + blocks/test.liquid: "{% stylesheet %}{% endstylesheet %}" + - name: allows stylesheet tags in snippets/test.liquid + files: + snippets/test.liquid: "{% stylesheet %}{% endstylesheet %}" + - name: reports stylesheet tags in templates/index.liquid + files: + templates/index.liquid: "{% stylesheet %}{% endstylesheet %}" + offenses: + - file: templates/index.liquid + message: "{% stylesheet %} is only valid in section, block, or snippet files." + start: [0, 0] + highlight: "{% stylesheet %}" diff --git a/specs/checks/translation-key-exists.yml b/specs/checks/translation-key-exists.yml new file mode 100644 index 000000000..df841ad43 --- /dev/null +++ b/specs/checks/translation-key-exists.yml @@ -0,0 +1,149 @@ +check: TranslationKeyExists +severity: error +cases: + - name: should report all keys if default locale file does not exist + files: + code.liquid: |- + {{"key" | t}} + {{"nested.key" | t}} + docset: default + offenses: + - file: code.liquid + message: "'key' does not have a matching entry in 'locales/en.default.json'" + start: [0, 2] + highlight: '"key"' + - file: code.liquid + message: "'nested.key' does not have a matching entry in 'locales/en.default.json'" + start: [1, 2] + highlight: '"nested.key"' + - name: should report nothing if default locale file is invalid + files: + locales/en.default.json: "{" + code.liquid: |- + {{"key" | t}} + {{"nested.key" | t}} + {% schema %} + { + "locales": { + "en": { + "key": "I will be ignored", + "nested": { + "key": "Me too!" + } + } + } + } + {% endschema %} + docset: default + - name: should report unknown keys even if schema is invalid + files: + locales/en.default.json: |- + {} + code.liquid: |- + {{"key" | t}} + {% schema %} + { + {% endschema %} + docset: default + offenses: + - file: code.liquid + message: "'key' does not have a matching entry in 'locales/en.default.json'" + start: [0, 2] + highlight: '"key"' + - name: should ignore existing keys + files: + locales/en.default.json: |- + { + "key": "", + "nested": { + "key": "" + } + } + code.liquid: |- + {{"key" | t}} + {{"nested.key" | t}} + docset: default + - name: should ignore keys included in schema + files: + code.liquid: |- + {{"submit" | t}} + {{ "this.does.not.exist" | translate }} + {% schema %} + { + "locales": { + "en": { + "submit": "Subscribe" + } + } + } + {% endschema %} + docset: default + offenses: + - file: code.liquid + message: "'this.does.not.exist' does not have a matching entry in 'locales/en.default.json' or 'code.liquid'" + start: [1, 3] + highlight: '"this.does.not.exist"' + - name: should not crash if the schema locales does not exist + files: + code.liquid: |- + {{ "this.does.not.exist" | translate }} + {% schema %} + { + "name": "Slideshow", + "tag": "section", + "class": "slideshow", + "limit": 1 + } + {% endschema %} + docset: default + offenses: + - file: code.liquid + message: "'this.does.not.exist' does not have a matching entry in 'locales/en.default.json'" + start: [0, 3] + highlight: '"this.does.not.exist"' + - name: should only report unknown keys for translate filter + files: + locales/en.default.json: |- + {} + code.liquid: |- + {{ "unknownkey" | t }} + {{ "do.not.report.me" | unknown_filter }} + {{ "unknown.nested.key" | translate }} + docset: default + offenses: + - file: code.liquid + message: "'unknownkey' does not have a matching entry in 'locales/en.default.json'" + start: [0, 3] + highlight: '"unknownkey"' + - file: code.liquid + message: "'unknown.nested.key' does not have a matching entry in 'locales/en.default.json'" + start: [2, 3] + highlight: '"unknown.nested.key"' + - name: should count Shopify-provided translations as defined + files: + locales/en.default.json: |- + {} + code.liquid: |- + {{ 'shopify.sentence.words_connector' | t }} + {{ 'shopify.this.does.not.exist' | t }} + docset: default + offenses: + - file: code.liquid + message: "'shopify.this.does.not.exist' does not have a matching entry in 'locales/en.default.json'" + start: [1, 3] + highlight: "'shopify.this.does.not.exist'" + - name: should handle key conflicts + files: + locales/en.default.json: |- + { + "product": { + "quantity": "TODO" + } + } + code.liquid: '{{"product.quantity.decrease" | t}}' + docset: default + offenses: + - file: code.liquid + message: "'product.quantity.decrease' does not have a matching entry in 'locales/en.default.json'" + start: [0, 2] + highlight: '"product.quantity.decrease"' diff --git a/specs/checks/unclosed-html-element.yml b/specs/checks/unclosed-html-element.yml new file mode 100644 index 000000000..7a1a1b902 --- /dev/null +++ b/specs/checks/unclosed-html-element.yml @@ -0,0 +1,352 @@ +check: UnclosedHTMLElement +severity: warning +cases: + - name: should not report for files without unbalanced tags (1) + files: + file.liquid: | +
    + {% if cond %} +
    + + {% else %} +

    + {% endif %} + + {% if cond %} +

    + {% else %} + + {% endif %} + + {% if cond %} +
    + {% endif %} +
    + - name: should not report for files without unbalanced tags (2) + files: + file.liquid: | +
    + {% if cond %} + {% else %} +

    + {% endif %} + + {% unless cond %} +

    + {% endunless %} +
    + - name: should not report for files without unbalanced tags (3) + files: + file.liquid: | +
    + {% case thing %} + {% when 'one', 'two' %} +

    + {% when 'three' %} +

    + {% else %} +

    + {% endcase %} + + {% case thing %} + {% when 'one','two' %} +

    + {% when 'three' %} + + {% else %} +

    + {% endunless %} +
    + - name: should not report offenses for similarly written, but not string-equal, conditions (1) + files: + file.liquid: | + {% if number>10 %} +

    + {% endif %} + + {% if number > 10 %} +

    + {% endif %} + - name: should not report offenses for similarly written, but not string-equal, conditions (2) + files: + file.liquid: | + {% if "string" %} +

    + {% endif %} + + {% if 'string' %} +

    + {% endif %} + - name: should not report offenses for similarly written, but not string-equal, conditions (3) + files: + file.liquid: | + {% if x == nil %} +

    + {% endif %} + + {% if x==null %} +

    + {% endif %} + - name: should not report offenses for similarly written, but not string-equal, conditions (4) + files: + file.liquid: | + {% if x.a %} +

    + {% endif %} + + {% if x["a"] %} +

    + {% endif %} + - name: should not report offenses for similarly written, but not string-equal, conditions (5) + files: + file.liquid: | + {% if (0..a) %} +

    + {% endif %} + + {% if ( 0 .. a ) %} +

    + {% endif %} + - name: should not report offenses for similarly written, but not string-equal, conditions (6) + files: + file.liquid: | + {% if a or b %} +

    + {% endif %} + + {% if a or b %} +

    + {% endif %} + - name: should report an offense for a branch open tag without its close tag + files: + file.liquid: | +
    + {% if cond %} +
    + {% endif %} +
    + offenses: + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `cond` in HtmlElement 'div'" + start: [2, 4] + highlight: "
    " + - name: should report an offense for a branch close tag without its open tag + files: + file.liquid: | +
    + {% if cond %} +
    + {% endif %} + + offenses: + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `cond` in HtmlElement 'div'" + start: [2, 4] + highlight: "" + - name: should report offenses when branches are closed in the wrong order + files: + file.liquid: | +
    + {% if cond %} +
    + + {% endif %} + + {% if cond %} +
    + + {% endif %} +
    + offenses: + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `cond` in HtmlElement 'div'" + start: [2, 4] + highlight: "
    " + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `cond` in HtmlElement 'div'" + start: [3, 6] + highlight: "" + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `cond` in HtmlElement 'div'" + start: [7, 4] + highlight: "
    " + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `cond` in HtmlElement 'div'" + start: [8, 4] + highlight: "" + - name: should report offenses when branches are closed in the wrong node + files: + file.liquid: | +
    + {% if cond %} +
    + + {% endif %} + +
    + {% if cond %} +
    + + {% endif %} +
    + + + {% if cond %} + + + {% endif %} + offenses: + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `cond` in HtmlElement 'div'" + start: [2, 4] + highlight: "
    " + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `cond` in HtmlElement 'div'" + start: [3, 6] + highlight: "" + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `cond` in HtmlElement 'div'" + start: [8, 6] + highlight: "
    " + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `cond` in HtmlElement 'div'" + start: [9, 6] + highlight: "" + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `cond` in Document 'Document'" + start: [15, 2] + highlight: "" + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `cond` in Document 'Document'" + start: [16, 2] + highlight: "" + - name: should not report for paired conditional open/close in for-loop boundary conditions (1) + files: + file.liquid: | + {% for item in items %} + {% if forloop.first or item_modulo == 1 %} +
    + {% endif %} + +
    {{ item }}
    + + {% if forloop.last or item_modulo == 0 %} +
    + {% endif %} + {% endfor %} + - name: should not report for paired conditional open/close in for-loop boundary conditions (2) + files: + file.liquid: | +
    + {% if condition_a %} +
    + {% endif %} + + {% if condition_b %} +
    + {% endif %} +
    + - name: should still report when cross-identifier tags do not balance by name + files: + file.liquid: | +
    + {% if condition_a %} +
    + {% endif %} + + {% if condition_b %} + + {% endif %} +
    + offenses: + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `condition_a` in HtmlElement 'div'" + start: [2, 4] + highlight: "
    " + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `condition_b` in HtmlElement 'div'" + start: [6, 4] + highlight: "" + - name: should report offenses when conditions do not match + files: + file.liquid: | +
    + {% if some_condition %} +
    + {% else %} +
    + {% else %} + + {% endif %} +
    + offenses: + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `some_condition` in HtmlElement 'div'" + start: [2, 4] + highlight: "
    " + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `-some_condition` in HtmlElement 'div'" + start: [4, 4] + highlight: "
    " + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `-other_condition` in HtmlElement 'div'" + start: [10, 4] + highlight: "" + - name: should report an offense for unclosed or unopened in case statements + files: + file.liquid: | +
    + {% case thing %} + {% when 'one' %} +

    + {% else %} +

    + {% endcase %} + + {% case thing %} + {% when 'two' %} +

    + {% endcase %} +
    + offenses: + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `case thing == 'one'` in HtmlElement 'div'" + start: [3, 6] + highlight: "

    " + - file: file.liquid + message: "Opening tag does not have a matching closing tag for condition `case thing` in HtmlElement 'div'" + start: [5, 6] + highlight: "

    " + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `case thing == 'two'` in HtmlElement 'div'" + start: [10, 6] + highlight: "

    " + - name: should report an offense for doubly unclosed of the same name + files: + file.liquid: | +
    + {% if thing %} + + {% endif %} + + {% if thing %} + + {% endif %} +
    + offenses: + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `thing` in HtmlElement 'div'" + start: [2, 4] + highlight: "" + - file: file.liquid + message: "Closing tag does not have a matching opening tag for condition `thing` in HtmlElement 'div'" + start: [6, 4] + highlight: "" diff --git a/specs/checks/undefined-object.yml b/specs/checks/undefined-object.yml new file mode 100644 index 000000000..4122443ec --- /dev/null +++ b/specs/checks/undefined-object.yml @@ -0,0 +1,758 @@ +check: UndefinedObject +severity: warning +cases: + - name: allows section in a snippet in app mode + mode: app + docset: default + files: + snippets/my-snippet.liquid: '{% doc %} @param {string} x - x {% enddoc %}{{ section }}' + - name: allows block in a snippet in app mode + mode: app + docset: default + files: + snippets/my-snippet.liquid: '{% doc %} @param {string} x - x {% enddoc %}{{ block }}' + - name: allows recommendations in a snippet in app mode + mode: app + docset: default + files: + snippets/my-snippet.liquid: '{% doc %} @param {string} x - x {% enddoc %}{{ recommendations }}' + - name: allows app in a snippet in app mode + mode: app + docset: default + files: + snippets/my-snippet.liquid: '{% doc %} @param {string} x - x {% enddoc %}{{ app }}' + - name: reports schema settings used as bare variables in an app block + mode: app + docset: default + files: + blocks/card.liquid: | +

    {{ heading }}

    + {% schema %}{"name":"Card","settings":[{"type":"text","id":"heading","label":"Heading"}]}{% endschema %} + offenses: + - file: blocks/card.liquid + message: Unknown object 'heading' used. + start: [0, 7] + highlight: heading + - name: should report an offense when object is undefined + files: + file.liquid: | + {{ my_var }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'my_var' used." + start: [0, 3] + highlight: my_var + - name: should report an offense when object with an attribute is undefined + files: + file.liquid: | + {{ my_var.my_attr }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'my_var' used." + start: [0, 3] + highlight: my_var.my_attr + - name: should report an offense when undefined object is used as an argument + files: + file.liquid: | + {{ product[my_object] }} + {{ product[my_object] }} + + {% comment %} string arguments should not be reported {% endcomment %} + {{ product["my_object"] }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'my_object' used." + start: [0, 11] + highlight: my_object + - file: file.liquid + message: "Unknown object 'my_object' used." + start: [1, 11] + highlight: my_object + - name: should report an offense when object is undefined in a Liquid tag + files: + file.liquid: | + {% liquid + echo my_var + %} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'my_var' used." + start: [1, 7] + highlight: my_var + - name: should not report an offense when object is defined with an assign tag + files: + file.liquid: | + {% assign my_var = "value" %} + {{ my_var }} + docset: default + - name: should not report an offense when object is defined with an assign tag and it is used as an argument + files: + file.liquid: | + {% assign prop = "title" %} + {{ product[prop] }} + docset: default + - name: should not report an offense when object is defined with an assign tag in a Liquid tag + files: + file.liquid: | + {% liquid + assign my_var = "value" + echo my_var + %} + docset: default + - name: should not report an offense when object is defined with a capture tag + files: + file.liquid: | + {% capture my_var %} value {% endcapture %} + {{ my_var }} + docset: default + - name: should not report an offense when object is defined in a for loop + files: + file.liquid: | + {% for c in collections %} + {{ c }} + {% endfor %} + docset: default + - name: should report an offense when object is defined in a for loop but used outside of the scope + files: + file.liquid: | + {% for c in collections %} + {{ c }} + {% endfor %}{{ c }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'c' used." + start: [2, 15] + highlight: c + - name: should report an offense when object is defined in a for loop but used outside of the scope (in scenarios where the same variable has multiple scopes in the file) + files: + file.liquid: | + {% for c in collections %} + {% comment %} -- Scope 1 -- {% endcomment %} + {{ c }} + {% endfor %} + {{ c }} + {% for c in collections %} + {% comment %} -- Scope 2 -- {% endcomment %} + {{ c }} + {% endfor %} + {{ c }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'c' used." + start: [4, 3] + highlight: c + - file: file.liquid + message: "Unknown object 'c' used." + start: [9, 3] + highlight: c + - name: should report an offense when undefined object defines another object + files: + file.liquid: | + {% assign my_object = my_var %} + {{ my_object }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'my_var' used." + start: [0, 22] + highlight: my_var + - name: should not report an offense when object is defined in a tablerow loop + files: + file.liquid: | + {% tablerow c in collections %} + {{ c }} + {% endtablerow %} + docset: default + - name: should report an offense when object is defined in a tablerow loop but used outside of the scope + files: + file.liquid: | + {% tablerow c in collections %} + {{ c }} + {% endtablerow %}{{ c }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'c' used." + start: [2, 20] + highlight: c + - name: should contextually report on the undefined nature of the paginate object (defined in paginate tag, undefined outside) + files: + file.liquid: | + {% assign col = 'string' | split: '' %} + {% paginate col by 5 %} + {{ paginate }} + {% endpaginate %}{{ paginate }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'paginate' used." + start: [3, 20] + highlight: paginate + - name: should contextually report on the undefined nature of the form object (defined in form tag, undefined outside) + files: + file.liquid: | + {% form "cart" %} + {{ form }} + {% endform %}{{ form }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'form' used." + start: [2, 16] + highlight: form + - name: should support {% layout none %} + files: + file.liquid: | + {% layout none %} + {{ none }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'none' used." + start: [1, 3] + highlight: none + - name: should support {% increment var %} and {% decrement var %} (1) + files: + file.liquid: | + {% increment var %} + {{ var }} + docset: default + - name: should support {% increment var %} and {% decrement var %} (2) + files: + file.liquid: | + {% decrement var %} + {{ var }} + docset: default + - name: 'should not report an offense when object is undefined in a "snippet" file with no presense of doc tags' + files: + snippets/file.liquid: | + {{ my_var }} + docset: default + - name: 'should report an offense when object is undefined in a "snippet" file with doc tags that are missing the associated param' + files: + snippets/file.liquid: | + {% doc %} + {% enddoc %} + {{ my_var }} + docset: default + offenses: + - file: snippets/file.liquid + message: "Unknown object 'my_var' used." + start: [2, 3] + highlight: my_var + - name: "should not report an offense when object is defined with @param in a snippet file" + files: + snippets/file.liquid: | + {% doc %} + @param {string} text + {% enddoc %} + + {{ text }} + docset: default + - name: should report an offense when object is not global + files: + file.liquid: | + {{ image }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'image' used." + start: [0, 3] + highlight: image + - name: should not report an offense for forloop/tablerowloop variables when in the correct context (1) + files: + file.liquid: | + {% for x in collections %} + {{ forloop }} + {% endfor %} + docset: default + - name: should not report an offense for forloop/tablerowloop variables when in the correct context (2) + files: + file.liquid: | + {% tablerow x in collections %} + {{ tablerowloop }} + {% endtablerow %} + docset: default + - name: should support contextual exceptions (1) + files: + sections/section.liquid: "{{ section }}" + docset: default + - name: should support contextual exceptions (2) + files: + file.liquid: "{{ section }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'section' used." + start: [0, 3] + highlight: section + - name: should support contextual exceptions (3) + files: + sections/predictive-search.liquid: "{{ predictive_search }}" + docset: default + - name: should support contextual exceptions (4) + files: + file.liquid: "{{ predictive_search }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'predictive_search' used." + start: [0, 3] + highlight: predictive_search + - name: should support contextual exceptions (5) + files: + sections/recommendations.liquid: "{{ recommendations }}" + docset: default + - name: should support contextual exceptions (6) + files: + file.liquid: "{{ recommendations }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'recommendations' used." + start: [0, 3] + highlight: recommendations + - name: should support contextual exceptions (7) + files: + sections/main-article.liquid: "{{ comment }}" + docset: default + - name: should support contextual exceptions (8) + files: + file.liquid: "{{ comment }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'comment' used." + start: [0, 3] + highlight: comment + - name: should support contextual exceptions (9) + files: + blocks/theme-app-extension.liquid: "{{ block }}" + docset: default + - name: should support contextual exceptions (10) + files: + file.liquid: "{{ block }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'block' used." + start: [0, 3] + highlight: block + - name: should support contextual exceptions (11) + files: + blocks/theme-app-extension.liquid: "{{ app }}" + docset: default + - name: should support contextual exceptions (12) + files: + file.liquid: "{{ app }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'app' used." + start: [0, 3] + highlight: app + - name: should support contextual exceptions (13) + files: + snippets/theme-app-extension.liquid: "{{ app }}" + docset: default + - name: should support contextual exceptions (14) + files: + file.liquid: "{{ app }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'app' used." + start: [0, 3] + highlight: app + - name: should allow app, section, and block in snippets when in theme mode (1) + files: + snippets/my-snippet.liquid: "{% doc %} @param {string} x - x {% enddoc %}{{ app }}" + docset: default + - name: should allow app, section, and block in snippets when in theme mode (2) + files: + snippets/my-snippet.liquid: "{% doc %} @param {string} x - x {% enddoc %}{{ section }}" + docset: default + - name: should allow app, section, and block in snippets when in theme mode (3) + files: + snippets/my-snippet.liquid: "{% doc %} @param {string} x - x {% enddoc %}{{ block }}" + docset: default + - name: should still flag block-only objects in snippets when in theme mode + files: + snippets/my-snippet.liquid: "{% doc %} @param {string} x - x {% enddoc %}{{ recommendations }}" + docset: default + offenses: + - file: snippets/my-snippet.liquid + message: "Unknown object 'recommendations' used." + start: [0, 47] + highlight: recommendations + - name: should support contextual exceptions for checkout.liquid (1) + files: + layout/checkout.liquid: "{{ locale }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (2) + files: + file.liquid: "{{ locale }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'locale' used." + start: [0, 3] + highlight: locale + - name: should support contextual exceptions for checkout.liquid (3) + files: + layout/checkout.liquid: "{{ direction }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (4) + files: + file.liquid: "{{ direction }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'direction' used." + start: [0, 3] + highlight: direction + - name: should support contextual exceptions for checkout.liquid (5) + files: + layout/checkout.liquid: "{{ skip_to_content_link }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (6) + files: + file.liquid: "{{ skip_to_content_link }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'skip_to_content_link' used." + start: [0, 3] + highlight: skip_to_content_link + - name: should support contextual exceptions for checkout.liquid (7) + files: + layout/checkout.liquid: "{{ checkout_html_classes }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (8) + files: + file.liquid: "{{ checkout_html_classes }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'checkout_html_classes' used." + start: [0, 3] + highlight: checkout_html_classes + - name: should support contextual exceptions for checkout.liquid (9) + files: + layout/checkout.liquid: "{{ checkout_stylesheets }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (10) + files: + file.liquid: "{{ checkout_stylesheets }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'checkout_stylesheets' used." + start: [0, 3] + highlight: checkout_stylesheets + - name: should support contextual exceptions for checkout.liquid (11) + files: + layout/checkout.liquid: "{{ checkout_scripts }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (12) + files: + file.liquid: "{{ checkout_scripts }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'checkout_scripts' used." + start: [0, 3] + highlight: checkout_scripts + - name: should support contextual exceptions for checkout.liquid (13) + files: + layout/checkout.liquid: "{{ content_for_logo }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (14) + files: + file.liquid: "{{ content_for_logo }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'content_for_logo' used." + start: [0, 3] + highlight: content_for_logo + - name: should support contextual exceptions for checkout.liquid (15) + files: + layout/checkout.liquid: "{{ breadcrumb }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (16) + files: + file.liquid: "{{ breadcrumb }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'breadcrumb' used." + start: [0, 3] + highlight: breadcrumb + - name: should support contextual exceptions for checkout.liquid (17) + files: + layout/checkout.liquid: "{{ order_summary_toggle }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (18) + files: + file.liquid: "{{ order_summary_toggle }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'order_summary_toggle' used." + start: [0, 3] + highlight: order_summary_toggle + - name: should support contextual exceptions for checkout.liquid (19) + files: + layout/checkout.liquid: "{{ content_for_order_summary }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (20) + files: + file.liquid: "{{ content_for_order_summary }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'content_for_order_summary' used." + start: [0, 3] + highlight: content_for_order_summary + - name: should support contextual exceptions for checkout.liquid (21) + files: + layout/checkout.liquid: "{{ alternative_payment_methods }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (22) + files: + file.liquid: "{{ alternative_payment_methods }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'alternative_payment_methods' used." + start: [0, 3] + highlight: alternative_payment_methods + - name: should support contextual exceptions for checkout.liquid (23) + files: + layout/checkout.liquid: "{{ content_for_footer }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (24) + files: + file.liquid: "{{ content_for_footer }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'content_for_footer' used." + start: [0, 3] + highlight: content_for_footer + - name: should support contextual exceptions for checkout.liquid (25) + files: + layout/checkout.liquid: "{{ tracking_code }}" + docset: default + - name: should support contextual exceptions for checkout.liquid (26) + files: + file.liquid: "{{ tracking_code }}" + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'tracking_code' used." + start: [0, 3] + highlight: tracking_code + - name: should report an offense for forloop/tablerowloop used outside of context + files: + file.liquid: | + {{ forloop }} + {{ tablerowloop }} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'forloop' used." + start: [0, 3] + highlight: forloop + - file: file.liquid + message: "Unknown object 'tablerowloop' used." + start: [1, 3] + highlight: tablerowloop + - name: should not report an offenses when definitions for global objects are unavailable + files: + file.liquid: | + {{ my_var }} + - name: "should not report an offense when object is defined with @param in a block file" + files: + blocks/my-custom-block.liquid: | + {% doc %} + @param {string} text + {% enddoc %} + + {{ text }} + docset: default + - name: "should report an offense when an undefined object is used alongside @param in a block file" + files: + blocks/my-custom-block.liquid: | + {% doc %} + @param {string} text + {% enddoc %} + + {{ text }} + {{ undefined_variable }} + docset: default + offenses: + - file: blocks/my-custom-block.liquid + message: "Unknown object 'undefined_variable' used." + start: [5, 3] + highlight: undefined_variable + - name: does not report the built-in content variable in a block file + files: + blocks/card.liquid: | + {{ content }} + {% if content != blank %}{{ content | upcase }}{% endif %} + docset: default + - name: reports content outside block files + files: + sections/main.liquid: "{{ content }}" + docset: default + offenses: + - file: sections/main.liquid + message: "Unknown object 'content' used." + start: [0, 3] + highlight: content + - name: does not report a schema setting used as a bare variable + files: + blocks/card.liquid: | +

    {{ heading }}

    + {% schema %}{"name":"Card","settings":[{"type":"text","id":"heading","label":"Heading"}]}{% endschema %} + docset: default + - name: does not report a schema setting and a LiquidDoc-only parameter in the same block + files: + blocks/card.liquid: | + {% doc %} + @param {string} [eyebrow] + {% enddoc %} +

    {{ eyebrow }}

    +

    {{ heading }}

    + {% schema %}{"name":"Card","settings":[{"type":"text","id":"heading","label":"Heading"}]}{% endschema %} + docset: default + - name: does not report a name declared by both schema and LiquidDoc + files: + blocks/card.liquid: | + {% doc %} + @param {string} heading + {% enddoc %} +

    {{ heading }}

    + {% schema %}{"name":"Card","settings":[{"type":"text","id":"heading","label":"Heading"}]}{% endschema %} + docset: default + - name: does not report content alongside schema and LiquidDoc variables + files: + blocks/card.liquid: | + {% doc %} + @param {string} [eyebrow] + {% enddoc %} +

    {{ eyebrow }}

    +

    {{ heading }}

    +
    {{ content }}
    + {% schema %}{"name":"Card","settings":[{"type":"text","id":"heading","label":"Heading"}]}{% endschema %} + docset: default + - name: reports an unknown root alongside schema and LiquidDoc variables + files: + blocks/card.liquid: | + {% doc %} + @param {string} [eyebrow] + {% enddoc %} +

    {{ eyebrow }}

    +

    {{ heading }}

    +

    {{ subtitle }}

    + {% schema %}{"name":"Card","settings":[{"type":"text","id":"heading","label":"Heading"}]}{% endschema %} + docset: default + offenses: + - file: blocks/card.liquid + message: "Unknown object 'subtitle' used." + start: [5, 7] + highlight: subtitle + - name: reports names from non-input schema entries and setting types + files: + blocks/card.liquid: | +
    + {{ layout_heading }} + {{ header }} + {{ color_background }} +
    + {% schema %}{"name":"Card","settings":[{"type":"header","id":"layout_heading","content":"Layout"},{"type":"color_background","id":"background","label":"Background"}]}{% endschema %} + docset: default + offenses: + - file: blocks/card.liquid + message: "Unknown object 'layout_heading' used." + start: [1, 5] + highlight: layout_heading + - file: blocks/card.liquid + message: "Unknown object 'header' used." + start: [2, 5] + highlight: header + - file: blocks/card.liquid + message: "Unknown object 'color_background' used." + start: [3, 5] + highlight: color_background + - name: keeps LiquidDoc parameters and content defined when the schema is invalid + files: + blocks/card.liquid: | + {% doc %} + @param {string} [eyebrow] + {% enddoc %} +

    {{ eyebrow }}

    +
    {{ content }}
    + {% schema %} + { "name": "Card", "settings": [ + {% endschema %} + docset: default + - name: reports schema settings used as bare variables in a section file + files: + sections/hero.liquid: | +

    {{ heading }}

    + {% schema %}{"name":"Card","settings":[{"type":"text","id":"heading","label":"Heading"}]}{% endschema %} + docset: default + offenses: + - file: sections/hero.liquid + message: "Unknown object 'heading' used." + start: [0, 7] + highlight: heading + - name: "should not report an offense when a self defined variable is defined with a @param tag" + files: + snippets/file.liquid: | + {% doc %} + @param {string} text + {% enddoc %} + + {% assign text = text | default: "value" %} + docset: default + - name: should report an offense when assigning an undefined variable to itself + files: + file.liquid: | + {% assign my_var = my_var | default: "fallback" %} + docset: default + offenses: + - file: file.liquid + message: "Unknown object 'my_var' used." + start: [0, 19] + highlight: my_var + - name: should not report an offense when using variables inside visible_if statements in a schema tag + files: + file.liquid: | + {% schema %} + { + "settings": [ + { + "type": "text", + "label": "foo", + "id": "foo", + } + { + "type": "text", + "label": "bar", + "id": "bar", + "visible_if": "{{ block.settings.foo }}", + } + ] + } + {% endschema %} + docset: default diff --git a/specs/checks/unique-doc-param-names.yml b/specs/checks/unique-doc-param-names.yml new file mode 100644 index 000000000..e1ea63a42 --- /dev/null +++ b/specs/checks/unique-doc-param-names.yml @@ -0,0 +1,28 @@ +check: UniqueDocParamNames +severity: error +cases: + - name: should not report an error when a all doc params have unique names + files: + file.liquid: | + {% doc %} + @param param1 - Example param + @param param2 - Example param + @param param3 - Example param + {% enddoc %} + - name: should report an error when a param name appears more than once + files: + file.liquid: | + {% doc %} + @param param1 - Example param + @param param1 - Example param + @param param1 - Example param + {% enddoc %} + offenses: + - file: file.liquid + message: "The parameter 'param1' is defined more than once." + start: [2, 9] + highlight: param1 + - file: file.liquid + message: "The parameter 'param1' is defined more than once." + start: [3, 9] + highlight: param1 diff --git a/specs/checks/unique-setting-id.yml b/specs/checks/unique-setting-id.yml new file mode 100644 index 000000000..20a12e299 --- /dev/null +++ b/specs/checks/unique-setting-id.yml @@ -0,0 +1,1202 @@ +check: UniqueSettingId +severity: error +cases: + - name: "Should report an error for duplicate id's in settings_schema (0)" + files: + file.json: |- + [ + { + "name": "theme_info", + "theme_name": "SKKN", + "theme_author": "Accenture", + "theme_version": "2.0.0", + "theme_documentation_url": "https://www.accenture.com/", + "theme_support_url": "https://www.accenture.com/" + }, + { + "name": "Colors", + "settings": [ + { + "type": "color", + "id": "theme_color", + "label": "Theme Primary Color", + "default": "#ff0000" + }, + { + "type": "color", + "id": "default_shade_color", + "label": "PDP default shade option", + "default": "#757575" + } + ] + }, + { + "name": "Logos", + "settings": [ + { + "type": "image_picker", + "id": "logo", + "label": "Logo image" + } + ] + }, + { + "name": "Favicon", + "settings": [ + { + "type": "image_picker", + "id": "favicon", + "label": "Favicon image", + "info": "Will be scaled down to 32 x 32px" + } + ] + }, + { + "name": "Social media", + "settings": [ + { + "type": "header", + "content": "Social sharing options" + }, + { + "type": "checkbox", + "id": "share_facebook", + "label": "Share on Facebook", + "default": true + }, + { + "type": "checkbox", + "id": "share_twitter", + "label": "Tweet on Twitter", + "default": true + }, + { + "type": "checkbox", + "id": "share_pinterest", + "label": "Pin on Pinterest", + "default": true + }, + { + "type": "header", + "content": "Social accounts" + }, + { + "type": "text", + "id": "social_twitter_link", + "label": "Twitter", + "info": "https://twitter.com/shopify" + }, + { + "type": "text", + "id": "social_facebook_link", + "label": "Facebook", + "info": "https://facebook.com/shopify" + }, + { + "type": "text", + "id": "social_pinterest_link", + "label": "Pinterest", + "info": "https://pinterest.com/shopify" + }, + { + "type": "text", + "id": "social_instagram_link", + "label": "Instagram", + "info": "http://instagram.com/shopify" + }, + { + "type": "text", + "id": "social_tiktok_link", + "label": "TikTok", + "info": "http://tiktok.com/shopify" + }, + { + "type": "text", + "id": "social_tumblr_link", + "label": "Tumblr", + "info": "http://shopify.tumblr.com" + }, + { + "type": "text", + "id": "social_snapchat_link", + "label": "Snapchat", + "info": "https://www.snapchat.com/add/shopify" + }, + { + "type": "text", + "id": "social_youtube_link", + "label": "YouTube", + "info": "https://www.youtube.com/shopify" + }, + { + "type": "text", + "id": "social_vimeo_link", + "label": "Vimeo", + "info": "https://vimeo.com/shopify" + } + ] + }, + { + "name": "Newsletter", + "settings": [ + { + "type": "header", + "content": "Attentive" + }, + { + "type": "text", + "id": "attentive_id_desktop", + "label": "Creative ID (Desktop)" + }, + { + "type": "text", + "id": "attentive_id_mobile", + "label": "Creative ID (Mobile)" + }, + { + "type": "richtext", + "id": "privacy_policy", + "label": "Content" + } + ] + }, + { + "name": "Nosto Account Id", + "settings": [ + { + "type": "text", + "id": "nosto_account_id", + "label": "Nosto account id", + "default": "shopify-62406787286", + "placeholder": "If empty, Nosto script won't load" + } + ] + }, + { + "name": "Image Sizes", + "settings": [ + { + "type": "range", + "id": "product_gallery_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Product Gallery", + "default": 800 + }, + { + "type": "range", + "id": "product_tile_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Product Tile", + "default": 800 + }, + { + "type": "range", + "id": "article_tile_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Article Tile", + "default": 800 + }, + { + "type": "range", + "id": "collection_tile_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Collection Tile", + "default": 800 + }, + { + "type": "range", + "id": "line_item_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Line Item Thumbnail", + "default": 400 + }, + { + "type": "text", + "id": "responsive_sizes", + "label": "Responsive Sized", + "default": "360,540,720,900,1080,1200,1440,1680,1920", + "info": "Comma-separated list of sizes" + } + ] + }, + { + "name": "Pagination", + "settings": [ + { + "type": "range", + "id": "products_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Collection Products", + "default": 12 + }, + { + "type": "range", + "id": "results_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Search Results", + "default": 12 + }, + { + "type": "range", + "id": "articles_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Blog Articles", + "default": 12 + }, + { + "type": "range", + "id": "comments_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Article Comments", + "default": 12 + }, + { + "type": "range", + "id": "addresses_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Customer Addresses", + "default": 12 + }, + { + "type": "range", + "id": "orders_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Customer Orders", + "default": 12 + } + ] + }, + { + "name": "Navigation", + "settings": [ + { + "type": "header", + "content": "Breadcrumbs" + }, + { + "type": "checkbox", + "id": "show_breadcrumbs_home", + "label": "Show Home link", + "default": true + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_page", + "label": "Enable on Page template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_product", + "label": "Enable on Product template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_collection", + "label": "Enable on Collection template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_blog", + "label": "Enable on Blog template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_article", + "label": "Enable on Article template", + "default": false + }, + { + "type": "text", + "id": "footer_handle", + "label": "Footer Handle" + }, + { + "type": "text", + "id": "checkout_footer_handle", + "label": "Checkout Footer Handle" + } + ] + }, + { + "name": "Nosto Account Id", + "settings": [ + { + "type": "text", + "id": "nosto_account_id", + "label": "Nosto account id", + "default": "shopify-62406787286", + "placeholder": "If empty, Nosto script won't load" + } + ] + }, + { + "name": "Loyalty", + "settings": [ + { + "type": "range", + "id": "loyalty_points_settingss", + "min": 0, + "max": 10, + "step": 0.5, + "label": "Loyalty Points", + "default": 0.5 + } + ] + }, + { + "name": "Cart", + "settings": [ + { + "type": "range", + "id": "free_shipping_threshold", + "min": 0, + "max": 150, + "step": 5, + "unit": "$", + "label": "Free Shipping Threshold", + "default": 75 + }, + { + "type": "product", + "id": "complete_collection_set_handle", + "label": "Complete Collection Set Handle" + }, + { + "type": "text", + "id": "complete_collection_set_text", + "label": "Complete Collection Set Message", + "default": "You’re receiving 5 complimentary samples with your purchase of The Complete Collection." + }, + { + "type": "text", + "id": "quantity_message_for_one_time_purchase", + "label": "Quantity Message For Onetime Purchase", + "default": "You can select a maximum of 10 products of this item per order" + }, + { + "type": "text", + "id": "quantity_message_for_subscription", + "label": "Quantity Message For Subscription", + "default": "You can select a maximum of 3 products of this item per order" + }, + { + "type": "number", + "id": "maximum_quantity_for_one_time_purchase", + "label": "Maximum Quantity For Onetime Purchase", + "default": 10 + }, + { + "type": "number", + "id": "maximum_quantity_for_subscription", + "label": "Maximum Quantity For Subscription", + "default": 3 + } + ] + }, + { + "name": "Global-E", + "settings": [ + { + "type": "checkbox", + "id": "enable_globale", + "label": "Enable Global-E", + "default": true + }, + { + "type": "select", + "id": "environment", + "label": "Environment", + "options": [ + { + "value": "INT", + "label": "Staging" + }, + { + "value": "PROD", + "label": "Production" + } + ], + "default": "INT" + }, + { + "type": "text", + "id": "globale_mid", + "label": "Global-E Merchant ID", + "default": "1164" + }, + { + "type": "text", + "id": "globale_emi", + "label": "Global-E EMI", + "default": "8uyw" + }, + { + "type": "text", + "id": "globale_site_id", + "label": "Global-E Site ID", + "default": "212b00eaaa7c" + }, + { + "type": "text", + "id": "globale_appurl", + "label": "Global-E App URL", + "default": "https://crossborder-integration-qa-int.bglobale.com/" + }, + { + "type": "text", + "id": "globale_geappurl", + "label": "Global-E GE App URL", + "default": "https://www.bglobale.com/" + }, + { + "type": "text", + "id": "globale_apiurl", + "label": "Global-E GE API URL", + "default": "https://connect.bglobale.com/" + }, + { + "type": "text", + "id": "globale_checkout_environment", + "label": "Global-E Checkout Environment ID", + "default": "www.bglobale.com" + }, + { + "type": "text", + "id": "globale_operated_countries_GLBE_PARAMS", + "label": "Operated Countries (GLBE_PARAMS)", + "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", + "default": "AD, AE, AT, BE, BG, BN, CA, CH, CN, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GL, GR, HR, HU, ID, IE, IS, IT, LT, LU, LV, MC, MT, MX, NL, NO, PL, PT, RO, SA, SE, SG, SI, SK, VA" + }, + { + "type": "text", + "id": "globale_operated_countries_outside_GLBE_PARAMS", + "label": "Operated Countries outside GLBE_PARAMS", + "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", + "default": "AD,AE,AT,BE,BG,BN,CA,CH,CN,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GL,GR,HR,HU,ID,IE,IS,IT,LT,LU,LV,MC,MT,MX,NL,NO,PL,PT,RO,SA,SE,SG,SI,SK,VA" + } + ] + }, + { + "name": "Checkout Killswitches", + "settings": [ + { + "type": "checkbox", + "id": "terms_of_service_checkout", + "label": "Enables Terms of Service at Checkout Payment Method Step" + } + ] + }, + { + "name": "Hide Discount Code", + "settings": [ + { + "type": "text", + "id": "bundle_not_eligible_for_welcome10_discount", + "label": "Display below message as WELCOME10 is Not eligible for Bundle Items", + "default": "Part of Bundle, Not eligible for discount" + }, + { + "type": "text", + "id": "tiered_off_promotion_code", + "label": "Tiered % Off promotion Code (should be exact) or leave it empty if there is no tiered % off promotion active", + "default": "TIEREDOFF" + } + ] + }, + { + "name": "Klaviyo - Back In Stock", + "settings": [ + { + "type": "text", + "id": "klaviyo_public_api_key", + "label": "public Klaviyo API Key", + "default": "UJ6swp" + }, + { + "type": "text", + "id": "listId", + "label": "list id", + "default": "S8qDcW" + } + ] + }, + { + "name": "Sample", + "settings": [ + { + "type": "text", + "id": "sample_message", + "label": "Display below message as when customer order again for sample" + }, + { + "type": "header", + "content": "Attentive Popup for Product Page" + }, + { + "type": "text", + "id": "attentive_id_desktop_sample", + "label": "Creative ID (Desktop)" + }, + { + "type": "text", + "id": "attentive_id_mobile_sample", + "label": "Creative ID (Mobile)" + } + ] + }, + { + "name": "Yotpo", + "settings": [ + { + "type": "header", + "content": "Yotpo - Checkout Redeem Points" + }, + { + "type": "text", + "id": "yotpo_checkout_widget_id", + "label": "Yotpo Checkout Widget ID", + "default": "JXB__Dq8om7IRNIBU7D8ag" + }, + { + "type": "text", + "id": "yotpo_checkout_instance_id", + "label": "Yotpo Checkout Instance ID", + "default": "472279" + }, + { + "type": "text", + "id": "yotpo_account_text", + "label": "Yotpo Account Text", + "default": "Apply your points at checkout to receive a discount on your purchase" + } + ] + } + ] + offenses: + - file: file.json + message: 'Duplicate setting id found: "nosto_account_id"' + start: [344, 8] + highlight: '"id": "nosto_account_id"' + - name: should not report any errors for valid file + files: + file.json: |- + [ + { + "name": "theme_info", + "theme_name": "SKKN", + "theme_author": "Accenture", + "theme_version": "2.0.0", + "theme_documentation_url": "https://www.accenture.com/", + "theme_support_url": "https://www.accenture.com/" + }, + { + "name": "Colors", + "settings": [ + { + "type": "color", + "id": "theme_color", + "label": "Theme Primary Color", + "default": "#ff0000" + }, + { + "type": "color", + "id": "default_shade_color", + "label": "PDP default shade option", + "default": "#757575" + } + ] + }, + { + "name": "Logos", + "settings": [ + { + "type": "image_picker", + "id": "logo", + "label": "Logo image" + } + ] + }, + { + "name": "Favicon", + "settings": [ + { + "type": "image_picker", + "id": "favicon", + "label": "Favicon image", + "info": "Will be scaled down to 32 x 32px" + } + ] + }, + { + "name": "Social media", + "settings": [ + { + "type": "header", + "content": "Social sharing options" + }, + { + "type": "checkbox", + "id": "share_facebook", + "label": "Share on Facebook", + "default": true + }, + { + "type": "checkbox", + "id": "share_twitter", + "label": "Tweet on Twitter", + "default": true + }, + { + "type": "checkbox", + "id": "share_pinterest", + "label": "Pin on Pinterest", + "default": true + }, + { + "type": "header", + "content": "Social accounts" + }, + { + "type": "text", + "id": "social_twitter_link", + "label": "Twitter", + "info": "https://twitter.com/shopify" + }, + { + "type": "text", + "id": "social_facebook_link", + "label": "Facebook", + "info": "https://facebook.com/shopify" + }, + { + "type": "text", + "id": "social_pinterest_link", + "label": "Pinterest", + "info": "https://pinterest.com/shopify" + }, + { + "type": "text", + "id": "social_instagram_link", + "label": "Instagram", + "info": "http://instagram.com/shopify" + }, + { + "type": "text", + "id": "social_tiktok_link", + "label": "TikTok", + "info": "http://tiktok.com/shopify" + }, + { + "type": "text", + "id": "social_tumblr_link", + "label": "Tumblr", + "info": "http://shopify.tumblr.com" + }, + { + "type": "text", + "id": "social_snapchat_link", + "label": "Snapchat", + "info": "https://www.snapchat.com/add/shopify" + }, + { + "type": "text", + "id": "social_youtube_link", + "label": "YouTube", + "info": "https://www.youtube.com/shopify" + }, + { + "type": "text", + "id": "social_vimeo_link", + "label": "Vimeo", + "info": "https://vimeo.com/shopify" + } + ] + }, + { + "name": "Newsletter", + "settings": [ + { + "type": "header", + "content": "Attentive" + }, + { + "type": "text", + "id": "attentive_id_desktop", + "label": "Creative ID (Desktop)" + }, + { + "type": "text", + "id": "attentive_id_mobile", + "label": "Creative ID (Mobile)" + }, + { + "type": "richtext", + "id": "privacy_policy", + "label": "Content" + } + ] + }, + { + "name": "Nosto Account Id", + "settings": [ + { + "type": "text", + "id": "nosto_account_id", + "label": "Nosto account id", + "default": "shopify-62406787286", + "placeholder": "If empty, Nosto script won't load" + } + ] + }, + { + "name": "Image Sizes", + "settings": [ + { + "type": "range", + "id": "product_gallery_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Product Gallery", + "default": 800 + }, + { + "type": "range", + "id": "product_tile_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Product Tile", + "default": 800 + }, + { + "type": "range", + "id": "article_tile_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Article Tile", + "default": 800 + }, + { + "type": "range", + "id": "collection_tile_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Collection Tile", + "default": 800 + }, + { + "type": "range", + "id": "line_item_image_max", + "min": 160, + "max": 1920, + "step": 40, + "label": "Line Item Thumbnail", + "default": 400 + }, + { + "type": "text", + "id": "responsive_sizes", + "label": "Responsive Sized", + "default": "360,540,720,900,1080,1200,1440,1680,1920", + "info": "Comma-separated list of sizes" + } + ] + }, + { + "name": "Pagination", + "settings": [ + { + "type": "range", + "id": "products_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Collection Products", + "default": 12 + }, + { + "type": "range", + "id": "results_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Search Results", + "default": 12 + }, + { + "type": "range", + "id": "articles_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Blog Articles", + "default": 12 + }, + { + "type": "range", + "id": "comments_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Article Comments", + "default": 12 + }, + { + "type": "range", + "id": "addresses_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Customer Addresses", + "default": 12 + }, + { + "type": "range", + "id": "orders_page_size", + "min": 4, + "max": 48, + "step": 1, + "label": "Customer Orders", + "default": 12 + } + ] + }, + { + "name": "Navigation", + "settings": [ + { + "type": "header", + "content": "Breadcrumbs" + }, + { + "type": "checkbox", + "id": "show_breadcrumbs_home", + "label": "Show Home link", + "default": true + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_page", + "label": "Enable on Page template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_product", + "label": "Enable on Product template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_collection", + "label": "Enable on Collection template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_blog", + "label": "Enable on Blog template", + "default": false + }, + { + "type": "checkbox", + "id": "enable_breadcrumbs_article", + "label": "Enable on Article template", + "default": false + }, + { + "type": "text", + "id": "footer_handle", + "label": "Footer Handle" + }, + { + "type": "text", + "id": "checkout_footer_handle", + "label": "Checkout Footer Handle" + } + ] + }, + { + "name": "Nosto Account Id", + "settings": [ + { + "type": "text", + "id": "nosto_account_id_fixed", + "label": "Nosto account id", + "default": "shopify-62406787286", + "placeholder": "If empty, Nosto script won't load" + } + ] + }, + { + "name": "Loyalty", + "settings": [ + { + "type": "range", + "id": "loyalty_points_settingss", + "min": 0, + "max": 10, + "step": 0.5, + "label": "Loyalty Points", + "default": 0.5 + } + ] + }, + { + "name": "Cart", + "settings": [ + { + "type": "range", + "id": "free_shipping_threshold", + "min": 0, + "max": 150, + "step": 5, + "unit": "$", + "label": "Free Shipping Threshold", + "default": 75 + }, + { + "type": "product", + "id": "complete_collection_set_handle", + "label": "Complete Collection Set Handle" + }, + { + "type": "text", + "id": "complete_collection_set_text", + "label": "Complete Collection Set Message", + "default": "You’re receiving 5 complimentary samples with your purchase of The Complete Collection." + }, + { + "type": "text", + "id": "quantity_message_for_one_time_purchase", + "label": "Quantity Message For Onetime Purchase", + "default": "You can select a maximum of 10 products of this item per order" + }, + { + "type": "text", + "id": "quantity_message_for_subscription", + "label": "Quantity Message For Subscription", + "default": "You can select a maximum of 3 products of this item per order" + }, + { + "type": "number", + "id": "maximum_quantity_for_one_time_purchase", + "label": "Maximum Quantity For Onetime Purchase", + "default": 10 + }, + { + "type": "number", + "id": "maximum_quantity_for_subscription", + "label": "Maximum Quantity For Subscription", + "default": 3 + } + ] + }, + { + "name": "Global-E", + "settings": [ + { + "type": "checkbox", + "id": "enable_globale", + "label": "Enable Global-E", + "default": true + }, + { + "type": "select", + "id": "environment", + "label": "Environment", + "options": [ + { + "value": "INT", + "label": "Staging" + }, + { + "value": "PROD", + "label": "Production" + } + ], + "default": "INT" + }, + { + "type": "text", + "id": "globale_mid", + "label": "Global-E Merchant ID", + "default": "1164" + }, + { + "type": "text", + "id": "globale_emi", + "label": "Global-E EMI", + "default": "8uyw" + }, + { + "type": "text", + "id": "globale_site_id", + "label": "Global-E Site ID", + "default": "212b00eaaa7c" + }, + { + "type": "text", + "id": "globale_appurl", + "label": "Global-E App URL", + "default": "https://crossborder-integration-qa-int.bglobale.com/" + }, + { + "type": "text", + "id": "globale_geappurl", + "label": "Global-E GE App URL", + "default": "https://www.bglobale.com/" + }, + { + "type": "text", + "id": "globale_apiurl", + "label": "Global-E GE API URL", + "default": "https://connect.bglobale.com/" + }, + { + "type": "text", + "id": "globale_checkout_environment", + "label": "Global-E Checkout Environment ID", + "default": "www.bglobale.com" + }, + { + "type": "text", + "id": "globale_operated_countries_GLBE_PARAMS", + "label": "Operated Countries (GLBE_PARAMS)", + "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", + "default": "AD, AE, AT, BE, BG, BN, CA, CH, CN, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GL, GR, HR, HU, ID, IE, IS, IT, LT, LU, LV, MC, MT, MX, NL, NO, PL, PT, RO, SA, SE, SG, SI, SK, VA" + }, + { + "type": "text", + "id": "globale_operated_countries_outside_GLBE_PARAMS", + "label": "Operated Countries outside GLBE_PARAMS", + "info": "Please enter a comma separated list with no spaces, ex: AT,BE,BE,BG", + "default": "AD,AE,AT,BE,BG,BN,CA,CH,CN,CY,CZ,DE,DK,EE,ES,FI,FR,GB,GL,GR,HR,HU,ID,IE,IS,IT,LT,LU,LV,MC,MT,MX,NL,NO,PL,PT,RO,SA,SE,SG,SI,SK,VA" + } + ] + }, + { + "name": "Checkout Killswitches", + "settings": [ + { + "type": "checkbox", + "id": "terms_of_service_checkout", + "label": "Enables Terms of Service at Checkout Payment Method Step" + } + ] + }, + { + "name": "Hide Discount Code", + "settings": [ + { + "type": "text", + "id": "bundle_not_eligible_for_welcome10_discount", + "label": "Display below message as WELCOME10 is Not eligible for Bundle Items", + "default": "Part of Bundle, Not eligible for discount" + }, + { + "type": "text", + "id": "tiered_off_promotion_code", + "label": "Tiered % Off promotion Code (should be exact) or leave it empty if there is no tiered % off promotion active", + "default": "TIEREDOFF" + } + ] + }, + { + "name": "Klaviyo - Back In Stock", + "settings": [ + { + "type": "text", + "id": "klaviyo_public_api_key", + "label": "public Klaviyo API Key", + "default": "UJ6swp" + }, + { + "type": "text", + "id": "listId", + "label": "list id", + "default": "S8qDcW" + } + ] + }, + { + "name": "Sample", + "settings": [ + { + "type": "text", + "id": "sample_message", + "label": "Display below message as when customer order again for sample" + }, + { + "type": "header", + "content": "Attentive Popup for Product Page" + }, + { + "type": "text", + "id": "attentive_id_desktop_sample", + "label": "Creative ID (Desktop)" + }, + { + "type": "text", + "id": "attentive_id_mobile_sample", + "label": "Creative ID (Mobile)" + } + ] + }, + { + "name": "Yotpo", + "settings": [ + { + "type": "header", + "content": "Yotpo - Checkout Redeem Points" + }, + { + "type": "text", + "id": "yotpo_checkout_widget_id", + "label": "Yotpo Checkout Widget ID", + "default": "JXB__Dq8om7IRNIBU7D8ag" + }, + { + "type": "text", + "id": "yotpo_checkout_instance_id", + "label": "Yotpo Checkout Instance ID", + "default": "472279" + }, + { + "type": "text", + "id": "yotpo_account_text", + "label": "Yotpo Account Text", + "default": "Apply your points at checkout to receive a discount on your purchase" + } + ] + } + ] diff --git a/specs/checks/unique-static-block-id.yml b/specs/checks/unique-static-block-id.yml new file mode 100644 index 000000000..bee93001b --- /dev/null +++ b/specs/checks/unique-static-block-id.yml @@ -0,0 +1,89 @@ +check: UniqueStaticBlockId +severity: error +cases: + - name: should report an error when two static blocks have the same id + files: + file.liquid: | + {% content_for "block", type: "text", id: "static-block" %} + {% content_for "block", type: "text", id: "static-block" %} + offenses: + - file: file.liquid + message: "The id 'static-block' is already being used by another static block" + start: [1, 42] + highlight: '"static-block"' + suggest: [] + - name: should not report an error when two static blocks have different id + files: + file.liquid: | + {% content_for "block", type: "text", id: "static-block1" %} + {% content_for "block", type: "text", id: "static-block2" %} + - name: should report an error when two static blocks have same id and different types + files: + file.liquid: | + {% content_for "block", type: "text", id: "static-block" %} + {% content_for "block", type: "url", id: "static-block" %} + offenses: + - file: file.liquid + message: "The id 'static-block' is already being used by another static block" + start: [1, 41] + highlight: '"static-block"' + suggest: [] + - name: should not report an error when two static blocks have different id and different types + files: + file.liquid: | + {% content_for "block", type: "text", id: "static-block1" %} + {% content_for "block", type: "url", id: "static-block2" %} + - name: should not report an error when blocks share the same id but have different arbitrary params + files: + file.liquid: | + {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} + {% content_for "block", type: "text", id: "header-menu", variant: "navigation_bar" %} + {% content_for "block", type: "text", id: "header-menu" %} + - name: should report an error when blocks share the same id and same arbitrary params + files: + file.liquid: | + {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} + {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} + offenses: + - file: file.liquid + message: "The id 'header-menu' is already being used by another static block" + start: [1, 42] + highlight: '"header-menu"' + suggest: [] + - name: should not report an error when blocks share the same id but have different values for the same param + files: + file.liquid: | + {% content_for "block", type: "text", id: "sidebar", color: "blue" %} + {% content_for "block", type: "text", id: "sidebar", color: "red" %} + - name: should report an error when blocks share the same id and multiple matching params + files: + file.liquid: | + {% content_for "block", type: "text", id: "sidebar", color: "blue", size: "large" %} + {% content_for "block", type: "text", id: "sidebar", color: "blue", size: "large" %} + offenses: + - file: file.liquid + message: "The id 'sidebar' is already being used by another static block" + start: [1, 42] + highlight: '"sidebar"' + suggest: [] + - name: should report an error when blocks have same params in different order + files: + file.liquid: | + {% content_for "block", type: "text", id: "sidebar", color: "blue", size: "large" %} + {% content_for "block", type: "text", id: "sidebar", size: "large", color: "blue" %} + offenses: + - file: file.liquid + message: "The id 'sidebar' is already being used by another static block" + start: [1, 42] + highlight: '"sidebar"' + suggest: [] + - name: should not report an error when any arbitrary param is a variable lookup + files: + file.liquid: | + {% content_for "block", type: "text", id: "header-menu", variant: "mobile" %} + {% content_for "block", type: "text", id: "header-menu", variant: some_variable %} + - name: should not report an error when two blocks use the same variable lookup + files: + file.liquid: | + {% content_for "block", type: "text", id: "header-menu", variant: some_variable %} + {% content_for "block", type: "text", id: "header-menu", variant: some_variable %} diff --git a/specs/checks/unknown-filter.yml b/specs/checks/unknown-filter.yml new file mode 100644 index 000000000..e308506b8 --- /dev/null +++ b/specs/checks/unknown-filter.yml @@ -0,0 +1,55 @@ +check: UnknownFilter +severity: error +cases: + - name: should report an offense when an unknown filter is used + docset: default + files: + file.liquid: '{{ "hello" | unknown_filter }}' + offenses: + - file: file.liquid + message: Unknown filter 'unknown_filter' used. + start: [0, 11] + highlight: "| unknown_filter" + - name: reports on unknown filter + docset: default + files: + file.liquid: '{{ "foo" | bar }}' + offenses: + - file: file.liquid + message: Unknown filter 'bar' used. + start: [0, 9] + highlight: "| bar" + - name: reports on unknown filter chained with known filters + docset: default + files: + file.liquid: '{{ "foo" | append: ".js" | bar }}' + offenses: + - file: file.liquid + message: Unknown filter 'bar' used. + start: [0, 25] + highlight: "| bar" + - name: does not report on chain of known filters + docset: default + files: + file.liquid: '{{ "foo" | append: ".js" | item_count_for_variant }}' + - name: should not report an offense when a known filter is used (0) + docset: default + files: + file.liquid: "{{ 'hello' | item_count_for_variant }}" + - name: should not report an offense when a known filter is used (1) + docset: default + files: + file.liquid: "{{ 'hello' | link_to_type }}" + - name: should not report an offense when a known filter is used (2) + docset: default + files: + file.liquid: "{{ 'hello' | link_to_vendor }}" + - name: reports from the pipe when a non-breaking space comes before it + docset: default + files: + file.liquid: "{{ 'foo'\u00a0| bar }}" + offenses: + - file: file.liquid + message: Unknown filter 'bar' used. + start: [0, 9] + highlight: "| bar" diff --git a/specs/checks/unrecognized-block-arguments.yml b/specs/checks/unrecognized-block-arguments.yml new file mode 100644 index 000000000..848d54286 --- /dev/null +++ b/specs/checks/unrecognized-block-arguments.yml @@ -0,0 +1,87 @@ +check: UnrecognizedBlockArguments +severity: warning +cases: + - name: accepts schema, LiquidDoc-only, and built-in content parameters + files: + blocks/card.liquid: |- + {% doc %} + @param {string} tracking_id - Developer-only tracking id + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', heading: 'Hello', tracking_id: 'hero', content: body %}{% endblock %}" + - name: accepts a schema parameter without LiquidDoc + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', heading: 'Hello' %}{% endblock %}" + - name: reports an argument outside the merged interface + files: + blocks/card.liquid: |- + {% doc %} + @param {string} tracking_id - Developer-only tracking id + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', heading: 'Hello', unknown: true %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Unknown argument 'unknown' in block tag for 'card'." + start: [0, 35] + highlight: "unknown: true" + - name: validates the LiquidDoc-only interface of a schema-less block + files: + blocks/card.liquid: |- + {% doc %} + @param {string} tracking_id - Developer-only tracking id + {% enddoc %} + templates/test.liquid: "{% block 'card', tracking_id: 'hero', unknown: true %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Unknown argument 'unknown' in block tag for 'card'." + start: [0, 38] + highlight: "unknown: true" + - name: preserves block.name as the supported caller-side system argument + files: + blocks/card.liquid: |- + {% doc %} + @param {string} tracking_id - Developer-only tracking id + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', block.name: 'Card' %}{% endblock %}" + - name: "leaves dotted arguments to LiquidSyntaxError in {% block 'card', block.settings.heading: 'Hello', unknown: true %}{% endblock %}" + files: + blocks/card.liquid: |- + {% doc %} + @param {string} tracking_id - Developer-only tracking id + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', block.settings.heading: 'Hello', unknown: true %}{% endblock %}" + - name: "leaves dotted arguments to LiquidSyntaxError in {% block 'card', block.unknown: 'value', unknown: true %}{% endblock %}" + files: + blocks/card.liquid: |- + {% doc %} + @param {string} tracking_id - Developer-only tracking id + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', block.unknown: 'value', unknown: true %}{% endblock %}" + - name: does not report unknown arguments for an unreadable missing target + files: + templates/test.liquid: "{% block 'card', unknown: 'value' %}{% endblock %}" + - name: does not report unknown arguments for an unreadable invalid target schema + files: + blocks/card.liquid: |- + {% schema %} + { invalid + {% endschema %} + templates/test.liquid: "{% block 'card', unknown: 'value' %}{% endblock %}" diff --git a/specs/checks/unrecognized-content-for-arguments.yml b/specs/checks/unrecognized-content-for-arguments.yml new file mode 100644 index 000000000..4598d3367 --- /dev/null +++ b/specs/checks/unrecognized-content-for-arguments.yml @@ -0,0 +1,97 @@ +check: UnrecognizedContentForArguments +severity: warning +cases: + - name: should report unknown arguments that are provided in the content_for markup + files: + blocks/card.liquid: | + {% doc %} + @param {string} required_string - A required string + @param {number} required_number - A required number + @param {boolean} required_boolean - A required boolean + @param {object} required_object - A required object + @param {string} [optional_string] - An optional string + @param {number} [optional_number] - An optional number + @param {object} [optional_object] - An optional object + @param {boolean} [optional_boolean] - An optional boolean + {% enddoc %} + file.liquid: | + {% content_for 'block', + type: 'card', + required_string: 'My Card', + required_number: 1, + required_boolean: true, + required_object: product, + unknown_param: 'unknown', + second_unknown_param: 'second unknown', + %} + offenses: + - file: file.liquid + message: "Unknown argument 'unknown_param' in content_for tag for static block 'card'." + start: [6, 0] + highlight: "unknown_param: 'unknown'" + suggest: + - message: "Remove 'unknown_param'" + result: | + {% content_for 'block', + type: 'card', + required_string: 'My Card', + required_number: 1, + required_boolean: true, + required_object: product, + second_unknown_param: 'second unknown', + %} + - file: file.liquid + message: "Unknown argument 'second_unknown_param' in content_for tag for static block 'card'." + start: [7, 0] + highlight: "second_unknown_param: 'second unknown'" + suggest: + - message: "Remove 'second_unknown_param'" + result: | + {% content_for 'block', + type: 'card', + required_string: 'My Card', + required_number: 1, + required_boolean: true, + required_object: product, + unknown_param: 'unknown' + %} + - name: should not report when static block has no doc comment + files: + blocks/card.liquid: "

    This block has no doc comment

    " + file.liquid: "{% content_for 'block', type: 'card', title: 'My Card' %}" + - name: should not report when LiquidDoc definition has no defined params + files: + blocks/card.liquid: | + {% doc %} + @description this is a description + @example this is an example + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% content_for 'block', type: 'card', title: 'My Card' %}" + - name: should not report when block name is a VariableLookup + files: + blocks/card.liquid: | + {% doc %} + @param {string} title - The title of the card + @param {string} description - The description of the card + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% assign block_name = 'card' %}{% content_for 'block', type: block_name, title: 'My Card' %}" + - name: "should not report when argument begins with `.closest`" + files: + blocks/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% content_for 'block', type: 'card', id: '123', closest.product: product %}" + - name: should not report when argument is a reserved word + files: + blocks/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% content_for 'block', type: 'card', id: '123', attributes: product %}" diff --git a/specs/checks/unrecognized-render-snippet-arguments.yml b/specs/checks/unrecognized-render-snippet-arguments.yml new file mode 100644 index 000000000..0c3b54f51 --- /dev/null +++ b/specs/checks/unrecognized-render-snippet-arguments.yml @@ -0,0 +1,142 @@ +check: UnrecognizedRenderSnippetArguments +severity: warning +cases: + - name: should report unknown arguments that are provided in the render markup + files: + snippets/card.liquid: | + {% doc %} + @param {string} required_string - A required string + @param {number} required_number - A required number + @param {boolean} required_boolean - A required boolean + @param {object} required_object - A required object + @param {string} [optional_string] - An optional string + @param {number} [optional_number] - An optional number + @param {object} [optional_object] - An optional object + @param {boolean} [optional_boolean] - An optional boolean + {% enddoc %} + file.liquid: | + {% render 'card', + required_string: 'My Card', + required_number: 1, + required_boolean: true, + required_object: product, + unknown_param: 'unknown', + second_unknown_param: 'second unknown', + %} + offenses: + - file: file.liquid + message: "Unknown argument 'unknown_param' in render tag for snippet 'card'." + start: [5, 0] + highlight: "unknown_param: 'unknown'" + suggest: + - message: "Remove 'unknown_param'" + result: | + {% render 'card', + required_string: 'My Card', + required_number: 1, + required_boolean: true, + required_object: product, + second_unknown_param: 'second unknown', + %} + - file: file.liquid + message: "Unknown argument 'second_unknown_param' in render tag for snippet 'card'." + start: [6, 0] + highlight: "second_unknown_param: 'second unknown'" + suggest: + - message: "Remove 'second_unknown_param'" + result: | + {% render 'card', + required_string: 'My Card', + required_number: 1, + required_boolean: true, + required_object: product, + unknown_param: 'unknown' + %} + - name: should not report when snippet has no doc comment + files: + snippets/card.liquid: "

    This snippet has no doc comment

    " + file.liquid: "{% render 'card', title: 'My Card' %}" + - name: should not report when LiquidDoc definition has no defined params + files: + snippets/card.liquid: | + {% doc %} + @description this is a description + @example this is an example + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% render 'card', title: 'My Card' %}" + - name: should not report when snippet name is a VariableLookup + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + @param {string} description - The description of the card + {% enddoc %} +
    {{ title }}
    +
    {{ description }}
    + file.liquid: "{% assign snippet_name = 'card' %}{% render snippet_name, title: 'My Card' %}" + - name: 'should report when "with/for" alias syntax is used (1)' + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card' with 'my-card' as unknown_param %}" + offenses: + - file: file.liquid + message: "Unknown argument 'unknown_param' in render tag for snippet 'card'." + start: [0, 17] + highlight: "with 'my-card' as unknown_param" + suggest: + - message: "Remove 'unknown_param'" + result: "{% render 'card' %}" + - name: 'should report when "with/for" alias syntax is used (2)' + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card' for array as unknown_param %}" + offenses: + - file: file.liquid + message: "Unknown argument 'unknown_param' in render tag for snippet 'card'." + start: [0, 17] + highlight: for array as unknown_param + suggest: + - message: "Remove 'unknown_param'" + result: "{% render 'card' %}" + - name: should correctly suggest removing aliases with variable whitespace + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card' with 'my-card' as unknown_param %}" + offenses: + - file: file.liquid + message: "Unknown argument 'unknown_param' in render tag for snippet 'card'." + start: [0, 17] + highlight: "with 'my-card' as unknown_param" + suggest: + - message: "Remove 'unknown_param'" + result: "{% render 'card' %}" + - name: removes a non-breaking space before the alias along with it + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title of the card + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card'\u00a0with 'my-card' as unknown_param %}" + offenses: + - file: file.liquid + message: "Unknown argument 'unknown_param' in render tag for snippet 'card'." + start: [0, 17] + highlight: "with 'my-card' as unknown_param" + suggest: + - message: "Remove 'unknown_param'" + result: "{% render 'card' %}" diff --git a/specs/checks/unsupported-doc-tag.yml b/specs/checks/unsupported-doc-tag.yml new file mode 100644 index 000000000..019062fd6 --- /dev/null +++ b/specs/checks/unsupported-doc-tag.yml @@ -0,0 +1,6 @@ +check: UnsupportedDocTag +severity: error +cases: + - name: "should not report an error when `doc` tag is used within snippets" + files: + "file://snippets/file.liquid": "{% doc %} @param param1 - Example param {% enddoc %}{{ 'hello' }}" diff --git a/specs/checks/unsupported-filter-arguments.yml b/specs/checks/unsupported-filter-arguments.yml new file mode 100644 index 000000000..3b7666df7 --- /dev/null +++ b/specs/checks/unsupported-filter-arguments.yml @@ -0,0 +1,81 @@ +check: UnsupportedFilterArguments +severity: error +cases: + - name: reports an offense when a render argument uses a filter + files: + file.liquid: "{% render 'foo', param1: 'bar', param2: 'hello' | append: ' world' %}" + offenses: + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'render' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [0, 48] + highlight: "| append: ' world'" + - name: reports an offense when an include argument uses a filter + files: + file.liquid: "{% include 'foo', x: bar | upcase %}" + offenses: + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'include' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [0, 25] + highlight: "| upcase" + - name: does not report when no filter is used + files: + file.liquid: "{% render 'foo', param1: 'bar', param2: 'hello' %}" + - name: reports an offense when a content_for argument uses a filter + files: + file.liquid: "{% content_for 'block', type: 'foo', id: 'bar' | append: 'x' %}" + offenses: + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'content_for' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [0, 47] + highlight: "| append: 'x'" + - name: does not report a false positive for a pipe inside a string literal + files: + file.liquid: "{% render 'foo', sep: 'a|b' %}" + - name: does not report a false positive for a quoted pipe even when strict parse fails + files: + file.liquid: "{% render 'foo', sep: 'a|b', invalid_argument %}" + - name: does not report a false positive for a double-quoted pipe even when strict parse fails + files: + file.liquid: "{% render 'foo', sep: \"a|b\", invalid_argument %}" + - name: highlights the real filter when a quoted pipe precedes it + files: + file.liquid: "{% render 'foo', sep: 'a|b', x: bar | upcase %}" + offenses: + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'render' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [0, 36] + highlight: "| upcase" + - name: reports distinct highlights for two render tags on one line + files: + file.liquid: "{% render 'foo', x: a | upcase %} {% render 'bar', y: b | downcase %}" + offenses: + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'render' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [0, 22] + highlight: "| upcase" + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'render' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [0, 56] + highlight: "| downcase" + - name: reports an offense for a render statement inside a {% liquid %} tag + files: + file.liquid: |- + {% liquid + render 'foo', x: bar | upcase + %} + offenses: + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'render' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [1, 23] + highlight: "| upcase" + - name: does not report on a plain render with no arguments + files: + file.liquid: "{% render 'foo' %}" + - name: reports offenses on nested render tags + files: + file.liquid: "{% if true %}{% render 'foo', x: bar | upcase %}{% endif %}" + offenses: + - file: file.liquid + message: "Filters cannot be used on arguments passed to the 'render' tag. Apply the filter beforehand (e.g. with {% assign %}) and pass the result instead." + start: [0, 37] + highlight: "| upcase" diff --git a/specs/checks/unused-assign.yml b/specs/checks/unused-assign.yml new file mode 100644 index 000000000..1e5b95d23 --- /dev/null +++ b/specs/checks/unused-assign.yml @@ -0,0 +1,229 @@ +check: UnusedAssign +severity: warning +cases: + - name: reports a variable that is assigned but not used + files: + file.liquid: | + {% assign unusedVar = "value" %} + {% assign usedVar = "anotherValue" %} + {{ usedVar }} + offenses: + - file: file.liquid + message: The variable 'unusedVar' is assigned but not used + start: [0, 0] + highlight: '{% assign unusedVar = "value" %}' + suggest: + - message: Remove the unused variable 'unusedVar' + result: | + + {% assign usedVar = "anotherValue" %} + {{ usedVar }} + + - name: does not report a variable that is assigned and used + files: + file.liquid: | + {% assign usedVar = "value" %} + {{ usedVar }} + + - name: suggests removing the only assign in a file + files: + file.liquid: | + {% assign unusedVar = "value" %} + offenses: + - file: file.liquid + message: The variable 'unusedVar' is assigned but not used + start: [0, 0] + highlight: '{% assign unusedVar = "value" %}' + suggest: + - message: Remove the unused variable 'unusedVar' + result: "\n" + + - name: counts uses inside a capture tag + files: + file.liquid: | + {% assign usedVar = "anotherValue" %} + {% capture foo %} + {{ usedVar }} + {% endcapture %} + {{ foo }} + + - name: reports an unused capture + files: + file.liquid: | + {% assign usedVar = "anotherValue" %} + {% capture foo %} + {{ usedVar }} + {% endcapture %} + offenses: + - file: file.liquid + message: The variable 'foo' is assigned but not used + start: [1, 0] + highlight: "{% capture foo %}" + suggest: + - message: Remove the unused variable 'foo' + result: "{% assign usedVar = \"anotherValue\" %}\n\n" + + - name: counts uses inside a style tag + files: + file.liquid: | + {% assign usedVar = 1 %} + {% style %} + {{ usedVar }} + {% endstyle %} + + - name: does not count uses inside a schema tag + files: + file.liquid: | + {% assign usedVar = 1 %} + {% schema %} + {{ usedVar }} + {% endschema %} + offenses: + - file: file.liquid + message: The variable 'usedVar' is assigned but not used + start: [0, 0] + highlight: "{% assign usedVar = 1 %}" + suggest: + - message: Remove the unused variable 'usedVar' + result: | + + {% schema %} + {{ usedVar }} + {% endschema %} + + - name: does not count uses inside a javascript tag + files: + file.liquid: | + {% assign usedVar = 1 %} + {% javascript %} + {{ usedVar }} + {% endjavascript %} + offenses: + - file: file.liquid + message: The variable 'usedVar' is assigned but not used + start: [0, 0] + highlight: "{% assign usedVar = 1 %}" + suggest: + - message: Remove the unused variable 'usedVar' + result: | + + {% javascript %} + {{ usedVar }} + {% endjavascript %} + + - name: does not count uses inside a stylesheet tag + files: + file.liquid: | + {% assign usedVar = 1 %} + {% stylesheet %} + {{ usedVar }} + {% endstylesheet %} + offenses: + - file: file.liquid + message: The variable 'usedVar' is assigned but not used + start: [0, 0] + highlight: "{% assign usedVar = 1 %}" + suggest: + - message: Remove the unused variable 'usedVar' + result: | + + {% stylesheet %} + {{ usedVar }} + {% endstylesheet %} + + - name: counts uses in a schema visible_if expression + files: + blocks/logo.liquid: | + {% assign has_logo_image = true %} + {% schema %} + { + "name": "Logo", + "settings": [ + { + "type": "range", + "id": "height", + "min": 16, + "max": 80, + "step": 4, + "default": 32, + "visible_if": "{{ has_logo_image == true }}" + } + ] + } + {% endschema %} + + - name: does not count other schema strings as uses + files: + blocks/example.liquid: | + {% assign unused_var = true %} + {% schema %} + { + "name": "unused_var", + "settings": [] + } + {% endschema %} + offenses: + - file: blocks/example.liquid + message: The variable 'unused_var' is assigned but not used + start: [0, 0] + highlight: "{% assign unused_var = true %}" + suggest: + - message: Remove the unused variable 'unused_var' + result: | + + {% schema %} + { + "name": "unused_var", + "settings": [] + } + {% endschema %} + + - name: counts uses inside an HTML style element + files: + file.liquid: | + {% assign usedVar = 1 %} + + + - name: counts uses inside an HTML script element + files: + file.liquid: | + {% assign usedVar = 1 %} + + + - name: ignores variables that start with an underscore + files: + file.liquid: | + {% assign _ = 1 %} + {% assign _unusedVar = 1 %} + {% capture _ %} + {% form "cart", cart %} + {% assign some_var = cart.successfully_posted? %} + {% endform %} + {% endcapture %} + {{ some_var }} + + - name: does not count uses inside a raw tag + files: + file.liquid: | + {% assign unusedVar = 1 %} + {% # It's a trap. It's not really used %} + {% raw %} + {{ unusedVar }} + {% endraw %} + offenses: + - file: file.liquid + message: The variable 'unusedVar' is assigned but not used + start: [0, 0] + highlight: "{% assign unusedVar = 1 %}" + suggest: + - message: Remove the unused variable 'unusedVar' + result: | + + {% # It's a trap. It's not really used %} + {% raw %} + {{ unusedVar }} + {% endraw %} diff --git a/specs/checks/unused-doc-param.yml b/specs/checks/unused-doc-param.yml new file mode 100644 index 000000000..8293df46f --- /dev/null +++ b/specs/checks/unused-doc-param.yml @@ -0,0 +1,119 @@ +check: UnusedDocParam +severity: warning +cases: + - name: should not report a warning when a variable is defined and used + files: + file.liquid: | + {% doc %} + @param param1 - Example param + {% enddoc %} + + {{ param1 }} + - name: should report a warning with suggestions when a variable is defined but not used + files: + file.liquid: | + {% doc %} + @param param1 - Example param + @param param2 - Example param + {% enddoc %} + + {{ param1 }} + offenses: + - file: file.liquid + message: "The parameter 'param2' is defined but not used in this file." + start: [2, 2] + highlight: "@param param2 - Example param" + suggest: + - message: "Remove unused parameter 'param2'" + result: | + {% doc %} + @param param1 - Example param + + {% enddoc %} + + {{ param1 }} + - name: should apply suggestion when a variable is defined but not used + files: + file.liquid: | + {% doc %} + @param param1 - Example param + @param param2 - Example param + {% enddoc %} + + {{ param1 }} + offenses: + - file: file.liquid + message: "The parameter 'param2' is defined but not used in this file." + start: [2, 2] + highlight: "@param param2 - Example param" + suggest: + - message: "Remove unused parameter 'param2'" + result: | + {% doc %} + @param param1 - Example param + + {% enddoc %} + + {{ param1 }} + - name: "should report a warning when a variable is defined but not used outside 'for' loop context" + files: + file.liquid: | + {% doc %} + @param param1 - Example param + @param param2 - Example param + {% enddoc %} + + {{ param1 }} + + {% for param2 in array %} + {{ param2 }} + {% endfor %} + offenses: + - file: file.liquid + message: "The parameter 'param2' is defined but not used in this file." + start: [2, 2] + highlight: "@param param2 - Example param" + suggest: + - message: "Remove unused parameter 'param2'" + result: | + {% doc %} + @param param1 - Example param + + {% enddoc %} + + {{ param1 }} + + {% for param2 in array %} + {{ param2 }} + {% endfor %} + - name: "should report a warning when a variable is defined but not used outside 'tablerow' loop context" + files: + file.liquid: | + {% doc %} + @param param1 - Example param + @param param2 - Example param + {% enddoc %} + + {{ param1 }} + + {% tablerow param2 in array %} + {{ param2 }} + {% endtablerow %} + offenses: + - file: file.liquid + message: "The parameter 'param2' is defined but not used in this file." + start: [2, 2] + highlight: "@param param2 - Example param" + suggest: + - message: "Remove unused parameter 'param2'" + result: | + {% doc %} + @param param1 - Example param + + {% enddoc %} + + {{ param1 }} + + {% tablerow param2 in array %} + {{ param2 }} + {% endtablerow %} diff --git a/specs/checks/valid-block-argument-types.yml b/specs/checks/valid-block-argument-types.yml new file mode 100644 index 000000000..bdae86200 --- /dev/null +++ b/specs/checks/valid-block-argument-types.yml @@ -0,0 +1,302 @@ +check: ValidBlockArgumentTypes +severity: warning +cases: + - name: checks literals against schema-derived primitive types + files: + blocks/card.liquid: |- + {% doc %} + @param {number} developer_count - Developer-only count + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"},{"id":"count","type":"number"},{"id":"product","type":"product"},{"id":"products","type":"product_list"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', heading: 42, count: 'many' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'heading': expected string, got number" + start: [0, 26] + highlight: "42" + - file: templates/test.liquid + message: "Type mismatch for argument 'count': expected number, got string" + start: [0, 37] + highlight: "'many'" + - name: accepts literals that match schema-derived types + files: + blocks/card.liquid: |- + {% doc %} + @param {number} developer_count - Developer-only count + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"},{"id":"count","type":"number"},{"id":"product","type":"product"},{"id":"products","type":"product_list"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', heading: 'Title', count: 2, product: product, products: products %}{% endblock %}" + - name: checks literals against LiquidDoc-only developer parameter types + files: + blocks/card.liquid: |- + {% doc %} + @param {number} developer_count - Developer-only count + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"},{"id":"count","type":"number"},{"id":"product","type":"product"},{"id":"products","type":"product_list"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', developer_count: 'many' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'developer_count': expected number, got string" + start: [0, 34] + highlight: "'many'" + - name: checks LiquidDoc-only parameter types from a schema-less block + files: + blocks/card.liquid: |- + {% doc %} + @param {number} developer_count - Developer-only count + {% enddoc %} + templates/test.liquid: "{% block 'card', developer_count: 'many' %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'developer_count': expected number, got string" + start: [0, 34] + highlight: "'many'" + - name: checks resource and list settings exactly + files: + blocks/card.liquid: |- + {% doc %} + @param {number} developer_count - Developer-only count + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"},{"id":"count","type":"number"},{"id":"product","type":"product"},{"id":"products","type":"product_list"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', product: 'handle', products: ['one', 'two'] %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'product': expected product, got string" + start: [0, 26] + highlight: "'handle'" + - file: templates/test.liquid + message: "Type mismatch for argument 'products': expected product[], got string[]" + start: [0, 46] + highlight: "['one', 'two']" + - name: requires arrays to match the element type exactly + files: + blocks/card.liquid: |- + {% doc %} + @param {number} developer_count - Developer-only count + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"},{"id":"count","type":"number"},{"id":"product","type":"product"},{"id":"products","type":"product_list"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', products: ['one', 2] %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'products': expected product[], got mixed[]" + start: [0, 27] + highlight: "['one', 2]" + - name: does not speculate about variable lookups or empty and partly unknown arrays + files: + blocks/card.liquid: |- + {% doc %} + @param {number} developer_count - Developer-only count + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"},{"id":"count","type":"number"},{"id":"product","type":"product"},{"id":"products","type":"product_list"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', product: selected_product, products: [], heading: [selected, 'x'] %}{% endblock %}" + - name: accepts any literal for an object parameter + files: + blocks/card.liquid: |- + {% doc %} + @param {object} a + @param {object} b + @param {object} c + @param {object} d + {% enddoc %} + templates/test.liquid: "{% block 'card', a: 'x', b: 1, c: (1..2), d: ['x'] %}{% endblock %}" + - name: accepts any scalar for a boolean parameter but not a range or array + files: + blocks/card.liquid: |- + {% doc %} + @param {boolean} a + @param {boolean} b + @param {boolean} c + @param {boolean} d + @param {boolean} e + {% enddoc %} + templates/test.liquid: "{% block 'card', a: 'yes', b: 1, c: nil, d: (1..2), e: [true] %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'd': expected boolean, got object" + start: [0, 44] + highlight: "(1..2)" + - file: templates/test.liquid + message: "Type mismatch for argument 'e': expected boolean, got boolean[]" + start: [0, 55] + highlight: "[true]" + - name: checks explicit content as a string without LiquidDoc + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[]} + {% endschema %} + templates/test.liquid: "{% block 'card', content: 42 %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'content': expected string, got number" + start: [0, 26] + highlight: "42" + - name: checks explicit content as a string when the schema content setting is not + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"number"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', content: 42 %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'content': expected string, got number" + start: [0, 26] + highlight: "42" + - name: does not report type mismatches for an unreadable missing target + files: + templates/test.liquid: "{% block 'card', heading: 42 %}{% endblock %}" + - name: does not report type mismatches for an unreadable invalid target schema + files: + blocks/card.liquid: |- + {% schema %} + { invalid + {% endschema %} + templates/test.liquid: "{% block 'card', heading: 42 %}{% endblock %}" + - name: accepts exact and less-specific compatible LiquidDoc types + files: + blocks/card.liquid: |- + {% doc %} + @param {object} [product] - Product + @param {product[]} [products] - Products + {% enddoc %} + {% schema %} + {"settings":[{"id":"product","type":"product"},{"id":"products","type":"product_list"}]} + {% endschema %} + - name: compares resource and list LiquidDoc types exactly + files: + blocks/card.liquid: |- + {% doc %} + @param {collection} [product] - Product + @param {product} [products] - Products + @param {product[]} [collections] - Collections + {% enddoc %} + {% schema %} + {"settings":[{"id":"product","type":"product"},{"id":"products","type":"product_list"},{"id":"collections","type":"collection_list"}]} + {% endschema %} + offenses: + - file: blocks/card.liquid + message: "The schema setting 'product' has Liquid type 'product', but LiquidDoc declares 'collection'. The schema setting type is authoritative." + start: [1, 9] + highlight: "{collection}" + - file: blocks/card.liquid + message: "The schema setting 'products' has Liquid type 'product[]', but LiquidDoc declares 'product'. The schema setting type is authoritative." + start: [2, 9] + highlight: "{product}" + - file: blocks/card.liquid + message: "The schema setting 'collections' has Liquid type 'collection[]', but LiquidDoc declares 'product[]'. The schema setting type is authoritative." + start: [3, 9] + highlight: "{product[]}" + - name: points incompatible type declarations at the LiquidDoc type + files: + blocks/card.liquid: |- + {% doc %} + @param {number} [heading] - Heading + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + offenses: + - file: blocks/card.liquid + message: "The schema setting 'heading' has Liquid type 'string', but LiquidDoc declares 'number'. The schema setting type is authoritative." + start: [1, 9] + highlight: "{number}" + - name: accepts an optional LiquidDoc declaration of a schema setting + files: + blocks/card.liquid: |- + {% doc %} + @param {string} [heading] - Heading + {% enddoc %} + {% schema %} + {"settings":[{"id":"heading","type":"text"}]} + {% endschema %} + - name: keeps the schema type of a setting that LiquidDoc declares as a string enum (1) + files: + blocks/card.liquid: |- + {% doc %} + @param {'heading' | 'small'} [variant] - Variant + {% enddoc %} + {% schema %} + {"settings":[{"id":"variant","type":"text"}]} + {% endschema %} + - name: keeps the schema type of a setting that LiquidDoc declares as a string enum (2) + files: + blocks/card.liquid: |- + {% doc %} + @param {'heading' | 'small'} [variant] - Variant + {% enddoc %} + {% schema %} + {"settings":[{"id":"variant","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', variant: 'body' %}{% endblock %}" + - name: keeps the schema type of a setting that LiquidDoc declares as a string enum (3) + files: + blocks/card.liquid: |- + {% doc %} + @param {'heading' | 'small'} [variant] - Variant + {% enddoc %} + {% schema %} + {"settings":[{"id":"variant","type":"text"}]} + {% endschema %} + templates/test.liquid: "{% block 'card', variant: 42 %}{% endblock %}" + offenses: + - file: templates/test.liquid + message: "Type mismatch for argument 'variant': expected string, got number" + start: [0, 26] + highlight: "42" + - name: does not speculate about omitted LiquidDoc or unmapped schema types + files: + blocks/card.liquid: |- + {% doc %} + @param [item] - Item + {% enddoc %} + {% schema %} + {"settings":[{"id":"item","type":"metaobject"}]} + {% endschema %} + - name: requires LiquidDoc content types to remain string-compatible + files: + blocks/card.liquid: |- + {% doc %} + @param {number} [content] - Body + {% enddoc %} + {% schema %} + {"settings":[]} + {% endschema %} + offenses: + - file: blocks/card.liquid + message: "The built-in parameter 'content' has Liquid type 'string', but LiquidDoc declares 'number'. The built-in parameter type is authoritative." + start: [1, 9] + highlight: "{number}" + - name: leaves a non-string schema content setting without LiquidDoc to ValidBlockContentSettingType + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"number"}]} + {% endschema %} + - name: leaves a non-string schema content setting with a LiquidDoc string echo to ValidBlockContentSettingType + files: + blocks/card.liquid: |- + {% doc %} + @param {string} [content] - Body + {% enddoc %} + {% schema %} + {"settings":[{"id":"content","type":"number"}]} + {% endschema %} + - name: accepts a string-compatible schema content setting + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"text"}]} + {% endschema %} diff --git a/specs/checks/valid-block-content-setting-type.yml b/specs/checks/valid-block-content-setting-type.yml new file mode 100644 index 000000000..ff109a62b --- /dev/null +++ b/specs/checks/valid-block-content-setting-type.yml @@ -0,0 +1,132 @@ +check: ValidBlockContentSettingType +severity: error +cases: + - name: reports a number schema setting named content on its type + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"number"}]} + {% endschema %} + offenses: + - file: blocks/card.liquid + message: "Schema setting 'content' has Liquid type 'number', but the built-in 'content' parameter has type 'string'." + start: [1, 36] + highlight: '"number"' + - name: reports a checkbox schema setting named content on its type + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"checkbox"}]} + {% endschema %} + offenses: + - file: blocks/card.liquid + message: "Schema setting 'content' has Liquid type 'boolean', but the built-in 'content' parameter has type 'string'." + start: [1, 36] + highlight: '"checkbox"' + - name: reports a product schema setting named content on its type + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"product"}]} + {% endschema %} + offenses: + - file: blocks/card.liquid + message: "Schema setting 'content' has Liquid type 'product', but the built-in 'content' parameter has type 'string'." + start: [1, 36] + highlight: '"product"' + - name: reports a product_list schema setting named content on its type + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"product_list"}]} + {% endschema %} + offenses: + - file: blocks/card.liquid + message: "Schema setting 'content' has Liquid type 'product[]', but the built-in 'content' parameter has type 'string'." + start: [1, 36] + highlight: '"product_list"' + - name: accepts a string-compatible text schema setting named content + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"text"}]} + {% endschema %} + - name: accepts a string-compatible textarea schema setting named content + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"textarea"}]} + {% endschema %} + - name: accepts a string-compatible richtext schema setting named content + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"richtext"}]} + {% endschema %} + - name: accepts a string-compatible inline_richtext schema setting named content + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"inline_richtext"}]} + {% endschema %} + - name: accepts a string-compatible html schema setting named content + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"html"}]} + {% endschema %} + - name: does not speculate about an unmapped schema setting type named content + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"metaobject"}]} + {% endschema %} + - name: accepts non-string schema settings with other IDs + files: + blocks/card.liquid: |- + {% schema %} + {"settings":[{"id":"count","type":"number"}]} + {% endschema %} + - name: does not report a non-string content setting outside theme block files + files: + sections/main.liquid: |- + {% schema %} + {"settings":[{"id":"content","type":"number"}]} + {% endschema %} + - name: reports the schema mismatch once alongside ValidBlockArgumentTypes + checks: [ValidBlockArgumentTypes, ValidBlockContentSettingType] + files: + blocks/card.liquid: |- + {% doc %} + @param {string} [content] - Body + {% enddoc %} + {% schema %} + {"settings":[{"id":"content","type":"number"}]} + {% endschema %} + offenses: + - check: ValidBlockContentSettingType + file: blocks/card.liquid + message: "Schema setting 'content' has Liquid type 'number', but the built-in 'content' parameter has type 'string'." + start: [4, 36] + highlight: '"number"' + - name: names the built-in parameter as the string authority when schema and LiquidDoc agree on another type + checks: [ValidBlockArgumentTypes, ValidBlockContentSettingType] + files: + blocks/card.liquid: |- + {% doc %} + @param {number} [content] - Body + {% enddoc %} + {% schema %} + {"settings":[{"id":"content","type":"number"}]} + {% endschema %} + offenses: + - check: ValidBlockContentSettingType + file: blocks/card.liquid + message: "Schema setting 'content' has Liquid type 'number', but the built-in 'content' parameter has type 'string'." + start: [4, 36] + highlight: '"number"' + - check: ValidBlockArgumentTypes + file: blocks/card.liquid + message: "The built-in parameter 'content' has Liquid type 'string', but LiquidDoc declares 'number'. The built-in parameter type is authoritative." + start: [1, 9] + highlight: "{number}" diff --git a/specs/checks/valid-block-tag-placement.yml b/specs/checks/valid-block-tag-placement.yml new file mode 100644 index 000000000..d8bfc8b75 --- /dev/null +++ b/specs/checks/valid-block-tag-placement.yml @@ -0,0 +1,620 @@ +check: ValidBlockTagPlacement +severity: error +cases: + - name: "allows 'a direct block tag' in 'templates/index.liquid'" + files: + templates/index.liquid: "{% block 'card' %}Card{% endblock %}" + - name: "allows 'a block tag under control flow' in 'templates/index.liquid'" + files: + templates/index.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + - name: "allows 'nested block tags' in 'templates/index.liquid'" + files: + templates/index.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - name: "allows 'a block tag nested through control fl…' in 'templates/index.liquid'" + files: + templates/index.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - name: "allows 'nested block tags inside a liquid tag' in 'templates/index.liquid'" + files: + templates/index.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + - name: "allows 'a direct block tag' in 'templates/metaobject/product.liquid'" + files: + templates/metaobject/product.liquid: "{% block 'card' %}Card{% endblock %}" + - name: "allows 'a block tag under control flow' in 'templates/metaobject/product.liquid'" + files: + templates/metaobject/product.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + - name: "allows 'nested block tags' in 'templates/metaobject/product.liquid'" + files: + templates/metaobject/product.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - name: "allows 'a block tag nested through control fl…' in 'templates/metaobject/product.liquid'" + files: + templates/metaobject/product.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - name: "allows 'nested block tags inside a liquid tag' in 'templates/metaobject/product.liquid'" + files: + templates/metaobject/product.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + - name: "allows 'a direct block tag' in 'templates/metaobject/custom/product.l…'" + files: + templates/metaobject/custom/product.liquid: "{% block 'card' %}Card{% endblock %}" + - name: "allows 'a block tag under control flow' in 'templates/metaobject/custom/product.l…'" + files: + templates/metaobject/custom/product.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + - name: "allows 'nested block tags' in 'templates/metaobject/custom/product.l…'" + files: + templates/metaobject/custom/product.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - name: "allows 'a block tag nested through control fl…' in 'templates/metaobject/custom/product.l…'" + files: + templates/metaobject/custom/product.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - name: "allows 'nested block tags inside a liquid tag' in 'templates/metaobject/custom/product.l…'" + files: + templates/metaobject/custom/product.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + - name: "allows 'a direct block tag' in 'layout/theme.liquid'" + files: + layout/theme.liquid: "{% block 'card' %}Card{% endblock %}" + - name: "allows 'a block tag under control flow' in 'layout/theme.liquid'" + files: + layout/theme.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + - name: "allows 'nested block tags' in 'layout/theme.liquid'" + files: + layout/theme.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - name: "allows 'a block tag nested through control fl…' in 'layout/theme.liquid'" + files: + layout/theme.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - name: "allows 'nested block tags inside a liquid tag' in 'layout/theme.liquid'" + files: + layout/theme.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + - name: "allows 'a direct block tag' in 'templates\\index.liquid'" + files: + "templates\\index.liquid": "{% block 'card' %}Card{% endblock %}" + - name: "allows 'a block tag under control flow' in 'templates\\index.liquid'" + files: + "templates\\index.liquid": "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + - name: "allows 'nested block tags' in 'templates\\index.liquid'" + files: + "templates\\index.liquid": "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - name: "allows 'a block tag nested through control fl…' in 'templates\\index.liquid'" + files: + "templates\\index.liquid": "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - name: "allows 'nested block tags inside a liquid tag' in 'templates\\index.liquid'" + files: + "templates\\index.liquid": |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + - name: "allows 'a direct block tag' in 'templates\\metaobject\\product.liquid'" + files: + "templates\\metaobject\\product.liquid": "{% block 'card' %}Card{% endblock %}" + - name: "allows 'a block tag under control flow' in 'templates\\metaobject\\product.liquid'" + files: + "templates\\metaobject\\product.liquid": "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + - name: "allows 'nested block tags' in 'templates\\metaobject\\product.liquid'" + files: + "templates\\metaobject\\product.liquid": "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - name: "allows 'a block tag nested through control fl…' in 'templates\\metaobject\\product.liquid'" + files: + "templates\\metaobject\\product.liquid": "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - name: "allows 'nested block tags inside a liquid tag' in 'templates\\metaobject\\product.liquid'" + files: + "templates\\metaobject\\product.liquid": |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + - name: "allows 'a direct block tag' in 'layout\\theme.liquid'" + files: + "layout\\theme.liquid": "{% block 'card' %}Card{% endblock %}" + - name: "allows 'a block tag under control flow' in 'layout\\theme.liquid'" + files: + "layout\\theme.liquid": "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + - name: "allows 'nested block tags' in 'layout\\theme.liquid'" + files: + "layout\\theme.liquid": "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - name: "allows 'a block tag nested through control fl…' in 'layout\\theme.liquid'" + files: + "layout\\theme.liquid": "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - name: "allows 'nested block tags inside a liquid tag' in 'layout\\theme.liquid'" + files: + "layout\\theme.liquid": |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + - name: "reports 'a direct block tag' in 'blocks/card.liquid'" + files: + blocks/card.liquid: "{% block 'card' %}Card{% endblock %}" + offenses: + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'a block tag under control flow' in 'blocks/card.liquid'" + files: + blocks/card.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + offenses: + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 26] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'nested block tags' in 'blocks/card.liquid'" + files: + blocks/card.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + offenses: + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 25] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'a block tag nested through control fl…' in 'blocks/card.liquid'" + files: + blocks/card.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + offenses: + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 45] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'nested block tags inside a liquid tag' in 'blocks/card.liquid'" + files: + blocks/card.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + offenses: + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [1, 2] + highlight: |- + block 'outer' + block 'inner' + endblock + endblock + - file: blocks/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [2, 4] + highlight: |- + block 'inner' + endblock + - name: "reports 'a direct block tag' in 'sections/main.liquid'" + files: + sections/main.liquid: "{% block 'card' %}Card{% endblock %}" + offenses: + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'a block tag under control flow' in 'sections/main.liquid'" + files: + sections/main.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + offenses: + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 26] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'nested block tags' in 'sections/main.liquid'" + files: + sections/main.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + offenses: + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 25] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'a block tag nested through control fl…' in 'sections/main.liquid'" + files: + sections/main.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + offenses: + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 45] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'nested block tags inside a liquid tag' in 'sections/main.liquid'" + files: + sections/main.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + offenses: + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [1, 2] + highlight: |- + block 'outer' + block 'inner' + endblock + endblock + - file: sections/main.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [2, 4] + highlight: |- + block 'inner' + endblock + - name: "reports 'a direct block tag' in 'snippets/card.liquid'" + files: + snippets/card.liquid: "{% block 'card' %}Card{% endblock %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'a block tag under control flow' in 'snippets/card.liquid'" + files: + snippets/card.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 26] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'nested block tags' in 'snippets/card.liquid'" + files: + snippets/card.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 25] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'a block tag nested through control fl…' in 'snippets/card.liquid'" + files: + snippets/card.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 45] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'nested block tags inside a liquid tag' in 'snippets/card.liquid'" + files: + snippets/card.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [1, 2] + highlight: |- + block 'outer' + block 'inner' + endblock + endblock + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [2, 4] + highlight: |- + block 'inner' + endblock + - name: "reports 'a direct block tag' in 'assets/card.liquid'" + files: + assets/card.liquid: "{% block 'card' %}Card{% endblock %}" + offenses: + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'a block tag under control flow' in 'assets/card.liquid'" + files: + assets/card.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + offenses: + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 26] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'nested block tags' in 'assets/card.liquid'" + files: + assets/card.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + offenses: + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 25] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'a block tag nested through control fl…' in 'assets/card.liquid'" + files: + assets/card.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + offenses: + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 45] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'nested block tags inside a liquid tag' in 'assets/card.liquid'" + files: + assets/card.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + offenses: + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [1, 2] + highlight: |- + block 'outer' + block 'inner' + endblock + endblock + - file: assets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [2, 4] + highlight: |- + block 'inner' + endblock + - name: "reports 'a direct block tag' in 'theme.liquid'" + files: + theme.liquid: "{% block 'card' %}Card{% endblock %}" + offenses: + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'a block tag under control flow' in 'theme.liquid'" + files: + theme.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + offenses: + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 26] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'nested block tags' in 'theme.liquid'" + files: + theme.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + offenses: + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 25] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'a block tag nested through control fl…' in 'theme.liquid'" + files: + theme.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + offenses: + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 45] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'nested block tags inside a liquid tag' in 'theme.liquid'" + files: + theme.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + offenses: + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [1, 2] + highlight: |- + block 'outer' + block 'inner' + endblock + endblock + - file: theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [2, 4] + highlight: |- + block 'inner' + endblock + - name: "reports 'a direct block tag' in 'layout/custom/theme.liquid'" + files: + layout/custom/theme.liquid: "{% block 'card' %}Card{% endblock %}" + offenses: + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'a block tag under control flow' in 'layout/custom/theme.liquid'" + files: + layout/custom/theme.liquid: "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + offenses: + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 26] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'nested block tags' in 'layout/custom/theme.liquid'" + files: + layout/custom/theme.liquid: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + offenses: + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 25] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'a block tag nested through control fl…' in 'layout/custom/theme.liquid'" + files: + layout/custom/theme.liquid: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + offenses: + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 45] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'nested block tags inside a liquid tag' in 'layout/custom/theme.liquid'" + files: + layout/custom/theme.liquid: |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + offenses: + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [1, 2] + highlight: |- + block 'outer' + block 'inner' + endblock + endblock + - file: layout/custom/theme.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [2, 4] + highlight: |- + block 'inner' + endblock + - name: "reports 'a direct block tag' in 'snippets\\card.liquid'" + files: + "snippets\\card.liquid": "{% block 'card' %}Card{% endblock %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'a block tag under control flow' in 'snippets\\card.liquid'" + files: + "snippets\\card.liquid": "{% if product.available %}{% block 'card' %}Card{% endblock %}{% endif %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 26] + highlight: "{% block 'card' %}Card{% endblock %}" + - name: "reports 'nested block tags' in 'snippets\\card.liquid'" + files: + "snippets\\card.liquid": "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}Before{% block 'inner' %}Inner{% endblock %}After{% endblock %}" + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 25] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'a block tag nested through control fl…' in 'snippets\\card.liquid'" + files: + "snippets\\card.liquid": "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 0] + highlight: "{% block 'outer' %}{% if product.available %}{% block 'inner' %}Inner{% endblock %}{% endif %}{% endblock %}" + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [0, 45] + highlight: "{% block 'inner' %}Inner{% endblock %}" + - name: "reports 'nested block tags inside a liquid tag' in 'snippets\\card.liquid'" + files: + "snippets\\card.liquid": |- + {% liquid + block 'outer' + block 'inner' + endblock + endblock + %} + offenses: + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [1, 2] + highlight: |- + block 'outer' + block 'inner' + endblock + endblock + - file: snippets/card.liquid + message: "The 'block' tag can only be used in templates/**/*.liquid and layout/*.liquid." + start: [2, 4] + highlight: |- + block 'inner' + endblock + - name: leaves malformed block tags with unstructured markup to LiquidSyntaxError + checks: [LiquidSyntaxError, ValidBlockTagPlacement] + files: + snippets/card.liquid: "{% block card %}{% endblock %}" + docset: default + offenses: + - check: LiquidSyntaxError + file: snippets/card.liquid + message: "Syntax error in 'block' tag" + start: [0, 0] + highlight: "{% block card %}{% endblock %}" + - name: leaves malformed block tags with structured markup to LiquidSyntaxError + checks: [LiquidSyntaxError, ValidBlockTagPlacement] + files: + snippets/card.liquid: "{% block 'card', block.bad: true %}Card{% endblock %}" + docset: default + offenses: + - check: LiquidSyntaxError + file: snippets/card.liquid + message: "Liquid syntax error: in 'block' - Use plain named arguments, for example: block 'name', heading: value" + start: [0, 17] + highlight: "block.bad: true" diff --git a/specs/checks/valid-block-target.yml b/specs/checks/valid-block-target.yml new file mode 100644 index 000000000..42263efcd --- /dev/null +++ b/specs/checks/valid-block-target.yml @@ -0,0 +1,2508 @@ +check: ValidBlockTarget +severity: error +cases: + - name: "should ignore @app and @theme block types in sections bucket" + files: + sections/app-theme.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@app" + }, + { + "type": "@theme" + } + ] + } + {% endschema %} + - name: should not report any errors when all block files exist (sections bucket) + files: + blocks/text.liquid: "" + sections/valid.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + - name: should report errors when root level block file does not exist (sections bucket) + files: + sections/invalid-section.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "invalid" + } + ] + } + {% endschema %} + offenses: + - file: sections/invalid-section.liquid + message: "Theme block 'blocks/invalid.liquid' does not exist." + start: [5, 14] + highlight: '"invalid"' + - name: should report an error when a preset defined block file does not exist (sections bucket) + files: + sections/invalid-block.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "missing_block" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/invalid-block.liquid + message: "Theme block 'blocks/missing_block.liquid' does not exist." + start: [13, 18] + highlight: '"missing_block"' + - name: should report an error when default defined block file does not exist + files: + sections/invalid-file.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "default": { + "title": "Default", + "blocks": [ + { + "name": "some block", + "type": "missing_block" + } + ] + } + } + {% endschema %} + offenses: + - file: sections/invalid-file.liquid + message: "Theme block 'blocks/missing_block.liquid' does not exist." + start: [13, 16] + highlight: '"missing_block"' + - name: should not report subsequent errors in present defined blocks if error in root level (sections bucket) + files: + sections/invalid-block.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "invalid" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "text" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/invalid-block.liquid + message: "Theme block 'blocks/invalid.liquid' does not exist." + start: [5, 14] + highlight: '"invalid"' + - name: should report an error when a preset defined block file does not exist with hash-style presets (sections bucket) + files: + sections/invalid-block.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "missing_block_hash": { + "type": "missing_block" + } + }, + "block_order": ["missing_block_hash"] + } + ] + } + {% endschema %} + offenses: + - file: sections/invalid-block.liquid + message: "Theme block 'blocks/missing_block.liquid' does not exist." + start: [13, 18] + highlight: '"missing_block"' + - name: should not report an error when a preset defined block file exists with hash-style presets (sections bucket) + files: + blocks/text.liquid: "" + sections/invalid-block.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "text_block": { + "type": "text" + } + }, + "block_order": ["text_block"] + } + ] + } + {% endschema %} + - name: "should ignore @app and @theme block types in blocks bucket" + files: + blocks/app-theme.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@app" + }, + { + "type": "@theme" + } + ] + } + {% endschema %} + - name: should not report any errors when all block files exist (blocks bucket) + files: + blocks/text.liquid: "" + blocks/valid.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + - name: should report errors when root level block file does not exist (blocks bucket) + files: + blocks/invalid-section.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "invalid" + } + ] + } + {% endschema %} + offenses: + - file: blocks/invalid-section.liquid + message: "Theme block 'blocks/invalid.liquid' does not exist." + start: [5, 14] + highlight: '"invalid"' + - name: should report an error when a preset defined block file does not exist (blocks bucket) + files: + blocks/invalid-block.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "missing_block" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/invalid-block.liquid + message: "Theme block 'blocks/missing_block.liquid' does not exist." + start: [13, 18] + highlight: '"missing_block"' + - name: should not report subsequent errors in present defined blocks if error in root level (blocks bucket) + files: + blocks/invalid-block.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "invalid" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "text" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/invalid-block.liquid + message: "Theme block 'blocks/invalid.liquid' does not exist." + start: [5, 14] + highlight: '"invalid"' + - name: should report an error when a preset defined block file does not exist with hash-style presets (blocks bucket) + files: + blocks/invalid-block.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "missing_block_hash": { + "type": "missing_block" + } + }, + "block_order": ["missing_block_hash"] + } + ] + } + {% endschema %} + offenses: + - file: blocks/invalid-block.liquid + message: "Theme block 'blocks/missing_block.liquid' does not exist." + start: [13, 18] + highlight: '"missing_block"' + - name: should not report an error when a preset defined block file exists with hash-style presets (blocks bucket) + files: + blocks/text.liquid: "" + blocks/invalid-block.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "text_block": { + "type": "text" + } + }, + "block_order": ["text_block"] + } + ] + } + {% endschema %} + - name: should not report errors for locally scoped blocks at root level + files: + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "local_block", + "name": "Local block" + } + ] + } + {% endschema %} + - name: should report an error when a preset defined block is not specified at root level (sections bucket) + files: + blocks/text.liquid: "" + sections/invalid-preset.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "text" + } + ], + "presets": [ + { + "name": "Default preset", + "blocks": [ + { + "type": "invalid_preset_block" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/invalid-preset.liquid + message: 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"invalid_preset_block"' + - file: sections/invalid-preset.liquid + message: "Theme block 'blocks/invalid_preset_block.liquid' does not exist." + start: [13, 18] + highlight: '"invalid_preset_block"' + - name: should report an error when a default defined block is not specified at root level + files: + blocks/text.liquid: "" + sections/invalid-preset.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "text" + } + ], + "default": { + "name": "Default", + "blocks": [ + { + "name": "invalid_preset_block", + "type": "invalid_preset_block" + } + ] + } + } + {% endschema %} + offenses: + - file: sections/invalid-preset.liquid + message: 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.' + start: [13, 16] + highlight: '"invalid_preset_block"' + - file: sections/invalid-preset.liquid + message: "Theme block 'blocks/invalid_preset_block.liquid' does not exist." + start: [13, 16] + highlight: '"invalid_preset_block"' + - name: "should report errors in presets for private blocks even when @theme is specified at root level (sections bucket)" + files: + blocks/_private.liquid: "" + sections/private-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/private-blocks.liquid + message: 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"_private"' + - name: "should report errors in default for private blocks even when @theme is specified at root level" + files: + blocks/_private.liquid: "" + sections/private-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "default": { + "name": "Default", + "blocks": [ + { + "name": "private", + "type": "_private" + } + ] + } + } + {% endschema %} + offenses: + - file: sections/private-blocks.liquid + message: 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [13, 16] + highlight: '"_private"' + - name: should report errors with correct indices (sections bucket) + files: + sections/private-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/private-blocks.liquid + message: 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"_private"' + - file: sections/private-blocks.liquid + message: "Theme block 'blocks/_private.liquid' does not exist." + start: [13, 18] + highlight: '"_private"' + - name: should not report errors for private blocks when listed at root level (sections bucket) + files: + blocks/_private.liquid: "" + sections/private-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "_private" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + {% endschema %} + - name: should handle mixed private and public blocks correctly (sections bucket) + files: + blocks/_private.liquid: "" + blocks/custom.liquid: "" + sections/mixed-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + }, + { + "type": "_private" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + }, + { + "type": "custom" + } + ] + } + ] + } + {% endschema %} + - name: should report an error when a preset defined block is not specified at root level with hash-style presets (sections bucket) + files: + blocks/text.liquid: "" + sections/invalid-preset.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "text" + } + ], + "presets": [ + { + "name": "Default preset", + "blocks": { + "invalid_preset_block_hash": { + "type": "invalid_preset_block" + } + }, + "block_order": ["invalid_preset_block_hash"] + } + ] + } + {% endschema %} + offenses: + - file: sections/invalid-preset.liquid + message: 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"invalid_preset_block"' + - file: sections/invalid-preset.liquid + message: "Theme block 'blocks/invalid_preset_block.liquid' does not exist." + start: [13, 18] + highlight: '"invalid_preset_block"' + - name: "should report errors for private blocks even when @theme is specified at root level with hash-style presets (sections bucket)" + files: + blocks/_private.liquid: "" + sections/private-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "private_hash": { + "type": "_private" + } + }, + "block_order": ["private_hash"] + } + ] + } + {% endschema %} + offenses: + - file: sections/private-blocks.liquid + message: 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"_private"' + - name: should not report errors for private blocks when listed at root level with hash-style presets (sections bucket) + files: + blocks/_private.liquid: "" + sections/private-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "_private" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "private_hash": { + "type": "_private" + } + }, + "block_order": ["private_hash"] + } + ] + } + {% endschema %} + - name: should report errors for nested blocks when they are not allowed (sections bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + sections/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 22] + highlight: '"group"' + - name: should report errors for nested private blocks when they are not allowed (sections bucket) + files: + blocks/image.liquid: "" + blocks/_private.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + sections/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Private block type "_private" is not allowed in "slide" blocks.' + start: [16, 22] + highlight: '"_private"' + - name: should report errors for nested blocks when they are not allowed with hash-style presets (sections bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + sections/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "slide_hash": { + "type": "slide", + "blocks": { + "group_hash": { + "type": "group" + } + } + } + } + } + ] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 23] + highlight: '"group"' + - name: should report errors for further nested blocks when they are not allowed (sections bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: | + {% schema %} + { + "name": "Group", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "group" + } + ] + } + {% endschema %} + sections/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group", + "blocks": [ + { + "type": "image" + } + ] + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Block type "image" is not allowed in "group" blocks. Allowed types are: text.' + start: [19, 26] + highlight: '"image"' + - name: Static Blocks used in a sections file > When presets are defined as a hash > When the static block is used in the same file > is silent for valid static blocks references + files: + sections/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": { + "static_id": { + "type": "static", + "static": true, + "settings": { + "text": "some text" + } + } + }, + "block_order": [] + } + ] + } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + - name: Static Blocks used in a sections file > When presets are defined as a hash > When the static block is used in the same file > reports a warning for missing (or mismatching) static blocks references + files: + sections/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": { + "mismatching_id": { + "type": "static", + "static": true, + "settings": { + "text": "some text" + } + } + }, + "block_order": [] + } + ] + } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in this file.' + start: [11, 12] + highlight: '"static"' + - name: Static Blocks used in a sections file > When presets are defined as a hash > When the static block preset is defined in a nested block > is silent for valid static blocks references + files: + sections/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "block_order": ["slideshow1"], + "blocks": { + "slideshow1": { + "type": "slideshow", + "blocks": { + "static_id": { + "type": "static", + "static": true + } + }, + "block_order": [] + } + } + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + - name: Static Blocks used in a sections file > When presets are defined as a hash > When the static block preset is defined in a nested block > reports warnings when using a mismatching (or nonexisting) static block + files: + sections/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "block_order": ["slideshow1"], + "blocks": { + "slideshow1": { + "type": "slideshow", + "blocks": { + "mismatching_id": { + "type": "static", + "static": true + } + }, + "block_order": [] + } + } + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + offenses: + - file: sections/slideshow-container.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in "blocks/slideshow.liquid".' + start: [15, 12] + highlight: '"static"' + - name: is silent for valid static blocks references (sections bucket) + files: + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{ "label": "some setting", "type": "text", "id": "text" }], + } + {% endschema %} + sections/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "static", + "id": "static_id", + "static": true, + "settings": { + "text": "some text" + } + } + ] + } + ] + } + {% endschema %} + - name: reports a warning for missing (or mismatching) static blocks references (sections bucket) + files: + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{ "label": "some setting", "type": "text", "id": "text" }], + } + {% endschema %} + sections/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "static", + "id": "mismatching_id", + "static": true, + "settings": { + "text": "some text" + } + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in this file.' + start: [12, 12] + highlight: '"static"' + - name: Static Blocks used in a sections file > When the static block preset is defined in a nested block > is silent for valid static blocks references + files: + sections/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "blocks": [ + { + "type": "slideshow", + "id": "slideshow1", + "blocks": [ + { + "id": "static_id", + "type": "static", + "static": true + } + ], + } + ] + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + - name: Static Blocks used in a sections file > When the static block preset is defined in a nested block > reports warnings when using a mismatching (or nonexisting) static block + files: + sections/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "blocks": [ + { + "type": "slideshow", + "id": "slideshow1", + "blocks": [ + { + "id": "mismatching_id", + "type": "static", + "static": true + } + ], + } + ] + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + offenses: + - file: sections/slideshow-container.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in "blocks/slideshow.liquid".' + start: [16, 12] + highlight: '"static"' + - name: should report errors with correct indices for nested blocks when they are not allowed (sections bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + sections/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 22] + highlight: '"group"' + - name: should not report errors for nested blocks when they are allowed (sections bucket) + files: + blocks/image.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + sections/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "image" + } + ] + } + ] + } + ] + } + {% endschema %} + - name: should report errors on the correct file for nested blocks when they are not allowed (sections bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + sections/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 22] + highlight: '"group"' + - name: should not report errors for local blocks in presets when no root-level blocks array exists (sections bucket) + files: + blocks/_card.liquid: | + {% schema %} + { + "name": "Card", + "blocks": [ + { "type": "_title" }, + { "type": "_image" } + ] + } + {% endschema %} + blocks/_title.liquid: "" + blocks/_image.liquid: "" + sections/cards.liquid: | + {% schema %} + { + "name": "Cards", + "presets": [ + { + "name": "Default", + "blocks": { + "card-1": { + "type": "_card", + "blocks": { + "title-1": { "type": "_title" }, + "image-1": { "type": "_image" } + } + } + } + } + ] + } + {% endschema %} + - name: "should still report errors for local blocks in presets when root-level blocks array contains only @app (sections bucket)" + files: + blocks/_card.liquid: "" + sections/cards.liquid: | + {% schema %} + { + "name": "Cards", + "blocks": [ + { "type": "@app" } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "card-1": { "type": "_card" } + } + } + ] + } + {% endschema %} + offenses: + - file: sections/cards.liquid + message: 'Theme block type "_card" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [10, 28] + highlight: '"_card"' + - name: should not crash or timeout with cyclical nested block relationships (1) + files: + blocks/block-b.liquid: | + {% schema %} + { + "name": "Block B", + "blocks": [ + { + "type": "block-c" + } + ] + } + {% endschema %} + blocks/block-a.liquid: | + {% schema %} + { + "name": "Block A", + "blocks": [ + { + "type": "block-b" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "block-b", + "blocks": [ + { + "type": "block-c" + } + ] + } + ] + } + ] + } + {% endschema %} + blocks/block-c.liquid: | + {% schema %} + { + "name": "Block C", + "blocks": [ + { + "type": "block-a" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "block-a", + "blocks": [ + { + "type": "block-b" + } + ] + } + ] + } + ] + } + {% endschema %} + - name: should report an error when a preset defined block is not specified at root level (blocks bucket) + files: + blocks/text.liquid: "" + blocks/invalid-preset.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "text" + } + ], + "presets": [ + { + "name": "Default preset", + "blocks": [ + { + "type": "invalid_preset_block" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/invalid-preset.liquid + message: 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"invalid_preset_block"' + - file: blocks/invalid-preset.liquid + message: "Theme block 'blocks/invalid_preset_block.liquid' does not exist." + start: [13, 18] + highlight: '"invalid_preset_block"' + - name: "should report errors in presets for private blocks even when @theme is specified at root level (blocks bucket)" + files: + blocks/_private.liquid: "" + blocks/private-blocks.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/private-blocks.liquid + message: 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"_private"' + - name: should report errors with correct indices (blocks bucket) + files: + blocks/private-blocks.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/private-blocks.liquid + message: 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"_private"' + - file: blocks/private-blocks.liquid + message: "Theme block 'blocks/_private.liquid' does not exist." + start: [13, 18] + highlight: '"_private"' + - name: should not report errors for private blocks when listed at root level (blocks bucket) + files: + blocks/_private.liquid: "" + blocks/private-blocks.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "_private" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + {% endschema %} + - name: should handle mixed private and public blocks correctly (blocks bucket) + files: + blocks/_private.liquid: "" + blocks/custom.liquid: "" + blocks/mixed-blocks.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@theme" + }, + { + "type": "_private" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "_private" + }, + { + "type": "custom" + } + ] + } + ] + } + {% endschema %} + - name: should report an error when a preset defined block is not specified at root level with hash-style presets (blocks bucket) + files: + blocks/text.liquid: "" + blocks/invalid-preset.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "text" + } + ], + "presets": [ + { + "name": "Default preset", + "blocks": { + "invalid_preset_block_hash": { + "type": "invalid_preset_block" + } + }, + "block_order": ["invalid_preset_block_hash"] + } + ] + } + {% endschema %} + offenses: + - file: blocks/invalid-preset.liquid + message: 'Theme block type "invalid_preset_block" must be allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"invalid_preset_block"' + - file: blocks/invalid-preset.liquid + message: "Theme block 'blocks/invalid_preset_block.liquid' does not exist." + start: [13, 18] + highlight: '"invalid_preset_block"' + - name: "should report errors for private blocks even when @theme is specified at root level with hash-style presets (blocks bucket)" + files: + blocks/_private.liquid: "" + blocks/private-blocks.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "@theme" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "private_hash": { + "type": "_private" + } + }, + "block_order": ["private_hash"] + } + ] + } + {% endschema %} + offenses: + - file: blocks/private-blocks.liquid + message: 'Theme block type "_private" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [13, 18] + highlight: '"_private"' + - name: should not report errors for private blocks when listed at root level with hash-style presets (blocks bucket) + files: + blocks/_private.liquid: "" + blocks/private-blocks.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "_private" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "private_hash": { + "type": "_private" + } + }, + "block_order": ["private_hash"] + } + ] + } + {% endschema %} + - name: should report errors for nested blocks when they are not allowed (blocks bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + blocks/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 22] + highlight: '"group"' + - name: should report errors for nested private blocks when they are not allowed (blocks bucket) + files: + blocks/image.liquid: "" + blocks/_private.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + blocks/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "_private" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Private block type "_private" is not allowed in "slide" blocks.' + start: [16, 22] + highlight: '"_private"' + - name: should report errors for nested blocks when they are not allowed with hash-style presets (blocks bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + blocks/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "slide_hash": { + "type": "slide", + "blocks": { + "group_hash": { + "type": "group" + } + } + } + } + } + ] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 23] + highlight: '"group"' + - name: should report errors for further nested blocks when they are not allowed (blocks bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: | + {% schema %} + { + "name": "Group", + "blocks": [ + { + "type": "text" + } + ] + } + {% endschema %} + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "group" + } + ] + } + {% endschema %} + blocks/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group", + "blocks": [ + { + "type": "image" + } + ] + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Block type "image" is not allowed in "group" blocks. Allowed types are: text.' + start: [19, 26] + highlight: '"image"' + - name: Static Blocks used in a blocks file > When presets are defined as a hash > When the static block is used in the same file > is silent for valid static blocks references + files: + blocks/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": { + "static_id": { + "type": "static", + "static": true, + "settings": { + "text": "some text" + } + } + }, + "block_order": [] + } + ] + } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + - name: Static Blocks used in a blocks file > When presets are defined as a hash > When the static block is used in the same file > reports a warning for missing (or mismatching) static blocks references + files: + blocks/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": { + "mismatching_id": { + "type": "static", + "static": true, + "settings": { + "text": "some text" + } + } + }, + "block_order": [] + } + ] + } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in this file.' + start: [11, 12] + highlight: '"static"' + - name: Static Blocks used in a blocks file > When presets are defined as a hash > When the static block preset is defined in a nested block > is silent for valid static blocks references + files: + blocks/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "block_order": ["slideshow1"], + "blocks": { + "slideshow1": { + "type": "slideshow", + "blocks": { + "static_id": { + "type": "static", + "static": true + } + }, + "block_order": [] + } + } + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + - name: Static Blocks used in a blocks file > When presets are defined as a hash > When the static block preset is defined in a nested block > reports warnings when using a mismatching (or nonexisting) static block + files: + blocks/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "block_order": ["slideshow1"], + "blocks": { + "slideshow1": { + "type": "slideshow", + "blocks": { + "mismatching_id": { + "type": "static", + "static": true + } + }, + "block_order": [] + } + } + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + offenses: + - file: blocks/slideshow-container.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in "blocks/slideshow.liquid".' + start: [15, 12] + highlight: '"static"' + - name: is silent for valid static blocks references (blocks bucket) + files: + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{ "label": "some setting", "type": "text", "id": "text" }], + } + {% endschema %} + blocks/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "static", + "id": "static_id", + "static": true, + "settings": { + "text": "some text" + } + } + ] + } + ] + } + {% endschema %} + - name: reports a warning for missing (or mismatching) static blocks references (blocks bucket) + files: + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{ "label": "some setting", "type": "text", "id": "text" }], + } + {% endschema %} + blocks/slideshow.liquid: |2 + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { + "name": "Slideshow", + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "static", + "id": "mismatching_id", + "static": true, + "settings": { + "text": "some text" + } + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in this file.' + start: [12, 12] + highlight: '"static"' + - name: Static Blocks used in a blocks file > When the static block preset is defined in a nested block > is silent for valid static blocks references + files: + blocks/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "blocks": [ + { + "type": "slideshow", + "id": "slideshow1", + "blocks": [ + { + "id": "static_id", + "type": "static", + "static": true + } + ], + } + ] + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + - name: Static Blocks used in a blocks file > When the static block preset is defined in a nested block > reports warnings when using a mismatching (or nonexisting) static block + files: + blocks/slideshow-container.liquid: |2 + # This file demonstrates how a static block can be used inside a nested block's preset + {% content_for "blocks" %} + + {% schema %} + { + "name": "Slideshow container", + "blocks": [{ "type": "slideshow" }], + "presets": [{ + "name": "default", + "blocks": [ + { + "type": "slideshow", + "id": "slideshow1", + "blocks": [ + { + "id": "mismatching_id", + "type": "static", + "static": true + } + ], + } + ] + }] + } + {% endschema %} + blocks/slideshow.liquid: | + # Static block is defined in the body of the file + {% content_for "block", type: "static", id: "static_id" %} + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + {% schema %} + { "name": "Slideshow" } + {% endschema %} + blocks/static.liquid: | + {% schema %} + { + "name": "some static block", + "settings": [{"type":"text","id":"text","label":"Text"}] + } + {% endschema %} + offenses: + - file: blocks/slideshow-container.liquid + message: 'Could not find a static block of type "static" with id "mismatching_id" in "blocks/slideshow.liquid".' + start: [16, 12] + highlight: '"static"' + - name: should report errors with correct indices for nested blocks when they are not allowed (blocks bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + blocks/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 22] + highlight: '"group"' + - name: should not report errors for nested blocks when they are allowed (blocks bucket) + files: + blocks/image.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + blocks/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "image" + } + ] + } + ] + } + ] + } + {% endschema %} + - name: should report errors on the correct file for nested blocks when they are not allowed (blocks bucket) + files: + blocks/image.liquid: "" + blocks/group.liquid: "" + blocks/text.liquid: "" + blocks/slide.liquid: | + {% schema %} + { + "name": "Slide", + "blocks": [ + { + "type": "text" + }, + { + "type": "image" + } + ] + } + {% endschema %} + blocks/slideshow.liquid: | + {% schema %} + { + "name": "Slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group" + } + ] + } + ] + } + ] + } + {% endschema %} + offenses: + - file: blocks/slideshow.liquid + message: 'Block type "group" is not allowed in "slide" blocks. Allowed types are: text, image.' + start: [16, 22] + highlight: '"group"' + - name: should not report errors for local blocks in presets when no root-level blocks array exists (blocks bucket) + files: + blocks/_card.liquid: | + {% schema %} + { + "name": "Card", + "blocks": [ + { "type": "_title" }, + { "type": "_image" } + ] + } + {% endschema %} + blocks/_title.liquid: "" + blocks/_image.liquid: "" + blocks/cards.liquid: | + {% schema %} + { + "name": "Cards", + "presets": [ + { + "name": "Default", + "blocks": { + "card-1": { + "type": "_card", + "blocks": { + "title-1": { "type": "_title" }, + "image-1": { "type": "_image" } + } + } + } + } + ] + } + {% endschema %} + - name: "should still report errors for local blocks in presets when root-level blocks array contains only @app (blocks bucket)" + files: + blocks/_card.liquid: "" + blocks/cards.liquid: | + {% schema %} + { + "name": "Cards", + "blocks": [ + { "type": "@app" } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "card-1": { "type": "_card" } + } + } + ] + } + {% endschema %} + offenses: + - file: blocks/cards.liquid + message: 'Theme block type "_card" is a private block so it must be explicitly allowed in "blocks" at the root of this schema.' + start: [10, 28] + highlight: '"_card"' + - name: should not crash or timeout with cyclical nested block relationships (2) + files: + blocks/block-b.liquid: | + {% schema %} + { + "name": "Block B", + "blocks": [ + { + "type": "block-c" + } + ] + } + {% endschema %} + blocks/block-a.liquid: | + {% schema %} + { + "name": "Block A", + "blocks": [ + { + "type": "block-b" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "block-b", + "blocks": [ + { + "type": "block-c" + } + ] + } + ] + } + ] + } + {% endschema %} + blocks/block-c.liquid: | + {% schema %} + { + "name": "Block C", + "blocks": [ + { + "type": "block-a" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "block-a", + "blocks": [ + { + "type": "block-b" + } + ] + } + ] + } + ] + } + {% endschema %} diff --git a/specs/checks/valid-content-for-argument-types.yml b/specs/checks/valid-content-for-argument-types.yml new file mode 100644 index 000000000..4c65fd441 --- /dev/null +++ b/specs/checks/valid-content-for-argument-types.yml @@ -0,0 +1,488 @@ +check: ValidContentForArgumentTypes +severity: warning +cases: + - name: "should accept 'hello' for string" + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 'hello' %}" + - name: "should accept '' for string" + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: '' %}" + - name: should accept product for string + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: product %}" + - name: should reject 123 for string + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 45] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', param: '' %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should reject true for string + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: true %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got boolean" + start: [0, 45] + highlight: "true" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', param: '' %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should accept 0 for number + files: + blocks/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 0 %}" + - name: should accept 123 for number + files: + blocks/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + - name: should accept -1 for number + files: + blocks/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: -1 %}" + - name: should accept product for number + files: + blocks/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: product %}" + - name: "should reject 'hello' for number" + files: + blocks/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 'hello' %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected number, got string" + start: [0, 45] + highlight: "'hello'" + suggest: + - message: "Replace with default value '0' for number" + result: "{% content_for 'block', type: 'card', param: 0 %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should reject true for number + files: + blocks/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: true %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected number, got boolean" + start: [0, 45] + highlight: "true" + suggest: + - message: "Replace with default value '0' for number" + result: "{% content_for 'block', type: 'card', param: 0 %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should accept true for boolean + files: + blocks/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: true %}" + - name: should accept false for boolean + files: + blocks/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: false %}" + - name: should accept nil for boolean + files: + blocks/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: nil %}" + - name: should accept empty for boolean + files: + blocks/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: empty %}" + - name: should accept product for boolean + files: + blocks/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: product %}" + - name: should accept 123 for boolean + files: + blocks/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + - name: "should accept 'hello' for boolean" + files: + blocks/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 'hello' %}" + - name: should accept product for object + files: + blocks/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: product %}" + - name: should accept (1..3) for object + files: + blocks/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: (1..3) %}" + - name: "should reject 'hello' for object" + files: + blocks/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 'hello' %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got string" + start: [0, 45] + highlight: "'hello'" + suggest: + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should reject 123 for object + files: + blocks/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got number" + start: [0, 45] + highlight: "123" + suggest: + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should reject true for object + files: + blocks/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: true %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got boolean" + start: [0, 45] + highlight: "true" + suggest: + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should reject empty for object + files: + blocks/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: empty %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got boolean" + start: [0, 45] + highlight: empty + suggest: + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should handle mixed case type annotations + files: + blocks/card.liquid: | + {% doc %} + @param {String} text - The text + @param {NUMBER} count - The count + @param {BOOLEAN} flag - The flag + @param {Object} data - The data + {% enddoc %} +
    {{ text }}{{ count }}{{ flag }}{{ data }}
    + file.liquid: "{% content_for 'block', type: 'card', text: \"hello\", count: 5, flag: true, data: product %}" + - name: should ignore variable lookups + files: + blocks/card.liquid: | + {% doc %} + @param {String} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% content_for 'block', type: 'card', title: product_title %}" + - name: should not report when snippet has no doc comment + files: + blocks/card.liquid: "

    This snippet has no doc comment

    " + file.liquid: "{% content_for 'block', type: 'card', title: 123 %}" + - name: should not enforce unsupported types + files: + blocks/card.liquid: | + {% doc %} + @param {Unsupported} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% content_for 'block', type: 'card', title: 123 %}" + - name: should not report for unrecognized arguments + files: + blocks/card.liquid: | + {% doc %} + @param {String} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% content_for 'block', type: 'card', title: \"hello\", unrecognized: 123 %}" + - name: should suggest replacing with default value for type or removing value + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 45] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', param: '' %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: "should allow users to fix a single argument when multiple are provided`" + files: + blocks/card.liquid: | + {% doc %} + @param {string} title - The title + @param {number} count - The count + {% enddoc %} +
    {{ title }} {{ count }}
    + file.liquid: "{% content_for 'block', type: 'card', title: 123, count: 5 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'title': expected string, got number" + start: [0, 45] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', title: '', count: 5 %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', title: , count: 5 %}" + - name: should handle arguments with trailing commas + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123, %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 45] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', param: '', %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: , %}" + - name: should handle arguments with complex spacing + files: + blocks/card.liquid: | + {% doc %} + @param {string} title - The title + @param {number} count - The count + {% enddoc %} + file.liquid: |- + {% content_for 'block', type: 'card', + title: 123, + count: 5 + %} + offenses: + - file: file.liquid + message: "Type mismatch for argument 'title': expected string, got number" + start: [1, 15] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: |- + {% content_for 'block', type: 'card', + title: '', + count: 5 + %} + - message: Remove value + result: |- + {% content_for 'block', type: 'card', + title: , + count: 5 + %} + - name: should handle argument with no space after colon + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param:123 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 44] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', param:'' %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should handle argument with multiple spaces after colon + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 49] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', param: '' %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: should handle argument with newlines + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: |- + {% content_for 'block', type: 'card', param: + 123 + %} + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [1, 8] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: |- + {% content_for 'block', type: 'card', param: + '' + %} + - message: Remove value + result: |- + {% content_for 'block', type: 'card', param: + + %} + - name: should suggest removal and replacement if expected type has a default value + files: + blocks/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 45] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% content_for 'block', type: 'card', param: '' %}" + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" + - name: "should only suggest removal if expected type default value is ''" + files: + blocks/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% content_for 'block', type: 'card', param: 123 %}" + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got number" + start: [0, 45] + highlight: "123" + suggest: + - message: Remove value + result: "{% content_for 'block', type: 'card', param: %}" diff --git a/specs/checks/valid-content-for-arguments.yml b/specs/checks/valid-content-for-arguments.yml new file mode 100644 index 000000000..e1795ce0c --- /dev/null +++ b/specs/checks/valid-content-for-arguments.yml @@ -0,0 +1,69 @@ +check: ValidContentForArguments +severity: error +cases: + - name: should not report offenses for strategies we do not know or support yet + files: + file.liquid: '{% content_for "snippet", closest.product: product %}' + - name: "should accept `closest.*` kwargs" + files: + file.liquid: '{% content_for "blocks", closest.product: product %}' + - name: "should not accept `context.*` kwargs" + files: + file.liquid: '{% content_for "blocks", context.product: product %}' + offenses: + - file: file.liquid + message: "{% content_for \"blocks\" %} only accepts 'closest.*' arguments" + start: [0, 25] + highlight: "context.product: product" + - name: "should report offenses for non-`closest.*` kwargs" + files: + file.liquid: '{% content_for "blocks", product: product %}' + offenses: + - file: file.liquid + message: "{% content_for \"blocks\" %} only accepts 'closest.*' arguments" + start: [0, 25] + highlight: "product: product" + - name: should accept valid arguments + files: + file.liquid: '{% content_for "block", type: "text", id: "static-block" %}' + - name: "should report an offense if 'type' is not a string" + files: + file.liquid: '{% content_for "block", type: 10, id: "static-block" %}' + offenses: + - file: file.liquid + message: "The 'type' argument should be a string" + start: [0, 30] + highlight: "10" + suggest: [] + - name: "should report an offense if 'id' is not a string" + files: + file.liquid: '{% content_for "block", type: "foo", id: (0..10) %}' + offenses: + - file: file.liquid + message: "The 'id' argument should be a string" + start: [0, 41] + highlight: "(0..10)" + suggest: [] + - name: should accept static arguments + files: + file.liquid: '{% content_for "block", type: "foo", id: "id", product: product %}' + - name: should report an offense for reserved arguments + files: + file.liquid: '{% content_for "block", type: "foo", id: "id", settings: settings %}' + offenses: + - file: file.liquid + message: "{% content_for \"block\" %} doesn't support 'settings' because it's a reserved argument." + start: [0, 47] + highlight: "settings: settings" + - name: should report offenses for multiple reserved arguments + files: + file.liquid: '{% content_for "block", type: "foo", id: "id", settings: settings, section: section %}' + offenses: + - file: file.liquid + message: "{% content_for \"block\" %} doesn't support 'settings' because it's a reserved argument." + start: [0, 47] + highlight: "settings: settings" + - file: file.liquid + message: "{% content_for \"block\" %} doesn't support 'section' because it's a reserved argument." + start: [0, 67] + highlight: "section: section" diff --git a/specs/checks/valid-doc-param-types.yml b/specs/checks/valid-doc-param-types.yml new file mode 100644 index 000000000..8514bf523 --- /dev/null +++ b/specs/checks/valid-doc-param-types.yml @@ -0,0 +1,456 @@ +check: ValidDocParamTypes +severity: error +cases: + - name: should not report an error when a valid basic parameter (string) type is used + files: + file.liquid: | + {% doc %} + @param {string} param1 - Example param + {% enddoc %} + docset: default + - name: should not report an error when a valid basic parameter (number) type is used + files: + file.liquid: | + {% doc %} + @param {number} param1 - Example param + {% enddoc %} + docset: default + - name: should not report an error when a valid basic parameter (boolean) type is used + files: + file.liquid: | + {% doc %} + @param {boolean} param1 - Example param + {% enddoc %} + docset: default + - name: should not report an error when a valid basic parameter (object) type is used + files: + file.liquid: | + {% doc %} + @param {object} param1 - Example param + {% enddoc %} + docset: default + - name: should not report an error when a valid liquid object parameter (product) type is used + files: + file.liquid: | + {% doc %} + @param {product} param1 - Example param + {% enddoc %} + docset: default + - name: "should not report an error when a valid liquid object array parameter (product[]) type is used" + files: + file.liquid: | + {% doc %} + @param {product[]} param1 - Example param + {% enddoc %} + docset: default + - name: "Module: ValidDocParamTypes > accepts the string enum {'heading' | 'small'}" + files: + file.liquid: |- + {% doc %} + @param {'heading' | 'small'} [variant] - Text style + {% enddoc %} + docset: default + - name: 'accepts the string enum {"Heading"|"small"}' + files: + file.liquid: |- + {% doc %} + @param {"Heading"|"small"} [variant] - Text style + {% enddoc %} + docset: default + - name: "accepts the string enum {'heading'}" + files: + file.liquid: |- + {% doc %} + @param {'heading'} [variant] - Text style + {% enddoc %} + docset: default + - name: "accepts the string enum {'' | ' small '}" + files: + file.liquid: |- + {% doc %} + @param {'' | ' small '} [variant] - Text style + {% enddoc %} + docset: default + - name: "accepts the string enum {'a|b' | 'a}b'}" + files: + file.liquid: |- + {% doc %} + @param {'a|b' | 'a}b'} [variant] - Text style + {% enddoc %} + docset: default + - name: "accepts the string enum {\"it's\" | 'say \"hi\"'}" + files: + file.liquid: |- + {% doc %} + @param {"it's" | 'say "hi"'} [variant] - Text style + {% enddoc %} + docset: default + - name: "reports the complete invalid enum {'heading' |} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 'heading' | } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 'heading' | ' is not supported." + start: [1, 11] + highlight: "{ 'heading' | }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {| 'heading'} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { | 'heading' } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' | 'heading' ' is not supported." + start: [1, 11] + highlight: "{ | 'heading' }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {'heading' || 'small'} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 'heading' || 'small' } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 'heading' || 'small' ' is not supported." + start: [1, 11] + highlight: "{ 'heading' || 'small' }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {'heading' 'small'} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 'heading' 'small' } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 'heading' 'small' ' is not supported." + start: [1, 11] + highlight: "{ 'heading' 'small' }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {'heading' | small} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 'heading' | small } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 'heading' | small ' is not supported." + start: [1, 11] + highlight: "{ 'heading' | small }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {'heading' | 'small} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 'heading' | 'small } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 'heading' | 'small ' is not supported." + start: [1, 11] + highlight: "{ 'heading' | 'small }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {'heading' | number} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 'heading' | number } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 'heading' | number ' is not supported." + start: [1, 11] + highlight: "{ 'heading' | number }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {heading | small} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { heading | small } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' heading | small ' is not supported." + start: [1, 11] + highlight: "{ heading | small }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {1 | 2} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 1 | 2 } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 1 | 2 ' is not supported." + start: [1, 11] + highlight: "{ 1 | 2 }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {true | false} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { true | false } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' true | false ' is not supported." + start: [1, 11] + highlight: "{ true | false }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {('heading' | 'small')} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { ('heading' | 'small') } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' ('heading' | 'small') ' is not supported." + start: [1, 11] + highlight: "{ ('heading' | 'small') }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: "reports the complete invalid enum {'heading'[]} and preserves the optional parameter in its fix" + files: + file.liquid: |- + {% doc %} + @param { 'heading'[] } [variant] - Text style + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' 'heading'[] ' is not supported." + start: [1, 11] + highlight: "{ 'heading'[] }" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - Text style + {% enddoc %} + - name: should report an error with suggestions when an invalid parameter type is used + files: + file.liquid: | + {% doc %} + @param {invalidType} param1 - Example param + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type 'invalidType' is not supported." + start: [1, 9] + highlight: "{invalidType}" + suggest: + - message: Remove invalid parameter type + result: | + {% doc %} + @param param1 - Example param + {% enddoc %} + - name: should apply suggestion when an invalid parameter type is used (1) + files: + file.liquid: "{% doc %} @param {invalidType} param1 - Example param {% enddoc %}" + docset: default + offenses: + - file: file.liquid + message: "The parameter type 'invalidType' is not supported." + start: [0, 17] + highlight: "{invalidType}" + suggest: + - message: Remove invalid parameter type + result: "{% doc %} @param param1 - Example param {% enddoc %}" + - name: should apply suggestion when an invalid parameter type is used (2) + files: + file.liquid: "{% doc %} @param { invalidType } param1 - Example param {% enddoc %}" + docset: default + offenses: + - file: file.liquid + message: "The parameter type ' invalidType ' is not supported." + start: [0, 19] + highlight: "{ invalidType }" + suggest: + - message: Remove invalid parameter type + result: "{% doc %} @param param1 - Example param {% enddoc %}" + - name: validates named types, arrays, and enums together + files: + file.liquid: |- + {% doc %} + @param {product} product + @param {string[]} labels + @param {'heading' | 'small'} [variant] + @param {unknown[]} unknown + @param {'heading' |} invalid_variant + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type 'unknown[]' is not supported." + start: [4, 13] + highlight: "{unknown[]}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param {product} product + @param {string[]} labels + @param {'heading' | 'small'} [variant] + @param unknown + @param {'heading' |} invalid_variant + {% enddoc %} + - file: file.liquid + message: "The parameter type ''heading' |' is not supported." + start: [5, 13] + highlight: "{'heading' |}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param {product} product + @param {string[]} labels + @param {'heading' | 'small'} [variant] + @param {unknown[]} unknown + @param invalid_variant + {% enddoc %} + - name: removes the invalid type without changing braces in its description + files: + file.liquid: |- + {% doc %} + @param {'heading' |} [variant] - example {foo} + {% enddoc %} + docset: default + offenses: + - file: file.liquid + message: "The parameter type ''heading' |' is not supported." + start: [1, 9] + highlight: "{'heading' |}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param [variant] - example {foo} + {% enddoc %} + - name: reports invalid string literal types + files: + file.liquid: |- + {% doc %} + @param {'heading' |} variant + {% enddoc %} + offenses: + - file: file.liquid + message: "The parameter type ''heading' |' is not supported." + start: [1, 10] + highlight: "{'heading' |}" + suggest: + - message: Remove invalid parameter type + result: |- + {% doc %} + @param variant + {% enddoc %} + - name: "without a docset > accepts the string enum {'heading' | 'small'}" + files: + file.liquid: |- + {% doc %} + @param {'heading' | 'small'} variant + {% enddoc %} + - name: 'accepts the string enum {"heading"}' + files: + file.liquid: |- + {% doc %} + @param {"heading"} variant + {% enddoc %} + - name: does not check the named type {product}, which requires the docset + files: + file.liquid: |- + {% doc %} + @param {product} variant + {% enddoc %} + - name: "does not check the named type {product[]}, which requires the docset" + files: + file.liquid: |- + {% doc %} + @param {product[]} variant + {% enddoc %} + - name: does not check the named type {invalidType}, which requires the docset + files: + file.liquid: |- + {% doc %} + @param {invalidType} variant + {% enddoc %} + - name: "does not check the named type {unknown[]}, which requires the docset" + files: + file.liquid: |- + {% doc %} + @param {unknown[]} variant + {% enddoc %} diff --git a/specs/checks/valid-html-translation.yml b/specs/checks/valid-html-translation.yml new file mode 100644 index 000000000..5d112736b --- /dev/null +++ b/specs/checks/valid-html-translation.yml @@ -0,0 +1,56 @@ +check: ValidHTMLTranslation +severity: warning +cases: + - name: should not report valid HTML in translations + files: + locales/en.json: |- + { + "hello_html": "

    Hello, world

    ", + "image_html": "", + "line_break_html": "
    ", + "self_closing_svg_html": "", + "foo": "bar" + } + - name: should report invalid HTML in translations + files: + locales/en.json: |- + { + "hello_html": "

    Hello, world" + } + offenses: + - file: locales/en.json + message: "LiquidHTMLParsingError: Attempting to end parsing before HtmlElement 'h1' was closed." + start: [1, 16] + highlight: '"

    Hello, world"' + - name: should report nested invalid HTML in translations + files: + locales/en.json: |- + { + "hello": { + "world": { + "today": { + "good": { + "day_html": "

    Hello, world" + } + } + } + } + } + offenses: + - file: locales/en.json + message: "LiquidHTMLParsingError: Attempting to end parsing before HtmlElement 'h1' was closed." + start: [5, 22] + highlight: '"

    Hello, world"' + - name: should report pluralized key with invalid HTML + files: + locales/en.json: |- + { + "hello_html": { + "one": "

    Hello, world" + } + } + offenses: + - file: locales/en.json + message: "LiquidHTMLParsingError: Attempting to end parsing before HtmlElement 'h1' was closed." + start: [2, 11] + highlight: '"

    Hello, world"' diff --git a/specs/checks/valid-json.yml b/specs/checks/valid-json.yml new file mode 100644 index 000000000..ebbc60abc --- /dev/null +++ b/specs/checks/valid-json.yml @@ -0,0 +1,10 @@ +check: ValidJSON +severity: error +cases: + - name: should not report any errors for valid JSON + files: + file.json: |- + { + "key1": "value1", + "key2": "value2" + } diff --git a/specs/checks/valid-local-blocks.yml b/specs/checks/valid-local-blocks.yml new file mode 100644 index 000000000..b1154819e --- /dev/null +++ b/specs/checks/valid-local-blocks.yml @@ -0,0 +1,231 @@ +check: ValidLocalBlocks +severity: error +cases: + - name: should report errors when root level local blocks are defined in a block bucket + files: + blocks/local-blocks.liquid: | + {% schema %} + { + "name": "Block name", + "blocks": [ + { + "type": "local_block", + "name": "Local Block" + } + ] + } + {% endschema %} + offenses: + - file: blocks/local-blocks.liquid + message: Local scoped blocks are not supported in theme blocks. + start: [5, 16] + highlight: '"local_block"' + - name: should report errors when sections use theme blocks together with locally scoped blocks in root level + files: + blocks/text.liquid: "" + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "text" + }, + { + "type": "local_block", + "name": "Local Block" + } + ] + } + {% endschema %} + offenses: + - file: sections/local-blocks.liquid + message: Sections cannot use theme blocks together with locally scoped blocks. + start: [8, 16] + highlight: '"local_block"' + - name: should report errors when sections use static theme blocks together with locally scoped blocks + files: + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "local_block", + "name": "Local Block" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "id": "static_block", + "type": "static_block", + "static": true + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/local-blocks.liquid + message: Sections cannot use static theme blocks together with locally scoped blocks. + start: [15, 20] + highlight: '"static_block"' + - name: should report errors with correct indices when sections use static theme blocks together with locally scoped blocks + files: + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "local_block", + "name": "Local Block" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "id": "static_block", + "type": "static_block", + "static": true + } + ] + } + ] + } + {% endschema %} + offenses: + - file: sections/local-blocks.liquid + message: Sections cannot use static theme blocks together with locally scoped blocks. + start: [15, 16] + highlight: '"static_block"' + - name: should not report errors for block setting types + files: + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@app" + }, + { + "type": "link_list", + "name": "Link list", + "settings": [ + { + "type": "inline_richtext", + "id": "heading", + "default": "Heading" + }, + { + "type": "link_list", + "id": "menu", + "default": "Footer" + } + ] + } + ] + } + {% endschema %} + - name: "should not report errors when @app is used alongside static blocks" + files: + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "@app" + }, + { + "type": "static_block", + "static": true + } + ] + } + {% endschema %} + - name: should not report errors on themes that define local blocks at the root level and use them in presets + files: + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "local_block", + "name": "Local Block" + } + ], + "presets": [ + { + "name": "Default", + "blocks": [ + { + "type": "local_block" + } + ] + } + ] + } + {% endschema %} + - name: should not report errors on older themes that define local blocks at the root level and use them in hash-style presets + files: + sections/local-blocks.liquid: | + {% schema %} + { + "name": "Section name", + "blocks": [ + { + "type": "local_block", + "name": "Local Block" + } + ], + "presets": [ + { + "name": "Default", + "blocks": { + "local_block": { + "type": "local_block" + } + } + } + ] + } + {% endschema %} + - name: should not report errors when preset blocks have a name property + files: + blocks/product-grid.liquid: | + {% schema %} + { + "name": "Product grid", + "blocks": [{ "type": "product-card" }], + "presets": [ + { + "name": "Product grid", + "blocks": { + "product-grid-card": { + "type": "product-card", + "static": true, + "blocks": { + "card_gallery_eK4Hr7": { + "type": "card-gallery", + "name": "potato", + }, + "group_4RMFLV": { + "type": "group" + } + }, + "block_order": ["card_gallery_eK4Hr7", "group_4RMFLV"] + } + } + } + ] + } + {% endschema %} diff --git a/specs/checks/valid-render-snippet-argument-types.yml b/specs/checks/valid-render-snippet-argument-types.yml new file mode 100644 index 000000000..46d3640fc --- /dev/null +++ b/specs/checks/valid-render-snippet-argument-types.yml @@ -0,0 +1,602 @@ +check: ValidRenderSnippetArgumentTypes +severity: warning +cases: + - name: "should accept 'hello' for string" + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 'hello' %}" + docset: default + - name: "should accept '' for string" + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: '' %}" + docset: default + - name: should accept product for string + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: product %}" + docset: default + - name: should reject 123 for string + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 25] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', param: '' %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: should reject true for string + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: true %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got boolean" + start: [0, 25] + highlight: "true" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', param: '' %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: should accept 0 for number + files: + snippets/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 0 %}" + docset: default + - name: should accept 123 for number + files: + snippets/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + - name: should accept -1 for number + files: + snippets/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: -1 %}" + docset: default + - name: should accept product for number + files: + snippets/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: product %}" + docset: default + - name: "should reject 'hello' for number" + files: + snippets/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 'hello' %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected number, got string" + start: [0, 25] + highlight: "'hello'" + suggest: + - message: "Replace with default value '0' for number" + result: "{% render 'card', param: 0 %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: should reject true for number + files: + snippets/card.liquid: | + {% doc %} + @param {number} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: true %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected number, got boolean" + start: [0, 25] + highlight: "true" + suggest: + - message: "Replace with default value '0' for number" + result: "{% render 'card', param: 0 %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: should accept true for boolean + files: + snippets/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: true %}" + docset: default + - name: should accept false for boolean + files: + snippets/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: false %}" + docset: default + - name: should accept nil for boolean + files: + snippets/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: nil %}" + docset: default + - name: should accept empty for boolean + files: + snippets/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: empty %}" + docset: default + - name: should accept product for boolean + files: + snippets/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: product %}" + docset: default + - name: should accept 123 for boolean + files: + snippets/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + - name: "should accept 'hello' for boolean" + files: + snippets/card.liquid: | + {% doc %} + @param {boolean} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 'hello' %}" + docset: default + - name: should accept product for object + files: + snippets/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: product %}" + docset: default + - name: should accept (1..3) for object + files: + snippets/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: (1..3) %}" + docset: default + - name: "should reject 'hello' for object" + files: + snippets/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 'hello' %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got string" + start: [0, 25] + highlight: "'hello'" + suggest: + - message: Remove value + result: "{% render 'card', param: %}" + - name: should reject 123 for object + files: + snippets/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got number" + start: [0, 25] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'card', param: %}" + - name: should reject true for object + files: + snippets/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: true %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got boolean" + start: [0, 25] + highlight: "true" + suggest: + - message: Remove value + result: "{% render 'card', param: %}" + - name: should reject empty for object + files: + snippets/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: empty %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got boolean" + start: [0, 25] + highlight: empty + suggest: + - message: Remove value + result: "{% render 'card', param: %}" + - name: should handle mixed case type annotations + files: + snippets/card.liquid: | + {% doc %} + @param {String} text - The text + @param {NUMBER} count - The count + @param {BOOLEAN} flag - The flag + @param {Object} data - The data + {% enddoc %} +
    {{ text }}{{ count }}{{ flag }}{{ data }}
    + file.liquid: "{% render 'card', text: \"hello\", count: 5, flag: true, data: product %}" + docset: default + - name: should ignore variable lookups + files: + snippets/card.liquid: | + {% doc %} + @param {String} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card', title: product_title %}" + docset: default + - name: should not report when snippet has no doc comment + files: + snippets/card.liquid: "

    This snippet has no doc comment

    " + file.liquid: "{% render 'card', title: 123 %}" + docset: default + - name: should not enforce unsupported types + files: + snippets/card.liquid: | + {% doc %} + @param {Unsupported} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card', title: 123 %}" + docset: default + - name: should not report for unrecognized arguments + files: + snippets/card.liquid: | + {% doc %} + @param {String} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card', title: \"hello\", unrecognized: 123 %}" + docset: default + - name: "should report when `with` alias is used" + files: + snippets/card.liquid: | + {% doc %} + @param {String} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card' with 12 as title %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'title': expected string, got number" + start: [0, 22] + highlight: "12" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card' with '' as title %}" + - message: Remove value + result: "{% render 'card' with as title %}" + - name: "should report when `for` alias is used" + files: + snippets/card.liquid: | + {% doc %} + @param {String} title - The title + {% enddoc %} +
    {{ title }}
    + file.liquid: "{% render 'card' for 123 as title %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'title': expected string, got number" + start: [0, 21] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card' for '' as title %}" + - message: Remove value + result: "{% render 'card' for as title %}" + - name: should suggest replacing with default value for type or removing value + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 25] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', param: '' %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: "should allow users to fix a single argument when multiple are provided`" + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title + @param {number} count - The count + {% enddoc %} +
    {{ title }} {{ count }}
    + file.liquid: "{% render 'card', title: 123, count: 5 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'title': expected string, got number" + start: [0, 25] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', title: '', count: 5 %}" + - message: Remove value + result: "{% render 'card', title: , count: 5 %}" + - name: should handle arguments with trailing commas + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123, %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 25] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', param: '', %}" + - message: Remove value + result: "{% render 'card', param: , %}" + - name: should handle arguments with complex spacing + files: + snippets/card.liquid: | + {% doc %} + @param {string} title - The title + @param {number} count - The count + {% enddoc %} + file.liquid: |- + {% render 'card', + title: 123, + count: 5 + %} + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'title': expected string, got number" + start: [1, 15] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: |- + {% render 'card', + title: '', + count: 5 + %} + - message: Remove value + result: |- + {% render 'card', + title: , + count: 5 + %} + - name: should handle argument with no space after colon + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param:123 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 24] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', param:'' %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: should handle argument with multiple spaces after colon + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 29] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', param: '' %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: should handle argument with newlines + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: |- + {% render 'card', param: + 123 + %} + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [1, 8] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: |- + {% render 'card', param: + '' + %} + - message: Remove value + result: |- + {% render 'card', param: + + %} + - name: should suggest removal and replacement if expected type has a default value + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 25] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card', param: '' %}" + - message: Remove value + result: "{% render 'card', param: %}" + - name: "should only suggest removal if expected type default value is ''" + files: + snippets/card.liquid: | + {% doc %} + @param {object} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card', param: 123 %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected object, got number" + start: [0, 25] + highlight: "123" + suggest: + - message: Remove value + result: "{% render 'card', param: %}" + - name: "should handle when aliases `with` syntax is used" + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card' with 123 as param %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 22] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card' with '' as param %}" + - message: Remove value + result: "{% render 'card' with as param %}" + - name: "should handle when aliases `for` syntax is used" + files: + snippets/card.liquid: | + {% doc %} + @param {string} param - Description + {% enddoc %} +
    {{ param }}
    + file.liquid: "{% render 'card' for 123 as param %}" + docset: default + offenses: + - file: file.liquid + message: "Type mismatch for argument 'param': expected string, got number" + start: [0, 21] + highlight: "123" + suggest: + - message: "Replace with default value '''' for string" + result: "{% render 'card' for '' as param %}" + - message: Remove value + result: "{% render 'card' for as param %}" diff --git a/specs/checks/valid-schema-name.yml b/specs/checks/valid-schema-name.yml new file mode 100644 index 000000000..fe5796ea3 --- /dev/null +++ b/specs/checks/valid-schema-name.yml @@ -0,0 +1,39 @@ +check: ValidSchemaName +severity: error +cases: + - name: reports offense with schema name over 25 characters long + files: + sections/file.liquid: | + {% schema %} + { + "name": "test-schema-name-that-is-too-long" + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "Schema name 'test-schema-name-that-is-too-long' is too long (max 25 characters)" + start: [2, 12] + highlight: '"test-schema-name-that-is-too-long"' + - name: reports no offenses with schema name translation that exists and is not over 25 chars long + files: + locales/en.default.schema.json: '{ "default": { "my_translation_key": "My translation is good."}}' + code.liquid: |- + {% schema %} + { + "name": "t:default.my_translation_key" + } + {% endschema %} + - name: reports an offense with schema name translation that exists and is over 25 chars long + files: + locales/en.default.schema.json: '{ "my_translation_key": "My translation is tooooooooo long."}' + blocks/code.liquid: |- + {% schema %} + { + "name": "t:my_translation_key" + } + {% endschema %} + offenses: + - file: blocks/code.liquid + message: "Schema name 'My translation is tooooooooo long.' from 'locales/en.default.schema.json' is too long (max 25 characters)" + start: [2, 12] + highlight: '"t:my_translation_key"' diff --git a/specs/checks/valid-schema-translations.yml b/specs/checks/valid-schema-translations.yml new file mode 100644 index 000000000..0a1eac7e5 --- /dev/null +++ b/specs/checks/valid-schema-translations.yml @@ -0,0 +1,139 @@ +check: ValidSchemaTranslations +severity: error +cases: + - name: reports no offense when schema has no translation keys + files: + locales/en.default.schema.json: |- + {} + sections/file.liquid: |- + {% schema %} + { + "name": "My Section", + "settings": [ + { + "type": "text", + "id": "title", + "label": "Title" + } + ] + } + {% endschema %} + - name: reports no offense when all translation keys exist + files: + locales/en.default.schema.json: |- + { + "sections": { + "header": { + "name": "Header", + "settings": { + "title": { + "label": "Title", + "info": "Enter a title" + } + } + } + } + } + sections/file.liquid: |- + {% schema %} + { + "name": "t:sections.header.name", + "settings": [ + { + "type": "text", + "id": "title", + "label": "t:sections.header.settings.title.label", + "info": "t:sections.header.settings.title.info" + } + ] + } + {% endschema %} + - name: reports an offense when a translation key is missing + files: + locales/en.default.schema.json: |- + { + "sections": { + "header": { + "name": "Header" + } + } + } + sections/file.liquid: |- + {% schema %} + { + "name": "t:sections.header.missing_key" + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "'t:sections.header.missing_key' does not have a matching entry in 'locales/en.default.schema.json'" + start: [2, 12] + highlight: '"t:sections.header.missing_key"' + - name: reports multiple offenses when multiple translation keys are missing + files: + locales/en.default.schema.json: |- + { + "sections": { + "header": { + "name": "Header" + } + } + } + sections/file.liquid: |- + {% schema %} + { + "name": "t:sections.header.name", + "settings": [ + { + "type": "text", + "id": "title", + "label": "t:sections.header.settings.title.label", + "info": "t:sections.header.settings.title.info" + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "'t:sections.header.settings.title.label' does not have a matching entry in 'locales/en.default.schema.json'" + start: [7, 17] + highlight: '"t:sections.header.settings.title.label"' + - file: sections/file.liquid + message: "'t:sections.header.settings.title.info' does not have a matching entry in 'locales/en.default.schema.json'" + start: [8, 16] + highlight: '"t:sections.header.settings.title.info"' + - name: reports offense for missing translation in nested arrays + files: + locales/en.default.schema.json: |- + { + "sections": { + "header": { + "name": "Header" + } + } + } + sections/file.liquid: |- + {% schema %} + { + "name": "t:sections.header.name", + "blocks": [ + { + "type": "text", + "name": "t:sections.header.blocks.text.name" + } + ] + } + {% endschema %} + offenses: + - file: sections/file.liquid + message: "'t:sections.header.blocks.text.name' does not have a matching entry in 'locales/en.default.schema.json'" + start: [6, 16] + highlight: '"t:sections.header.blocks.text.name"' + - name: reports no offense when schema is invalid JSON + files: + locales/en.default.schema.json: |- + {} + sections/file.liquid: |- + {% schema %} + { invalid json } + {% endschema %} diff --git a/specs/checks/valid-schema.yml b/specs/checks/valid-schema.yml new file mode 100644 index 000000000..06b494a93 --- /dev/null +++ b/specs/checks/valid-schema.yml @@ -0,0 +1,13 @@ +check: ValidSchema +severity: error +cases: + - name: should not complain when a schema is invalid outside of sections + files: + file.liquid: |2 + {% schema %} + { + "shouldBeString": 420, + "shouldBeNumber": true, + "shouldBeIntegerLessThan51": 51 + } + {% endschema %} diff --git a/specs/checks/valid-scoped-css-class.yml b/specs/checks/valid-scoped-css-class.yml new file mode 100644 index 000000000..7523b76f8 --- /dev/null +++ b/specs/checks/valid-scoped-css-class.yml @@ -0,0 +1,317 @@ +check: ValidScopedCSSClass +severity: warning +cases: + - name: reports no offense when a class is defined in the local stylesheet + files: + sections/section.liquid: | + {% stylesheet %} + .my-class { color: red; } + {% endstylesheet %} +
    Hello
    + - name: does not report when a class is not defined anywhere in the theme + files: + sections/section.liquid: | + {% stylesheet %} + .defined-class { color: red; } + {% endstylesheet %} +
    Hello
    + - name: reports an offense when a class exists in the theme but is out of scope + files: + sections/section.liquid: | + {% stylesheet %} + .local-class { color: red; } + {% endstylesheet %} +
    Hello
    + snippets/other.liquid: | + {% stylesheet %} + .other-class { color: blue; } + {% endstylesheet %} + offenses: + - file: sections/section.liquid + message: "CSS class 'other-class' may be defined outside the scope of this file." + start: [3, 12] + highlight: other-class + - name: reports only out-of-scope theme classes, ignores unknown classes + files: + sections/section.liquid: | + {% stylesheet %} + .a { color: red; } + .c { color: blue; } + {% endstylesheet %} +
    Hello
    + snippets/other.liquid: | + {% stylesheet %} + .b { font-size: 14px; } + {% endstylesheet %} + offenses: + - file: sections/section.liquid + message: "CSS class 'b' may be defined outside the scope of this file." + start: [4, 14] + highlight: b + - name: handles multiple stylesheet tags + files: + sections/section.liquid: | + {% stylesheet %} + .from-first { color: red; } + {% endstylesheet %} + {% stylesheet %} + .from-second { color: blue; } + {% endstylesheet %} +
    Hello
    + - name: does not report when a class is in scope even if also defined out of scope + files: + sections/section.liquid: | + {% stylesheet %} + .shared-class { color: red; } + {% endstylesheet %} +
    Hello
    + snippets/unrelated.liquid: | + {% stylesheet %} + .shared-class { color: blue; } + {% endstylesheet %} + - name: skips dynamic Liquid output in class attribute values + files: + sections/section.liquid: | + {% stylesheet %} + .static { color: red; } + {% endstylesheet %} +
    Hello
    + - name: checks static parts and skips dynamic parts in mixed attributes + files: + sections/section.liquid: | + {% stylesheet %} + .static { color: red; } + {% endstylesheet %} +
    Hello
    + snippets/other.liquid: | + {% stylesheet %} + .out-of-scope { color: blue; } + {% endstylesheet %} + offenses: + - file: sections/section.liquid + message: "CSS class 'out-of-scope' may be defined outside the scope of this file." + start: [3, 19] + highlight: out-of-scope + - name: does not report partial tokens glued to Liquid nodes + files: + sections/section.liquid: | + {% stylesheet %} + .static { color: red; } + {% endstylesheet %} +
    Hello
    + snippets/other.liquid: | + {% stylesheet %} + .btn- { color: blue; } + {% endstylesheet %} + - name: extracts classes from complex selectors + files: + sections/section.liquid: | + {% stylesheet %} + .parent > .child { color: red; } + .a + .b ~ .c { color: blue; } + {% endstylesheet %} +
    Hello
    + - name: extracts classes from pseudo-class selectors + files: + sections/section.liquid: | + {% stylesheet %} + .btn:hover { color: red; } + .link::before { content: ''; } + {% endstylesheet %} + + - name: extracts classes from media queries + files: + sections/section.liquid: | + {% stylesheet %} + @media (min-width: 768px) { + .responsive { display: block; } + } + {% endstylesheet %} +
    Hello
    + - name: does not extract classes from CSS comments + files: + sections/section.liquid: | + {% stylesheet %} + /* .commented-out { color: red; } */ + .real { color: blue; } + {% endstylesheet %} +
    Hello
    + snippets/other.liquid: | + {% stylesheet %} + .commented-out { color: green; } + {% endstylesheet %} + offenses: + - file: sections/section.liquid + message: "CSS class 'commented-out' may be defined outside the scope of this file." + start: [4, 12] + highlight: commented-out + - name: checks void elements + files: + sections/section.liquid: | + {% stylesheet %} + .valid { color: red; } + {% endstylesheet %} + + snippets/other.liquid: | + {% stylesheet %} + .invalid { color: blue; } + {% endstylesheet %} + offenses: + - file: sections/section.liquid + message: "CSS class 'invalid' may be defined outside the scope of this file." + start: [3, 14] + highlight: invalid + - name: checks self-closing elements + files: + sections/section.liquid: | + {% stylesheet %} + .valid { color: red; } + {% endstylesheet %} + + snippets/other.liquid: | + {% stylesheet %} + .invalid { color: blue; } + {% endstylesheet %} + offenses: + - file: sections/section.liquid + message: "CSS class 'invalid' may be defined outside the scope of this file." + start: [3, 12] + highlight: invalid + - name: checks SVG elements and their children + files: + sections/section.liquid: | + {% stylesheet %} + .valid { color: red; } + {% endstylesheet %} + + + + + snippets/other.liquid: | + {% stylesheet %} + .out-of-scope { color: blue; } + {% endstylesheet %} + offenses: + - file: sections/section.liquid + message: "CSS class 'out-of-scope' may be defined outside the scope of this file." + start: [3, 12] + highlight: out-of-scope + - file: sections/section.liquid + message: "CSS class 'out-of-scope' may be defined outside the scope of this file." + start: [4, 15] + highlight: out-of-scope + - file: sections/section.liquid + message: "CSS class 'out-of-scope' may be defined outside the scope of this file." + start: [5, 12] + highlight: out-of-scope + - name: does not report for empty class attribute + files: + sections/section.liquid: | + {% stylesheet %} + .a { color: red; } + {% endstylesheet %} +
    Hello
    + - name: considers classes from an ancestor file as in scope + files: + sections/section.liquid: | + {% stylesheet %} + .parent-class { color: red; } + {% endstylesheet %} + {% render 'child' %} + snippets/child.liquid: | +
    Hello
    + references: + - { source: sections/section.liquid, target: snippets/child.liquid } + - name: follows grandparent ancestors via direct references + files: + layout/theme.liquid: | + {% stylesheet %} + .layout-class { color: red; } + {% endstylesheet %} + {% section 'section' %} + sections/section.liquid: | + {% render 'snippet' %} + snippets/snippet.liquid: | +
    Hello
    + references: + - { source: sections/section.liquid, target: snippets/snippet.liquid } + - { source: layout/theme.liquid, target: sections/section.liquid } + - name: does not consider indirect references as ancestors + files: + sections/section.liquid: | + {% stylesheet %} + .parent-class { color: red; } + {% endstylesheet %} + snippets/child.liquid: | + {% stylesheet %} + .local-class { color: blue; } + {% endstylesheet %} +
    Hello
    + references: + - { source: sections/section.liquid, target: snippets/child.liquid, type: indirect } + offenses: + - file: snippets/child.liquid + message: "CSS class 'parent-class' may be defined outside the scope of this file." + start: [3, 24] + highlight: parent-class + - name: considers classes from a rendered snippet as in scope + files: + sections/section.liquid: | + {% render 'styles' %} +
    Hello
    + snippets/styles.liquid: | + {% stylesheet %} + .snippet-class { color: red; } + {% endstylesheet %} + references: + - { source: sections/section.liquid, target: snippets/styles.liquid } + - name: considers classes from snippet descendants (recursive) + files: + sections/section.liquid: | + {% render 'outer' %} +
    Hello
    + snippets/outer.liquid: | + {% render 'inner' %} + snippets/inner.liquid: | + {% stylesheet %} + .deep-class { color: red; } + {% endstylesheet %} + references: + - { source: sections/section.liquid, target: snippets/outer.liquid } + - { source: snippets/outer.liquid, target: snippets/inner.liquid } + - name: considers classes from snippets rendered by ancestors + files: + sections/section.liquid: | + {% render 'styles' %} + {% render 'child' %} + snippets/styles.liquid: | + {% stylesheet %} + .style-class { color: red; } + {% endstylesheet %} + snippets/child.liquid: | +
    Hello
    + references: + - { source: sections/section.liquid, target: snippets/child.liquid } + - { source: sections/section.liquid, target: snippets/styles.liquid } + - name: considers classes from .css files in the assets folder + files: + assets/theme.css: '.asset-class { color: red; }' + sections/section.liquid: | +
    Hello
    + - name: considers classes from multiple .css asset files + files: + assets/base.css: '.base-class { color: red; }' + assets/components.css: '.component-class { color: blue; }' + sections/section.liquid: | +
    Hello
    + - name: reports no offenses when no CSS classes are defined in scope + files: + sections/section.liquid: | +
    Hello
    + - name: reports no offenses when there are no class attributes + files: + sections/section.liquid: | + {% stylesheet %} + .some-class { color: red; } + {% endstylesheet %} +
    Hello
    diff --git a/specs/checks/valid-settings-key.yml b/specs/checks/valid-settings-key.yml new file mode 100644 index 000000000..e16ff41e9 --- /dev/null +++ b/specs/checks/valid-settings-key.yml @@ -0,0 +1,171 @@ +check: ValidSettingsKey +severity: error +cases: + - name: does not report an error when default setting exists + files: + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"default":{"settings":{"existent-setting":"value"}}} + {% endschema %} + - name: reports an error when default setting does not exist + files: + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"default":{"settings":{"non-existent-setting":"value"}}} + {% endschema %} + offenses: + - file: sections/example.liquid + message: "Setting 'non-existent-setting' does not exist in schema." + start: [1, 127] + highlight: '"non-existent-setting"' + - name: does not report an error when missing default settings is referenced from a block schema + files: + blocks/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"default":{"settings":{"non-existent-setting":"value"}}} + {% endschema %} + - name: does not report an error when presets setting exists + files: + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"presets":[{"settings":{"existent-setting":"value"}}]} + {% endschema %} + - name: reports an error when presets setting does not exist + files: + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"presets":[{"settings":{"non-existent-setting-1":"value"}},{"settings":{"non-existent-setting-2":"value"}}]} + {% endschema %} + offenses: + - file: sections/example.liquid + message: "Setting 'non-existent-setting-1' does not exist in schema." + start: [1, 128] + highlight: '"non-existent-setting-1"' + - file: sections/example.liquid + message: "Setting 'non-existent-setting-2' does not exist in schema." + start: [1, 176] + highlight: '"non-existent-setting-2"' + - name: does not report an error when default block setting exists in referenced file + files: + blocks/referenced.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}]} + {% endschema %} + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"default":{"blocks":[{"type":"referenced","settings":{"existent-setting":"value"}}]}} + {% endschema %} + - name: default block settings > referenced blocks > does not report an error when referenced file does not exist + files: + blocks/referenced.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}]} + {% endschema %} + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"default":{"blocks":[{"type":"non-existent-file","settings":{"non-existent-setting":"value"}}]}} + {% endschema %} + - name: reports an error when default block setting does not exist in referenced file + files: + blocks/referenced.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}]} + {% endschema %} + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"default":{"blocks":[{"type":"referenced","settings":{"non-existent-setting":"value"}}]}} + {% endschema %} + offenses: + - file: sections/example.liquid + message: "Setting 'non-existent-setting' does not exist in 'blocks/referenced.liquid'." + start: [1, 158] + highlight: '"non-existent-setting"' + - name: reports an error when default block setting does not exist in existing local block + files: + sections/example.liquid: | + {% schema %} + {"blocks":[{"type":"local-block","name":"Local block","settings":[{"id":"local-setting","type":"text","label":"Local Setting"}]}],"default":{"blocks":[{"type":"local-block","settings":{"non-existent-setting-1":"value","non-existent-setting-2":"value"}}]}} + {% endschema %} + offenses: + - file: sections/example.liquid + message: "Setting 'non-existent-setting-1' does not exist in schema." + start: [1, 187] + highlight: '"non-existent-setting-1"' + - file: sections/example.liquid + message: "Setting 'non-existent-setting-2' does not exist in schema." + start: [1, 220] + highlight: '"non-existent-setting-2"' + - name: does not report an error when default block setting does not exist in non-existent local block + files: + sections/example.liquid: | + {% schema %} + {"blocks":[{"type":"local-block","name":"Local block","settings":[{"id":"local-setting","type":"text","label":"Local Setting"}]}],"default":{"blocks":[{"type":"non-existent-local-block","settings":{"non-existent-setting":"value"}}]}} + {% endschema %} + - name: does not report an error when default block setting exists in local block + files: + sections/example.liquid: | + {% schema %} + {"blocks":[{"type":"local-block","name":"Local block","settings":[{"id":"local-setting","type":"text","label":"Local Setting"}]}],"default":{"blocks":[{"type":"local-block","settings":{"local-setting":"value"}}]}} + {% endschema %} + - name: does not report an error when presets block setting exists in referenced file + files: + blocks/referenced.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}]} + {% endschema %} + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"presets":[{"blocks":[{"type":"referenced","settings":{"existent-setting":"value"}}]}]} + {% endschema %} + - name: presets block settings > referenced blocks > does not report an error when referenced file does not exist + files: + blocks/referenced.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}]} + {% endschema %} + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"presets":[{"blocks":[{"type":"non-existent-file","settings":{"non-existent-setting":"value"}}]}]} + {% endschema %} + - name: reports an error when presets block setting does not exist in referenced file + files: + blocks/referenced.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}]} + {% endschema %} + sections/example.liquid: | + {% schema %} + {"name":"Example","settings":[{"id":"existent-setting","type":"text","label":"Example Text Setting"}],"presets":[{"blocks":[{"type":"referenced","settings":{"non-existent-setting":"value"}}]}]} + {% endschema %} + offenses: + - file: sections/example.liquid + message: "Setting 'non-existent-setting' does not exist in 'blocks/referenced.liquid'." + start: [1, 159] + highlight: '"non-existent-setting"' + - name: reports an error when presets block setting does not exist in existing local block + files: + sections/example.liquid: | + {% schema %} + {"blocks":[{"type":"local-block","name":"Local block","settings":[{"id":"local-setting","type":"text","label":"Local Setting"}]}],"presets":[{"blocks":[{"type":"local-block","settings":{"non-existent-setting-1":"value","non-existent-setting-2":"value"}}]}]} + {% endschema %} + offenses: + - file: sections/example.liquid + message: "Setting 'non-existent-setting-1' does not exist in schema." + start: [1, 188] + highlight: '"non-existent-setting-1"' + - file: sections/example.liquid + message: "Setting 'non-existent-setting-2' does not exist in schema." + start: [1, 221] + highlight: '"non-existent-setting-2"' + - name: does not report an error when presets block setting does not exist in non-existent local block + files: + sections/example.liquid: | + {% schema %} + {"blocks":[{"type":"local-block","name":"Local block","settings":[{"id":"local-setting","type":"text","label":"Local Setting"}]}],"presets":[{"blocks":[{"type":"non-existent-local-block","settings":{"non-existent-setting":"value"}}]}]} + {% endschema %} + - name: does not report an error when presets block setting exists in local block + files: + sections/example.liquid: | + {% schema %} + {"blocks":[{"type":"local-block","name":"Local block","settings":[{"id":"local-setting","type":"text","label":"Local Setting"}]}],"presets":[{"blocks":[{"type":"local-block","settings":{"local-setting":"value"}}]}]} + {% endschema %} diff --git a/specs/checks/valid-standard-event-data.yml b/specs/checks/valid-standard-event-data.yml new file mode 100644 index 000000000..202d880b9 --- /dev/null +++ b/specs/checks/valid-standard-event-data.yml @@ -0,0 +1,129 @@ +check: ValidStandardEventData +severity: error +cases: + - name: reports an offense when the context is not a supported value for products + files: + file.liquid: "{{ product | standard_event_data: 'view', context: 'homepage' }}" + offenses: + - file: file.liquid + message: "Unsupported context 'homepage' for product. Valid values: page, search, collection, dialog, recommendation. The 'context' argument can also be omitted." + start: [0, 51] + highlight: "'homepage'" + - name: reports an offense when the context is not a supported value for carts + files: + file.liquid: "{{ cart | standard_event_data: 'view', context: 'banner' }}" + offenses: + - file: file.liquid + message: "Unsupported context 'banner' for cart. Valid values: page, dialog. The 'context' argument can also be omitted." + start: [0, 48] + highlight: "'banner'" + - name: reports an offense when the context is valid for products but the input is a cart + files: + file.liquid: "{{ cart | standard_event_data: 'view', context: 'recommendation' }}" + offenses: + - file: file.liquid + message: "Unsupported context 'recommendation' for cart. Valid values: page, dialog. The 'context' argument can also be omitted." + start: [0, 48] + highlight: "'recommendation'" + - name: does not report an offense on supported product context values (1) + files: + file.liquid: "{{ product | standard_event_data: 'view', context: 'page' }}" + - name: does not report an offense on supported product context values (2) + files: + file.liquid: "{{ product | standard_event_data: 'view', context: 'search' }}" + - name: does not report an offense on supported product context values (3) + files: + file.liquid: "{{ product | standard_event_data: 'view', context: 'collection' }}" + - name: does not report an offense on supported product context values (4) + files: + file.liquid: "{{ product | standard_event_data: 'view', context: 'dialog' }}" + - name: does not report an offense on supported product context values (5) + files: + file.liquid: "{{ product | standard_event_data: 'view', context: 'recommendation' }}" + - name: does not report an offense on supported cart context values (1) + files: + file.liquid: "{{ cart | standard_event_data: 'view', context: 'page' }}" + - name: does not report an offense on supported cart context values (2) + files: + file.liquid: "{{ cart | standard_event_data: 'view', context: 'dialog' }}" + - name: does not report an offense on collections, whose context is ignored + files: + file.liquid: "{{ collection | standard_event_data: 'view', context: 'homepage' }}" + - name: falls back to the union of contexts when the input is not a known global + files: + file.liquid: "{{ line_item.product | standard_event_data: 'view', context: 'recommendation' }}" + - name: falls back to the union of contexts when the filter input is chained + files: + file.liquid: "{{ cart | default: other_cart | standard_event_data: 'view', context: 'recommendation' }}" + - name: reports an offense when the context is outside the union for an unknown input + files: + file.liquid: "{{ line_item.product | standard_event_data: 'view', context: 'homepage' }}" + offenses: + - file: file.liquid + message: "Unsupported context 'homepage'. Valid values: page, search, collection, dialog, recommendation. The 'context' argument can also be omitted." + start: [0, 61] + highlight: "'homepage'" + - name: does not report an offense when the context is a variable + files: + file.liquid: "{{ product | standard_event_data: 'view', context: section.settings.context }}" + - name: reports an offense when the context is a non-string literal + files: + file.liquid: "{{ product | standard_event_data: 'view', context: 123 }}" + offenses: + - file: file.liquid + message: "Unsupported context for product. Valid values: page, search, collection, dialog, recommendation. The 'context' argument can also be omitted." + start: [0, 51] + highlight: "123" + - name: does not report an offense when the context argument is omitted + files: + file.liquid: "{{ collection | standard_event_data: 'view' }}" + - name: reports an offense when the event type is not supported + files: + file.liquid: "{{ product | standard_event_data: 'click', context: 'page' }}" + offenses: + - file: file.liquid + message: "Unsupported event type 'click'. The only supported event type is 'view'." + start: [0, 34] + highlight: "'click'" + - name: reports an offense when the event type is not supported on a collection + files: + file.liquid: "{{ collection | standard_event_data: 'click' }}" + offenses: + - file: file.liquid + message: "Unsupported event type 'click'. The only supported event type is 'view'." + start: [0, 37] + highlight: "'click'" + - name: does not report an offense when the event type is a variable + files: + file.liquid: "{{ product | standard_event_data: event_type }}" + - name: reports an offense when the event type is a non-string literal + files: + file.liquid: "{{ product | standard_event_data: 123 }}" + offenses: + - file: file.liquid + message: "Unsupported event type. The only supported event type is 'view'." + start: [0, 34] + highlight: "123" + - name: reports an offense when the event type is a boolean literal + files: + file.liquid: "{{ product | standard_event_data: true }}" + offenses: + - file: file.liquid + message: "Unsupported event type. The only supported event type is 'view'." + start: [0, 34] + highlight: "true" + - name: reports both offenses when the event type and the context are invalid + files: + file.liquid: "{{ product | standard_event_data: 'click', context: 'homepage' }}" + offenses: + - file: file.liquid + message: "Unsupported event type 'click'. The only supported event type is 'view'." + start: [0, 34] + highlight: "'click'" + - file: file.liquid + message: "Unsupported context 'homepage' for product. Valid values: page, search, collection, dialog, recommendation. The 'context' argument can also be omitted." + start: [0, 52] + highlight: "'homepage'" + - name: does not report an offense on other filters + files: + file.liquid: "{{ product | json }}{{ 'homepage' | append: 'view' }}" diff --git a/specs/checks/valid-static-block-type.yml b/specs/checks/valid-static-block-type.yml new file mode 100644 index 000000000..683201a6b --- /dev/null +++ b/specs/checks/valid-static-block-type.yml @@ -0,0 +1,17 @@ +check: ValidStaticBlockType +severity: error +cases: + - name: should not report any offenses if type is valid + files: + blocks/valid.liquid: Hello world! + blocks/file.liquid: '{% content_for "block", type: "valid", id: "static-block" %}' + - name: should report an offense if type is invalid + files: + blocks/valid.liquid: Hello world! + blocks/file.liquid: '{% content_for "block", type: "invalid", id: "static-block" %}' + offenses: + - file: blocks/file.liquid + message: "'blocks/invalid.liquid' does not exist" + start: [0, 30] + highlight: '"invalid"' + suggest: [] diff --git a/specs/checks/valid-visible-if.yml b/specs/checks/valid-visible-if.yml new file mode 100644 index 000000000..ddeb0c438 --- /dev/null +++ b/specs/checks/valid-visible-if.yml @@ -0,0 +1,1395 @@ +check: ValidVisibleIf +severity: error +cases: + - name: reports no error for a valid reference to a block schema setting (simple lookup) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings.some-block-setting }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a valid reference to a block schema setting (expression) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings.some-block-setting != \"null\" }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a valid reference to a global setting (simple lookup) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ settings.some-global-setting }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a valid reference to a global setting (expression) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ \"null\" != settings.some-global-setting }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a variable assigned in the Liquid file + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% liquid + assign effective_fill = 'image' + %} + + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ effective_fill == \"image\" }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a variable assigned after the schema tag + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ effective_fill == \"image\" }}" + } + ] + } + {% endschema %} + + {% assign effective_fill = 'image' %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a valid reference to a section schema (simple lookup) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ section.settings.some-section-setting }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a valid reference to a section schema (simple lookup) in a block schema + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "visible_if": "{{ section.settings.some-section-setting }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a valid reference to a section schema (expression) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ section.settings.some-section-setting != \"null\" }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports no error for a valid reference to a global setting (in theme settings) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ settings.some-global-setting }}" + } + ] + } + ] + - name: reports an error for an invalid reference to a global variable (in block) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ some-nonexistent-whatever }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: blocks/example.liquid + message: 'Invalid variable: "some-nonexistent-whatever" was not found.' + start: [9, 24] + highlight: some-nonexistent-whatever + - name: reports an error for an invalid reference to a block schema setting (simple lookup) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings.some-nonexistent-block-setting }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: blocks/example.liquid + message: 'Invalid variable: "block.settings.some-nonexistent-block-setting" was not found.' + start: [9, 24] + highlight: block.settings.some-nonexistent-block-setting + - name: reports an error for an invalid reference to a block schema setting (expression) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings.some-nonexistent-block-setting != \"null\" }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: blocks/example.liquid + message: 'Invalid variable: "block.settings.some-nonexistent-block-setting" was not found.' + start: [9, 24] + highlight: block.settings.some-nonexistent-block-setting + - name: reports an error for an invalid reference to a section schema (simple lookup) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ section.settings.some-nonexistent-section-setting }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: sections/example.liquid + message: 'Invalid variable: "section.settings.some-nonexistent-section-setting" was not found.' + start: [9, 24] + highlight: section.settings.some-nonexistent-section-setting + - name: reports an error for an invalid reference to a section schema (expression) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ section.settings.some-nonexistent-section-setting != \"null\" }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: sections/example.liquid + message: 'Invalid variable: "section.settings.some-nonexistent-section-setting" was not found.' + start: [9, 24] + highlight: section.settings.some-nonexistent-section-setting + - name: reports an error for an invalid reference to a global setting (in theme settings) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ settings.some-non-existent-setting }}" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: config/settings_schema.json + message: 'Invalid variable: "settings.some-non-existent-setting" was not found.' + start: [19, 26] + highlight: settings.some-non-existent-setting + - name: reports a detailed error when trying to use a block var in a section + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings.some-section-setting != \"null\" }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: sections/example.liquid + message: "Invalid visible_if: can't refer to \"block\" when not in a block file." + start: [9, 24] + highlight: block.settings.some-section-setting + - name: reports a detailed error when trying to use a block var in theme settings + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings.some-block-setting }}" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: config/settings_schema.json + message: "Invalid visible_if: can't refer to \"block\" when not in a block file." + start: [19, 26] + highlight: block.settings.some-block-setting + - name: reports a detailed error when attempting to use a namespace as a var + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: blocks/example.liquid + message: 'Invalid variable: "block.settings" refers to a namespace, but is being used here as a variable.' + start: [9, 24] + highlight: block.settings + - name: reports a detailed error when attempting to use a var as a namespace + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ block.settings.some-block-setting.another-thing }}" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: blocks/example.liquid + message: 'Invalid variable: "block.settings.some-block-setting" refers to a variable, but is being used here as a namespace.' + start: [9, 24] + highlight: block.settings.some-block-setting.another-thing + - name: reports visible_if fields declared with incorrect or missing brackets + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{% section.settings.some-section-setting != \"null\" %}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: sections/example.liquid + message: 'Invalid visible_if expression. It should take the form "{{ }}".' + start: [9, 20] + highlight: '"{% section.settings.some-section-setting != \"null\" %}"' + - name: reports malformed visible_if fields + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ section.settings.some-section-setting !r=erf \"null\" }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: sections/example.liquid + message: "Syntax error: cannot parse visible_if expression." + start: [9, 20] + highlight: '"{{ section.settings.some-section-setting !r=erf \"null\" }}"' + - name: does not reports when true or false is hardcoded (1) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ true }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: does not reports when true or false is hardcoded (2) + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ false }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + - name: reports a warning for unparseable expressions + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ something $ unparseable }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: sections/example.liquid + message: "Syntax error: cannot parse visible_if expression." + start: [9, 20] + highlight: '"{{ something $ unparseable }}"' + - name: reports when no variable lookup is found + checks: [ValidVisibleIf, ValidVisibleIf] + files: + sections/example.liquid: |2 + {% schema %} + { + "name": "My Section", + "settings": [ + { + "id": "some-section-setting" + }, + { + "id": "some-other-setting", + "visible_if": "{{ \"some random string\" }}" + } + ] + } + {% endschema %} + blocks/example.liquid: |2 + {% schema %} + { + "name": "My Block", + "settings": [ + { + "id": "some-block-setting" + } + ] + } + {% endschema %} + config/settings_schema.json: |- + [ + { + "name": "theme_info", + "theme_name": "Test Theme", + "theme_version": "0.1.0", + "theme_author": "Shopify" + }, + { + "name": "t:some.namespace", + "settings": [ + { + "type": "header", + "content": "t:some.other.namespace" + }, + { + "id": "some-global-setting" + } + ] + } + ] + offenses: + - check: ValidVisibleIf + file: sections/example.liquid + message: visible_if expression contains no references to any settings. This is likely an error. + start: [9, 20] + highlight: '"{{ \"some random string\" }}"' diff --git a/specs/checks/variable-name.yml b/specs/checks/variable-name.yml new file mode 100644 index 000000000..684e3bd09 --- /dev/null +++ b/specs/checks/variable-name.yml @@ -0,0 +1,81 @@ +check: VariableName +severity: warning +cases: + - name: should report an error when a variable is using wrong naming format + files: + file.liquid: '{% assign variableName = "value" %}' + offenses: + - file: file.liquid + message: "The variable 'variableName' uses wrong naming format" + start: [0, 10] + highlight: 'variableName = "value" ' + suggest: + - message: "Change variable 'variableName' to 'variable_name'" + result: '{% assign variable_name = "value" %}' + - name: should not report an error when a variable is using correct format + files: + file.liquid: '{% assign variable_name = "value" %}' + - name: should allow a single leading underscore + files: + file.liquid: | + {% assign _variable_name = "value" %} + {% capture _captured_value %}value{% endcapture %} + - name: should still enforce the naming format after a leading underscore + files: + file.liquid: '{% assign _variableName = "value" %}' + offenses: + - file: file.liquid + message: "The variable '_variableName' uses wrong naming format" + start: [0, 10] + highlight: '_variableName = "value" ' + suggest: + - message: "Change variable '_variableName' to '_variable_name'" + result: '{% assign _variable_name = "value" %}' + - name: should reject more than one leading underscore + files: + file.liquid: '{% assign __variable_name = "value" %}' + offenses: + - file: file.liquid + message: "The variable '__variable_name' uses wrong naming format" + start: [0, 10] + highlight: '__variable_name = "value" ' + suggest: + - message: "Change variable '__variable_name' to '_variable_name'" + result: '{% assign _variable_name = "value" %}' + - name: should provide a suggestion to change the variable naming + files: + file.liquid: '{% assign variableName = "value" %}' + offenses: + - file: file.liquid + message: "The variable 'variableName' uses wrong naming format" + start: [0, 10] + highlight: 'variableName = "value" ' + suggest: + - message: "Change variable 'variableName' to 'variable_name'" + result: '{% assign variable_name = "value" %}' + - name: should fix the code by changing the wrongly formatted variable + files: + file.liquid: '{% assign variableName = "value" %}' + offenses: + - file: file.liquid + message: "The variable 'variableName' uses wrong naming format" + start: [0, 10] + highlight: 'variableName = "value" ' + suggest: + - message: "Change variable 'variableName' to 'variable_name'" + result: '{% assign variable_name = "value" %}' + - name: should not complain about numbers inside variable names (1) + files: + file.liquid: '{% assign first_b_2_b_model = "value" %}' + - name: should not complain about numbers inside variable names (2) + files: + file.liquid: '{% assign first_b2_b_model = "value" %}' + - name: should not complain about numbers inside variable names (3) + files: + file.liquid: '{% assign first_b2b_model = "value" %}' + - name: should not complain about numbers inside variable names (4) + files: + file.liquid: '{% assign first_3_d_model = "value" %}' + - name: should not complain about numbers inside variable names (5) + files: + file.liquid: '{% assign first3_d_model = "value" %}' diff --git a/specs/graph/README.md b/specs/graph/README.md new file mode 100644 index 000000000..7b13a6e01 --- /dev/null +++ b/specs/graph/README.md @@ -0,0 +1,65 @@ +# Graph specs + +Executable, language-neutral specs for the theme graph: which files depend on which. Every +implementation runs them. In TypeScript that's `packages/theme-graph/src/specs.spec.ts`, which +runs with the rest of the vitest suite. To change what the graph does, change its spec first. + +## Format + +```yaml +cases: + - name: follows a render + mode: app # optional: theme or app. Without it, theme. + files: # the theme, by path from its root + sections/main.liquid: | + {% render 'card' %} + snippets/card.liquid: | + Card + entryPoints: [sections/main.liquid] # optional: every entry point + modules: # optional: every module + - { file: sections/main.liquid, type: Liquid, kind: section } + - { file: snippets/card.liquid, type: Liquid, kind: snippet } + edges: # every edge. Without it, none. + - source: sections/main.liquid + start: [0, 0] # line and character + text: "{% render 'card' %}" # from start to the edge's end + target: snippets/card.liquid +``` + +An edge goes from the file that depends to the file it depends on. `start` is where in the source +it happens: line and character, 0-based, in UTF-16 code units. `text` is what the edge covers. +It's cut out of the file at the range the graph reported, never searched for, so `start` and +`text` pin the range even when the text repeats. An edge that comes from no particular place, like +a template's layout, has neither. `type` is `direct` (the default), `indirect` or `preset`. + +An edge to a custom element's definition also has `targetStart` and `targetText`, where the +script defines it: + +```yaml +edges: + - source: sections/main.liquid + start: [0, 0] + text: "Shared snippet + entryPoints: [blocks/app-block.liquid] + modules: + - { file: blocks/app-block.liquid, type: Liquid, kind: block } + - { file: snippets/shared.liquid, type: Liquid, kind: snippet } + edges: + - source: blocks/app-block.liquid + start: [0, 0] + text: "{% render 'shared' %}" + target: snippets/shared.liquid diff --git a/specs/graph/json.yml b/specs/graph/json.yml new file mode 100644 index 000000000..7eda61077 --- /dev/null +++ b/specs/graph/json.yml @@ -0,0 +1,180 @@ +cases: + - name: "follows a template's sections and blocks, and its layout" + files: + layout/theme.liquid: | + {{ content_for_layout }} + templates/index.json: | + { + "sections": { + "main": { + "type": "custom-section", + "blocks": [ + { + "type": "group", + "blocks": { + "text1": { + "type": "text", + }, + }, + "block_order": [ + "text1" + ] + } + ] + } + } + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "custom section", + "blocks": [{ "type": "@theme" }, { "type": "_private" }] + } + {% endschema %} + blocks/group.liquid: | + Group + blocks/text.liquid: | + hello world + + {% schema %} + { + "name": "Text block", + "settings": [], + "presets": [ + { + "name": "Text block", + "category": "Text" + } + ] + } + {% endschema %} + edges: + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/group.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/text.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 37] + text: '"type": "_private"' + target: blocks/_private.liquid + - source: templates/index.json + target: layout/theme.liquid + type: indirect + - source: templates/index.json + start: [3, 6] + text: '"type": "custom-section"' + target: sections/custom-section.liquid + - source: templates/index.json + start: [6, 10] + text: '"type": "group"' + target: blocks/group.liquid + - source: templates/index.json + start: [9, 14] + text: '"type": "text"' + target: blocks/text.liquid + - name: "follows a section group's sections, blocks and static blocks" + files: + layout/theme.liquid: | + {% sections 'header-group' %} + sections/header-group.json: | + { + "sections": { + "header": { + "type": "custom-section", + "block_order": [ + "header", + "render-static", + ], + "blocks": { + "header": { + "type": "group", + "block_order": [ + "text" + ], + "blocks": { + "text": { + "type": "text", + } + }, + }, + "render-static": { + "type": "render-static", + "blocks": { + "static": { + "type": "_static", + "static": true, + } + } + } + } + } + }, + "order": [ + "header" + ], + } + sections/custom-section.liquid: | + {% schema %} + { + "name": "custom section", + "blocks": [{ "type": "@theme" }, { "type": "_private" }] + } + {% endschema %} + blocks/group.liquid: | + Group + blocks/text.liquid: | + Text + blocks/render-static.liquid: | + Render static + blocks/_static.liquid: | + Static + edges: + - source: layout/theme.liquid + start: [0, 0] + text: "{% sections 'header-group' %}" + target: sections/header-group.json + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/group.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/render-static.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/text.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 37] + text: '"type": "_private"' + target: blocks/_private.liquid + - source: sections/header-group.json + start: [3, 6] + text: '"type": "custom-section"' + target: sections/custom-section.liquid + - source: sections/header-group.json + start: [10, 10] + text: '"type": "group"' + target: blocks/group.liquid + - source: sections/header-group.json + start: [16, 14] + text: '"type": "text"' + target: blocks/text.liquid + - source: sections/header-group.json + start: [21, 10] + text: '"type": "render-static"' + target: blocks/render-static.liquid + - source: sections/header-group.json + start: [24, 14] + text: '"type": "_static"' + target: blocks/_static.liquid diff --git a/specs/graph/liquid.yml b/specs/graph/liquid.yml new file mode 100644 index 000000000..54cf375b8 --- /dev/null +++ b/specs/graph/liquid.yml @@ -0,0 +1,141 @@ +cases: + - name: "follows a layout's renders, assets, blocks and section groups" + files: + layout/theme.liquid: | + + + + + Document + + {{ 'theme.css' | asset_url | stylesheet_tag }} + {{ content_for_header }} + + + {% render 'layout-parent' %} + {% block 'layout-block' %}{% endblock %} + {% sections 'header-group' %} + {{ content_for_layout }} + + + snippets/layout-parent.liquid: | + {% render 'layout-child' %} + snippets/layout-child.liquid: | + Layout child + blocks/layout-block.liquid: | + {% render 'block-child' %} + snippets/block-child.liquid: | + Block child + sections/header-group.json: | + { "type": "header", "name": "Header", "sections": {}, "order": [] } + assets/theme.js: "" + assets/theme.css: "" + edges: + - source: blocks/layout-block.liquid + start: [0, 0] + text: "{% render 'block-child' %}" + target: snippets/block-child.liquid + - source: layout/theme.liquid + start: [5, 34] + text: "'theme.js' | asset_url " + target: assets/theme.js + - source: layout/theme.liquid + start: [6, 7] + text: "'theme.css' | asset_url | stylesheet_tag " + target: assets/theme.css + - source: layout/theme.liquid + start: [10, 4] + text: "{% render 'layout-parent' %}" + target: snippets/layout-parent.liquid + - source: layout/theme.liquid + start: [11, 4] + text: "{% block 'layout-block' %}{% endblock %}" + target: blocks/layout-block.liquid + - source: layout/theme.liquid + start: [12, 4] + text: "{% sections 'header-group' %}" + target: sections/header-group.json + - source: snippets/layout-parent.liquid + start: [0, 0] + text: "{% render 'layout-child' %}" + target: snippets/layout-child.liquid + - name: an app block in a block tag isn't followed, and theme blocks still are + files: + layout/theme.liquid: | + {% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %} + {% block 'text' %}{% endblock %} + blocks/text.liquid: | + Text + modules: + - { file: blocks/text.liquid, type: Liquid, kind: block } + - { file: layout/theme.liquid, type: Liquid, kind: layout } + edges: + - source: layout/theme.liquid + start: [1, 0] + text: "{% block 'text' %}{% endblock %}" + target: blocks/text.liquid + - name: an app block in a liquid tag isn't followed, and theme blocks still are + files: + layout/theme.liquid: | + {% liquid + block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' + endblock + %} + {% block 'text' %}{% endblock %} + blocks/text.liquid: | + Text + modules: + - { file: blocks/text.liquid, type: Liquid, kind: block } + - { file: layout/theme.liquid, type: Liquid, kind: layout } + edges: + - source: layout/theme.liquid + start: [4, 0] + text: "{% block 'text' %}{% endblock %}" + target: blocks/text.liquid + - name: follows a static block rendered with content_for + files: + sections/main.liquid: | + {% content_for 'block', type: 'render-static', id: 'render-static' %} + blocks/render-static.liquid: | + {% doc %} + This block renders the private _static block. Has no schema. + {% enddoc %} + +
    + {% content_for 'block', + type: '_static', + id: 'static', + content: 'hello world' + %} +
    + + {% schema %} + { + "name": "render static block", + "presets": [ + { + "name": "Render Static Block" + } + ] + } + {% endschema %} + blocks/_static.liquid: | + {% doc %} + this block is statically rendered + + @param {string} content + + {% enddoc %} + +
    + {{ content }} +
    + edges: + - source: blocks/render-static.liquid + start: [5, 2] + text: "{% content_for 'block',\n type: '_static',\n id: 'static',\n content: 'hello world'\n " + target: blocks/_static.liquid + - source: sections/main.liquid + start: [0, 0] + text: "{% content_for 'block', type: 'render-static', id: 'render-static' " + target: blocks/render-static.liquid diff --git a/specs/graph/modules.yml b/specs/graph/modules.yml new file mode 100644 index 000000000..6d9ebe18d --- /dev/null +++ b/specs/graph/modules.yml @@ -0,0 +1,102 @@ +cases: + - name: "a module's type and kind follow from its folder" + files: + layout/theme.liquid: | + {{ 'theme.js' | asset_url | script_tag }} + {{ 'theme.css' | asset_url | stylesheet_tag }} + {{ 'icon.svg' | asset_url }} + {{ 'logo.png' | asset_url }} + {% sections 'header-group' %} + {{ content_for_layout }} + templates/index.json: | + { + "sections": { + "main": { "type": "main" } + }, + "order": ["main"] + } + templates/page.liquid: | + {% render 'snippet' %} + sections/main.liquid: | + {% content_for 'block', type: 'text', id: 'text' %} + sections/header-group.json: | + { "type": "header", "name": "Header", "sections": {}, "order": [] } + blocks/text.liquid: | + {% render 'snippet' %} + snippets/snippet.liquid: | + Snippet + assets/theme.js: "" + assets/theme.css: "" + assets/icon.svg: | + + assets/logo.png: "" + modules: + - { file: assets/icon.svg, type: SVG, kind: unused } + - { file: assets/logo.png, type: Image, kind: unused } + - { file: assets/theme.css, type: CSS, kind: unused } + - { file: assets/theme.js, type: JavaScript, kind: unused } + - { file: blocks/text.liquid, type: Liquid, kind: block } + - { file: layout/theme.liquid, type: Liquid, kind: layout } + - { file: sections/header-group.json, type: JSON, kind: section-group } + - { file: sections/main.liquid, type: Liquid, kind: section } + - { file: snippets/snippet.liquid, type: Liquid, kind: snippet } + - { file: templates/index.json, type: JSON, kind: template } + - { file: templates/page.liquid, type: Liquid, kind: template } + edges: + - source: blocks/text.liquid + start: [0, 0] + text: "{% render 'snippet' %}" + target: snippets/snippet.liquid + - source: layout/theme.liquid + start: [0, 3] + text: "'theme.js' | asset_url | script_tag " + target: assets/theme.js + - source: layout/theme.liquid + start: [1, 3] + text: "'theme.css' | asset_url | stylesheet_tag " + target: assets/theme.css + - source: layout/theme.liquid + start: [2, 3] + text: "'icon.svg' | asset_url " + target: assets/icon.svg + - source: layout/theme.liquid + start: [3, 3] + text: "'logo.png' | asset_url " + target: assets/logo.png + - source: layout/theme.liquid + start: [4, 0] + text: "{% sections 'header-group' %}" + target: sections/header-group.json + - source: sections/main.liquid + start: [0, 0] + text: "{% content_for 'block', type: 'text', id: 'text' " + target: blocks/text.liquid + - source: templates/index.json + target: layout/theme.liquid + type: indirect + - source: templates/index.json + start: [2, 14] + text: '"type": "main"' + target: sections/main.liquid + - source: templates/page.liquid + start: [0, 0] + text: "{% render 'snippet' %}" + target: snippets/snippet.liquid + - name: "a file that's referenced but missing is a module that doesn't exist" + files: + sections/main.liquid: | + {% render 'missing' %} + {{ 'missing.js' | asset_url }} + modules: + - { file: assets/missing.js, type: JavaScript, kind: unused, exists: false } + - { file: sections/main.liquid, type: Liquid, kind: section } + - { file: snippets/missing.liquid, type: Liquid, kind: snippet, exists: false } + edges: + - source: sections/main.liquid + start: [0, 0] + text: "{% render 'missing' %}" + target: snippets/missing.liquid + - source: sections/main.liquid + start: [1, 3] + text: "'missing.js' | asset_url " + target: assets/missing.js diff --git a/specs/graph/theme-blocks.yml b/specs/graph/theme-blocks.yml new file mode 100644 index 000000000..ded91db99 --- /dev/null +++ b/specs/graph/theme-blocks.yml @@ -0,0 +1,136 @@ +cases: + - name: "@theme in a section's schema means every public theme block, and private ones are named" + files: + sections/custom-section.liquid: | + {% schema %} + { + "name": "custom section", + "blocks": [{ "type": "@theme" }, { "type": "_private" }] + } + {% endschema %} + blocks/group.liquid: | + Group + blocks/layout-block.liquid: | + Layout block + blocks/render-static.liquid: | + Render static + blocks/text.liquid: | + Text + blocks/_private.liquid: | + hoho I'm private + blocks/_static.liquid: | + Static + edges: + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/group.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/layout-block.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/render-static.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 15] + text: '"type": "@theme"' + target: blocks/text.liquid + type: indirect + - source: sections/custom-section.liquid + start: [3, 37] + text: '"type": "_private"' + target: blocks/_private.liquid + - name: "@theme and presets in a block's schema" + files: + sections/main.liquid: | + {% content_for 'block', type: 'group', id: 'group' %} + blocks/group.liquid: | + {% doc %} + Fixture block component that passes its blocks to the parent snippet + {% enddoc %} + + {% capture children %} + {%- content_for "blocks" -%} + {% endcapture %} + + {% render 'parent', children: children %} + + {% schema %} + { + "name": "Group block", + "blocks": [{ "type": "@theme" }], + "presets": [ + { + "name": "Group block", + "category": "Group", + "blocks": [ + { + "type": "text" + } + ] + } + ] + } + {% endschema %} + blocks/layout-block.liquid: | + Layout block + blocks/render-static.liquid: | + Render static + blocks/text.liquid: | + hello world + + {% schema %} + { + "name": "Text block", + "settings": [], + "presets": [ + { + "name": "Text block", + "category": "Text" + } + ] + } + {% endschema %} + blocks/_private.liquid: | + Private + snippets/parent.liquid: | + Parent + edges: + - source: blocks/group.liquid + start: [8, 0] + text: "{% render 'parent', children: children %}" + target: snippets/parent.liquid + - source: blocks/group.liquid + start: [13, 15] + text: '"type": "@theme"' + target: blocks/group.liquid + type: indirect + - source: blocks/group.liquid + start: [13, 15] + text: '"type": "@theme"' + target: blocks/layout-block.liquid + type: indirect + - source: blocks/group.liquid + start: [13, 15] + text: '"type": "@theme"' + target: blocks/render-static.liquid + type: indirect + - source: blocks/group.liquid + start: [13, 15] + text: '"type": "@theme"' + target: blocks/text.liquid + type: indirect + - source: blocks/group.liquid + start: [20, 10] + text: '"type": "text"' + target: blocks/text.liquid + type: preset + - source: sections/main.liquid + start: [0, 0] + text: "{% content_for 'block', type: 'group', id: 'group' " + target: blocks/group.liquid diff --git a/specs/graph/web-components.yml b/specs/graph/web-components.yml new file mode 100644 index 000000000..a0e63e46d --- /dev/null +++ b/specs/graph/web-components.yml @@ -0,0 +1,64 @@ +cases: + - name: links custom elements to the scripts that define them + files: + sections/main.liquid: | + {% render 'parent' %} + snippets/parent.liquid: | + {% doc %} + Parent snippet that renders the children and depends on the parent-element custom element + + @param {string} children + {% enddoc %} + + + {% render "child", children: children %} + + snippets/child.liquid: | + {% doc %} + Child fixture that depends on the child-element custom element + + @param {string} children + {% enddoc %} + + + {{ children }} + + assets/theme.js: | + window.customElements.define('parent-element', class extends HTMLElement { + + }); + + customElements.define('child-element', class extends HTMLElement { + + }); + edges: + - source: sections/main.liquid + start: [0, 0] + text: "{% render 'parent' %}" + target: snippets/parent.liquid + - source: snippets/child.liquid + start: [6, 0] + text: " + assets/theme.js: | + customElements.define('product-card', class extends HTMLElement {}); + - name: a theme without an assets folder has no web components + files: + sections/main.liquid: | + diff --git a/vitest.config.mjs b/vitest.config.mjs index e873d6399..ba1a2745e 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -13,8 +13,6 @@ const ciExclude = ['./packages/prettier-plugin-liquid']; */ const isolatedTests = [ 'packages/release-orchestrator/src/steps/getPackageJsonRecord.spec.ts', - 'packages/theme-check-common/src/checks/asset-size-css/index.spec.ts', - 'packages/theme-check-common/src/checks/asset-size-javascript/index.spec.ts', 'packages/theme-check-docs-updater/src/themeLiquidDocsManager.spec.ts', 'packages/theme-language-server-common/src/completions/providers/ContentForParameterCompletionProvider.spec.ts', 'packages/theme-language-server-common/src/completions/providers/ObjectCompletionProvider.spec.ts',