@atlaskit/eslint-plugin-platform 2.0.1 → 2.1.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/CHANGELOG.md +17 -0
- package/dist/cjs/index.js +3 -0
- package/dist/cjs/rules/compiled/expand-background-shorthand/index.js +2 -17
- package/dist/cjs/rules/compiled/expand-spacing-shorthand/index.js +196 -0
- package/dist/cjs/rules/util/compiled-utils.js +21 -0
- package/dist/cjs/rules/util/context-compat.js +16 -2
- package/dist/es2019/index.js +3 -0
- package/dist/es2019/rules/compiled/expand-background-shorthand/index.js +1 -15
- package/dist/es2019/rules/compiled/expand-spacing-shorthand/index.js +162 -0
- package/dist/es2019/rules/util/compiled-utils.js +15 -0
- package/dist/es2019/rules/util/context-compat.js +15 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/rules/compiled/expand-background-shorthand/index.js +1 -16
- package/dist/esm/rules/compiled/expand-spacing-shorthand/index.js +189 -0
- package/dist/esm/rules/util/compiled-utils.js +16 -0
- package/dist/esm/rules/util/context-compat.js +15 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/rules/compiled/expand-spacing-shorthand/index.d.ts +3 -0
- package/dist/types/rules/util/compiled-utils.d.ts +3 -0
- package/dist/types/rules/util/context-compat.d.ts +22 -1
- package/dist/types-ts4.5/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/compiled/expand-spacing-shorthand/index.d.ts +3 -0
- package/dist/types-ts4.5/rules/util/compiled-utils.d.ts +3 -0
- package/dist/types-ts4.5/rules/util/context-compat.d.ts +22 -1
- package/package.json +3 -3
- package/src/index.tsx +3 -0
- package/src/rules/compiled/expand-background-shorthand/__tests__/rule.test.ts +1 -1
- package/src/rules/compiled/expand-background-shorthand/index.tsx +1 -25
- package/src/rules/compiled/expand-spacing-shorthand/README.md +38 -0
- package/src/rules/compiled/expand-spacing-shorthand/__tests__/rule.test.ts +344 -0
- package/src/rules/compiled/expand-spacing-shorthand/index.ts +150 -0
- package/src/rules/util/__tests__/context-compat.test.ts +59 -1
- package/src/rules/util/compiled-utils.ts +27 -0
- package/src/rules/util/context-compat.ts +14 -1
- package/tsconfig.app.json +0 -1
- package/tsconfig.dev.json +0 -1
- package/tsconfig.json +0 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { outdent } from 'outdent';
|
|
2
|
+
import { tester } from '../../../../__tests__/utils/_tester';
|
|
3
|
+
import { expandSpacingShorthand } from '../index';
|
|
4
|
+
|
|
5
|
+
const included_compiled_libraries = [
|
|
6
|
+
'@compiled/react',
|
|
7
|
+
'@atlaskit/css'
|
|
8
|
+
];
|
|
9
|
+
const exempt_libraries = [
|
|
10
|
+
'@atlaskit/primitives',
|
|
11
|
+
'@emotion',
|
|
12
|
+
'styled-components',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const validTestCases = (property: string) => {
|
|
16
|
+
return [
|
|
17
|
+
...exempt_libraries.map((imp) => ({
|
|
18
|
+
name: `${property}: do not have to handle non-compiled packages (${imp})`,
|
|
19
|
+
code: outdent`
|
|
20
|
+
import {css} from '${imp}';
|
|
21
|
+
const styles = css({
|
|
22
|
+
${property}: token('space.200'),
|
|
23
|
+
});
|
|
24
|
+
`,
|
|
25
|
+
})),
|
|
26
|
+
{
|
|
27
|
+
name: `${property}: no spacing shorthand`,
|
|
28
|
+
code: outdent`
|
|
29
|
+
import {css} from '@compiled/react';
|
|
30
|
+
const styles = css({
|
|
31
|
+
${property}Top: token('space.200'),
|
|
32
|
+
${property}Bottom: token('space.200'),
|
|
33
|
+
${property}Left: token('space.200'),
|
|
34
|
+
${property}Right: token('space.200'),
|
|
35
|
+
});
|
|
36
|
+
`,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: `${property}: do not have to handle plain string values`,
|
|
40
|
+
code: outdent`
|
|
41
|
+
import {css} from '@compiled/react';
|
|
42
|
+
const styles = css({
|
|
43
|
+
${property}: '0 auto',
|
|
44
|
+
});
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: `${property}: do not handle dynamic style`,
|
|
49
|
+
code: outdent`
|
|
50
|
+
import {css} from '@compiled/react';
|
|
51
|
+
const styles = css({
|
|
52
|
+
${property}: ({ isCompact }) => \`\${isCompact ? token('space.050', '4px') : token('space.075', '6px')}\`
|
|
53
|
+
});
|
|
54
|
+
`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: `${property}: do not handle calc(...) as value`,
|
|
58
|
+
code: outdent`
|
|
59
|
+
import {css} from '@compiled/react';
|
|
60
|
+
const styles = css({
|
|
61
|
+
${property}: \`calc(var(--preview-block-width) + \${token('space.200', '16px')})\`,
|
|
62
|
+
});
|
|
63
|
+
`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: `${property}: do not handle template literal if no token(...) in the expressions`,
|
|
67
|
+
code: outdent`
|
|
68
|
+
import {css} from '@compiled/react';
|
|
69
|
+
const styles = css({
|
|
70
|
+
${property}: \`0px 2\`,
|
|
71
|
+
});
|
|
72
|
+
`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: `${property}: do not handle where value is a function call which is not a token`,
|
|
76
|
+
code: outdent`
|
|
77
|
+
import { css } from '@compiled/react';
|
|
78
|
+
import { functionCall } from 'somewhere';
|
|
79
|
+
const styles = css({
|
|
80
|
+
${property}: functionCall(),
|
|
81
|
+
});
|
|
82
|
+
`,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const invalidTestCases = (property: string) => {
|
|
88
|
+
return [
|
|
89
|
+
// Value of spacing property is plain token() call
|
|
90
|
+
...included_compiled_libraries.map((imp) => ({
|
|
91
|
+
name: `expand ${property} with value as token: (${imp})`,
|
|
92
|
+
code: outdent`
|
|
93
|
+
import {css} from '${imp}';
|
|
94
|
+
const styles = css({
|
|
95
|
+
${property}: token('space.200'),
|
|
96
|
+
});
|
|
97
|
+
`,
|
|
98
|
+
output: outdent`
|
|
99
|
+
import {css} from '${imp}';
|
|
100
|
+
const styles = css({
|
|
101
|
+
${property}Top: token('space.200'),
|
|
102
|
+
${property}Right: token('space.200'),
|
|
103
|
+
${property}Bottom: token('space.200'),
|
|
104
|
+
${property}Left: token('space.200'),
|
|
105
|
+
});
|
|
106
|
+
`,
|
|
107
|
+
errors: [{ messageId: 'expandSpacingShorthand' }],
|
|
108
|
+
})),
|
|
109
|
+
// Value of spacing property is a template string
|
|
110
|
+
...included_compiled_libraries.map((imp) => ({
|
|
111
|
+
name: `${property}: template string case (one value) (${imp})`,
|
|
112
|
+
code: outdent`
|
|
113
|
+
import {css} from '${imp}';
|
|
114
|
+
const styles = css({
|
|
115
|
+
${property}: \`\${token('space.200')}\`,
|
|
116
|
+
});
|
|
117
|
+
`,
|
|
118
|
+
output: outdent`
|
|
119
|
+
import {css} from '${imp}';
|
|
120
|
+
const styles = css({
|
|
121
|
+
${property}Top: token('space.200'),
|
|
122
|
+
${property}Right: token('space.200'),
|
|
123
|
+
${property}Bottom: token('space.200'),
|
|
124
|
+
${property}Left: token('space.200'),
|
|
125
|
+
});
|
|
126
|
+
`,
|
|
127
|
+
errors: [{ messageId: 'expandSpacingShorthand' }],
|
|
128
|
+
})),
|
|
129
|
+
...included_compiled_libraries.map((imp) => ({
|
|
130
|
+
name: `${property}: template string case (two values) (${imp})`,
|
|
131
|
+
code: outdent`
|
|
132
|
+
import {css} from '${imp}';
|
|
133
|
+
const styles = css({
|
|
134
|
+
${property}: \`\${token('space.100')} 0\`,
|
|
135
|
+
});
|
|
136
|
+
`,
|
|
137
|
+
output: outdent`
|
|
138
|
+
import {css} from '${imp}';
|
|
139
|
+
const styles = css({
|
|
140
|
+
${property}Top: token('space.100'),
|
|
141
|
+
${property}Right: 0,
|
|
142
|
+
${property}Bottom: token('space.100'),
|
|
143
|
+
${property}Left: 0,
|
|
144
|
+
});
|
|
145
|
+
`,
|
|
146
|
+
errors: [{ messageId: 'expandSpacingShorthand' }],
|
|
147
|
+
})),
|
|
148
|
+
...included_compiled_libraries.map((imp) => ({
|
|
149
|
+
name: `${property}: template string case (three values) (${imp})`,
|
|
150
|
+
code: outdent`
|
|
151
|
+
import {css} from '${imp}';
|
|
152
|
+
const styles = css({
|
|
153
|
+
${property}: \`\${token('space.100')} 0 2px\`,
|
|
154
|
+
});
|
|
155
|
+
`,
|
|
156
|
+
output: outdent`
|
|
157
|
+
import {css} from '${imp}';
|
|
158
|
+
const styles = css({
|
|
159
|
+
${property}Top: token('space.100'),
|
|
160
|
+
${property}Right: 0,
|
|
161
|
+
${property}Bottom: '2px',
|
|
162
|
+
${property}Left: 0,
|
|
163
|
+
});
|
|
164
|
+
`,
|
|
165
|
+
errors: [{ messageId: 'expandSpacingShorthand' }],
|
|
166
|
+
})),
|
|
167
|
+
...included_compiled_libraries.map((imp) => ({
|
|
168
|
+
name: `${property}: template string case (four values) (${imp})`,
|
|
169
|
+
code: outdent`
|
|
170
|
+
import {css} from '${imp}';
|
|
171
|
+
const styles = css({
|
|
172
|
+
${property}: \`\${token('space.100')} 0 0px 12px\`,
|
|
173
|
+
});
|
|
174
|
+
const styles2 = css({
|
|
175
|
+
${property}: \`\${token('space.100')} \${token('space.200')} \${token('space.300')} \${token('space.400')}\`,
|
|
176
|
+
});
|
|
177
|
+
`,
|
|
178
|
+
output: outdent`
|
|
179
|
+
import {css} from '${imp}';
|
|
180
|
+
const styles = css({
|
|
181
|
+
${property}Top: token('space.100'),
|
|
182
|
+
${property}Right: 0,
|
|
183
|
+
${property}Bottom: '0px',
|
|
184
|
+
${property}Left: '12px',
|
|
185
|
+
});
|
|
186
|
+
const styles2 = css({
|
|
187
|
+
${property}Top: token('space.100'),
|
|
188
|
+
${property}Right: token('space.200'),
|
|
189
|
+
${property}Bottom: token('space.300'),
|
|
190
|
+
${property}Left: token('space.400'),
|
|
191
|
+
});
|
|
192
|
+
`,
|
|
193
|
+
errors: Array.from(Array(2), () => ({ messageId: 'expandSpacingShorthand' })),
|
|
194
|
+
})),
|
|
195
|
+
{
|
|
196
|
+
name: `${property}: compiled import (styled.div, @compiled/react)`,
|
|
197
|
+
code: outdent`
|
|
198
|
+
import { styled } from '@compiled/react';
|
|
199
|
+
const styledDiv = styled.div({
|
|
200
|
+
${property}: \`0 \${token('space.100', '8px')} 0 0\`,
|
|
201
|
+
});
|
|
202
|
+
`,
|
|
203
|
+
output: outdent`
|
|
204
|
+
import { styled } from '@compiled/react';
|
|
205
|
+
const styledDiv = styled.div({
|
|
206
|
+
${property}Top: 0,
|
|
207
|
+
${property}Right: token('space.100', '8px'),
|
|
208
|
+
${property}Bottom: 0,
|
|
209
|
+
${property}Left: 0,
|
|
210
|
+
});
|
|
211
|
+
`,
|
|
212
|
+
errors: [{ messageId: 'expandSpacingShorthand' }],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: `${property}: compiled import (cssMap, @compiled/react)`,
|
|
216
|
+
code: outdent`
|
|
217
|
+
import { cssMap } from '@compiled/react';
|
|
218
|
+
const backgroundMap = cssMap({
|
|
219
|
+
firstDiv: { ${property}: token('space.100') },
|
|
220
|
+
secondDiv: { ${property}: token('space.100') },
|
|
221
|
+
});
|
|
222
|
+
`,
|
|
223
|
+
output: outdent`
|
|
224
|
+
import { cssMap } from '@compiled/react';
|
|
225
|
+
const backgroundMap = cssMap({
|
|
226
|
+
firstDiv: { ${property}Top: token('space.100'),
|
|
227
|
+
${property}Right: token('space.100'),
|
|
228
|
+
${property}Bottom: token('space.100'),
|
|
229
|
+
${property}Left: token('space.100') },
|
|
230
|
+
secondDiv: { ${property}Top: token('space.100'),
|
|
231
|
+
${property}Right: token('space.100'),
|
|
232
|
+
${property}Bottom: token('space.100'),
|
|
233
|
+
${property}Left: token('space.100') },
|
|
234
|
+
});
|
|
235
|
+
`,
|
|
236
|
+
errors: Array.from(Array(2), () => ({ messageId: 'expandSpacingShorthand' })),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: `${property}: compiled import with alias`,
|
|
240
|
+
code: outdent`
|
|
241
|
+
import { styled as styled2 } from '@compiled/react';
|
|
242
|
+
const style = styled2.span({
|
|
243
|
+
${property}: \`0 \${token('space.100', '8px')} 0 0\`,
|
|
244
|
+
});
|
|
245
|
+
`,
|
|
246
|
+
output: outdent`
|
|
247
|
+
import { styled as styled2 } from '@compiled/react';
|
|
248
|
+
const style = styled2.span({
|
|
249
|
+
${property}Top: 0,
|
|
250
|
+
${property}Right: token('space.100', '8px'),
|
|
251
|
+
${property}Bottom: 0,
|
|
252
|
+
${property}Left: 0,
|
|
253
|
+
});
|
|
254
|
+
`,
|
|
255
|
+
errors: [{ messageId: 'expandSpacingShorthand' }],
|
|
256
|
+
},
|
|
257
|
+
// Miscellaneous
|
|
258
|
+
{
|
|
259
|
+
name: `${property}: new property should not be created if existing property already exists`,
|
|
260
|
+
code: outdent`
|
|
261
|
+
import {css} from '@compiled/react';
|
|
262
|
+
const styles = css({
|
|
263
|
+
${property}: token('space.100'),
|
|
264
|
+
${property}Top: token('space.200'),
|
|
265
|
+
});
|
|
266
|
+
`,
|
|
267
|
+
output: outdent`
|
|
268
|
+
import {css} from '@compiled/react';
|
|
269
|
+
const styles = css({
|
|
270
|
+
${property}Top: token('space.200'),
|
|
271
|
+
${property}Right: token('space.100'),
|
|
272
|
+
${property}Bottom: token('space.100'),
|
|
273
|
+
${property}Left: token('space.100'),
|
|
274
|
+
});
|
|
275
|
+
`,
|
|
276
|
+
errors: [{ messageId: 'expandSpacingShorthand' }],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: `${property}: property with nested selectors`,
|
|
280
|
+
code: outdent`
|
|
281
|
+
import {css} from '@compiled/react';
|
|
282
|
+
const styles = css({
|
|
283
|
+
${property}: token('space.100'),
|
|
284
|
+
'.some-selector': {
|
|
285
|
+
${property}Top: token('space.200'),
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const styles2 = css({
|
|
289
|
+
${property}Top: token('space.100'),
|
|
290
|
+
'.some-selector': {
|
|
291
|
+
${property}: token('space.200'),
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
const styles3 = css({
|
|
295
|
+
'.some-selector': {
|
|
296
|
+
${property}: token('space.200'),
|
|
297
|
+
${property}Top: token('space.100'),
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
`,
|
|
301
|
+
output: outdent`
|
|
302
|
+
import {css} from '@compiled/react';
|
|
303
|
+
const styles = css({
|
|
304
|
+
${property}Top: token('space.100'),
|
|
305
|
+
${property}Right: token('space.100'),
|
|
306
|
+
${property}Bottom: token('space.100'),
|
|
307
|
+
${property}Left: token('space.100'),
|
|
308
|
+
'.some-selector': {
|
|
309
|
+
${property}Top: token('space.200'),
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
const styles2 = css({
|
|
313
|
+
${property}Top: token('space.100'),
|
|
314
|
+
'.some-selector': {
|
|
315
|
+
${property}Top: token('space.200'),
|
|
316
|
+
${property}Right: token('space.200'),
|
|
317
|
+
${property}Bottom: token('space.200'),
|
|
318
|
+
${property}Left: token('space.200'),
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
const styles3 = css({
|
|
322
|
+
'.some-selector': {
|
|
323
|
+
${property}Top: token('space.100'),
|
|
324
|
+
${property}Right: token('space.200'),
|
|
325
|
+
${property}Bottom: token('space.200'),
|
|
326
|
+
${property}Left: token('space.200'),
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
`,
|
|
330
|
+
errors: Array.from(Array(3), () => ({ messageId: 'expandSpacingShorthand' })),
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
tester.run('expand-spacing-shorthand', expandSpacingShorthand, {
|
|
336
|
+
valid: [
|
|
337
|
+
...validTestCases('padding'),
|
|
338
|
+
...validTestCases('margin'),
|
|
339
|
+
],
|
|
340
|
+
invalid: [
|
|
341
|
+
...invalidTestCases('padding'),
|
|
342
|
+
...invalidTestCases('margin'),
|
|
343
|
+
],
|
|
344
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
import type { Property, Node, TemplateLiteral, CallExpression } from 'estree';
|
|
3
|
+
import { getSourceCode } from '../../util/context-compat';
|
|
4
|
+
import { isCompiledAPI } from '../../util/compiled-utils';
|
|
5
|
+
|
|
6
|
+
const spacingPos = ["Top", "Right", "Bottom", "Left"] as const;
|
|
7
|
+
|
|
8
|
+
interface ExpandSpacingPropertiesType {
|
|
9
|
+
context: Rule.RuleContext,
|
|
10
|
+
node: Property & { parent?: Node },
|
|
11
|
+
propertyValues: string[],
|
|
12
|
+
fixer: Rule.RuleFixer,
|
|
13
|
+
propertyShorthand: string,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Checks if node is a call expression with identifier 'token'
|
|
17
|
+
const isTokenCallExpression = (node: CallExpression) => {
|
|
18
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'token') {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Given a TemplateLiteral node, returns the value of the spacing property as an array of strings
|
|
25
|
+
// e.g. `0 ${token('a')} ${token('b')}` -> ['0', 'token('a')', 'token('b')']
|
|
26
|
+
const parseTemplateLiteral = (templateLiteral: TemplateLiteral, context: Rule.RuleContext) => {
|
|
27
|
+
const expressions = templateLiteral.expressions;
|
|
28
|
+
const quasis = templateLiteral.quasis;
|
|
29
|
+
|
|
30
|
+
let propertyValues: string[] = [];
|
|
31
|
+
for (let i = 0; i < expressions.length || i < quasis.length; i++) {
|
|
32
|
+
if (i < quasis.length) {
|
|
33
|
+
const cookedQuasi = quasis[i].value.cooked;
|
|
34
|
+
if (cookedQuasi) {
|
|
35
|
+
const splitQuasi = cookedQuasi.split(" ").filter(str => str.length > 0);
|
|
36
|
+
splitQuasi.forEach(str => {
|
|
37
|
+
propertyValues.push(isNaN(Number(str)) ? `'${str}'` : str);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (i < expressions.length) {
|
|
42
|
+
const expr = getSourceCode(context).getText(expressions[i]);
|
|
43
|
+
propertyValues.push(expr);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return propertyValues;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const checkValidPropertyValues = (propertyValues: string[]) => {
|
|
50
|
+
if (!propertyValues.some(str => str.includes("token("))) { return false; }
|
|
51
|
+
if (propertyValues.length < 1 || propertyValues.length > 4) { return false; }
|
|
52
|
+
if (propertyValues.some(str => str.includes("calc("))) { return false; }
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// To fix spacing shorthands, given a list of spacing property values, expands the spacing property and adds autofix fixes
|
|
57
|
+
const expandSpacingProperties = ({ context, node, propertyValues, fixer, propertyShorthand }: ExpandSpacingPropertiesType) => {
|
|
58
|
+
const [top, right = top, bottom = top, left = right] = propertyValues;
|
|
59
|
+
const spacing: string[] = [top, right, bottom, left];
|
|
60
|
+
|
|
61
|
+
const fixes: Rule.Fix[] = [];
|
|
62
|
+
|
|
63
|
+
const parentNode = node.parent;
|
|
64
|
+
if (parentNode && parentNode.type === 'ObjectExpression') {
|
|
65
|
+
for (var prop of parentNode.properties) {
|
|
66
|
+
if (prop.type !== 'Property') { continue; }
|
|
67
|
+
if (prop.key.type === 'Identifier' && prop.range && prop.key.name !== `${propertyShorthand}`) {
|
|
68
|
+
for (let i = 0; i < spacing.length; i++) {
|
|
69
|
+
if (prop.key.name === `${propertyShorthand}${spacingPos[i]}`) {
|
|
70
|
+
let [start, end] = prop.range;
|
|
71
|
+
// Remove the entire line for the duplicate property
|
|
72
|
+
while (getSourceCode(context).text[end] !== "\n") {
|
|
73
|
+
end += 1;
|
|
74
|
+
}
|
|
75
|
+
while (getSourceCode(context).text[start] !== "\n") {
|
|
76
|
+
start -= 1;
|
|
77
|
+
}
|
|
78
|
+
spacing[i] = getSourceCode(context).getText(prop.value);
|
|
79
|
+
fixes.push(fixer.removeRange([start, end]));
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fixes.push(fixer.insertTextAfter(node, `${propertyShorthand}Top: ${spacing[0]},\n`));
|
|
88
|
+
fixes.push(fixer.insertTextAfter(node, `\t${propertyShorthand}Right: ${spacing[1]},\n`));
|
|
89
|
+
fixes.push(fixer.insertTextAfter(node, `\t${propertyShorthand}Bottom: ${spacing[2]},\n`));
|
|
90
|
+
fixes.push(fixer.insertTextAfter(node, `\t${propertyShorthand}Left: ${spacing[3]}`));
|
|
91
|
+
fixes.push(fixer.remove(node));
|
|
92
|
+
return fixes;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const executeExpandSpacingRule = (context: Rule.RuleContext, node: Property, propertyShorthand: string) => {
|
|
96
|
+
if (!isCompiledAPI(context, node)) { return; }
|
|
97
|
+
if (node.value.type === 'TemplateLiteral') {
|
|
98
|
+
// Value of spacing property is a TemplateLiteral type that contains a token, e.g. padding: `0 token('a')`
|
|
99
|
+
const propertyValues = parseTemplateLiteral(node.value, context);
|
|
100
|
+
if (!checkValidPropertyValues(propertyValues)) { return; }
|
|
101
|
+
context.report({
|
|
102
|
+
node,
|
|
103
|
+
messageId: 'expandSpacingShorthand',
|
|
104
|
+
data: {
|
|
105
|
+
property: propertyShorthand,
|
|
106
|
+
},
|
|
107
|
+
fix(fixer) {
|
|
108
|
+
return expandSpacingProperties({context, node, propertyValues, fixer, propertyShorthand});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
} else if (node.value.type === 'CallExpression' && isTokenCallExpression(node.value)) {
|
|
112
|
+
// Value of spacing property is a token CallExpression type, e.g. margin: token('space.100', '8px')
|
|
113
|
+
const propertyValues = [ getSourceCode(context).getText(node.value) ];
|
|
114
|
+
context.report({
|
|
115
|
+
node,
|
|
116
|
+
messageId: 'expandSpacingShorthand',
|
|
117
|
+
data: {
|
|
118
|
+
property: propertyShorthand,
|
|
119
|
+
},
|
|
120
|
+
fix(fixer) {
|
|
121
|
+
return expandSpacingProperties({context, node, propertyValues, fixer, propertyShorthand});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const expandSpacingShorthand: Rule.RuleModule = {
|
|
128
|
+
meta: {
|
|
129
|
+
docs: {
|
|
130
|
+
url: 'https://bitbucket.org/atlassian/atlassian-frontend-monorepo/src/master/platform/packages/platform/eslint-plugin/src/rules/compiled/expand-spacing-shorthand/',
|
|
131
|
+
},
|
|
132
|
+
messages: {
|
|
133
|
+
expandSpacingShorthand: 'Use {{ property }}Top, {{ property }}Right, {{ property }}Bottom and {{ property }}Left instead of {{ property }} shorthand',
|
|
134
|
+
},
|
|
135
|
+
type: 'problem',
|
|
136
|
+
fixable: 'code',
|
|
137
|
+
},
|
|
138
|
+
create(context) {
|
|
139
|
+
return {
|
|
140
|
+
'Property[key.name="padding"]': function (node: Property) {
|
|
141
|
+
executeExpandSpacingRule(context, node, 'padding');
|
|
142
|
+
},
|
|
143
|
+
'Property[key.name="margin"]': function (node: Property) {
|
|
144
|
+
executeExpandSpacingRule(context, node, 'margin');
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default expandSpacingShorthand;
|
|
@@ -1,9 +1,67 @@
|
|
|
1
1
|
import type { Rule, Scope, SourceCode } from 'eslint';
|
|
2
2
|
import type { Node } from 'estree';
|
|
3
3
|
|
|
4
|
-
import { getScope } from '../context-compat';
|
|
4
|
+
import { getAncestors, getScope } from '../context-compat';
|
|
5
5
|
|
|
6
6
|
describe('context-compat', () => {
|
|
7
|
+
describe('getAncestors', () => {
|
|
8
|
+
let context: Rule.RuleContext;
|
|
9
|
+
let node: Node;
|
|
10
|
+
let sourceCode: SourceCode;
|
|
11
|
+
let ancestors: Node[];
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
node = {} as Node;
|
|
15
|
+
ancestors = [];
|
|
16
|
+
sourceCode = {
|
|
17
|
+
getAncestors: jest.fn().mockReturnValue(ancestors),
|
|
18
|
+
} as unknown as SourceCode;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return ancestor nodes from context.sourceCode.getAncestors if available', () => {
|
|
22
|
+
context = {
|
|
23
|
+
sourceCode,
|
|
24
|
+
} as unknown as Rule.RuleContext;
|
|
25
|
+
|
|
26
|
+
const result = getAncestors(context, node);
|
|
27
|
+
expect(result).toBe(ancestors);
|
|
28
|
+
expect(sourceCode.getAncestors).toHaveBeenCalledWith(node);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return ancestors from context.getSourceCode().getAncestors if context.sourceCode is not available', () => {
|
|
32
|
+
context = {
|
|
33
|
+
getSourceCode: jest.fn().mockReturnValue(sourceCode),
|
|
34
|
+
} as unknown as Rule.RuleContext;
|
|
35
|
+
|
|
36
|
+
const result = getAncestors(context, node);
|
|
37
|
+
expect(result).toBe(ancestors);
|
|
38
|
+
expect(context.getSourceCode).toHaveBeenCalled();
|
|
39
|
+
expect(sourceCode.getAncestors).toHaveBeenCalledWith(node);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return scope from context.getAncestors if neither context.sourceCode nor context.getSourceCode().getAncestors is available', () => {
|
|
43
|
+
context = {
|
|
44
|
+
getSourceCode: jest.fn().mockReturnValue({}),
|
|
45
|
+
getAncestors: jest.fn().mockReturnValue(ancestors),
|
|
46
|
+
} as unknown as Rule.RuleContext;
|
|
47
|
+
|
|
48
|
+
const result = getAncestors(context, node);
|
|
49
|
+
expect(result).toBe(ancestors);
|
|
50
|
+
expect(context.getAncestors).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return scope from context.getAncestors if context.sourceCode does not have getAncestors', () => {
|
|
54
|
+
context = {
|
|
55
|
+
sourceCode: {},
|
|
56
|
+
getAncestors: jest.fn().mockReturnValue(ancestors),
|
|
57
|
+
} as unknown as Rule.RuleContext;
|
|
58
|
+
|
|
59
|
+
const result = getAncestors(context, node);
|
|
60
|
+
expect(result).toBe(ancestors);
|
|
61
|
+
expect(context.getAncestors).toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
7
65
|
describe('getScope', () => {
|
|
8
66
|
let context: Rule.RuleContext;
|
|
9
67
|
let node: Node;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Node } from 'estree';
|
|
2
|
+
import { getAncestors, getScope } from './context-compat';
|
|
3
|
+
import type { Rule } from 'eslint';
|
|
4
|
+
import {
|
|
5
|
+
getImportSources,
|
|
6
|
+
isCompiled,
|
|
7
|
+
isAtlasKitCSS,
|
|
8
|
+
} from '@atlaskit/eslint-utils/is-supported-import';
|
|
9
|
+
|
|
10
|
+
// Checks if the function that holds the property is using a Compiled import package that this rule is targeting
|
|
11
|
+
export const isCompiledAPI = (context: Rule.RuleContext, node: Node): boolean => {
|
|
12
|
+
const importSources = getImportSources(context);
|
|
13
|
+
const { references } = getScope(context, node);
|
|
14
|
+
const ancestors = getAncestors(context, node);
|
|
15
|
+
if (
|
|
16
|
+
ancestors.some(
|
|
17
|
+
(ancestor) =>
|
|
18
|
+
ancestor.type === 'CallExpression' &&
|
|
19
|
+
ancestor.callee &&
|
|
20
|
+
(isCompiled(ancestor.callee, references, importSources) ||
|
|
21
|
+
isAtlasKitCSS(ancestor.callee, references, importSources)),
|
|
22
|
+
)
|
|
23
|
+
) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
@@ -12,7 +12,7 @@ import type { Node } from 'estree';
|
|
|
12
12
|
* `context.getSourceCode()` is deprecated in v8 and removed in v9.
|
|
13
13
|
* @param context - The ESLint rule context
|
|
14
14
|
*/
|
|
15
|
-
const getSourceCode = (context: Rule.RuleContext): SourceCode => {
|
|
15
|
+
export const getSourceCode = (context: Rule.RuleContext): SourceCode => {
|
|
16
16
|
return context.sourceCode ?? context.getSourceCode();
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -20,9 +20,22 @@ const getSourceCode = (context: Rule.RuleContext): SourceCode => {
|
|
|
20
20
|
* A compatibility layer to support older versions of ESLint.
|
|
21
21
|
* `context.sourceCode.getScope()` is the preferred way to access Scope, as
|
|
22
22
|
* `context.getScope()` was removed in v9.
|
|
23
|
+
* https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#context.getscope()
|
|
23
24
|
* @param context - The ESLint rule context
|
|
24
25
|
* @param node - The node to get the scope for
|
|
25
26
|
*/
|
|
26
27
|
export const getScope = (context: Rule.RuleContext, node: Node): Scope.Scope => {
|
|
27
28
|
return getSourceCode(context)?.getScope?.(node) ?? context.getScope();
|
|
28
29
|
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A compatibility layer to support older versions of ESLint.
|
|
33
|
+
* `context.sourceCode.getAncestors()` is the preferred way to access Ancestors, as
|
|
34
|
+
* `context.getScope()` was removed in v9.
|
|
35
|
+
* https://eslint.org/blog/2023/09/preparing-custom-rules-eslint-v9/#context.getancestors()
|
|
36
|
+
* @param context - The ESLint rule context
|
|
37
|
+
* @param node - The node to get the scope for
|
|
38
|
+
*/
|
|
39
|
+
export const getAncestors = (context: Rule.RuleContext, node: Node): Node[] => {
|
|
40
|
+
return getSourceCode(context)?.getAncestors?.(node) ?? context.getAncestors();
|
|
41
|
+
};
|
package/tsconfig.app.json
CHANGED
package/tsconfig.dev.json
CHANGED