stylelint-plugin-rhythmguard 3.10.2 → 3.12.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 +77 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/audit/args.js +22 -2
- package/src/audit/baseline.js +184 -27
- package/src/audit/codemod.js +39 -14
- package/src/audit/config.js +25 -31
- package/src/audit/contract.js +96 -10
- package/src/audit/render-github.js +4 -0
- package/src/audit/render-html.js +7 -0
- package/src/audit/render-markdown.js +27 -0
- package/src/audit/render-text.js +23 -1
- package/src/audit/report.js +60 -17
- package/src/audit/scan/files.js +154 -27
- package/src/audit/scan/stylesheets.js +87 -34
- package/src/audit/scan/templates.js +22 -7
- package/src/audit/token-chains.js +18 -7
- package/src/cli/audit.js +7 -3
- package/src/cli/doctor.js +48 -7
- package/src/cli/fix.js +56 -15
- package/src/cli/init.js +136 -23
- package/src/cli/quickstart.js +34 -8
- package/src/core/fs-cache.js +6 -0
- package/src/core/length.js +15 -0
- package/src/core/options.js +8 -1
- package/src/core/scale-inference.js +99 -68
- package/src/core/tailwind-class-analysis.js +26 -16
- package/src/core/tailwind-motion-analysis.js +12 -3
- package/src/core/token-index.js +76 -24
- package/src/core/token-map.js +23 -10
- package/src/core/token-source-entries.js +64 -0
- package/src/core/token-sources.js +361 -69
- package/src/core/value-nodes.js +280 -54
- package/src/eslint/literal-spans.js +319 -0
- package/src/eslint/rules/tailwind-class-use-motion-scale.js +9 -32
- package/src/eslint/rules/tailwind-class-use-scale.js +45 -40
- package/src/rules/no-offscale-transform/index.js +35 -14
- package/src/rules/prefer-token/index.js +72 -48
- package/src/rules/report.js +3 -1
- package/src/rules/use-motion-scale/index.js +68 -5
- package/src/rules/use-scale/index.js +54 -39
- package/types/__checks__/eslint-config.ts +37 -0
- package/types/audit.d.ts +10 -2
- package/types/shared.d.ts +21 -3
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
isHairlineLength,
|
|
8
8
|
numbersEqual,
|
|
9
9
|
parseLengthToken,
|
|
10
|
+
pxOnScale,
|
|
10
11
|
toPx,
|
|
11
12
|
} = require('../../core/length');
|
|
12
13
|
const {
|
|
@@ -15,14 +16,17 @@ const {
|
|
|
15
16
|
} = require('../../core/options');
|
|
16
17
|
const {
|
|
17
18
|
collectMathBounds,
|
|
19
|
+
declarationValueSource,
|
|
18
20
|
isBoundFunction,
|
|
19
21
|
isKeyword,
|
|
20
22
|
isMathFunction,
|
|
21
|
-
isTokenFunction,
|
|
22
23
|
propertyMatches,
|
|
24
|
+
sassOperandNodes,
|
|
23
25
|
shouldLintMathArgument,
|
|
26
|
+
skipTokenFunction,
|
|
24
27
|
walkRootValueNodes,
|
|
25
28
|
walkTransformTranslateNodes,
|
|
29
|
+
writeDeclarationValue,
|
|
26
30
|
} = require('../../core/value-nodes');
|
|
27
31
|
const { buildEffectiveTokenMap } = require('../../core/token-map');
|
|
28
32
|
|
|
@@ -36,9 +40,9 @@ const { validatePrimary, validatePreferTokenSecondaryOptions } = require('../val
|
|
|
36
40
|
|
|
37
41
|
const {
|
|
38
42
|
negateReplacement,
|
|
43
|
+
signedTokensForLength,
|
|
39
44
|
tokenFitsProperty,
|
|
40
45
|
tokenIndexFromDefinitions,
|
|
41
|
-
tokensForLength,
|
|
42
46
|
} = require('../../core/token-index');
|
|
43
47
|
|
|
44
48
|
const ruleName = 'rhythmguard/prefer-token';
|
|
@@ -61,30 +65,31 @@ function listTokens(tokens) {
|
|
|
61
65
|
: references.join('');
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* The token for a raw length, and the text the fix writes. A key with the
|
|
70
|
+
* literal's sign holds exactly that value, so its token is written as is; a
|
|
71
|
+
* negative literal that only matches the positive key gets the token negated.
|
|
72
|
+
* Keys are signed, so a positive literal never finds a negative token (issue
|
|
73
|
+
* #221).
|
|
74
|
+
*/
|
|
69
75
|
function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
const unit = parsedLength.unit || 'px';
|
|
77
|
+
const negative = parsedLength.number < 0;
|
|
78
|
+
// [key, negate]: the literal as written, then normalized, then in px.
|
|
79
|
+
const lookups = [[raw, false], [formatLength(parsedLength.number, unit), false]];
|
|
80
|
+
if (negative) lookups.push([formatLength(Math.abs(parsedLength.number), unit), true]);
|
|
81
|
+
if (options.unitStrategy === 'convert') {
|
|
82
|
+
const px = toPx(parsedLength.number, parsedLength.unit, options.baseFontSize);
|
|
83
|
+
if (px !== null) {
|
|
84
|
+
lookups.push([`${px}px`, false]);
|
|
85
|
+
if (negative) lookups.push([`${Math.abs(px)}px`, true]);
|
|
86
|
+
}
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (Object.prototype.hasOwnProperty.call(tokenMap, pxKey)) {
|
|
86
|
-
return found(tokenMap[pxKey]);
|
|
87
|
-
}
|
|
89
|
+
for (const [key, negate] of lookups) {
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(tokenMap, key)) {
|
|
91
|
+
const token = tokenMap[key];
|
|
92
|
+
return { replacement: negate ? negateReplacement(token) : token, token };
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -98,15 +103,16 @@ function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
|
|
|
98
103
|
* token in another unit, or several tokens, are named but never written.
|
|
99
104
|
*/
|
|
100
105
|
function resolveFromIndex(tokenIndex, parsedLength, prop, options) {
|
|
101
|
-
const px = toPx(
|
|
106
|
+
const px = toPx(parsedLength.number, parsedLength.unit || 'px', options.baseFontSize);
|
|
102
107
|
if (px === null || tokenIndex.length === 0) {
|
|
103
108
|
return { candidates: [] };
|
|
104
109
|
}
|
|
105
110
|
const unit = parsedLength.unit || 'px';
|
|
106
|
-
const sameUnit =
|
|
107
|
-
const
|
|
111
|
+
const sameUnit = signedTokensForLength(tokenIndex, px, unit, { prop });
|
|
112
|
+
const found = sameUnit.tokens.length > 0 || options.unitStrategy !== 'convert'
|
|
108
113
|
? sameUnit
|
|
109
|
-
:
|
|
114
|
+
: signedTokensForLength(tokenIndex, px, unit, { anyUnit: true, prop });
|
|
115
|
+
const candidates = found.tokens;
|
|
110
116
|
if (candidates.length !== 1) {
|
|
111
117
|
return { candidates };
|
|
112
118
|
}
|
|
@@ -114,13 +120,19 @@ function resolveFromIndex(tokenIndex, parsedLength, prop, options) {
|
|
|
114
120
|
const entry = tokenIndex.find((item) => item.token === candidates[0]);
|
|
115
121
|
return {
|
|
116
122
|
candidates,
|
|
117
|
-
fixable: sameUnit.length === 1,
|
|
123
|
+
fixable: sameUnit.tokens.length === 1,
|
|
118
124
|
origin: entry ? entry.file : null,
|
|
119
|
-
replacement:
|
|
125
|
+
replacement: found.negate ? negateReplacement(token) : token,
|
|
120
126
|
token,
|
|
121
127
|
};
|
|
122
128
|
}
|
|
123
129
|
|
|
130
|
+
/** The custom property a replacement such as `var(--space-2)` or `calc(-1 * var(--space-2))` writes. */
|
|
131
|
+
function replacementToken(replacement) {
|
|
132
|
+
const match = typeof replacement === 'string' ? replacement.match(/var\(\s*(--[^\s,)]+)/) : null;
|
|
133
|
+
return match ? match[1] : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
const ruleFunction = (primary, secondaryOptions) => {
|
|
125
137
|
return (root, result) => {
|
|
126
138
|
const valid = validatePrimary(result, ruleName, primary);
|
|
@@ -143,7 +155,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
143
155
|
|
|
144
156
|
withResolvedScale(options, root);
|
|
145
157
|
|
|
146
|
-
const tokenRegex = createTokenRegex(options.tokenPattern, result, ruleName);
|
|
158
|
+
const tokenRegex = createTokenRegex(options.tokenPattern, { result, root, ruleName });
|
|
147
159
|
const origins = {};
|
|
148
160
|
const tokenMap = buildEffectiveTokenMap({
|
|
149
161
|
options,
|
|
@@ -175,24 +187,36 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
175
187
|
return;
|
|
176
188
|
}
|
|
177
189
|
|
|
178
|
-
const
|
|
190
|
+
const source = declarationValueSource(decl);
|
|
191
|
+
const parsed = valueParser(source.parseText);
|
|
179
192
|
const { scaleByUnit, scalePx } = getScaleStateForProperty(prop);
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
// Operands of a Sass expression are reported, never rewritten (issue #234).
|
|
194
|
+
const sassOperands = sassOperandNodes(parsed, decl.value);
|
|
195
|
+
|
|
196
|
+
// `avoidTokens`: the var() names a fallback literal falls back from. A
|
|
197
|
+
// fallback held by one of them restates that token and passes; it is
|
|
198
|
+
// never replaced with the token it is the fallback of (issue #240).
|
|
199
|
+
const reportNode = (node, parsedLength, position = null, avoidTokens = []) => {
|
|
200
|
+
const fixable = !sassOperands.has(node);
|
|
183
201
|
const mapped = resolveTokenReplacement(tokenMap, node.value, parsedLength, options);
|
|
184
202
|
if (mapped && tokenFitsProperty(mapped.token, prop)) {
|
|
203
|
+
if (avoidTokens.includes(replacementToken(mapped.token))) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
185
206
|
const origin = origins[mapped.token] || null;
|
|
186
|
-
reportValueNode({ decl, message: messages.rejected(node.value, mapped.replacement, origin, options.note, [], position), node, replacement: mapped.replacement, result, ruleName });
|
|
207
|
+
reportValueNode({ decl, message: messages.rejected(node.value, mapped.replacement, origin, options.note, [], position), node, replacement: fixable ? mapped.replacement : null, result, ruleName });
|
|
187
208
|
return;
|
|
188
209
|
}
|
|
189
210
|
const indexed = resolveFromIndex(tokenIndex, parsedLength, prop, options);
|
|
211
|
+
if (indexed.candidates.some((candidate) => avoidTokens.includes(candidate))) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
190
214
|
const named = indexed.replacement || null;
|
|
191
215
|
reportValueNode({
|
|
192
216
|
decl,
|
|
193
217
|
message: messages.rejected(node.value, named, indexed.origin || null, options.note, indexed.candidates, position),
|
|
194
218
|
node,
|
|
195
|
-
replacement: indexed.fixable ? named : null,
|
|
219
|
+
replacement: indexed.fixable && fixable ? named : null,
|
|
196
220
|
result,
|
|
197
221
|
ruleName,
|
|
198
222
|
});
|
|
@@ -253,7 +277,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
253
277
|
} else {
|
|
254
278
|
const absPx = toPx(Math.abs(parsedLength.number), parsedLength.unit, options.baseFontSize);
|
|
255
279
|
if (absPx !== null) {
|
|
256
|
-
const onScale = scalePx
|
|
280
|
+
const onScale = pxOnScale(scalePx, absPx, parsedLength.unit, options.baseFontSize);
|
|
257
281
|
if (onScale) {
|
|
258
282
|
return false;
|
|
259
283
|
}
|
|
@@ -261,15 +285,16 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
261
285
|
}
|
|
262
286
|
}
|
|
263
287
|
|
|
264
|
-
reportNode(node, parsedLength, position);
|
|
288
|
+
reportNode(node, parsedLength, position, (context && context.tokenFallbackOf) || []);
|
|
265
289
|
return true;
|
|
266
290
|
};
|
|
267
291
|
|
|
268
292
|
if (prop === 'transform') {
|
|
269
293
|
walkTransformTranslateNodes(parsed, (node, context) => {
|
|
270
294
|
if (node.type === 'function') {
|
|
271
|
-
|
|
272
|
-
|
|
295
|
+
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
296
|
+
if (skipToken) {
|
|
297
|
+
return skipToken;
|
|
273
298
|
}
|
|
274
299
|
|
|
275
300
|
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
@@ -283,22 +308,23 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
283
308
|
return false;
|
|
284
309
|
}
|
|
285
310
|
|
|
286
|
-
|
|
311
|
+
checkWordNode(node, context);
|
|
287
312
|
return false;
|
|
288
313
|
});
|
|
289
314
|
} else {
|
|
290
315
|
const checkedBounds = new Set();
|
|
291
316
|
walkRootValueNodes(parsed, (node, context) => {
|
|
292
317
|
if (node.type === 'function') {
|
|
293
|
-
|
|
294
|
-
|
|
318
|
+
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
319
|
+
if (skipToken) {
|
|
320
|
+
return skipToken;
|
|
295
321
|
}
|
|
296
322
|
|
|
297
323
|
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
298
324
|
for (const bound of collectMathBounds(node)) {
|
|
299
325
|
if (!checkedBounds.has(bound.node)) {
|
|
300
326
|
checkedBounds.add(bound.node);
|
|
301
|
-
|
|
327
|
+
checkWordNode(bound.node, context, bound.position);
|
|
302
328
|
}
|
|
303
329
|
}
|
|
304
330
|
}
|
|
@@ -314,14 +340,12 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
314
340
|
return false;
|
|
315
341
|
}
|
|
316
342
|
|
|
317
|
-
|
|
343
|
+
checkWordNode(node, context);
|
|
318
344
|
return false;
|
|
319
345
|
});
|
|
320
346
|
}
|
|
321
347
|
|
|
322
|
-
|
|
323
|
-
decl.value = parsed.toString();
|
|
324
|
-
}
|
|
348
|
+
writeDeclarationValue(decl, source, parsed);
|
|
325
349
|
});
|
|
326
350
|
};
|
|
327
351
|
};
|
package/src/rules/report.js
CHANGED
|
@@ -13,12 +13,14 @@ const { declarationValueIndex } = require('../core/value-nodes');
|
|
|
13
13
|
* against the root and replaced with the historical default so the file still
|
|
14
14
|
* lints instead of throwing inside Stylelint.
|
|
15
15
|
*/
|
|
16
|
-
function createTokenRegex(tokenPattern, result, ruleName) {
|
|
16
|
+
function createTokenRegex(tokenPattern, { result, root, ruleName }) {
|
|
17
17
|
try {
|
|
18
18
|
return new RegExp(tokenPattern);
|
|
19
19
|
} catch {
|
|
20
|
+
// Stylelint throws on a report without a node, so the root carries it (issue #222).
|
|
20
21
|
stylelint.utils.report({
|
|
21
22
|
message: `Invalid tokenPattern regex: ${tokenPattern}`,
|
|
23
|
+
node: root,
|
|
22
24
|
result,
|
|
23
25
|
ruleName,
|
|
24
26
|
});
|
|
@@ -8,7 +8,10 @@ const {
|
|
|
8
8
|
numbersEqual,
|
|
9
9
|
} = require('../../core/length');
|
|
10
10
|
const {
|
|
11
|
+
declarationValueSource,
|
|
12
|
+
isMathFunction,
|
|
11
13
|
walkRootValueNodes,
|
|
14
|
+
writeDeclarationValue,
|
|
12
15
|
} = require('../../core/value-nodes');
|
|
13
16
|
const {
|
|
14
17
|
formatTime,
|
|
@@ -39,6 +42,7 @@ const EASING_PROPERTIES = new Set([
|
|
|
39
42
|
'animation',
|
|
40
43
|
]);
|
|
41
44
|
const EASING_FUNCTIONS = new Set(['cubic-bezier', 'linear', 'steps']);
|
|
45
|
+
const SHORTHAND_PROPERTIES = new Set(['transition', 'animation']);
|
|
42
46
|
|
|
43
47
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
44
48
|
invalidDuration: (value, note = '') =>
|
|
@@ -96,13 +100,62 @@ function getFixedNodeValue(parsedTime, nearestMs, options) {
|
|
|
96
100
|
return null;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
return formatTime(converted, parsedTime.unit);
|
|
103
|
+
return formatTime(parsedTime.number < 0 ? -converted : converted, parsedTime.unit);
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
function functionToString(node) {
|
|
103
107
|
return valueParser.stringify(node);
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
/**
|
|
111
|
+
* The time nodes of a transition or animation shorthand that are its
|
|
112
|
+
* durations: the first time of each comma-separated layer (#236). The second
|
|
113
|
+
* is the delay, which may be negative. A function before the first time, such
|
|
114
|
+
* as var() or calc(), may itself be the duration, so that layer has no time
|
|
115
|
+
* known to be a duration and none is returned for it.
|
|
116
|
+
*/
|
|
117
|
+
function shorthandDurationNodes(parsed) {
|
|
118
|
+
const durations = new Set();
|
|
119
|
+
let settled = false;
|
|
120
|
+
|
|
121
|
+
for (const node of parsed.nodes) {
|
|
122
|
+
if (node.type === 'div' && node.value === ',') {
|
|
123
|
+
settled = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (settled) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (node.type === 'function') {
|
|
132
|
+
settled = !EASING_FUNCTIONS.has(node.value.toLowerCase());
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (node.type === 'word' && parseTimeToken(node.value)) {
|
|
137
|
+
durations.add(node);
|
|
138
|
+
settled = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return durations;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Whether a negative time in this place is an error: true for durations, false for delays. */
|
|
146
|
+
function createDurationRole(prop, parsed) {
|
|
147
|
+
if (prop.endsWith('-duration')) {
|
|
148
|
+
return () => true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!SHORTHAND_PROPERTIES.has(prop)) {
|
|
152
|
+
return () => false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const durations = shorthandDurationNodes(parsed);
|
|
156
|
+
return (node) => durations.has(node);
|
|
157
|
+
}
|
|
158
|
+
|
|
106
159
|
const ruleFunction = (primary, secondaryOptions) => {
|
|
107
160
|
return (root, result) => {
|
|
108
161
|
if (!validatePrimary(result, ruleName, primary) || !validateSecondaryOptions(result, secondaryOptions)) {
|
|
@@ -117,7 +170,9 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
117
170
|
return;
|
|
118
171
|
}
|
|
119
172
|
|
|
120
|
-
const
|
|
173
|
+
const source = declarationValueSource(decl);
|
|
174
|
+
const parsed = valueParser(source.parseText);
|
|
175
|
+
const isDuration = createDurationRole(prop, parsed);
|
|
121
176
|
let changed = false;
|
|
122
177
|
|
|
123
178
|
const reportDuration = (node, nearest, fixedValue = null) => {
|
|
@@ -161,6 +216,12 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
161
216
|
walkRootValueNodes(parsed, (node) => {
|
|
162
217
|
if (node.type === 'function') {
|
|
163
218
|
const functionName = node.value.toLowerCase();
|
|
219
|
+
// A time inside calc(), clamp(), min() or max() is a term of the
|
|
220
|
+
// expression, not a duration on its own (#232).
|
|
221
|
+
if (isMathFunction(functionName)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
164
225
|
if (!EASING_PROPERTIES.has(prop) || !EASING_FUNCTIONS.has(functionName)) {
|
|
165
226
|
return false;
|
|
166
227
|
}
|
|
@@ -180,7 +241,9 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
180
241
|
return false;
|
|
181
242
|
}
|
|
182
243
|
|
|
183
|
-
|
|
244
|
+
// A negative duration is invalid; a negative delay starts the motion
|
|
245
|
+
// partway through, so its size is checked like any other (#236).
|
|
246
|
+
if (parsedTime.number < 0 && isDuration(node)) {
|
|
184
247
|
reportDuration(node, null);
|
|
185
248
|
return false;
|
|
186
249
|
}
|
|
@@ -189,7 +252,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
189
252
|
return false;
|
|
190
253
|
}
|
|
191
254
|
|
|
192
|
-
const msValue = toMs(parsedTime.number, parsedTime.unit);
|
|
255
|
+
const msValue = toMs(Math.abs(parsedTime.number), parsedTime.unit);
|
|
193
256
|
if (msValue === null) {
|
|
194
257
|
return false;
|
|
195
258
|
}
|
|
@@ -214,7 +277,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
214
277
|
});
|
|
215
278
|
|
|
216
279
|
if (changed) {
|
|
217
|
-
decl
|
|
280
|
+
writeDeclarationValue(decl, source, parsed);
|
|
218
281
|
}
|
|
219
282
|
});
|
|
220
283
|
};
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
nearestText,
|
|
10
10
|
numbersEqual,
|
|
11
11
|
parseLengthToken,
|
|
12
|
+
pxOnScale,
|
|
12
13
|
toPx,
|
|
13
14
|
} = require('../../core/length');
|
|
14
15
|
const {
|
|
@@ -16,15 +17,20 @@ const {
|
|
|
16
17
|
createPropertyScaleResolver,
|
|
17
18
|
} = require('../../core/options');
|
|
18
19
|
const {
|
|
20
|
+
blankBlockComments,
|
|
19
21
|
collectMathBounds,
|
|
22
|
+
declarationValueSource,
|
|
20
23
|
isBoundFunction,
|
|
21
24
|
isKeyword,
|
|
22
25
|
isMathFunction,
|
|
23
|
-
isTokenFunction,
|
|
24
26
|
propertyMatches,
|
|
27
|
+
sassOperandNodes,
|
|
25
28
|
shouldLintMathArgument,
|
|
29
|
+
skipTokenFunction,
|
|
26
30
|
walkRootValueNodes,
|
|
27
31
|
walkTransformTranslateNodes,
|
|
32
|
+
writeDeclarationValue,
|
|
33
|
+
writeValueEdits,
|
|
28
34
|
} = require('../../core/value-nodes');
|
|
29
35
|
|
|
30
36
|
const {
|
|
@@ -72,6 +78,7 @@ function readDecisions(options, { result, root }) {
|
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
function checkLengthValue({
|
|
81
|
+
avoidTokens = [],
|
|
75
82
|
decl,
|
|
76
83
|
evaluated = null,
|
|
77
84
|
fixable = true,
|
|
@@ -149,7 +156,7 @@ function checkLengthValue({
|
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
const fixedValue = options.fixToScale && fixable
|
|
152
|
-
? replacementFor(parsedLength, nearest.nearest, options, decl.prop)
|
|
159
|
+
? replacementFor(parsedLength, nearest.nearest, options, decl.prop, avoidTokens)
|
|
153
160
|
: null;
|
|
154
161
|
|
|
155
162
|
report(node.value, decl, node, nearest, fixedValue, unit, evaluated ? evaluatedNote(evaluated, options) : '');
|
|
@@ -162,7 +169,7 @@ function checkLengthValue({
|
|
|
162
169
|
return false;
|
|
163
170
|
}
|
|
164
171
|
|
|
165
|
-
const isOnScale = scalePx
|
|
172
|
+
const isOnScale = pxOnScale(scalePx, pxValue, parsedLength.unit, options.baseFontSize);
|
|
166
173
|
if (isOnScale) {
|
|
167
174
|
return false;
|
|
168
175
|
}
|
|
@@ -173,7 +180,7 @@ function checkLengthValue({
|
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
const fixedValue = options.fixToScale && fixable
|
|
176
|
-
? replacementFor(parsedLength, nearest.nearest, options, decl.prop)
|
|
183
|
+
? replacementFor(parsedLength, nearest.nearest, options, decl.prop, avoidTokens)
|
|
177
184
|
: null;
|
|
178
185
|
|
|
179
186
|
report(node.value, decl, node, nearest, fixedValue, 'px', evaluated ? evaluatedNote(evaluated, options) : '');
|
|
@@ -193,14 +200,17 @@ function evaluatedNote(evaluated, options) {
|
|
|
193
200
|
* alone, as they always were.
|
|
194
201
|
*/
|
|
195
202
|
function checkSassValue({ decl, options, report, resolveVariable, scaleByUnit, scalePx }) {
|
|
196
|
-
|
|
203
|
+
// Offsets are offsets into the value as written, comments included (issue #233).
|
|
204
|
+
const source = declarationValueSource(decl);
|
|
205
|
+
const terms = evaluateScssValueList(blankBlockComments(source.parseText), resolveVariable, new Set());
|
|
197
206
|
if (!terms) {
|
|
198
207
|
// Not fully evaluable (an unknown variable, interpolation, a keyword): the
|
|
199
208
|
// literal walk below still checks the plain lengths in the value.
|
|
200
209
|
return false;
|
|
201
210
|
}
|
|
202
|
-
//
|
|
203
|
-
//
|
|
211
|
+
// Fixes are collected as spans of the written value and written once at the
|
|
212
|
+
// end. Stylelint orders the warnings by position.
|
|
213
|
+
const edits = [];
|
|
204
214
|
for (const term of [...terms].reverse()) {
|
|
205
215
|
if (!term.unit || term.number === 0) {
|
|
206
216
|
continue;
|
|
@@ -208,9 +218,9 @@ function checkSassValue({ decl, options, report, resolveVariable, scaleByUnit, s
|
|
|
208
218
|
const node = {
|
|
209
219
|
sourceIndex: term.start,
|
|
210
220
|
type: 'word',
|
|
211
|
-
get value() { return term.
|
|
221
|
+
get value() { return source.text.slice(term.start, term.end); },
|
|
212
222
|
// The fix writes into the declaration at the term's span.
|
|
213
|
-
set value(replacement) {
|
|
223
|
+
set value(replacement) { edits.push({ end: term.end, start: term.start, text: replacement }); },
|
|
214
224
|
};
|
|
215
225
|
checkLengthValue({
|
|
216
226
|
decl,
|
|
@@ -224,6 +234,7 @@ function checkSassValue({ decl, options, report, resolveVariable, scaleByUnit, s
|
|
|
224
234
|
scalePx,
|
|
225
235
|
});
|
|
226
236
|
}
|
|
237
|
+
writeValueEdits(decl, source, edits);
|
|
227
238
|
return true;
|
|
228
239
|
}
|
|
229
240
|
|
|
@@ -249,7 +260,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
249
260
|
options.decisions = readDecisions(options, { result, root });
|
|
250
261
|
withResolvedScale(options, root);
|
|
251
262
|
|
|
252
|
-
const tokenRegex = createTokenRegex(options.tokenPattern, result, ruleName);
|
|
263
|
+
const tokenRegex = createTokenRegex(options.tokenPattern, { result, root, ruleName });
|
|
253
264
|
if (options.fixWith === 'token') {
|
|
254
265
|
options.tokenIndex = tokenIndexFromDefinitions(
|
|
255
266
|
collectTokenDefinitions({
|
|
@@ -269,7 +280,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
269
280
|
const report = (value, decl, node, nearest, fixedValue = null, nearestUnit = 'px', extraNote = '', position = null) => {
|
|
270
281
|
const lower = nearest ? formatLength(nearest.lower, nearestUnit) : 'n/a';
|
|
271
282
|
const upper = nearest ? formatLength(nearest.upper, nearestUnit) : 'n/a';
|
|
272
|
-
const tokenNote = nearest ? tokenHoldsNote(fixedValue, formatLength(nearest.nearest, nearestUnit)) : '';
|
|
283
|
+
const tokenNote = nearest ? tokenHoldsNote(fixedValue, formatLength(nearest.nearest, nearestUnit), String(value).trim().startsWith('-')) : '';
|
|
273
284
|
reportValueNode({
|
|
274
285
|
decl,
|
|
275
286
|
message: messages.rejected(value, lower, upper, [fallbackNote, tokenNote, extraNote, options.note].filter(Boolean).join(' '), position, position ? pxAside(value, options.baseFontSize) : '', getScaleStateForProperty(decl.prop.toLowerCase()).group),
|
|
@@ -312,15 +323,18 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
312
323
|
return;
|
|
313
324
|
}
|
|
314
325
|
|
|
315
|
-
const
|
|
316
|
-
|
|
326
|
+
const source = declarationValueSource(decl);
|
|
327
|
+
const parsed = valueParser(source.parseText);
|
|
328
|
+
// Operands of a Sass expression that did not resolve are reported, never rewritten (issue #234).
|
|
329
|
+
const sassOperands = sassOperandNodes(parsed, decl.value);
|
|
317
330
|
|
|
318
331
|
if (prop === 'transform') {
|
|
319
332
|
walkTransformTranslateNodes(parsed, (node, context) => {
|
|
320
333
|
|
|
321
334
|
if (node.type === 'function') {
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
336
|
+
if (skipToken) {
|
|
337
|
+
return skipToken;
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
@@ -342,15 +356,16 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
342
356
|
return false;
|
|
343
357
|
}
|
|
344
358
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
359
|
+
checkLengthValue({
|
|
360
|
+
avoidTokens: context.tokenFallbackOf,
|
|
361
|
+
decl,
|
|
362
|
+
fixable: !sassOperands.has(node),
|
|
363
|
+
node,
|
|
364
|
+
options,
|
|
365
|
+
report,
|
|
366
|
+
scaleByUnit,
|
|
367
|
+
scalePx,
|
|
368
|
+
});
|
|
354
369
|
|
|
355
370
|
return false;
|
|
356
371
|
});
|
|
@@ -359,8 +374,9 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
359
374
|
walkRootValueNodes(parsed, (node, context) => {
|
|
360
375
|
|
|
361
376
|
if (node.type === 'function') {
|
|
362
|
-
|
|
363
|
-
|
|
377
|
+
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
378
|
+
if (skipToken) {
|
|
379
|
+
return skipToken;
|
|
364
380
|
}
|
|
365
381
|
|
|
366
382
|
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
@@ -373,7 +389,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
373
389
|
continue;
|
|
374
390
|
}
|
|
375
391
|
const boundReport = (value, d, n, nearest, fixed, unit, extra) => report(value, d, n, nearest, fixed, unit, extra, bound.position);
|
|
376
|
-
|
|
392
|
+
checkLengthValue({ avoidTokens: context.tokenFallbackOf, decl, fixable: !sassOperands.has(bound.node), node: bound.node, options, report: boundReport, scaleByUnit, scalePx });
|
|
377
393
|
}
|
|
378
394
|
}
|
|
379
395
|
|
|
@@ -399,23 +415,22 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
399
415
|
return false;
|
|
400
416
|
}
|
|
401
417
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
418
|
+
checkLengthValue({
|
|
419
|
+
avoidTokens: context.tokenFallbackOf,
|
|
420
|
+
decl,
|
|
421
|
+
fixable: !sassOperands.has(node),
|
|
422
|
+
node,
|
|
423
|
+
options,
|
|
424
|
+
report,
|
|
425
|
+
scaleByUnit,
|
|
426
|
+
scalePx,
|
|
427
|
+
});
|
|
411
428
|
|
|
412
429
|
return false;
|
|
413
430
|
});
|
|
414
431
|
}
|
|
415
432
|
|
|
416
|
-
|
|
417
|
-
decl.value = parsed.toString();
|
|
418
|
-
}
|
|
433
|
+
writeDeclarationValue(decl, source, parsed);
|
|
419
434
|
});
|
|
420
435
|
};
|
|
421
436
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time check that the ESLint plugin types fit ESLint's own config
|
|
3
|
+
* types (#242): a typed `eslint.config.ts` that registers the plugin with
|
|
4
|
+
* `defineConfig` must compile. Nothing here runs; it only has to type-check.
|
|
5
|
+
*/
|
|
6
|
+
import type { ESLint, Linter } from 'eslint';
|
|
7
|
+
import { defineConfig } from 'eslint/config';
|
|
8
|
+
import rhythmguard from 'eslint-plugin-rhythmguard';
|
|
9
|
+
import companion from 'stylelint-plugin-rhythmguard/eslint';
|
|
10
|
+
|
|
11
|
+
const config = defineConfig([
|
|
12
|
+
{
|
|
13
|
+
files: ['**/*.{js,jsx,mjs,cjs}'],
|
|
14
|
+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
|
|
15
|
+
plugins: { 'rhythmguard-tailwind': rhythmguard },
|
|
16
|
+
rules: {
|
|
17
|
+
'rhythmguard-tailwind/tailwind-class-use-scale': ['error', { scale: [0, 4, 8, 12, 16, 24, 32] }],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
files: ['**/*.jsx'],
|
|
22
|
+
plugins: { 'rhythmguard-tailwind': companion },
|
|
23
|
+
rules: companion.configs.recommended.rules,
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const asPlugin: ESLint.Plugin = rhythmguard;
|
|
28
|
+
const companionAsPlugin: ESLint.Plugin = companion;
|
|
29
|
+
const flatConfig: Linter.Config = {
|
|
30
|
+
plugins: { 'rhythmguard-tailwind': rhythmguard },
|
|
31
|
+
rules: rhythmguard.configs.recommended.rules,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
void config;
|
|
35
|
+
void asPlugin;
|
|
36
|
+
void companionAsPlugin;
|
|
37
|
+
void flatConfig;
|