Skip to content

Interpolation @{var} in an at-rule prelude fails to parse (@media @{mq-xs}) #180

Description

@dshovchko
  • Node Version: 24.19.0
  • NPM Version: 11.17.0
  • postcss Version: 8.5.26
  • postcss-less Version: 6.0.0

Variable interpolation in an at-rule prelude - e.g. @media @{mq-xs} - throws CssSyntaxError: Unknown word. The same interpolation works fine in a selector position (@{mq-xs} { ... }), so the gap looks specific to at-rules.

LESS

@media @{mq-xs} { .a { color: red } }
@media @{mq-xs} and (min-width: 5px) { .a { color: red } }

/* these two parse fine today */
@media @mq-xs { .a { color: red } }
@{mq-xs} { .a { color: red } }

JavaScript

const postcssLess = require('postcss-less');

const samples = [
  '@media @{mq-xs} { .a { color: red } }',
  '@media @{mq-xs} and (min-width: 5px) { .a { color: red } }',
  '@media @mq-xs { .a { color: red } }',
  '@{mq-xs} { .a { color: red } }'
];

for (const css of samples) {
  try {
    const root = postcssLess.parse(css, { from: 'test.less' });
    console.log('OK    ', css, '->', JSON.stringify(root.first.params ?? root.first.selector));
  } catch (error) {
    console.log('THROWS', css, '->', error.message.split('\n')[0]);
  }
}

Errors

THROWS @media @{mq-xs} { .a { color: red } }
        -> test.less:1:10: Unknown word mq-xs
THROWS @media @{mq-xs} and (min-width: 5px) { .a { color: red } }
        -> test.less:1:10: Unknown word mq-xs
OK     @media @mq-xs { .a { color: red } } -> "@mq-xs"
OK     @{mq-xs} { .a { color: red } } -> "@{mq-xs}"

Expected Behavior

@media @{mq-xs} { ... } parses into an AtRule with name === 'media' and params === '@{mq-xs}', mirroring how the already-supported selector case @{mq-xs} { ... } is handled.

Actual Behavior

A CssSyntaxError is thrown.

How can we reproduce the behavior?

mkdir repro && cd repro
npm init -y && npm install postcss-less@6.0.0
# save the JavaScript snippet above as repro.js
node repro.js

Analysis

The tokenizer emits the interpolation as three separate tokens:

at-word "@media" | space | at-word "@" | { | word "mq-xs" | } | space | {

PostCSS's Parser#atrule only tracks ( and [ in its brackets stack, so on the interpolation's { it takes brackets.length === 0open = true; break. The prelude is cut short and mq-xs ends up parsed as an unknown word inside the block.

LessParser#atrule calls interpolation() only on the at-rule's first token, which returns false for @media (length > 1), so interpolations in the prelude are never merged. LessParser#rule already works around the same problem for selectors - this is the missing at-rule counterpart.

A fix that merges @{...} token sequences in the prelude before delegating to super.atrule() resolves all cases above with no regressions on my side (@import, @plugin, variable declarations, mixin calls, detached rulesets, @supports, @media with variables inside features, and @@var indirection all still round-trip). Happy to open a PR with tests if that's welcome.

Why this matters now

Less >=4.7.x deprecates the bare form and points users at exactly the syntax that cannot be parsed:

DEPRECATED WARNING: A bare @variable in an at-rule prelude is deprecated.
Use @{variable} interpolation instead.

So any codebase migrating off the deprecation becomes unparseable by postcss-less, and therefore un-lintable by stylelint. In our case that's ~1800 occurrences across ~900 files.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions