🚨 Latest Research:Tanstack npm Packages Compromised in Ongoing Mini Shai-Hulud Supply-Chain Attack.Learn More
Socket
Book a DemoSign in
Socket

@wordpress/eslint-plugin

Package Overview
Dependencies
Maintainers
23
Versions
240
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@wordpress/eslint-plugin - npm Package Compare versions

Comparing version
24.2.1-next.v.202602241322.0
to
24.3.0
+81
rules/__tests__/no-ds-tokens.js
import { RuleTester } from 'eslint';
import rule from '../no-ds-tokens';
const ruleTester = new RuleTester( {
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true,
},
},
} );
ruleTester.run( 'no-ds-tokens', rule, {
valid: [
{
code: `const style = 'color: var(--my-custom-prop)';`,
},
{
code: `const style = 'color: blue';`,
},
{
code: 'const style = `border: 1px solid var(--other-prefix-token)`;',
},
{
code: `const name = 'something--wpds-color';`,
},
{
code: `<div style={ { color: 'var(--my-custom-prop)' } } />`,
},
],
invalid: [
{
code: `const style = 'color: var(--wpds-color-fg-content-neutral)';`,
errors: [
{
messageId: 'disallowed',
},
],
},
{
code: 'const style = `color: var(--wpds-color-fg-content-neutral)`;',
errors: [
{
messageId: 'disallowed',
},
],
},
{
code: `<div style={ { color: 'var(--wpds-color-fg-content-neutral)' } } />`,
errors: [
{
messageId: 'disallowed',
},
],
},
{
code: 'const style = `border: 1px solid var(--wpds-border-color, var(--wpds-border-fallback))`;',
errors: [
{
messageId: 'disallowed',
},
],
},
{
code: `const token = '--wpds-color-fg';`,
errors: [
{
messageId: 'disallowed',
},
],
},
{
code: 'const style = `--wpds-color-fg: red`;',
errors: [
{
messageId: 'disallowed',
},
],
},
],
} );
/**
* External dependencies
*/
import { RuleTester } from 'eslint';
/**
* Internal dependencies
*/
import rule from '../no-i18n-in-save';
const ruleTester = new RuleTester( {
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
} );
ruleTester.run( 'no-i18n-in-save', rule, {
valid: [
{
code: `
function edit() {
return __( 'Hello World' );
}
`,
},
{
code: `
const edit = () => {
return __( 'Hello World' );
};
`,
},
{
code: `
const settings = {
edit() {
return __( 'Hello World' );
},
};
`,
},
{
code: `
// Translation functions are fine in non-save files
function render() {
return __( 'Hello World' );
}
`,
},
{
code: `
// Translation in edit function
export default function Edit() {
return <div>{ __( 'Hello World' ) }</div>;
}
`,
},
{
code: `
// Translation in deprecated save function is allowed
function save() {
return __( 'Hello World' );
}
`,
filename: '/path/to/block/deprecated.js',
},
{
code: `
// Translation in deprecated save function with Windows path
function save() {
return __( 'Hello World' );
}
`,
filename: 'D:\\path\\to\\block\\deprecated.js',
},
],
invalid: [
{
code: `
// Translation in save file outside save function
function render() {
return __( 'Hello World' );
}
`,
filename: '/path/to/block/save.js',
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
return __( 'Hello World' );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const save = () => {
return __( 'Hello World' );
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const save = function() {
return __( 'Hello World' );
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
export default function save() {
return <span>{ __( 'Hello World' ) }</span>;
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const settings = {
save() {
return __( 'Hello World' );
},
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const settings = {
save: () => __( 'Hello World' ),
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
return _x( 'Hello', 'greeting' );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
const count = 5;
return _n( 'One item', 'Multiple items', count );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
const count = 5;
return _nx( 'One item', 'Multiple items', count, 'context' );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
return (
<button>
<span>{ __( 'Click me' ) }</span>
</button>
);
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
// Multiple translation calls in save
function save() {
const label = __( 'Label' );
return <div title={ _x( 'Title', 'context' ) }>{ label }</div>;
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
// Translation after a nested inner function named save must still be caught
function save() {
function save() {}
return __( 'Hello World' );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
],
} );
const DS_TOKEN_PREFIX = 'wpds-';
const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );
module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
meta: {
type: 'problem',
docs: {
description: 'Disallow any usage of --wpds-* CSS custom properties',
},
schema: [],
messages: {
disallowed:
'Design System tokens (--wpds-*) should not be used in this context.',
},
},
create( context ) {
const selector = `:matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral TemplateElement[value.raw=${ wpdsTokensRegex }])`;
return {
/** @param {import('estree').Literal | import('estree').TemplateElement} node */
[ selector ]( node ) {
context.report( {
node,
messageId: 'disallowed',
} );
},
};
},
} );
/**
* Internal dependencies
*/
const {
TRANSLATION_FUNCTIONS,
getTranslateFunctionName,
} = require( '../utils' );
module.exports = {
meta: {
type: 'problem',
schema: [],
messages: {
noI18nInSave:
'Translation functions should not be used in block save functions. Translated content is saved to the database and will not update if the language changes.',
},
docs: {
description: 'Disallow translation functions in block save methods',
category: 'Best Practices',
recommended: false,
},
},
create( context ) {
let saveFunctionDepth = 0;
const filename = context.getFilename();
// Skip deprecated files as they preserve old behavior including translation functions
const normalizedFilename = filename.replace( /\\/g, '/' );
const isDeprecatedFile =
normalizedFilename.includes( '/deprecated.js' ) ||
normalizedFilename.includes( '/deprecated.ts' ) ||
normalizedFilename.includes( '/deprecated.jsx' ) ||
normalizedFilename.includes( '/deprecated.tsx' );
if ( isDeprecatedFile ) {
return {};
}
const isSaveFile =
normalizedFilename.endsWith( '/save.js' ) ||
normalizedFilename.endsWith( '/save.ts' ) ||
normalizedFilename.endsWith( '/save.jsx' ) ||
normalizedFilename.endsWith( '/save.tsx' );
return {
// Track when we enter a function named 'save'
FunctionDeclaration( node ) {
if ( node.id && node.id.name === 'save' ) {
saveFunctionDepth++;
}
},
'FunctionDeclaration:exit'( node ) {
if ( node.id && node.id.name === 'save' ) {
saveFunctionDepth--;
}
},
// Track arrow functions assigned to 'save'
VariableDeclarator( node ) {
if (
node.id &&
node.id.name === 'save' &&
node.init &&
( node.init.type === 'ArrowFunctionExpression' ||
node.init.type === 'FunctionExpression' )
) {
saveFunctionDepth++;
}
},
'VariableDeclarator:exit'( node ) {
if (
node.id &&
node.id.name === 'save' &&
node.init &&
( node.init.type === 'ArrowFunctionExpression' ||
node.init.type === 'FunctionExpression' )
) {
saveFunctionDepth--;
}
},
// Track object properties named 'save'
'Property[key.name="save"]'( node ) {
if (
node.value &&
( node.value.type === 'FunctionExpression' ||
node.value.type === 'ArrowFunctionExpression' )
) {
saveFunctionDepth++;
}
},
'Property[key.name="save"]:exit'( node ) {
if (
node.value &&
( node.value.type === 'FunctionExpression' ||
node.value.type === 'ArrowFunctionExpression' )
) {
saveFunctionDepth--;
}
},
// Check for translation function calls
CallExpression( node ) {
const { callee } = node;
const functionName = getTranslateFunctionName( callee );
if ( ! TRANSLATION_FUNCTIONS.has( functionName ) ) {
return;
}
// Report if we're in a save file or inside a save function
if ( isSaveFile || saveFunctionDepth > 0 ) {
context.report( {
node,
messageId: 'noI18nInSave',
} );
}
},
};
},
};
+5
-5
{
"name": "@wordpress/eslint-plugin",
"version": "24.2.1-next.v.202602241322.0+bce7cff88",
"version": "24.3.0",
"description": "ESLint plugin for WordPress development.",

@@ -43,5 +43,5 @@ "author": "The WordPress Contributors",

"@typescript-eslint/parser": "^6.4.1",
"@wordpress/babel-preset-default": "^8.40.1-next.v.202602241322.0+bce7cff88",
"@wordpress/prettier-config": "^4.40.1-next.v.202602241322.0+bce7cff88",
"@wordpress/theme": "^0.8.1-next.v.202602241322.0+bce7cff88",
"@wordpress/babel-preset-default": "^8.41.0",
"@wordpress/prettier-config": "^4.41.0",
"@wordpress/theme": "^0.8.0",
"cosmiconfig": "^7.0.0",

@@ -82,3 +82,3 @@ "eslint-config-prettier": "^8.3.0",

},
"gitHead": "943dde7f0b600ce238726c36284bc9f70ce0ffa4"
"gitHead": "8bfc179b9aed74c0a6dd6e8edf7a49e40e4f87cc"
}

@@ -87,2 +87,3 @@ # ESLint Plugin

| [components-no-unsafe-button-disabled](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-unsafe-button-disabled.md) | Disallow using `disabled` on Button without `accessibleWhenDisabled`. | ✓ |
| [no-i18n-in-save](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-i18n-in-save.md) | Disallow translation functions in block save methods. | |
| [no-unguarded-get-range-at](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unguarded-get-range-at.md) | Disallow the usage of unguarded `getRangeAt` calls. | ✓ |

@@ -89,0 +90,0 @@ | [no-unsafe-wp-apis](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md) | Disallow the usage of unsafe APIs from `@wordpress/*` packages | ✓ |

@@ -30,2 +30,14 @@ import { RuleTester } from 'eslint';

},
{
code: `const token = 'var(--wpds-color-fg-content-neutral)';`,
},
{
code: `const name = 'something--wpds-color';`,
},
{
code: '`${ prefix }: var(--wpds-color-fg-content-neutral)`',
},
{
code: '`var(--wpds-color-fg-content-neutral) ${ suffix }`',
},
],

@@ -38,2 +50,5 @@ invalid: [

messageId: 'onlyKnownTokens',
data: {
tokenNames: "'--wpds-nonexistent-token'",
},
},

@@ -58,6 +73,77 @@ ],

messageId: 'onlyKnownTokens',
data: {
tokenNames: "'--wpds-nonexistent'",
},
},
],
},
{
code: `const token = 'var(--wpds-nonexistent-token)';`,
errors: [
{
messageId: 'onlyKnownTokens',
data: {
tokenNames: "'--wpds-nonexistent-token'",
},
},
],
},
{
code: 'const token = `var(--wpds-nonexistent-token)`;',
errors: [
{
messageId: 'onlyKnownTokens',
data: {
tokenNames: "'--wpds-nonexistent-token'",
},
},
],
},
{
code: 'const token = `var(--wpds-dimension-gap-${ size })`;',
errors: [
{
messageId: 'dynamicToken',
},
],
},
{
code: '<div style={ { gap: `var(--wpds-dimension-gap-${ size })` } } />',
errors: [
{
messageId: 'dynamicToken',
},
],
},
{
code: `const token = '--wpds-nonexistent-token';`,
errors: [
{
messageId: 'onlyKnownTokens',
data: {
tokenNames: "'--wpds-nonexistent-token'",
},
},
],
},
{
code: 'const style = `--wpds-dimension-gap-${ size }`;',
errors: [
{
messageId: 'dynamicToken',
},
],
},
{
code: '`${ prefix }: var(--wpds-nonexistent-token)`',
errors: [
{
messageId: 'onlyKnownTokens',
data: {
tokenNames: "'--wpds-nonexistent-token'",
},
},
],
},
],
} );

