From c72da113ad8a05edf04e1566cefe33f08ba2fc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claud=C3=A9ric=20Demers?= Date: Tue, 20 Oct 2026 07:44:37 -0400 Subject: [PATCH 1/3] Add a remoteFileSize dependency AssetSizeCSS and AssetSizeJavaScript called fetch directly to get the size of a remote file, so the only way to fake it was vi.mock. They now get it from a new optional `remoteFileSize` dependency, which defaults to the same HEAD request. Co-Authored-By: Claude Opus 5.5 --- .changeset/remote-file-size-dependency.md | 5 +++++ .../theme-check-common/src/checks/asset-size-css/index.ts | 2 +- .../src/checks/asset-size-javascript/index.ts | 2 +- packages/theme-check-common/src/index.ts | 2 ++ packages/theme-check-common/src/types.ts | 7 +++++++ packages/theme-check-common/src/utils/file-utils.ts | 7 +++++-- 6 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .changeset/remote-file-size-dependency.md 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/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.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/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/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; } From 5a6dc07da7c349ec01e54e67a0979ec14fbded0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claud=C3=A9ric=20Demers?= Date: Tue, 20 Oct 2026 07:44:38 -0400 Subject: [PATCH 2/3] Replace check tests with executable specs Each check gets a language-neutral spec in specs/checks: a YAML list of cases, each a theme and every offense the check reports on it, with fixes and suggestions given as the resulting files. They live at the repo root because they're the contract every implementation runs. specs/checks/README.md describes the format: docsets, check options, app mode, several checks at once, autofix, the theme graph, generated files and remote files. packages/theme-check-common/src/specs.spec.ts runs them with the rest of the suite, and fails if a check has no spec. It also checks what a case doesn't state: each offense has its check's severity, and positions that agree with its file. Every check() call the check tests made became a spec case, recorded from the suite, re-run from the YAML and compared. A test was deleted when all of its calls became cases and everything it asserted was a function of what those calls returned. Statement, branch and function coverage of theme-check-common is unchanged. These stay in TypeScript: - tests that call no check, or assert on thrown errors or a reporter; - tests that need a dependency a spec can't state yet: JSON schemas, block or section schemas, doc definitions, or a custom file system; - the two ValidScopedCSSClass tests of running without the theme graph, since a spec always has one; - the two ValidStaticBlockType tests, which run every check and reach code in other checks that no other test does. Its spec has the same cases for the check alone; - the LiquidSyntaxError parity scenarios. Co-Authored-By: Claude Opus 5.5 --- .../app-block-missing-schema/index.spec.ts | 121 - .../checks/app-block-valid-tags/index.spec.ts | 79 +- .../src/checks/asset-preload/index.spec.ts | 100 - .../asset-size-app-block-css/index.spec.ts | 88 - .../index.spec.ts | 66 - .../src/checks/asset-size-css/index.spec.ts | 166 -- .../asset-size-javascript/index.spec.ts | 184 -- .../checks/block-content-usage/index.spec.ts | 52 - .../src/checks/block-id-usage/index.spec.ts | 76 - .../src/checks/cdn-preconnect/index.spec.ts | 40 - .../index.spec.ts | 65 - .../checks/deprecate-bgsizes/index.spec.ts | 41 - .../checks/deprecate-lazysizes/index.spec.ts | 26 - .../checks/deprecated-filter/index.spec.ts | 274 -- .../index.spec.ts | 613 ---- .../index.spec.ts | 120 - .../index.spec.ts | 102 - .../src/checks/deprecated-tag/index.spec.ts | 68 - .../duplicate-block-arguments/index.spec.ts | 68 - .../index.spec.ts | 98 - .../index.spec.ts | 120 - .../checks/empty-block-content/index.spec.ts | 117 - .../excessive-settings-count/index.spec.ts | 144 - .../src/checks/hardcoded-routes/index.spec.ts | 58 - .../checks/img-width-and-height/index.spec.ts | 79 - .../checks/json-missing-block/index.spec.ts | 435 --- .../checks/json-missing-section/index.spec.ts | 247 -- .../checks/json-syntax-error/index.spec.ts | 64 - .../checks/liquid-complexity/index.spec.ts | 218 -- .../checks/liquid-free-settings/index.spec.ts | 203 -- .../checks/InvalidBooleanExpression.spec.ts | 43 - .../checks/InvalidConditionalNode.spec.ts | 385 --- .../InvalidConditionalNodeParenthesis.spec.ts | 52 - .../checks/InvalidEchoValue.spec.ts | 108 - .../checks/InvalidFilterName.spec.ts | 223 -- .../checks/InvalidLoopArguments.spec.ts | 144 - .../checks/InvalidLoopRange.spec.ts | 84 - .../checks/InvalidPipeSyntax.spec.ts | 184 -- .../checks/MultipleAssignValues.spec.ts | 82 - .../liquid-html-syntax-error/index.spec.ts | 135 - .../checks/liquid-nesting-depth/index.spec.ts | 169 -- .../checks/liquid-syntax-error/index.spec.ts | 1789 +----------- .../matching-translations/index.spec.ts | 427 --- .../src/checks/max-file-size/index.spec.ts | 189 -- .../src/checks/missing-asset/index.spec.ts | 107 - .../missing-block-arguments/index.spec.ts | 200 -- .../index.spec.ts | 144 - .../index.spec.ts | 230 -- .../src/checks/missing-template/index.spec.ts | 110 - .../src/checks/orphaned-snippet/index.spec.ts | 58 - .../src/checks/pagination-size/index.spec.ts | 158 -- .../parser-blocking-script/index.spec.ts | 225 -- .../src/checks/raw-tags/index.spec.ts | 136 - .../src/checks/remote-asset/index.spec.ts | 345 --- .../index.spec.ts | 135 - .../reserved-doc-param-names/index.spec.ts | 51 - .../schema-presets-block-order/index.spec.ts | 344 --- .../index.spec.ts | 145 - .../index.spec.ts | 257 -- .../translation-key-exists/index.spec.ts | 191 -- .../unclosed-html-element/index.spec.ts | 315 --- .../src/checks/undefined-object/index.spec.ts | 667 ----- .../unique-doc-param-names/index.spec.ts | 35 - .../checks/unique-settings-id/index.spec.ts | 24 - .../checks/unique-settings-id/test-data.ts | 1191 -------- .../unique-static-block-id/index.spec.ts | 142 - .../src/checks/unknown-filter/index.spec.ts | 59 - .../index.spec.ts | 90 - .../index.spec.ts | 145 - .../index.spec.ts | 184 -- .../checks/unsupported-doc-tag/index.spec.ts | 10 - .../index.spec.ts | 121 - .../src/checks/unused-assign/index.spec.ts | 202 -- .../src/checks/unused-doc-param/index.spec.ts | 89 - .../valid-block-argument-types/index.spec.ts | 288 +- .../index.spec.ts | 110 - .../valid-block-tag-placement/index.spec.ts | 114 - .../checks/valid-block-target/index.spec.ts | 1389 +-------- .../index.spec.ts | 382 --- .../valid-content-for-arguments/index.spec.ts | 107 - .../valid-doc-param-types/index.spec.ts | 166 -- .../valid-html-translation/index.spec.ts | 74 - .../src/checks/valid-json/index.spec.ts | 10 - .../checks/valid-local-blocks/index.spec.ts | 286 -- .../index.spec.ts | 468 --- .../checks/valid-schema-name/index.spec.ts | 61 +- .../valid-schema-translations/index.spec.ts | 178 -- .../src/checks/valid-schema/index.spec.ts | 12 - .../valid-scoped-css-class/index.spec.ts | 750 +---- .../checks/valid-settings-key/index.spec.ts | 262 -- .../valid-standard-event-data/index.spec.ts | 185 -- .../src/checks/valid-visible-if/index.spec.ts | 656 ----- .../src/checks/variable-name/index.spec.ts | 95 - .../src/disabled-checks/index.spec.ts | 300 -- .../src/disabled-checks/test-checks.ts | 63 - .../src/liquid-doc/enum-arguments.spec.ts | 221 -- packages/theme-check-common/src/specs.spec.ts | 246 ++ .../src/test/block-fixtures.ts | 37 - specs/checks/README.md | 119 + specs/checks/app-block-missing-schema.yml | 98 + specs/checks/app-block-valid-tags.yml | 81 + specs/checks/asset-preload.yml | 72 + specs/checks/asset-size-app-block-css.yml | 54 + .../asset-size-app-block-javascript.yml | 41 + specs/checks/asset-size-css.yml | 84 + specs/checks/asset-size-javascript.yml | 92 + specs/checks/block-content-usage.yml | 77 + specs/checks/block-id-usage.yml | 50 + specs/checks/cdn-preconnect.yml | 27 + .../content-for-header-modification.yml | 44 + specs/checks/deprecate-bgsizes.yml | 30 + specs/checks/deprecate-lazysizes.yml | 16 + specs/checks/deprecated-filter.yml | 1464 ++++++++++ ...eprecated-fonts-on-sections-and-blocks.yml | 495 ++++ .../deprecated-fonts-on-settings-data.yml | 85 + .../deprecated-fonts-on-settings-schema.yml | 72 + specs/checks/deprecated-tag.yml | 76 + specs/checks/disabled-checks.yml | 461 +++ specs/checks/docsets/default.yml | 51 + specs/checks/duplicate-block-arguments.yml | 60 + .../duplicate-content-for-arguments.yml | 86 + .../duplicate-render-snippet-arguments.yml | 110 + specs/checks/empty-block-content.yml | 161 ++ specs/checks/excessive-settings-count.yml | 293 ++ specs/checks/hardcoded-routes.yml | 55 + specs/checks/img-width-and-height.yml | 58 + specs/checks/javascript-once-per-file.yml | 18 + specs/checks/javascript-tag-in-wrong-file.yml | 20 + specs/checks/json-missing-block.yml | 358 +++ specs/checks/json-missing-section.yml | 173 ++ specs/checks/json-syntax-error.yml | 57 + specs/checks/liquid-complexity.yml | 189 ++ specs/checks/liquid-doc-enum-arguments.yml | 2492 ++++++++++++++++ specs/checks/liquid-free-settings.yml | 311 ++ specs/checks/liquid-html-syntax-error.yml | 1714 +++++++++++ specs/checks/liquid-nesting-depth.yml | 170 ++ specs/checks/liquid-syntax-error.yml | 1400 +++++++++ specs/checks/matching-translations.yml | 819 ++++++ specs/checks/max-file-size.yml | 191 ++ specs/checks/missing-asset.yml | 48 + specs/checks/missing-block-arguments.yml | 219 ++ .../checks/missing-content-for-arguments.yml | 124 + .../missing-render-snippet-arguments.yml | 192 ++ specs/checks/missing-template.yml | 63 + specs/checks/orphaned-snippet.yml | 20 + specs/checks/pagination-size.yml | 109 + specs/checks/parser-blocking-script.yml | 114 + specs/checks/remote-asset.yml | 218 ++ specs/checks/required-layout-theme-object.yml | 116 + specs/checks/reserved-doc-param-names.yml | 93 + specs/checks/schema-once-per-file.yml | 18 + specs/checks/schema-presets-block-order.yml | 297 ++ specs/checks/schema-presets-static-blocks.yml | 130 + specs/checks/schema-section-or-block-only.yml | 25 + .../static-stylesheet-and-javascript-tags.yml | 167 ++ specs/checks/stylesheet-once-per-file.yml | 18 + specs/checks/stylesheet-tag-in-wrong-file.yml | 20 + specs/checks/translation-key-exists.yml | 149 + specs/checks/unclosed-html-element.yml | 352 +++ specs/checks/undefined-object.yml | 758 +++++ specs/checks/unique-doc-param-names.yml | 28 + specs/checks/unique-setting-id.yml | 1202 ++++++++ specs/checks/unique-static-block-id.yml | 89 + specs/checks/unknown-filter.yml | 55 + specs/checks/unrecognized-block-arguments.yml | 87 + .../unrecognized-content-for-arguments.yml | 97 + .../unrecognized-render-snippet-arguments.yml | 142 + specs/checks/unsupported-doc-tag.yml | 6 + specs/checks/unsupported-filter-arguments.yml | 81 + specs/checks/unused-assign.yml | 229 ++ specs/checks/unused-doc-param.yml | 119 + specs/checks/valid-block-argument-types.yml | 302 ++ .../valid-block-content-setting-type.yml | 132 + specs/checks/valid-block-tag-placement.yml | 620 ++++ specs/checks/valid-block-target.yml | 2508 +++++++++++++++++ .../valid-content-for-argument-types.yml | 488 ++++ specs/checks/valid-content-for-arguments.yml | 69 + specs/checks/valid-doc-param-types.yml | 456 +++ specs/checks/valid-html-translation.yml | 56 + specs/checks/valid-json.yml | 10 + specs/checks/valid-local-blocks.yml | 231 ++ .../valid-render-snippet-argument-types.yml | 602 ++++ specs/checks/valid-schema-name.yml | 39 + specs/checks/valid-schema-translations.yml | 139 + specs/checks/valid-schema.yml | 13 + specs/checks/valid-scoped-css-class.yml | 317 +++ specs/checks/valid-settings-key.yml | 171 ++ specs/checks/valid-standard-event-data.yml | 129 + specs/checks/valid-static-block-type.yml | 17 + specs/checks/valid-visible-if.yml | 1395 +++++++++ specs/checks/variable-name.yml | 81 + vitest.config.mjs | 2 - 192 files changed, 25738 insertions(+), 20464 deletions(-) delete mode 100644 packages/theme-check-common/src/checks/app-block-missing-schema/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/asset-preload/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/asset-size-app-block-css/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/asset-size-app-block-javascript/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/asset-size-css/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/asset-size-javascript/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/block-id-usage/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/cdn-preconnect/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/content-for-header-modification/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/deprecate-bgsizes/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/deprecate-lazysizes/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/deprecated-filter/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/deprecated-fonts-on-sections-and-blocks/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/deprecated-fonts-on-settings-data/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/deprecated-fonts-on-settings-schema/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/deprecated-tag/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/duplicate-block-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/duplicate-content-for-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/duplicate-render-snippet-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/empty-block-content/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/excessive-settings-count/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/hardcoded-routes/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/img-width-and-height/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/json-missing-block/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/json-missing-section/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/json-syntax-error/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-complexity/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-free-settings/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidBooleanExpression.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNode.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidConditionalNodeParenthesis.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidEchoValue.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidFilterName.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopArguments.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidLoopRange.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/InvalidPipeSyntax.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/MultipleAssignValues.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/liquid-nesting-depth/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/matching-translations/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/max-file-size/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/missing-asset/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/missing-block-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/missing-content-for-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/missing-render-snippet-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/missing-template/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/orphaned-snippet/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/pagination-size/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/parser-blocking-script/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/raw-tags/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/remote-asset/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/required-layout-theme-object/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/reserved-doc-param-names/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/schema-presets-block-order/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/schema-presets-static-blocks/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/static-stylesheet-and-javascript-tags/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/translation-key-exists/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unclosed-html-element/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/undefined-object/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unique-doc-param-names/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unique-settings-id/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unique-settings-id/test-data.ts delete mode 100644 packages/theme-check-common/src/checks/unique-static-block-id/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unknown-filter/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unrecognized-block-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unrecognized-content-for-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unrecognized-render-snippet-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unsupported-filter-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unused-assign/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/unused-doc-param/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-content-for-argument-types/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-content-for-arguments/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-doc-param-types/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-html-translation/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-local-blocks/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-render-snippet-argument-types/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-schema-translations/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-standard-event-data/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-visible-if/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/variable-name/index.spec.ts delete mode 100644 packages/theme-check-common/src/disabled-checks/index.spec.ts delete mode 100644 packages/theme-check-common/src/disabled-checks/test-checks.ts create mode 100644 packages/theme-check-common/src/specs.spec.ts delete mode 100644 packages/theme-check-common/src/test/block-fixtures.ts create mode 100644 specs/checks/README.md create mode 100644 specs/checks/app-block-missing-schema.yml create mode 100644 specs/checks/app-block-valid-tags.yml create mode 100644 specs/checks/asset-preload.yml create mode 100644 specs/checks/asset-size-app-block-css.yml create mode 100644 specs/checks/asset-size-app-block-javascript.yml create mode 100644 specs/checks/asset-size-css.yml create mode 100644 specs/checks/asset-size-javascript.yml create mode 100644 specs/checks/block-content-usage.yml create mode 100644 specs/checks/block-id-usage.yml create mode 100644 specs/checks/cdn-preconnect.yml create mode 100644 specs/checks/content-for-header-modification.yml create mode 100644 specs/checks/deprecate-bgsizes.yml create mode 100644 specs/checks/deprecate-lazysizes.yml create mode 100644 specs/checks/deprecated-filter.yml create mode 100644 specs/checks/deprecated-fonts-on-sections-and-blocks.yml create mode 100644 specs/checks/deprecated-fonts-on-settings-data.yml create mode 100644 specs/checks/deprecated-fonts-on-settings-schema.yml create mode 100644 specs/checks/deprecated-tag.yml create mode 100644 specs/checks/disabled-checks.yml create mode 100644 specs/checks/docsets/default.yml create mode 100644 specs/checks/duplicate-block-arguments.yml create mode 100644 specs/checks/duplicate-content-for-arguments.yml create mode 100644 specs/checks/duplicate-render-snippet-arguments.yml create mode 100644 specs/checks/empty-block-content.yml create mode 100644 specs/checks/excessive-settings-count.yml create mode 100644 specs/checks/hardcoded-routes.yml create mode 100644 specs/checks/img-width-and-height.yml create mode 100644 specs/checks/javascript-once-per-file.yml create mode 100644 specs/checks/javascript-tag-in-wrong-file.yml create mode 100644 specs/checks/json-missing-block.yml create mode 100644 specs/checks/json-missing-section.yml create mode 100644 specs/checks/json-syntax-error.yml create mode 100644 specs/checks/liquid-complexity.yml create mode 100644 specs/checks/liquid-doc-enum-arguments.yml create mode 100644 specs/checks/liquid-free-settings.yml create mode 100644 specs/checks/liquid-html-syntax-error.yml create mode 100644 specs/checks/liquid-nesting-depth.yml create mode 100644 specs/checks/liquid-syntax-error.yml create mode 100644 specs/checks/matching-translations.yml create mode 100644 specs/checks/max-file-size.yml create mode 100644 specs/checks/missing-asset.yml create mode 100644 specs/checks/missing-block-arguments.yml create mode 100644 specs/checks/missing-content-for-arguments.yml create mode 100644 specs/checks/missing-render-snippet-arguments.yml create mode 100644 specs/checks/missing-template.yml create mode 100644 specs/checks/orphaned-snippet.yml create mode 100644 specs/checks/pagination-size.yml create mode 100644 specs/checks/parser-blocking-script.yml create mode 100644 specs/checks/remote-asset.yml create mode 100644 specs/checks/required-layout-theme-object.yml create mode 100644 specs/checks/reserved-doc-param-names.yml create mode 100644 specs/checks/schema-once-per-file.yml create mode 100644 specs/checks/schema-presets-block-order.yml create mode 100644 specs/checks/schema-presets-static-blocks.yml create mode 100644 specs/checks/schema-section-or-block-only.yml create mode 100644 specs/checks/static-stylesheet-and-javascript-tags.yml create mode 100644 specs/checks/stylesheet-once-per-file.yml create mode 100644 specs/checks/stylesheet-tag-in-wrong-file.yml create mode 100644 specs/checks/translation-key-exists.yml create mode 100644 specs/checks/unclosed-html-element.yml create mode 100644 specs/checks/undefined-object.yml create mode 100644 specs/checks/unique-doc-param-names.yml create mode 100644 specs/checks/unique-setting-id.yml create mode 100644 specs/checks/unique-static-block-id.yml create mode 100644 specs/checks/unknown-filter.yml create mode 100644 specs/checks/unrecognized-block-arguments.yml create mode 100644 specs/checks/unrecognized-content-for-arguments.yml create mode 100644 specs/checks/unrecognized-render-snippet-arguments.yml create mode 100644 specs/checks/unsupported-doc-tag.yml create mode 100644 specs/checks/unsupported-filter-arguments.yml create mode 100644 specs/checks/unused-assign.yml create mode 100644 specs/checks/unused-doc-param.yml create mode 100644 specs/checks/valid-block-argument-types.yml create mode 100644 specs/checks/valid-block-content-setting-type.yml create mode 100644 specs/checks/valid-block-tag-placement.yml create mode 100644 specs/checks/valid-block-target.yml create mode 100644 specs/checks/valid-content-for-argument-types.yml create mode 100644 specs/checks/valid-content-for-arguments.yml create mode 100644 specs/checks/valid-doc-param-types.yml create mode 100644 specs/checks/valid-html-translation.yml create mode 100644 specs/checks/valid-json.yml create mode 100644 specs/checks/valid-local-blocks.yml create mode 100644 specs/checks/valid-render-snippet-argument-types.yml create mode 100644 specs/checks/valid-schema-name.yml create mode 100644 specs/checks/valid-schema-translations.yml create mode 100644 specs/checks/valid-schema.yml create mode 100644 specs/checks/valid-scoped-css-class.yml create mode 100644 specs/checks/valid-settings-key.yml create mode 100644 specs/checks/valid-standard-event-data.yml create mode 100644 specs/checks/valid-static-block-type.yml create mode 100644 specs/checks/valid-visible-if.yml create mode 100644 specs/checks/variable-name.yml 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 = ` -
- {% for block in section.blocks %} - {% case block.type %} - {% when 'link' %} - - - {% when 'custom-text' %} -
- {{ block.settings.custom-text-field }} -
- {% endcase %} - {% endfor %} -
- `; - - 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 = ` -
- {% for block in section.blocks %} - {% case block.type %} - {% when 'link' %} - - - {% when 'custom-text' %} -
- {{ block.settings.custom-text-field }} -
- {% endcase %} - {% endfor %} -
- `; - - 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 = ` -
- {% 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 %}`; - - 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-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/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/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/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/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', From 84fe4afa71c409b951707b9c25c910dc22af683a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claud=C3=A9ric=20Demers?= Date: Tue, 20 Oct 2026 08:28:51 -0400 Subject: [PATCH 3/3] Add executable graph specs The theme graph gets the same kind of specs as the checks: YAML in specs/graph/, run by packages/theme-graph/src/specs.spec.ts. Each case is a small theme and every edge the graph has for it, with positions as [line, character]. Cases about entry points or modules list those too. Each edge must also be a dependency of its source and a reference of its target. The 15 cases replace 12 of the 13 buildThemeGraph tests, which checked the skeleton and theme-app-extension fixtures, mostly with arrayContaining. Every fixture file is in some case verbatim, with the same outgoing edges it had in the fixture's graph, and theme-graph's coverage only goes up. The 'have a root URI' test stays, and so does the skeleton fixture it uses. Co-Authored-By: Claude Opus 5.5 --- .../theme-app-extension/assets/.gitkeep | 0 .../blocks/app-block.liquid | 9 - .../shopify.extension.toml | 2 - .../snippets/shared.liquid | 1 - packages/theme-graph/package.json | 3 +- packages/theme-graph/src/graph/build.spec.ts | 275 +----------------- .../theme-graph/src/graph/test-helpers.ts | 1 - packages/theme-graph/src/specs.spec.ts | 133 +++++++++ pnpm-lock.yaml | 3 + specs/graph/README.md | 65 +++++ specs/graph/entry-points.yml | 51 ++++ specs/graph/json.yml | 180 ++++++++++++ specs/graph/liquid.yml | 141 +++++++++ specs/graph/modules.yml | 102 +++++++ specs/graph/theme-blocks.yml | 136 +++++++++ specs/graph/web-components.yml | 64 ++++ 16 files changed, 880 insertions(+), 286 deletions(-) delete mode 100644 packages/theme-graph/fixtures/theme-app-extension/assets/.gitkeep delete mode 100644 packages/theme-graph/fixtures/theme-app-extension/blocks/app-block.liquid delete mode 100644 packages/theme-graph/fixtures/theme-app-extension/shopify.extension.toml delete mode 100644 packages/theme-graph/fixtures/theme-app-extension/snippets/shared.liquid create mode 100644 packages/theme-graph/src/specs.spec.ts create mode 100644 specs/graph/README.md create mode 100644 specs/graph/entry-points.yml create mode 100644 specs/graph/json.yml create mode 100644 specs/graph/liquid.yml create mode 100644 specs/graph/modules.yml create mode 100644 specs/graph/theme-blocks.yml create mode 100644 specs/graph/web-components.yml 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/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: | +