@wordpress/eslint-plugin 24.2.1-next.v.202602200903.0 → 24.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +5 -5
- package/rules/__tests__/no-ds-tokens.js +81 -0
- package/rules/__tests__/no-i18n-in-save.js +270 -0
- package/rules/__tests__/no-unknown-ds-tokens.js +86 -0
- package/rules/no-ds-tokens.js +29 -0
- package/rules/no-i18n-in-save.js +121 -0
- package/rules/no-unknown-ds-tokens.js +74 -5
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ The granular rulesets will not define any environment globals. As such, if they
|
|
|
84
84
|
| [no-base-control-with-label-without-id](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-base-control-with-label-without-id.md) | Disallow the usage of BaseControl component with a label prop set but omitting the id property. | ✓ |
|
|
85
85
|
| [components-no-missing-40px-size-prop](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-missing-40px-size-prop.md) | Disallow missing `__next40pxDefaultSize` prop on `@wordpress/components` components. | ✓ |
|
|
86
86
|
| [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`. | ✓ |
|
|
87
|
+
| [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. | |
|
|
87
88
|
| [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. | ✓ |
|
|
88
89
|
| [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 | ✓ |
|
|
89
90
|
| [no-unused-vars-before-return](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md) | Disallow assigning variable values if unused before a return. | ✓ |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wordpress/eslint-plugin",
|
|
3
|
-
"version": "24.
|
|
3
|
+
"version": "24.3.0",
|
|
4
4
|
"description": "ESLint plugin for WordPress development.",
|
|
5
5
|
"author": "The WordPress Contributors",
|
|
6
6
|
"license": "GPL-2.0-or-later",
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"@babel/eslint-parser": "7.25.7",
|
|
41
41
|
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
|
42
42
|
"@typescript-eslint/parser": "^6.4.1",
|
|
43
|
-
"@wordpress/babel-preset-default": "^8.
|
|
44
|
-
"@wordpress/prettier-config": "^4.
|
|
45
|
-
"@wordpress/theme": "^0.8.
|
|
43
|
+
"@wordpress/babel-preset-default": "^8.41.0",
|
|
44
|
+
"@wordpress/prettier-config": "^4.41.0",
|
|
45
|
+
"@wordpress/theme": "^0.8.0",
|
|
46
46
|
"cosmiconfig": "^7.0.0",
|
|
47
47
|
"eslint-config-prettier": "^8.3.0",
|
|
48
48
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
"publishConfig": {
|
|
79
79
|
"access": "public"
|
|
80
80
|
},
|
|
81
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "8bfc179b9aed74c0a6dd6e8edf7a49e40e4f87cc"
|
|
82
82
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../no-ds-tokens';
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester( {
|
|
5
|
+
parserOptions: {
|
|
6
|
+
ecmaVersion: 6,
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
ruleTester.run( 'no-ds-tokens', rule, {
|
|
14
|
+
valid: [
|
|
15
|
+
{
|
|
16
|
+
code: `const style = 'color: var(--my-custom-prop)';`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: `const style = 'color: blue';`,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: 'const style = `border: 1px solid var(--other-prefix-token)`;',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: `const name = 'something--wpds-color';`,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: `<div style={ { color: 'var(--my-custom-prop)' } } />`,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
invalid: [
|
|
32
|
+
{
|
|
33
|
+
code: `const style = 'color: var(--wpds-color-fg-content-neutral)';`,
|
|
34
|
+
errors: [
|
|
35
|
+
{
|
|
36
|
+
messageId: 'disallowed',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: 'const style = `color: var(--wpds-color-fg-content-neutral)`;',
|
|
42
|
+
errors: [
|
|
43
|
+
{
|
|
44
|
+
messageId: 'disallowed',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
code: `<div style={ { color: 'var(--wpds-color-fg-content-neutral)' } } />`,
|
|
50
|
+
errors: [
|
|
51
|
+
{
|
|
52
|
+
messageId: 'disallowed',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
code: 'const style = `border: 1px solid var(--wpds-border-color, var(--wpds-border-fallback))`;',
|
|
58
|
+
errors: [
|
|
59
|
+
{
|
|
60
|
+
messageId: 'disallowed',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
code: `const token = '--wpds-color-fg';`,
|
|
66
|
+
errors: [
|
|
67
|
+
{
|
|
68
|
+
messageId: 'disallowed',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
code: 'const style = `--wpds-color-fg: red`;',
|
|
74
|
+
errors: [
|
|
75
|
+
{
|
|
76
|
+
messageId: 'disallowed',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
} );
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { RuleTester } from 'eslint';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import rule from '../no-i18n-in-save';
|
|
10
|
+
|
|
11
|
+
const ruleTester = new RuleTester( {
|
|
12
|
+
parserOptions: {
|
|
13
|
+
ecmaVersion: 6,
|
|
14
|
+
sourceType: 'module',
|
|
15
|
+
ecmaFeatures: {
|
|
16
|
+
jsx: true,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} );
|
|
20
|
+
|
|
21
|
+
ruleTester.run( 'no-i18n-in-save', rule, {
|
|
22
|
+
valid: [
|
|
23
|
+
{
|
|
24
|
+
code: `
|
|
25
|
+
function edit() {
|
|
26
|
+
return __( 'Hello World' );
|
|
27
|
+
}
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: `
|
|
32
|
+
const edit = () => {
|
|
33
|
+
return __( 'Hello World' );
|
|
34
|
+
};
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: `
|
|
39
|
+
const settings = {
|
|
40
|
+
edit() {
|
|
41
|
+
return __( 'Hello World' );
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: `
|
|
48
|
+
// Translation functions are fine in non-save files
|
|
49
|
+
function render() {
|
|
50
|
+
return __( 'Hello World' );
|
|
51
|
+
}
|
|
52
|
+
`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: `
|
|
56
|
+
// Translation in edit function
|
|
57
|
+
export default function Edit() {
|
|
58
|
+
return <div>{ __( 'Hello World' ) }</div>;
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: `
|
|
64
|
+
// Translation in deprecated save function is allowed
|
|
65
|
+
function save() {
|
|
66
|
+
return __( 'Hello World' );
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
filename: '/path/to/block/deprecated.js',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
code: `
|
|
73
|
+
// Translation in deprecated save function with Windows path
|
|
74
|
+
function save() {
|
|
75
|
+
return __( 'Hello World' );
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
filename: 'D:\\path\\to\\block\\deprecated.js',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
invalid: [
|
|
82
|
+
{
|
|
83
|
+
code: `
|
|
84
|
+
// Translation in save file outside save function
|
|
85
|
+
function render() {
|
|
86
|
+
return __( 'Hello World' );
|
|
87
|
+
}
|
|
88
|
+
`,
|
|
89
|
+
filename: '/path/to/block/save.js',
|
|
90
|
+
errors: [
|
|
91
|
+
{
|
|
92
|
+
messageId: 'noI18nInSave',
|
|
93
|
+
type: 'CallExpression',
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
code: `
|
|
99
|
+
function save() {
|
|
100
|
+
return __( 'Hello World' );
|
|
101
|
+
}
|
|
102
|
+
`,
|
|
103
|
+
errors: [
|
|
104
|
+
{
|
|
105
|
+
messageId: 'noI18nInSave',
|
|
106
|
+
type: 'CallExpression',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
code: `
|
|
112
|
+
const save = () => {
|
|
113
|
+
return __( 'Hello World' );
|
|
114
|
+
};
|
|
115
|
+
`,
|
|
116
|
+
errors: [
|
|
117
|
+
{
|
|
118
|
+
messageId: 'noI18nInSave',
|
|
119
|
+
type: 'CallExpression',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
code: `
|
|
125
|
+
const save = function() {
|
|
126
|
+
return __( 'Hello World' );
|
|
127
|
+
};
|
|
128
|
+
`,
|
|
129
|
+
errors: [
|
|
130
|
+
{
|
|
131
|
+
messageId: 'noI18nInSave',
|
|
132
|
+
type: 'CallExpression',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
code: `
|
|
138
|
+
export default function save() {
|
|
139
|
+
return <span>{ __( 'Hello World' ) }</span>;
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
errors: [
|
|
143
|
+
{
|
|
144
|
+
messageId: 'noI18nInSave',
|
|
145
|
+
type: 'CallExpression',
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
code: `
|
|
151
|
+
const settings = {
|
|
152
|
+
save() {
|
|
153
|
+
return __( 'Hello World' );
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
`,
|
|
157
|
+
errors: [
|
|
158
|
+
{
|
|
159
|
+
messageId: 'noI18nInSave',
|
|
160
|
+
type: 'CallExpression',
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
code: `
|
|
166
|
+
const settings = {
|
|
167
|
+
save: () => __( 'Hello World' ),
|
|
168
|
+
};
|
|
169
|
+
`,
|
|
170
|
+
errors: [
|
|
171
|
+
{
|
|
172
|
+
messageId: 'noI18nInSave',
|
|
173
|
+
type: 'CallExpression',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
code: `
|
|
179
|
+
function save() {
|
|
180
|
+
return _x( 'Hello', 'greeting' );
|
|
181
|
+
}
|
|
182
|
+
`,
|
|
183
|
+
errors: [
|
|
184
|
+
{
|
|
185
|
+
messageId: 'noI18nInSave',
|
|
186
|
+
type: 'CallExpression',
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
code: `
|
|
192
|
+
function save() {
|
|
193
|
+
const count = 5;
|
|
194
|
+
return _n( 'One item', 'Multiple items', count );
|
|
195
|
+
}
|
|
196
|
+
`,
|
|
197
|
+
errors: [
|
|
198
|
+
{
|
|
199
|
+
messageId: 'noI18nInSave',
|
|
200
|
+
type: 'CallExpression',
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
code: `
|
|
206
|
+
function save() {
|
|
207
|
+
const count = 5;
|
|
208
|
+
return _nx( 'One item', 'Multiple items', count, 'context' );
|
|
209
|
+
}
|
|
210
|
+
`,
|
|
211
|
+
errors: [
|
|
212
|
+
{
|
|
213
|
+
messageId: 'noI18nInSave',
|
|
214
|
+
type: 'CallExpression',
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
code: `
|
|
220
|
+
function save() {
|
|
221
|
+
return (
|
|
222
|
+
<button>
|
|
223
|
+
<span>{ __( 'Click me' ) }</span>
|
|
224
|
+
</button>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
`,
|
|
228
|
+
errors: [
|
|
229
|
+
{
|
|
230
|
+
messageId: 'noI18nInSave',
|
|
231
|
+
type: 'CallExpression',
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
code: `
|
|
237
|
+
// Multiple translation calls in save
|
|
238
|
+
function save() {
|
|
239
|
+
const label = __( 'Label' );
|
|
240
|
+
return <div title={ _x( 'Title', 'context' ) }>{ label }</div>;
|
|
241
|
+
}
|
|
242
|
+
`,
|
|
243
|
+
errors: [
|
|
244
|
+
{
|
|
245
|
+
messageId: 'noI18nInSave',
|
|
246
|
+
type: 'CallExpression',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
messageId: 'noI18nInSave',
|
|
250
|
+
type: 'CallExpression',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
code: `
|
|
256
|
+
// Translation after a nested inner function named save must still be caught
|
|
257
|
+
function save() {
|
|
258
|
+
function save() {}
|
|
259
|
+
return __( 'Hello World' );
|
|
260
|
+
}
|
|
261
|
+
`,
|
|
262
|
+
errors: [
|
|
263
|
+
{
|
|
264
|
+
messageId: 'noI18nInSave',
|
|
265
|
+
type: 'CallExpression',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
} );
|
|
@@ -27,6 +27,18 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
|
|
|
27
27
|
{
|
|
28
28
|
code: '<div style={ { color: `var(--wpds-color-fg-content-neutral)` } } />',
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
code: `const token = 'var(--wpds-color-fg-content-neutral)';`,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
code: `const name = 'something--wpds-color';`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
code: '`${ prefix }: var(--wpds-color-fg-content-neutral)`',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
code: '`var(--wpds-color-fg-content-neutral) ${ suffix }`',
|
|
41
|
+
},
|
|
30
42
|
],
|
|
31
43
|
invalid: [
|
|
32
44
|
{
|
|
@@ -34,6 +46,9 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
|
|
|
34
46
|
errors: [
|
|
35
47
|
{
|
|
36
48
|
messageId: 'onlyKnownTokens',
|
|
49
|
+
data: {
|
|
50
|
+
tokenNames: "'--wpds-nonexistent-token'",
|
|
51
|
+
},
|
|
37
52
|
},
|
|
38
53
|
],
|
|
39
54
|
},
|
|
@@ -53,6 +68,77 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
|
|
|
53
68
|
errors: [
|
|
54
69
|
{
|
|
55
70
|
messageId: 'onlyKnownTokens',
|
|
71
|
+
data: {
|
|
72
|
+
tokenNames: "'--wpds-nonexistent'",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
code: `const token = 'var(--wpds-nonexistent-token)';`,
|
|
79
|
+
errors: [
|
|
80
|
+
{
|
|
81
|
+
messageId: 'onlyKnownTokens',
|
|
82
|
+
data: {
|
|
83
|
+
tokenNames: "'--wpds-nonexistent-token'",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
code: 'const token = `var(--wpds-nonexistent-token)`;',
|
|
90
|
+
errors: [
|
|
91
|
+
{
|
|
92
|
+
messageId: 'onlyKnownTokens',
|
|
93
|
+
data: {
|
|
94
|
+
tokenNames: "'--wpds-nonexistent-token'",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
code: 'const token = `var(--wpds-dimension-gap-${ size })`;',
|
|
101
|
+
errors: [
|
|
102
|
+
{
|
|
103
|
+
messageId: 'dynamicToken',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
code: '<div style={ { gap: `var(--wpds-dimension-gap-${ size })` } } />',
|
|
109
|
+
errors: [
|
|
110
|
+
{
|
|
111
|
+
messageId: 'dynamicToken',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
code: `const token = '--wpds-nonexistent-token';`,
|
|
117
|
+
errors: [
|
|
118
|
+
{
|
|
119
|
+
messageId: 'onlyKnownTokens',
|
|
120
|
+
data: {
|
|
121
|
+
tokenNames: "'--wpds-nonexistent-token'",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
code: 'const style = `--wpds-dimension-gap-${ size }`;',
|
|
128
|
+
errors: [
|
|
129
|
+
{
|
|
130
|
+
messageId: 'dynamicToken',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
code: '`${ prefix }: var(--wpds-nonexistent-token)`',
|
|
136
|
+
errors: [
|
|
137
|
+
{
|
|
138
|
+
messageId: 'onlyKnownTokens',
|
|
139
|
+
data: {
|
|
140
|
+
tokenNames: "'--wpds-nonexistent-token'",
|
|
141
|
+
},
|
|
56
142
|
},
|
|
57
143
|
],
|
|
58
144
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DS_TOKEN_PREFIX = 'wpds-';
|
|
2
|
+
|
|
3
|
+
const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );
|
|
4
|
+
|
|
5
|
+
module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow any usage of --wpds-* CSS custom properties',
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
disallowed:
|
|
14
|
+
'Design System tokens (--wpds-*) should not be used in this context.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create( context ) {
|
|
18
|
+
const selector = `:matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral TemplateElement[value.raw=${ wpdsTokensRegex }])`;
|
|
19
|
+
return {
|
|
20
|
+
/** @param {import('estree').Literal | import('estree').TemplateElement} node */
|
|
21
|
+
[ selector ]( node ) {
|
|
22
|
+
context.report( {
|
|
23
|
+
node,
|
|
24
|
+
messageId: 'disallowed',
|
|
25
|
+
} );
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
} );
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
const {
|
|
5
|
+
TRANSLATION_FUNCTIONS,
|
|
6
|
+
getTranslateFunctionName,
|
|
7
|
+
} = require( '../utils' );
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
noI18nInSave:
|
|
15
|
+
'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.',
|
|
16
|
+
},
|
|
17
|
+
docs: {
|
|
18
|
+
description: 'Disallow translation functions in block save methods',
|
|
19
|
+
category: 'Best Practices',
|
|
20
|
+
recommended: false,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
create( context ) {
|
|
24
|
+
let saveFunctionDepth = 0;
|
|
25
|
+
const filename = context.getFilename();
|
|
26
|
+
|
|
27
|
+
// Skip deprecated files as they preserve old behavior including translation functions
|
|
28
|
+
const normalizedFilename = filename.replace( /\\/g, '/' );
|
|
29
|
+
const isDeprecatedFile =
|
|
30
|
+
normalizedFilename.includes( '/deprecated.js' ) ||
|
|
31
|
+
normalizedFilename.includes( '/deprecated.ts' ) ||
|
|
32
|
+
normalizedFilename.includes( '/deprecated.jsx' ) ||
|
|
33
|
+
normalizedFilename.includes( '/deprecated.tsx' );
|
|
34
|
+
|
|
35
|
+
if ( isDeprecatedFile ) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isSaveFile =
|
|
40
|
+
normalizedFilename.endsWith( '/save.js' ) ||
|
|
41
|
+
normalizedFilename.endsWith( '/save.ts' ) ||
|
|
42
|
+
normalizedFilename.endsWith( '/save.jsx' ) ||
|
|
43
|
+
normalizedFilename.endsWith( '/save.tsx' );
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
// Track when we enter a function named 'save'
|
|
47
|
+
FunctionDeclaration( node ) {
|
|
48
|
+
if ( node.id && node.id.name === 'save' ) {
|
|
49
|
+
saveFunctionDepth++;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
'FunctionDeclaration:exit'( node ) {
|
|
53
|
+
if ( node.id && node.id.name === 'save' ) {
|
|
54
|
+
saveFunctionDepth--;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Track arrow functions assigned to 'save'
|
|
59
|
+
VariableDeclarator( node ) {
|
|
60
|
+
if (
|
|
61
|
+
node.id &&
|
|
62
|
+
node.id.name === 'save' &&
|
|
63
|
+
node.init &&
|
|
64
|
+
( node.init.type === 'ArrowFunctionExpression' ||
|
|
65
|
+
node.init.type === 'FunctionExpression' )
|
|
66
|
+
) {
|
|
67
|
+
saveFunctionDepth++;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
'VariableDeclarator:exit'( node ) {
|
|
71
|
+
if (
|
|
72
|
+
node.id &&
|
|
73
|
+
node.id.name === 'save' &&
|
|
74
|
+
node.init &&
|
|
75
|
+
( node.init.type === 'ArrowFunctionExpression' ||
|
|
76
|
+
node.init.type === 'FunctionExpression' )
|
|
77
|
+
) {
|
|
78
|
+
saveFunctionDepth--;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Track object properties named 'save'
|
|
83
|
+
'Property[key.name="save"]'( node ) {
|
|
84
|
+
if (
|
|
85
|
+
node.value &&
|
|
86
|
+
( node.value.type === 'FunctionExpression' ||
|
|
87
|
+
node.value.type === 'ArrowFunctionExpression' )
|
|
88
|
+
) {
|
|
89
|
+
saveFunctionDepth++;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
'Property[key.name="save"]:exit'( node ) {
|
|
93
|
+
if (
|
|
94
|
+
node.value &&
|
|
95
|
+
( node.value.type === 'FunctionExpression' ||
|
|
96
|
+
node.value.type === 'ArrowFunctionExpression' )
|
|
97
|
+
) {
|
|
98
|
+
saveFunctionDepth--;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// Check for translation function calls
|
|
103
|
+
CallExpression( node ) {
|
|
104
|
+
const { callee } = node;
|
|
105
|
+
const functionName = getTranslateFunctionName( callee );
|
|
106
|
+
|
|
107
|
+
if ( ! TRANSLATION_FUNCTIONS.has( functionName ) ) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Report if we're in a save file or inside a save function
|
|
112
|
+
if ( isSaveFile || saveFunctionDepth > 0 ) {
|
|
113
|
+
context.report( {
|
|
114
|
+
node,
|
|
115
|
+
messageId: 'noI18nInSave',
|
|
116
|
+
} );
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -36,7 +36,7 @@ function extractCSSVariables( value, prefix = '' ) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const knownTokens = new Set( tokenList );
|
|
39
|
-
const wpdsTokensRegex = new RegExp( `[^\\w]--${ DS_TOKEN_PREFIX }`, 'i' );
|
|
39
|
+
const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );
|
|
40
40
|
|
|
41
41
|
module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
|
|
42
42
|
meta: {
|
|
@@ -48,13 +48,84 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
|
|
|
48
48
|
messages: {
|
|
49
49
|
onlyKnownTokens:
|
|
50
50
|
'The following CSS variables are not valid Design System tokens: {{ tokenNames }}',
|
|
51
|
+
dynamicToken:
|
|
52
|
+
'Design System tokens must not be dynamically constructed, as they cannot be statically verified for correctness or processed automatically to inject fallbacks.',
|
|
51
53
|
},
|
|
52
54
|
},
|
|
53
55
|
create( context ) {
|
|
54
|
-
const
|
|
56
|
+
const dynamicTemplateLiteralAST = `TemplateLiteral[expressions.length>0]:has(TemplateElement[value.raw=${ wpdsTokensRegex }])`;
|
|
57
|
+
const staticTokensAST = `:matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral[expressions.length=0] TemplateElement[value.raw=${ wpdsTokensRegex }])`;
|
|
58
|
+
const dynamicTokenEndRegex = new RegExp(
|
|
59
|
+
`--${ DS_TOKEN_PREFIX }[\\w-]*$`
|
|
60
|
+
);
|
|
61
|
+
|
|
55
62
|
return {
|
|
63
|
+
/**
|
|
64
|
+
* For template literals with expressions, check each quasi
|
|
65
|
+
* individually: flag as dynamic only when a `--wpds-*` token
|
|
66
|
+
* name is split across a quasi/expression boundary, and
|
|
67
|
+
* validate any complete static tokens normally.
|
|
68
|
+
*
|
|
69
|
+
* @param {import('estree').TemplateLiteral} node
|
|
70
|
+
*/
|
|
71
|
+
[ dynamicTemplateLiteralAST ]( node ) {
|
|
72
|
+
let hasDynamic = false;
|
|
73
|
+
const unknownTokens = [];
|
|
74
|
+
|
|
75
|
+
for ( const quasi of node.quasis ) {
|
|
76
|
+
const raw = quasi.value.raw;
|
|
77
|
+
const value = quasi.value.cooked ?? raw;
|
|
78
|
+
const isFollowedByExpression = ! quasi.tail;
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
isFollowedByExpression &&
|
|
82
|
+
dynamicTokenEndRegex.test( raw )
|
|
83
|
+
) {
|
|
84
|
+
hasDynamic = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tokens = extractCSSVariables(
|
|
88
|
+
value,
|
|
89
|
+
DS_TOKEN_PREFIX
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Remove the trailing incomplete token — it's the one
|
|
93
|
+
// being dynamically constructed by the next expression.
|
|
94
|
+
if ( isFollowedByExpression ) {
|
|
95
|
+
const endMatch = value.match( /(--([\w-]+))$/ );
|
|
96
|
+
if ( endMatch ) {
|
|
97
|
+
tokens.delete( endMatch[ 1 ] );
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for ( const token of tokens ) {
|
|
102
|
+
if ( ! knownTokens.has( token ) ) {
|
|
103
|
+
unknownTokens.push( token );
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ( hasDynamic ) {
|
|
109
|
+
context.report( {
|
|
110
|
+
node,
|
|
111
|
+
messageId: 'dynamicToken',
|
|
112
|
+
} );
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ( unknownTokens.length > 0 ) {
|
|
116
|
+
context.report( {
|
|
117
|
+
node,
|
|
118
|
+
messageId: 'onlyKnownTokens',
|
|
119
|
+
data: {
|
|
120
|
+
tokenNames: unknownTokens
|
|
121
|
+
.map( ( token ) => `'${ token }'` )
|
|
122
|
+
.join( ', ' ),
|
|
123
|
+
},
|
|
124
|
+
} );
|
|
125
|
+
}
|
|
126
|
+
},
|
|
56
127
|
/** @param {import('estree').Literal | import('estree').TemplateElement} node */
|
|
57
|
-
[
|
|
128
|
+
[ staticTokensAST ]( node ) {
|
|
58
129
|
let computedValue;
|
|
59
130
|
|
|
60
131
|
if ( ! node.value ) {
|
|
@@ -62,13 +133,11 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
|
|
|
62
133
|
}
|
|
63
134
|
|
|
64
135
|
if ( typeof node.value === 'string' ) {
|
|
65
|
-
// Get the node's value when it's a "string"
|
|
66
136
|
computedValue = node.value;
|
|
67
137
|
} else if (
|
|
68
138
|
typeof node.value === 'object' &&
|
|
69
139
|
'raw' in node.value
|
|
70
140
|
) {
|
|
71
|
-
// Get the node's value when it's a `template literal`
|
|
72
141
|
computedValue = node.value.cooked ?? node.value.raw;
|
|
73
142
|
}
|
|
74
143
|
|