Skip to content

Add executable check specs - #1347

Open
clauderic wants to merge 3 commits into
liquid-unclosed-block-endfrom
executable-specs
Open

clauderic wants to merge 3 commits into
liquid-unclosed-block-endfrom
executable-specs

Conversation

@clauderic

@clauderic clauderic commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1357, a parser fix. Review that first; this PR's base moves to main once it merges. The specs record the end-of-file positions that #1352 fixed.

What are you adding in this PR?

The check tests become executable, language-neutral specs: one YAML file per check in specs/checks/, at the repo root because every implementation runs them. Each case is a theme and every offense the check reports on it:

check: UnusedAssign
severity: warning
cases:
  - name: reports a variable that is assigned but not used
    files:
      snippets/example.liquid: |
        {% assign unused = 1 %}
    offenses:
      - file: snippets/example.liquid
        message: The variable 'unused' is assigned but not used
        start: [0, 0]
        highlight: "{% assign unused = 1 %}"
        suggest:
          - message: Remove the unused variable 'unused'
            result: "\n"
  • src/specs.spec.ts runs every spec as part of pnpm test, so they run in CI on every platform the suite does. It fails if a check has no spec.
  • An offense is where it starts, a 0-based [line, character] in UTF-16, and the text it highlights. The runner cuts the highlight out of the file at the range the check reported, so the two pin the range even when the text repeats. Fixes and suggestions are given as the resulting file.
  • A file too large to write out, like MaxFileSize's 20 MB ones, is a list of parts such as [{ repeat: a, times: 20971521 }], and so is a highlight of one.
  • Another implementation of theme-check can run the same files and know it behaves the same.
  • specs/checks/README.md covers the rest of the format: docsets, check options, app mode, several checks in one case, autofix, the theme graph (references), generated files and remote files.

The specs replace the tests they cover:

  • Before: theme-check-common had 5,361 tests.
  • After: it has 3,959 TypeScript tests and 1,680 spec cases, in 91 spec files.
  • 80 test files are deleted, and 14 more get smaller.

The theme graph gets the same kind of specs, in specs/graph/. Each case is a small theme and every edge the graph has for it:

edges:
  - source: sections/main.liquid
    start: [0, 0]
    text: "{% render 'card' %}"
    target: snippets/card.liquid
  • src/specs.spec.ts in theme-graph runs them.
  • Cases about entry points or module kinds list those too.
  • specs/graph/README.md covers the format.
  • The 13 cases replace 11 of the 12 buildThemeGraph tests, which checked two fixture themes with arrayContaining.
  • Every file of those fixtures is in some case verbatim, with the same outgoing edges, and theme-graph's coverage only goes up.
  • The have a root URI test stays, and so does the skeleton fixture it uses.

There are three commits:

  1. Add a remoteFileSize dependency. The one production change, described below.
  2. Replace check tests with executable specs. The format, the runner, the specs and the tests they replace. A few checks were converted by hand to shape the format, and a script converted the rest.
  3. Add executable graph specs.

How the conversion was done, and checked

  • Every check() call in the check tests was recorded from the running suite: its theme, checks, options, dependencies and offenses.
  • Each call became a case. The case was re-run from its YAML and had to give the same offenses, fixes and suggestions as the recorded call. Calls that didn't match stayed in TypeScript.
  • A test was deleted only when every call it made became a case, and every expect in it was about what those calls returned. The script checks this with the TypeScript compiler API, not by matching text.
  • Statement, branch and function coverage of theme-check-common is the same before and after the deletions.
  • Three cases are new, for what no test checked: DeprecatedFilter, UnknownFilter and UnrecognizedRenderSnippetArguments after a non-breaking space, which is one UTF-16 unit but two UTF-8 bytes. They caught a bug in the Rust port.
  • The Rust port in Add a Rust port of theme-check #1356 passes all 1,680 cases too.

A small production change

Before, AssetSizeCSS and AssetSizeJavaScript called fetch directly, so their tests could only fake remote sizes with vi.mock. There's now an optional remoteFileSize dependency, which defaults to the same HEAD request. A spec states a remote file in files, keyed by its URL, and the check sees only its size. It has a minor changeset.

What stays in TypeScript

  • Tests that call no check, or that assert on thrown errors or on a custom reporter.
  • Tests that need a dependency a spec can't state yet: JSON schemas (9 calls), block or section schemas (6), doc definitions (3) and a custom file system (2).
  • The two ValidScopedCSSClass tests that run without the theme graph, since a spec always has one.
  • Two UnsupportedDocTag tests that name their file file://sections/file.liquid, which isn't a theme path.
  • Two ValidStaticBlockType tests that run every check at once, and reach code in other checks that no other test does. Its spec has the same cases for the check alone.
  • The 3,600 generated LiquidSyntaxError parity scenarios in parity/.

What's next? Any followup issues?

What did you learn?

Comparing coverage before and after the deletions caught two tests the conversion shouldn't have deleted: ValidScopedCSSClass running without the theme graph. Their cases passed, but only because the specs runner always provides the graph, so they no longer tested that path. They're back in TypeScript.

Converting the asset-size tests also turned up one that only passed because an earlier test's mockReturnValue(true) survived vi.clearAllMocks().

Tophatting

  • npx vitest run packages/theme-check-common/src/specs.spec.ts runs just the specs.
  • npx vitest run packages/theme-graph/src/specs.spec.ts runs the 13 graph cases.
  • To see a spec fail, change any message, start or highlight in a spec file.

Before you deploy

  • I included a minor bump changeset
  • My feature is backward compatible

🤖 Generated with Claude Code

@clauderic
clauderic requested a review from a team as a code owner October 20, 2026 02:57
Base automatically changed from fix-trailing-pipes to main October 20, 2026 12:09
@clauderic
clauderic changed the base branch from main to web-components-without-assets October 20, 2026 12:46
@clauderic clauderic mentioned this pull request Oct 20, 2026
2 tasks done
@clauderic
clauderic changed the base branch from web-components-without-assets to offense-end-at-eof October 20, 2026 13:15
@clauderic
clauderic added this pull request to stack #1353 October 20, 2026 13:15
Comment thread specs/checks/valid-html-translation.yml Outdated
@clauderic
clauderic force-pushed the executable-specs branch 2 times, most recently from 8e89fa0 to b209a11 Compare October 20, 2026 15:28
@clauderic
clauderic force-pushed the executable-specs branch 2 times, most recently from 4065875 to b07c810 Compare October 20, 2026 16:23
Base automatically changed from offense-end-at-eof to main October 20, 2026 16:28
@clauderic
clauderic force-pushed the executable-specs branch 3 times, most recently from cd2693c to d07a87e Compare October 20, 2026 18:02
@clauderic clauderic mentioned this pull request Oct 20, 2026
1 task done
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 <noreply@anthropic.com>
@clauderic
clauderic removed this pull request from stack #1353 October 21, 2026 00:28
@clauderic
clauderic changed the base branch from main to liquid-unclosed-block-end October 21, 2026 00:28
@clauderic
clauderic added this pull request to stack #1358 October 21, 2026 00:29
clauderic and others added 2 commits October 21, 2026 18:15
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.

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 <noreply@anthropic.com>
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.

The 12 cases replace 11 of the 12 buildThemeGraph tests, which checked
the skeleton and theme-app-extension fixtures 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 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants