@plumeria/eslint-plugin 6.3.2 → 7.0.1
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 +5 -0
- package/dist/index.js +4 -0
- package/dist/rules/no-combinator.d.ts +2 -0
- package/dist/rules/no-combinator.js +225 -0
- package/dist/rules/no-destructure.js +1 -1
- package/dist/rules/sort-properties.js +33 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Below are the available rules and the recommended configuration.
|
|
|
7
7
|
|
|
8
8
|
The `plugin:@plumeria/recommended` config enables the following:
|
|
9
9
|
|
|
10
|
+
- `@plumeria/no-combinator`: **error**
|
|
10
11
|
- `@plumeria/no-destructure`: **error**
|
|
11
12
|
- `@plumeria/no-inner-call`: **error**
|
|
12
13
|
- `@plumeria/no-unused-keys`: **warn**
|
|
@@ -21,6 +22,10 @@ export default [plumeria.flatConfigs.recommended];
|
|
|
21
22
|
|
|
22
23
|
## Rules
|
|
23
24
|
|
|
25
|
+
### no-combinator
|
|
26
|
+
|
|
27
|
+
Disallow combinators `>`, `+`, `~` and descendant combinator (space) unless inside functional pseudo-classes.
|
|
28
|
+
|
|
24
29
|
### no-destructure
|
|
25
30
|
|
|
26
31
|
Disallow destructuring `css.create` and `css.props`, etc.
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.plumeria = void 0;
|
|
4
|
+
const no_combinator_1 = require("./rules/no-combinator");
|
|
4
5
|
const no_destructure_1 = require("./rules/no-destructure");
|
|
5
6
|
const no_inner_call_1 = require("./rules/no-inner-call");
|
|
6
7
|
const no_unused_keys_1 = require("./rules/no-unused-keys");
|
|
7
8
|
const sort_properties_1 = require("./rules/sort-properties");
|
|
8
9
|
const validate_values_1 = require("./rules/validate-values");
|
|
9
10
|
const rules = {
|
|
11
|
+
'no-combinator': no_combinator_1.noCombinator,
|
|
10
12
|
'no-destructure': no_destructure_1.noDestructure,
|
|
11
13
|
'no-inner-call': no_inner_call_1.noInnerCall,
|
|
12
14
|
'no-unused-keys': no_unused_keys_1.noUnusedKeys,
|
|
@@ -17,6 +19,7 @@ const configs = {
|
|
|
17
19
|
recommended: {
|
|
18
20
|
plugins: ['@plumeria'],
|
|
19
21
|
rules: {
|
|
22
|
+
'@plumeria/no-combinator': 'error',
|
|
20
23
|
'@plumeria/no-destructure': 'error',
|
|
21
24
|
'@plumeria/no-inner-call': 'error',
|
|
22
25
|
'@plumeria/no-unused-keys': 'warn',
|
|
@@ -33,6 +36,7 @@ const flatConfigs = {
|
|
|
33
36
|
},
|
|
34
37
|
},
|
|
35
38
|
rules: {
|
|
39
|
+
'@plumeria/no-combinator': 'error',
|
|
36
40
|
'@plumeria/no-destructure': 'error',
|
|
37
41
|
'@plumeria/no-inner-call': 'error',
|
|
38
42
|
'@plumeria/no-unused-keys': 'warn',
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noCombinator = void 0;
|
|
4
|
+
exports.noCombinator = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Disallow combinators >, +, ~ and descendant combinator (space) unless inside functional pseudo-classes.',
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
noCombinator: 'Combinator "{{combinator}}" is not allowed unless inside functional pseudo-classes.',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
const plumeriaAliases = {};
|
|
17
|
+
function isCombinatorAllowed(selector) {
|
|
18
|
+
const s = selector.trim();
|
|
19
|
+
if (s.startsWith('@')) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
const len = s.length;
|
|
23
|
+
let i = 0;
|
|
24
|
+
while (i < len) {
|
|
25
|
+
const char = s[i];
|
|
26
|
+
if (char === '"' || char === "'") {
|
|
27
|
+
i = skipString(s, i);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === '(') {
|
|
31
|
+
i = skipBlock(s, i, '(', ')');
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (char === '[') {
|
|
35
|
+
i = skipBlock(s, i, '[', ']');
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (char === '>' || char === '+' || char === '~') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (isSpace(char)) {
|
|
42
|
+
let next = i + 1;
|
|
43
|
+
while (next < len && isSpace(s[next])) {
|
|
44
|
+
next++;
|
|
45
|
+
}
|
|
46
|
+
if (next < len) {
|
|
47
|
+
const nextChar = s[next];
|
|
48
|
+
const prevChar = s[i - 1];
|
|
49
|
+
if (!isCombinatorOrSeparator(prevChar) &&
|
|
50
|
+
!isCombinatorOrSeparator(nextChar)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
i = next;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
function skipString(s, start) {
|
|
62
|
+
const quote = s[start];
|
|
63
|
+
let i = start + 1;
|
|
64
|
+
while (i < s.length) {
|
|
65
|
+
if (s[i] === '\\') {
|
|
66
|
+
i += 2;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (s[i] === quote) {
|
|
70
|
+
return i + 1;
|
|
71
|
+
}
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
return i;
|
|
75
|
+
}
|
|
76
|
+
function skipBlock(s, start, open, close) {
|
|
77
|
+
let depth = 1;
|
|
78
|
+
let i = start + 1;
|
|
79
|
+
while (i < s.length && depth > 0) {
|
|
80
|
+
const char = s[i];
|
|
81
|
+
if (char === '\\') {
|
|
82
|
+
i += 2;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (char === '"' || char === "'") {
|
|
86
|
+
i = skipString(s, i);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (char === open) {
|
|
90
|
+
depth++;
|
|
91
|
+
}
|
|
92
|
+
else if (char === close) {
|
|
93
|
+
depth--;
|
|
94
|
+
}
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
return i;
|
|
98
|
+
}
|
|
99
|
+
function isSpace(char) {
|
|
100
|
+
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
101
|
+
}
|
|
102
|
+
function isCombinatorOrSeparator(char) {
|
|
103
|
+
return (char === '>' ||
|
|
104
|
+
char === '+' ||
|
|
105
|
+
char === '~' ||
|
|
106
|
+
char === ',' ||
|
|
107
|
+
char === undefined);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
ImportDeclaration(node) {
|
|
111
|
+
if (node.source.value === '@plumeria/core') {
|
|
112
|
+
node.specifiers.forEach((specifier) => {
|
|
113
|
+
if (specifier.type === 'ImportNamespaceSpecifier' ||
|
|
114
|
+
specifier.type === 'ImportDefaultSpecifier') {
|
|
115
|
+
plumeriaAliases[specifier.local.name] = 'NAMESPACE';
|
|
116
|
+
}
|
|
117
|
+
else if (specifier.type === 'ImportSpecifier') {
|
|
118
|
+
const importedName = specifier.imported.type === 'Identifier'
|
|
119
|
+
? specifier.imported.name
|
|
120
|
+
: String(specifier.imported.value);
|
|
121
|
+
plumeriaAliases[specifier.local.name] = importedName;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
CallExpression(node) {
|
|
127
|
+
if (node.callee.type === 'MemberExpression') {
|
|
128
|
+
if (node.callee.object.type === 'Identifier' &&
|
|
129
|
+
plumeriaAliases[node.callee.object.name] === 'NAMESPACE') {
|
|
130
|
+
const propertyName = node.callee.property.type === 'Identifier'
|
|
131
|
+
? node.callee.property.name
|
|
132
|
+
: null;
|
|
133
|
+
if (propertyName === 'create' || propertyName === 'variants') {
|
|
134
|
+
node.arguments.forEach((arg) => {
|
|
135
|
+
if (arg.type === 'ObjectExpression') {
|
|
136
|
+
checkForCombinatorsRecursively(arg);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else if (propertyName === 'createStatic') {
|
|
141
|
+
node.arguments.forEach((arg) => {
|
|
142
|
+
if (arg.type === 'ObjectExpression') {
|
|
143
|
+
checkCreateStaticValues(arg);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (node.callee.type === 'Identifier') {
|
|
150
|
+
const alias = plumeriaAliases[node.callee.name];
|
|
151
|
+
if (alias === 'create' || alias === 'variants') {
|
|
152
|
+
node.arguments.forEach((arg) => {
|
|
153
|
+
if (arg.type === 'ObjectExpression') {
|
|
154
|
+
checkForCombinatorsRecursively(arg);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else if (alias === 'createStatic') {
|
|
159
|
+
node.arguments.forEach((arg) => {
|
|
160
|
+
if (arg.type === 'ObjectExpression') {
|
|
161
|
+
checkCreateStaticValues(arg);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
function checkForCombinatorsRecursively(node) {
|
|
169
|
+
for (const prop of node.properties) {
|
|
170
|
+
if (prop.type === 'Property') {
|
|
171
|
+
let keyName = '';
|
|
172
|
+
if (prop.key.type === 'Identifier') {
|
|
173
|
+
keyName = prop.key.name;
|
|
174
|
+
}
|
|
175
|
+
else if (prop.key.type === 'Literal') {
|
|
176
|
+
keyName = String(prop.key.value);
|
|
177
|
+
}
|
|
178
|
+
if (keyName) {
|
|
179
|
+
checkAndReport(keyName, prop.key);
|
|
180
|
+
}
|
|
181
|
+
if (prop.value.type === 'ObjectExpression') {
|
|
182
|
+
checkForCombinatorsRecursively(prop.value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function checkCreateStaticValues(node) {
|
|
188
|
+
for (const prop of node.properties) {
|
|
189
|
+
if (prop.type === 'Property') {
|
|
190
|
+
if (prop.value.type === 'Literal') {
|
|
191
|
+
const value = String(prop.value.value);
|
|
192
|
+
checkAndReport(value, prop.value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function checkAndReport(text, node) {
|
|
198
|
+
if (text.includes('>') ||
|
|
199
|
+
text.includes('+') ||
|
|
200
|
+
text.includes('~') ||
|
|
201
|
+
text.includes(' ') ||
|
|
202
|
+
text.includes('\t') ||
|
|
203
|
+
text.includes('\n')) {
|
|
204
|
+
if (!isCombinatorAllowed(text)) {
|
|
205
|
+
let found = '';
|
|
206
|
+
if (text.includes('>'))
|
|
207
|
+
found = '>';
|
|
208
|
+
else if (text.includes('+'))
|
|
209
|
+
found = '+';
|
|
210
|
+
else if (text.includes('~'))
|
|
211
|
+
found = '~';
|
|
212
|
+
else if (text.includes(' ') ||
|
|
213
|
+
text.includes('\t') ||
|
|
214
|
+
text.includes('\n'))
|
|
215
|
+
found = '(space)';
|
|
216
|
+
context.report({
|
|
217
|
+
node: node,
|
|
218
|
+
messageId: 'noCombinator',
|
|
219
|
+
data: { combinator: found },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
@@ -5,7 +5,7 @@ exports.noDestructure = {
|
|
|
5
5
|
meta: {
|
|
6
6
|
type: 'problem',
|
|
7
7
|
docs: {
|
|
8
|
-
description: 'Disallow destructuring
|
|
8
|
+
description: 'Disallow destructuring API',
|
|
9
9
|
},
|
|
10
10
|
messages: {
|
|
11
11
|
noDestructure: 'Do not destructure "{{property}}" from "{{object}}". Use dot notation instead.',
|
|
@@ -59,25 +59,51 @@ exports.sortProperties = {
|
|
|
59
59
|
ObjectExpression(node) {
|
|
60
60
|
const sourceCode = getSourceCode(context);
|
|
61
61
|
const isTopLevel = !node.parent || node.parent.type !== 'Property';
|
|
62
|
-
const properties = node.properties.filter((prop) => 'key' in prop && !!prop.key);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
const properties = node.properties.filter((prop) => ('key' in prop && !!prop.key) || prop.type === 'SpreadElement');
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let currentChunk = [];
|
|
65
|
+
properties.forEach((prop) => {
|
|
66
|
+
if (prop.type === 'SpreadElement') {
|
|
67
|
+
if (currentChunk.length > 0)
|
|
68
|
+
chunks.push(currentChunk);
|
|
69
|
+
chunks.push([prop]);
|
|
70
|
+
currentChunk = [];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
currentChunk.push(prop);
|
|
74
|
+
}
|
|
67
75
|
});
|
|
76
|
+
if (currentChunk.length > 0)
|
|
77
|
+
chunks.push(currentChunk);
|
|
78
|
+
const sorted = chunks
|
|
79
|
+
.map((chunk) => {
|
|
80
|
+
if (chunk.length === 1 && chunk[0].type === 'SpreadElement') {
|
|
81
|
+
return chunk;
|
|
82
|
+
}
|
|
83
|
+
return [...chunk].sort((a, b) => {
|
|
84
|
+
const indexA = getPropertyIndex(a, isTopLevel);
|
|
85
|
+
const indexB = getPropertyIndex(b, isTopLevel);
|
|
86
|
+
return indexA === null || indexB === null ? 0 : indexA - indexB;
|
|
87
|
+
});
|
|
88
|
+
})
|
|
89
|
+
.flat();
|
|
68
90
|
const misordered = properties.filter((prop, i) => prop !== sorted[i]);
|
|
69
91
|
if (misordered.length === 0)
|
|
70
92
|
return;
|
|
71
93
|
const match = sourceCode.getText(node).match(/^{\s*\n(\s*)/);
|
|
72
94
|
const indent = match ? match[1] : '';
|
|
73
95
|
const lineEnding = match ? '\n' : ' ';
|
|
96
|
+
const closingIndentMatch = sourceCode.getText(node).match(/\n(\s*)}$/);
|
|
97
|
+
const closingIndent = closingIndentMatch ? closingIndentMatch[1] : '';
|
|
74
98
|
misordered.forEach((prop) => {
|
|
75
99
|
context.report({
|
|
76
100
|
node: prop,
|
|
77
101
|
messageId: 'sortProperties',
|
|
78
102
|
data: {
|
|
79
103
|
position: String(sorted.indexOf(prop) + 1),
|
|
80
|
-
property:
|
|
104
|
+
property: prop.type === 'SpreadElement'
|
|
105
|
+
? '...spread'
|
|
106
|
+
: getPropertyName(prop),
|
|
81
107
|
},
|
|
82
108
|
fix(fixer) {
|
|
83
109
|
const newText = sorted
|
|
@@ -88,7 +114,7 @@ exports.sortProperties = {
|
|
|
88
114
|
return fixer.replaceTextRange([
|
|
89
115
|
node.range[0] + 1,
|
|
90
116
|
node.range[1] - 1,
|
|
91
|
-
], `${lineEnding}${newText}${lineEnding}`);
|
|
117
|
+
], `${lineEnding}${newText}${lineEnding}${closingIndent}`);
|
|
92
118
|
},
|
|
93
119
|
});
|
|
94
120
|
});
|