Skip to content

feat(theme-check-common): warn against missing block.shopify_attributes when tag is null#1198

Open
SinhSinhAn wants to merge 1 commit intoShopify:mainfrom
SinhSinhAn:feat/block-missing-shopify-attributes
Open

feat(theme-check-common): warn against missing block.shopify_attributes when tag is null#1198
SinhSinhAn wants to merge 1 commit intoShopify:mainfrom
SinhSinhAn:feat/block-missing-shopify-attributes

Conversation

@SinhSinhAn
Copy link
Copy Markdown
Contributor

Summary

Closes #867.

When a block's {% schema %} declares "tag": null, Shopify generates no wrapping element. The block's own outermost element must contain {{ block.shopify_attributes }} for the theme editor to recognise it; without that, the block is invisible in the preview and reordering blocks leaves orphaned markup (docs). Today the linter is silent on this very common footgun — exactly the case @benjaminsehl shared from Shopify/horizon#2780.

Scope

Phase 1 from @benjaminsehl's comment on the issue: a single-file check. Cases where the block delegates to a snippet, or where block.shopify_attributes ends up rendered multiple times, are explicitly out of scope and can be layered on later.

The implementation closely mirrors @charlespwd's sketch on the issue, with the visitor name corrected (VariableLookup, not LiquidVariable) and the schema check moved into onCodePathEnd so traversal order doesn't matter.

What the check does

  • Runs on blocks/*.liquid only — section files and snippets are skipped via isBlock(context.file.uri).
  • Tracks any reference to block.shopify_attributes via the VariableLookup visitor. Both dot access and bracket access (block["shopify_attributes"]) are accepted because the parser normalises both into a String lookup with the same value.
  • In onCodePathEnd, parses the schema. Only flags when the tag field is explicitly null — a missing tag falls back to Shopify's default <div> wrapper, which receives the attributes itself, so the requirement does not apply.
  • Reports the offense at the JSON tag field's null value position so the editor squiggle lands inside the schema.

Behaviour matrix

Source tag renders block.shopify_attributes Reports?
blocks/text.liquid null no ✅ yes (1 offense)
blocks/text.liquid null yes ({{ block.shopify_attributes }}) no
blocks/text.liquid null yes (bracket form) no
blocks/text.liquid missing no no
blocks/text.liquid "section" no no
sections/foo.liquid null no no
snippets/helper.liquid n/a n/a no

Files

  • packages/theme-check-common/src/checks/block-missing-shopify-attributes/index.ts (new, 90 LOC)
  • packages/theme-check-common/src/checks/block-missing-shopify-attributes/index.spec.ts (new, 8 test cases)
  • packages/theme-check-common/src/checks/index.ts (registered in allChecks)
  • packages/theme-check-node/configs/all.yml and recommended.yml regenerated via yarn build
  • .changeset/block-missing-shopify-attributes.md (@shopify/theme-check-common: minor)

Test plan

  • 8 new tests covering each row in the table above
  • All 954 existing theme-check-common tests still pass
  • yarn build clean
  • prettier --check clean on all touched files

If maintainers prefer the offense to land on the entire {% schema %} opening line (matching EmptyBlockContent) rather than on the tag: null value specifically, happy to swap that with a one-line change.

Closes Shopify#867.

When a block's `{% schema %}` declares `"tag": null`, Shopify generates
no wrapping element for the block. The block's own outermost element
must include `{{ block.shopify_attributes }}` for the theme editor to
recognise it; without that, the block is unrecognised in the preview
and merchants reordering blocks leaves orphaned markup behind
(documented at https://shopify.dev/docs/storefronts/themes/architecture/blocks/theme-blocks/schema#tag).

This adds a new theme-check rule, scoped to phase 1 of the plan
@benjaminsehl outlined on the issue: a single-file check that runs on
`blocks/*.liquid`, parses the schema, and verifies the file's Liquid
output references `block.shopify_attributes` somewhere when the tag
is explicitly null.

Implementation notes:
- Uses `isBlock` to scope the check; section files and snippets are
  ignored.
- Tracks `block.shopify_attributes` references via the `VariableLookup`
  visitor. Both dot access and bracket access (`block["shopify_attributes"]`)
  are accepted because the parser normalises both to `String` lookups
  with the same `value`.
- Reports the offense on the JSON `tag` field's value (`null`) so the
  editor squiggle lands inside the schema, not on the entire file.
- Skipped when `tag` is missing or non-null: in those cases Shopify's
  own wrapper element receives the attributes, so the requirement does
  not apply.

Out of scope (phase 2/3 from the issue): cross-file checks for snippets
that supply the markup, and detecting `block.shopify_attributes`
rendered multiple times. Either can be layered on top of this check
without changing the public API.

8 new spec cases:
- Reports on `tag: null` with no usage (the bug)
- Accepts dot access on the wrapper
- Accepts bracket access
- Skips files with no `tag` field
- Skips files with a non-null `tag`
- Skips section files
- Skips snippet files
- Verifies the offense range falls on the `tag` value

954 / 954 tests pass overall.
@SinhSinhAn SinhSinhAn requested a review from a team as a code owner May 2, 2026 22:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warn against missing use of {{ block.shopify_attributes }} in blocks with tag: null

1 participant