Skip to content

Add ValidScopedCSSClass theme check#1172

Merged
aswamy merged 1 commit intomainfrom
cross-file-css-usage-check
Apr 15, 2026
Merged

Add ValidScopedCSSClass theme check#1172
aswamy merged 1 commit intomainfrom
cross-file-css-usage-check

Conversation

@aswamy
Copy link
Copy Markdown
Contributor

@aswamy aswamy commented Apr 9, 2026

🗒️ Summary

Adds a new theme check ValidScopedCSSClass that warns when a CSS class used in an HTML class attribute may be defined outside the scope of the current file.

This helps theme developers catch cases where a CSS class is used in a file but is only defined in a stylesheet tag that isn't accessible from that file's scope — indicating a potential dependency issue or misplaced style.

❓ What is considered "in scope"

A CSS class is considered in scope for a file if it is defined in any of the following:

  • Local {% stylesheet %} tag — the file's own stylesheet
  • Ancestor {% stylesheet %} tags — any file that directly renders this file (via {% render %}, {% section %}, etc.), traversed upward through the full ancestor chain. Only direct type references count (not indirect or preset)
  • Rendered snippet stylesheets — any snippet the file renders via {% render %}, including that snippet's own snippet descendants (recursive)
  • Ancestor-rendered snippet stylesheets — any snippet rendered by an ancestor file, including descendants
  • CSS asset files — all .css files in the theme's assets/ directory are globally in scope

👍 What is NOT reported

  • Classes not defined anywhere in the theme — classes from external sources (CDNs, utility frameworks like Tailwind, inline JS injection, etc.) are silently ignored. The check only reports a class if it exists somewhere in the theme but isn't reachable from the current file's scope
  • Dynamic class attributes — Liquid output tags ({{ variable }}) in class attributes are skipped since they can't be statically analyzed

🎩 Tophatting