@@ -39,3 +39,3 @@ const tokenListModule = require( '@wordpress/theme/design-tokens.js' );

const knownTokens = new Set( tokenList );
const wpdsTokensRegex = new RegExp( `[^\\w]--${ DS_TOKEN_PREFIX }`, 'i' );
const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );

@@ -52,9 +52,80 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {

'The following CSS variables are not valid Design System tokens: {{ tokenNames }}',
dynamicToken:
'Design System tokens must not be dynamically constructed, as they cannot be statically verified for correctness or processed automatically to inject fallbacks.',
},
},
create( context ) {
const disallowedTokensAST = `JSXAttribute[name.name="style"] :matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral TemplateElement[value.raw=${ wpdsTokensRegex }])`;
const dynamicTemplateLiteralAST = `TemplateLiteral[expressions.length>0]:has(TemplateElement[value.raw=${ wpdsTokensRegex }])`;
const staticTokensAST = `:matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral[expressions.length=0] TemplateElement[value.raw=${ wpdsTokensRegex }])`;
const dynamicTokenEndRegex = new RegExp(
`--${ DS_TOKEN_PREFIX }[\\w-]*$`
);
return {
/**
* For template literals with expressions, check each quasi
* individually: flag as dynamic only when a `--wpds-*` token
* name is split across a quasi/expression boundary, and
* validate any complete static tokens normally.
*
* @param {import('estree').TemplateLiteral} node
*/
[ dynamicTemplateLiteralAST ]( node ) {
let hasDynamic = false;
const unknownTokens = [];
for ( const quasi of node.quasis ) {
const raw = quasi.value.raw;
const value = quasi.value.cooked ?? raw;
const isFollowedByExpression = ! quasi.tail;
if (
isFollowedByExpression &&
dynamicTokenEndRegex.test( raw )
) {
hasDynamic = true;
}
const tokens = extractCSSVariables(
value,
DS_TOKEN_PREFIX
);
// Remove the trailing incomplete token — it's the one
// being dynamically constructed by the next expression.
if ( isFollowedByExpression ) {
const endMatch = value.match( /(--([\w-]+))$/ );
if ( endMatch ) {
tokens.delete( endMatch[ 1 ] );
}
}
for ( const token of tokens ) {
if ( ! knownTokens.has( token ) ) {
unknownTokens.push( token );
}
}
}
if ( hasDynamic ) {
context.report( {
node,
messageId: 'dynamicToken',
} );
}
if ( unknownTokens.length > 0 ) {
context.report( {
node,
messageId: 'onlyKnownTokens',
data: {
tokenNames: unknownTokens
.map( ( token ) => `'${ token }'` )
.join( ', ' ),
},
} );
}
},
/** @param {import('estree').Literal | import('estree').TemplateElement} node */
[ disallowedTokensAST ]( node ) {
[ staticTokensAST ]( node ) {
let computedValue;

@@ -67,3 +138,2 @@

if ( typeof node.value === 'string' ) {
// Get the node's value when it's a "string"
computedValue = node.value;

@@ -74,3 +144,2 @@ } else if (

) {
// Get the node's value when it's a `template literal`
computedValue = node.value.cooked ?? node.value.raw;

@@ -77,0 +146,0 @@ }