Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/eslint-plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Added [`no-unsafe-render-order`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/no-unsafe-render-order.md) rule to flag unsafe render-prop composition with `VisuallyHidden` and `Link`/`Text` ([#77428](https://github.com/WordPress/gutenberg/pull/77428)).

### Enhancements

- Improved the Design System token lint rules to recognize CSS declaration strings more accurately, avoiding false bare-token reports for declarations while still flagging invalid token assignments and unknown token names.
Comment thread
mirka marked this conversation as resolved.
Resolved

## 25.0.0 (2026-04-15)

### Breaking Changes
Expand Down
31 changes: 29 additions & 2 deletions packages/eslint-plugin/docs/rules/no-setting-ds-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Design System tokens (CSS custom properties beginning with `--wpds-`) are meant to be consumed, not set. Setting these properties can lead to unexpected behavior and breaks the Design System's theming capabilities. To customize parts of the Design System, use the `ThemeProvider` component from the [`@wordpress/theme`](https://wordpress.github.io/gutenberg/?path=/docs/design-system-theme-introduction--docs) package.

This rule lints all object property keys in JavaScript/TypeScript files. For CSS files, use the [corresponding Stylelint rule](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-theme/#stylelint-plugins) from the `@wordpress/theme` package.
This rule lints object property keys as well as CSS declaration strings and template literals when the declaration is clearly assigning into the `--wpds-*` namespace in JavaScript/TypeScript files. For CSS files, use the [corresponding Stylelint rule](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-theme/#stylelint-plugins) from the `@wordpress/theme` package.

## Rule details

Expand All @@ -16,17 +16,44 @@ Examples of **incorrect** code for this rule:
const styles = { '--wpds-color-fg-content-neutral': 'red' };
```

```js
const css = '--wpds-color-fg-content-neutral: red;';
```

```js
const css = `--wpds-color-fg-content-neutral: ${ value };`;
```

```js
const css = `--wpds-color-${ suffix }: red;`;
```

```jsx
<style>{ `--wpds-color-${ suffix }: red;` }</style>
```

Examples of **correct** code for this rule:

```jsx
<div style={ { color: 'var(--wpds-color-fg-content-neutral)' } } />
```

```js
const css = '--my-custom-prop: red;';
```

```js
const css = `--my-custom-prop-${ suffix }: red;`;
```

```jsx
<style>{ `--my-custom-prop-${ suffix }: red;` }</style>
```

```jsx
<div style={ { '--my-custom-prop': 'value' } } />
```

```jsx
<div style={ { margin: '10px' } } />
```

2 changes: 2 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unknown-ds-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Additionally, token names must not be dynamically constructed (e.g. via template

Tokens must also be wrapped in `var()` syntax (e.g. `var(--wpds-color-fg-content-neutral)`). The build tooling relies on this pattern to inject fallback values so that components render correctly without a ThemeProvider. Bare token references like `'--wpds-color-fg-content-neutral'` will not receive fallbacks.

When a token name appears on the left-hand side of a CSS declaration (for example, `'--wpds-color-fg-content-neutral: red;'`), the bare token check is skipped for that occurrence. Those cases are instead reported by [`no-setting-ds-tokens`](./no-setting-ds-tokens.md).

This rule lints all string literals and template literals in JavaScript/TypeScript files. For CSS files, use the [corresponding Stylelint rule](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-theme/#stylelint-plugins) from the `@wordpress/theme` package.

## Rule details
Expand Down
52 changes: 52 additions & 0 deletions packages/eslint-plugin/rules/__tests__/no-setting-ds-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ ruleTester.run( 'no-setting-ds-tokens', rule, {
{
code: `const { '--wpds-color-fg-content-neutral': neutralColor } = styles;`,
},
{
code: `const css = '--my-custom-prop: red;';`,
},
{
code: 'const css = `--my-custom-prop-${ suffix }: red;`;',
},
{
code: 'const css = `--my-custom-prop: red;`;',
},
{
code: '<style>{ `--my-custom-prop-${ suffix }: red;` }</style>',
},
],
invalid: [
{
Expand Down Expand Up @@ -77,5 +89,45 @@ ruleTester.run( 'no-setting-ds-tokens', rule, {
},
],
},
{
code: `const css = '--wpds-color-fg-content-neutral: red;';`,
errors: [
{
messageId: 'disallowedSet',
},
],
},
{
code: 'const css = `--wpds-color-fg-content-neutral: red;`;',
errors: [
{
messageId: 'disallowedSet',
},
],
},
{
code: 'const css = `--wpds-color-fg-content-neutral: ${ value };`;',
errors: [
{
messageId: 'disallowedSet',
},
],
},
{
code: 'const css = `--wpds-color-${ suffix }: red;`;',
errors: [
{
messageId: 'disallowedSet',
},
],
},
{
code: '<style>{ `--wpds-color-${ suffix }: red;` }</style>',
errors: [
{
messageId: 'disallowedSet',
},
],
},
],
} );
54 changes: 54 additions & 0 deletions packages/eslint-plugin/rules/__tests__/no-unknown-ds-tokens.js
Comment thread
mirka marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
{
code: `const style = { '--wpds-color-fg-content-neutral': 'red' };`,
},
{
code: `const css = '--wpds-color-fg-content-neutral: red;';`,
},
{
code: 'const css = `--wpds-color-fg-content-neutral: red;`;',
},
{
code: 'const css = `--wpds-color-fg-content-neutral: ${ value };`;',
},
],
invalid: [
{
Expand Down Expand Up @@ -147,6 +156,17 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
},
],
},
{
code: `const css = '--wpds-nonexistent-token: red;';`,
errors: [
{
messageId: 'onlyKnownTokens',
data: {
tokenNames: "'--wpds-nonexistent-token'",
},
},
],
},
{
code: `const token = '--wpds-color-fg-content-neutral';`,
errors: [
Expand Down Expand Up @@ -202,5 +222,39 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
},
],
},
{
code: `const css = '--wpds-color-fg-content-neutral: red; color: --wpds-color-bg-surface-neutral;';`,
errors: [
{
messageId: 'bareToken',
data: {
tokenNames: "'--wpds-color-bg-surface-neutral'",
},
},
],
},
{
code: `const css = '--wpds-color-fg-content-neutral: red; background: --wpds-color-fg-content-neutral;';`,
errors: [
{
messageId: 'bareToken',
data: {
tokenNames: "'--wpds-color-fg-content-neutral'",
},
},
],
},
{
code: `const css = '--wpds-other-nonexistent-token: red; color: var(--wpds-nonexistent-token);';`,
errors: [
{
messageId: 'onlyKnownTokens',
data: {
tokenNames:
"'--wpds-other-nonexistent-token', '--wpds-nonexistent-token'",
},
},
],
},
],
} );
4 changes: 1 addition & 3 deletions packages/eslint-plugin/rules/no-ds-tokens.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
const DS_TOKEN_PREFIX = 'wpds-';

const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );
const { wpdsTokensRegex } = require( '../utils/ds-token-utils' );

module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
meta: {
Expand Down
57 changes: 57 additions & 0 deletions packages/eslint-plugin/rules/no-setting-ds-tokens.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
const {
DS_TOKEN_PREFIX,
collectTokenOccurrences,
getStaticNodeValue,
wpdsTokensRegex,
} = require( '../utils/ds-token-utils' );

const wpdsDeclarationRegex = new RegExp(
`(?:^|[^\\w])--${ DS_TOKEN_PREFIX }[\\w-]+\\s*:`,
'i'
);
const dynamicDeclarationStartRegex = new RegExp(
`--${ DS_TOKEN_PREFIX }[\\w-]*$`
);
const dynamicDeclarationEndRegex = /^[\w-]*\s*:/;

module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
meta: {
type: 'problem',
Expand All @@ -12,6 +28,9 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
},
},
create( context ) {
const staticDeclarationAST = `:matches(Literal[value=${ wpdsDeclarationRegex }], TemplateLiteral[expressions.length=0] TemplateElement[value.raw=${ wpdsDeclarationRegex }])`;
const dynamicTemplateLiteralAST = `TemplateLiteral[expressions.length>0]:has(TemplateElement[value.raw=${ wpdsTokensRegex }])`;

return {
/** @param {import('estree').Property} node */
'ObjectExpression > Property[key.value=/^--wpds-/]'( node ) {
Expand All @@ -20,6 +39,44 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
messageId: 'disallowedSet',
} );
},
/** @param {import('estree').Literal | import('estree').TemplateElement} node */
[ staticDeclarationAST ]( node ) {
context.report( {
node,
messageId: 'disallowedSet',
} );
},
/** @param {import('estree').TemplateLiteral} node */
[ dynamicTemplateLiteralAST ]( node ) {
for ( let index = 0; index < node.quasis.length; index++ ) {
const quasi = node.quasis[ index ];
const value = getStaticNodeValue( quasi );
const nextValue = node.quasis[ index + 1 ]
? getStaticNodeValue( node.quasis[ index + 1 ] )
: undefined;

if ( ! value ) {
continue;
}
const hasStaticDeclaration = collectTokenOccurrences(
value,
DS_TOKEN_PREFIX
).some( ( { declaration } ) => declaration );
const hasSplitDeclaration =
! quasi.tail &&
dynamicDeclarationStartRegex.test( value ) &&
nextValue &&
dynamicDeclarationEndRegex.test( nextValue );

if ( hasStaticDeclaration || hasSplitDeclaration ) {
context.report( {
node,
messageId: 'disallowedSet',
} );
return;
}
}
},
};
},
} );
Loading
Loading