Via VSCode Extension

  • Checkout this branch
  • Run yarn install && yarn build
  • Press F5 to run the VScode extension in another window (might be fn + F5 for some)
  • Open a theme (or create one using Shopify CLI by running npx shopify theme init)
  • Add a few CSS classes in stylesheet tags or assets/*.css files

Run cli-like environment

  • Run the following command against the theme from this repo's root node packages/theme-check-node/dist/cli.js [path-to-theme] > result (replace [path-to-theme] with an actual path to theme)
    • See the theme check results from above appear in the result file it generated

@aswamy aswamy force-pushed the cross-file-css-usage-check branch 2 times, most recently from c4d206f to 70d6111 Compare April 10, 2026 15:23
@aswamy aswamy changed the title Add ValidCSSClassWithinStylesheet theme check Add ValidScopedCSSClass theme check Apr 10, 2026
"vscode-uri": "^3.0.7"
},
"devDependencies": {
"@shopify/theme-check-node": "^3.24.0"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to move it to a dev-dependency so we can let theme-check-node be dependent on the theme-graph.

Why? Because CLI needs the theme-graph to be able to run the theme-check to detect cross-file CSS usage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im going to use theme-check-common instead of theme-check-node in the binary and move any functions over that doesn't exist. I dont think anyone uses this raw binary anyway.

fs: NodeFileSystem,
getSectionSchema: getSectionSchemaFn as any,
getBlockSchema: getBlockSchemaFn as any,
getWebComponentDefinitionReference: () => undefined,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to recreate the whole graph because we don't need it for this theme-check, but might silently error out if we add a theme-check that needs this in the future 🤔

(but then again, we would silently fail if getReferences or getDependencies wasn't defined either so maybe not a HUGE deal 🤷)

@aswamy aswamy force-pushed the cross-file-css-usage-check branch 2 times, most recently from 5a957d9 to af42032 Compare April 13, 2026 19:11
@aswamy aswamy marked this pull request as ready for review April 13, 2026 21:28
@aswamy aswamy requested a review from a team as a code owner April 13, 2026 21:28
globPattern: '**/*.liquid',
},
{
globPattern: '**/assets/*.css',
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need this or CSS files in assets are going to remain cached from initial load when running themecheck

Comment thread packages/theme-check-node/src/index.ts Outdated
"vscode-uri": "^3.0.7"
},
"devDependencies": {
"@shopify/theme-check-node": "^3.24.0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

// 7. Collect ALL CSS classes defined anywhere in the theme
const allThemeClasses = await getAllThemeCSSClasses(fs, toUri, getCSSClassesForURI);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: getAllThemeCSSClasses is called inside onCodePathEnd, which runs once per checked file. Each invocation does a full recursive directory traversal and reads+parses every .liquid and .css file in the theme. For a theme with N files using class attributes and M total theme files, this means N full-theme scans. Even in the CLI path where getCSSClassesForURI is memoized per-URI, the directory listing itself repeats on every invocation. The same applies to getAssetCSSClasses.

The set of all theme CSS classes is a theme-wide constant. The established pattern in this codebase (e.g., sectionSchemas, blockSchemas, docDefinitions) is to precompute expensive run-scoped data once and share it through injected dependencies. Since this check is enabled by default, the cost scales with every theme that uses class attributes.

Suggestion: Cache the theme-wide class index once per lint run and share it through the injected dependencies, following the pattern used by other expensive run-scoped lookups in the codebase. For example, add getAllThemeCSSClasses?: () => Promise<Set<string>> to the Dependencies interface and have the host compute and cache it once. Alternatively, memoize the result within the check's create() closure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue isn't really a problem on VSCode Extension since we cache the entire fs. But on CLI, there is partial concern to what you're saying because even though getCSSClassesForURI is memoed, we are iterating over the files individually using recursiveReadDirectory. We could have just made getAllThemeCSSClasses like you suggested which would do it in bulk, but i feel like this breaks the current pattern we are going for here. We always pass a URI for a specific file and it returns its memoized content.

Ill try it out in a subsequent PR though because i still think we take around 18sec of wall-clock time and any gains is nice.

Comment thread packages/theme-check-common/src/utils/styles.ts Outdated
Copy link
Copy Markdown

@stephanie-shopify stephanie-shopify left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran a top hat, found one possible case to look at
Grandparent chain:
Layout → section → snippet: When a layout file defines a CSS class and renders a section, which in turn renders a snippet that uses that class, the check warns that the class is out of scope.

The setup:

  • layout/theme.liquid has {% stylesheet %} .layout-class { ... } {% endstylesheet %} and {{ content_for_layout }}
  • sections/section.liquid renders {% render 'deep-snippet' %}
  • snippets/deep-snippet.liquid uses <div class="layout-class">

@aswamy
Copy link
Copy Markdown
Contributor Author

aswamy commented Apr 15, 2026

Ran a top hat, found one possible case to look at Grandparent chain: Layout → section → snippet: When a layout file defines a CSS class and renders a section, which in turn renders a snippet that uses that class, the check warns that the class is out of scope.

The setup:

  • layout/theme.liquid has {% stylesheet %} .layout-class { ... } {% endstylesheet %} and {{ content_for_layout }}
  • sections/section.liquid renders {% render 'deep-snippet' %}
  • snippets/deep-snippet.liquid uses <div class="layout-class">

@stephanie-shopify Layout's are not considered direct parents of sections. Their relation is sort of just implied because of content_for_layout.

You'll see in the side bar, that the parent of sections/hello-world.liquid isn't a layout file, but the template json. I assume that's true for your situation as well. There are no true parents for those json files.

image

@aswamy
Copy link
Copy Markdown
Contributor Author

aswamy commented Apr 15, 2026

@graygilmore Thanks for the thorough tophat and review Gray!

I've completed the following fixes:
✅ Case sensitivity
✅ SVGs - a little tougher than i thought because we consider the SVG tag a "raw" node so i had to add a helper to parse it
✅ Dynamic Classes - we do make sure we distinguish between btn-{{ size }} and btn {{ size }}. The first one the entire class is considered dynamic and discarded. The second one, we only check btn to see if it appears in other stylesheet tag

I think "Conditional classes" and "Orphaned snippets" might be a little bit too niche. We can always update it once we get feedback on this.

if you have two orphaned snippets that create their own little tree you'll get a false positive warning when running theme check (but you won't get the warning when viewing the file in the editor it seems?):

That's a weird bug. I noticed that too. Happens in CLI but not VSCode extension? I wonder if it's a theme-graph issue for orphaned files because the orphan-styles file doesn't have any references in the side panel on VSCode. It should at least have orphan. Either way, orphaned files isn't too much of a concern for us.

@aswamy aswamy force-pushed the cross-file-css-usage-check branch 2 times, most recently from 8a684b7 to 4a2c9a4 Compare April 15, 2026 19:26
Copy link
Copy Markdown
Contributor

@graygilmore graygilmore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more small edge case

Comment thread packages/theme-check-common/src/utils/styles.ts Outdated
Add a new theme check that warns when a CSS class used in an HTML
class attribute may be defined outside the scope of the current file,
helping catch misplaced styles and dependency issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aswamy aswamy force-pushed the cross-file-css-usage-check branch from 4a2c9a4 to 7f6fdb4 Compare April 15, 2026 21:24
@aswamy aswamy merged commit 78a55fc into main Apr 15, 2026
8 checks passed
@aswamy aswamy deleted the cross-file-css-usage-check branch April 15, 2026 21:28
@aswamy aswamy added the #gsd:48741 Content Subsetting label Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:48741 Content Subsetting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants