stylelint-plugin-rhythmguard 3.12.0 → 3.13.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 +40 -0
- package/README.md +1 -1
- package/package.json +23 -1
- package/src/audit/args.js +23 -4
- package/src/audit/baseline.js +15 -2
- package/src/audit/codemod.js +56 -5
- package/src/audit/config.js +2 -1
- package/src/audit/contract.js +3 -0
- package/src/audit/decisions.js +45 -16
- package/src/audit/render-markdown.js +22 -21
- package/src/audit/render-text.js +1 -0
- package/src/audit/render-utils.js +28 -1
- package/src/audit/report.js +14 -2
- package/src/audit/scan/files.js +139 -30
- package/src/audit/token-chains.js +4 -1
- package/src/audit/token-ladders.js +23 -2
- package/src/cli/doctor.js +28 -3
- package/src/cli/fix.js +7 -6
- package/src/cli/index.js +2 -2
- package/src/cli/init.js +41 -5
- package/src/cli/quickstart.js +46 -8
- package/src/configs/embed.js +2 -0
- package/src/configs/motion.js +1 -0
- package/src/configs/recommended.js +1 -0
- package/src/configs/strict.js +1 -0
- package/src/configs/tailwind.js +1 -0
- package/src/core/decisions.js +2 -1
- package/src/core/json.js +25 -0
- package/src/core/length.js +11 -0
- package/src/core/options.js +39 -20
- package/src/core/scale-inference.js +78 -23
- package/src/core/tailwind-class-analysis.js +3 -2
- package/src/core/token-map.js +2 -1
- package/src/core/token-sources.js +2 -1
- package/src/eslint/index.js +14 -8
- package/src/eslint/rules/tailwind-class-use-motion-scale.js +1 -0
- package/src/eslint/rules/tailwind-class-use-scale.js +1 -0
- package/src/rules/no-offscale-transform/index.js +58 -53
- package/src/rules/prefer-token/index.js +31 -46
- package/src/rules/use-motion-scale/index.js +14 -1
- package/src/rules/use-scale/index.js +75 -102
- package/src/rules/validate.js +13 -4
- package/types/__checks__/consumer.ts +20 -1
- package/types/__checks__/eslint-config.ts +8 -0
- package/types/__checks__/shipped-configs.ts +96 -0
- package/types/audit.d.ts +101 -16
- package/types/index.d.ts +15 -0
- package/types/shared.d.ts +126 -20
|
@@ -34,6 +34,7 @@ function maybeCheckNodeText(node, sourceCode, context, analyzer, allowFix) {
|
|
|
34
34
|
|
|
35
35
|
module.exports = {
|
|
36
36
|
meta: {
|
|
37
|
+
type: 'suggestion',
|
|
37
38
|
docs: {
|
|
38
39
|
url: 'https://github.com/PetriLahdelma/stylelint-plugin-rhythmguard/blob/main/docs/rules/tailwind-class-use-scale.md',
|
|
39
40
|
description: 'Enforce spacing scale for Tailwind arbitrary spacing utilities in class strings',
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
nearestText,
|
|
10
10
|
numbersEqual,
|
|
11
11
|
parseLengthToken,
|
|
12
|
+
pxAside,
|
|
12
13
|
pxOnScale,
|
|
13
14
|
toPx,
|
|
14
15
|
} = require('../../core/length');
|
|
@@ -17,7 +18,9 @@ const {
|
|
|
17
18
|
createPropertyScaleResolver,
|
|
18
19
|
} = require('../../core/options');
|
|
19
20
|
const {
|
|
21
|
+
collectMathBounds,
|
|
20
22
|
declarationValueSource,
|
|
23
|
+
isBoundFunction,
|
|
21
24
|
isMathFunction,
|
|
22
25
|
shouldLintMathArgument,
|
|
23
26
|
skipTokenFunction,
|
|
@@ -41,8 +44,13 @@ const ruleName = 'rhythmguard/no-offscale-transform';
|
|
|
41
44
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
42
45
|
invalidPreset: (presetName, presetNames) =>
|
|
43
46
|
`Unknown scale preset "${presetName}". Available presets: ${presetNames.join(', ')}.`,
|
|
44
|
-
rejected: (value, lower, upper, note = '') =>
|
|
45
|
-
`Unexpected transform translation value "${value}". Use scale values (${nearestText(lower, upper)}).${note ? ` ${note}` : ''}`,
|
|
47
|
+
rejected: (value, lower, upper, note = '', position = null, aside = '') =>
|
|
48
|
+
`Unexpected transform translation ${position ? `${position} "${value}"${aside}` : `value "${value}"`}. Use scale values (${nearestText(lower, upper)}).${note ? ` ${note}` : ''}`,
|
|
49
|
+
// A value the options forbid outright has no nearest scale value to name (issue #274).
|
|
50
|
+
rejectedNegative: (value, note = '', position = null) =>
|
|
51
|
+
`Unexpected transform translation ${position || 'value'} "${value}". Negative values are disabled for this rule.${note ? ` ${note}` : ''}`,
|
|
52
|
+
rejectedPercentage: (value, note = '', position = null) =>
|
|
53
|
+
`Unexpected transform translation ${position || 'value'} "${value}". Percentages are disabled for this rule.${note ? ` ${note}` : ''}`,
|
|
46
54
|
});
|
|
47
55
|
|
|
48
56
|
const ruleFunction = (primary, secondaryOptions) => {
|
|
@@ -99,15 +107,17 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
99
107
|
const { scaleByUnit, scalePx } = getScaleStateForProperty(prop);
|
|
100
108
|
let changed = false;
|
|
101
109
|
|
|
102
|
-
// `
|
|
103
|
-
const report = (node, nearest, nearestUnit, fixedValue = null) => {
|
|
110
|
+
// `position` names a clamp(), min() or max() bound (issue #272).
|
|
111
|
+
const report = (node, nearest, nearestUnit, fixedValue = null, position = null) => {
|
|
104
112
|
reportValueNode({
|
|
105
113
|
decl,
|
|
106
114
|
message: messages.rejected(
|
|
107
115
|
node.value,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
[
|
|
116
|
+
formatLength(nearest.lower, nearestUnit),
|
|
117
|
+
formatLength(nearest.upper, nearestUnit),
|
|
118
|
+
[tokenHoldsNote(fixedValue, formatLength(nearest.nearest, nearestUnit), node.value.trim().startsWith('-')), options.note].filter(Boolean).join(' '),
|
|
119
|
+
position,
|
|
120
|
+
position ? pxAside(node.value, options.baseFontSize) : '',
|
|
111
121
|
),
|
|
112
122
|
node,
|
|
113
123
|
replacement: fixedValue,
|
|
@@ -116,7 +126,12 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
116
126
|
});
|
|
117
127
|
};
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
// A value the options forbid outright says so, with no fix (issue #274).
|
|
130
|
+
const reportForbidden = (node, message, position) => {
|
|
131
|
+
reportValueNode({ decl, message: message(node.value, options.note, position), node, result, ruleName });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const checkNode = (node, avoidTokens = [], position = null) => {
|
|
120
135
|
const parsedLength = parseLengthToken(node.value);
|
|
121
136
|
if (!parsedLength || parsedLength.number === 0) {
|
|
122
137
|
return;
|
|
@@ -128,7 +143,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
128
143
|
|
|
129
144
|
if (parsedLength.unit === '%') {
|
|
130
145
|
if (!options.allowPercentages) {
|
|
131
|
-
|
|
146
|
+
reportForbidden(node, messages.rejectedPercentage, position);
|
|
132
147
|
}
|
|
133
148
|
return;
|
|
134
149
|
}
|
|
@@ -138,7 +153,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
if (!options.allowNegative && parsedLength.number < 0) {
|
|
141
|
-
|
|
156
|
+
reportForbidden(node, messages.rejectedNegative, position);
|
|
142
157
|
return;
|
|
143
158
|
}
|
|
144
159
|
|
|
@@ -178,7 +193,7 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
178
193
|
? replacementFor(parsedLength, nearest.nearest, options, decl.prop, avoidTokens)
|
|
179
194
|
: null;
|
|
180
195
|
|
|
181
|
-
report(node, nearest, unit, fixedValue);
|
|
196
|
+
report(node, nearest, unit, fixedValue, position);
|
|
182
197
|
changed = true;
|
|
183
198
|
return;
|
|
184
199
|
}
|
|
@@ -202,62 +217,52 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
202
217
|
? replacementFor(parsedLength, nearest.nearest, options, decl.prop, avoidTokens)
|
|
203
218
|
: null;
|
|
204
219
|
|
|
205
|
-
report(node, nearest, 'px', fixedValue);
|
|
220
|
+
report(node, nearest, 'px', fixedValue, position);
|
|
206
221
|
changed = true;
|
|
207
222
|
};
|
|
208
223
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return false;
|
|
224
|
+
// The bounds of clamp(), min() and max() are checked as in use-scale
|
|
225
|
+
// (issue #272).
|
|
226
|
+
const checkedBounds = new Set();
|
|
227
|
+
const visit = (node, context) => {
|
|
228
|
+
if (node.type === 'function') {
|
|
229
|
+
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
230
|
+
if (skipToken) {
|
|
231
|
+
return skipToken;
|
|
222
232
|
}
|
|
223
233
|
|
|
224
|
-
if (
|
|
225
|
-
|
|
234
|
+
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
235
|
+
for (const bound of collectMathBounds(node)) {
|
|
236
|
+
if (!checkedBounds.has(bound.node)) {
|
|
237
|
+
checkedBounds.add(bound.node);
|
|
238
|
+
checkNode(bound.node, context.tokenFallbackOf, bound.position);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
226
241
|
}
|
|
227
242
|
|
|
228
|
-
if (
|
|
229
|
-
return
|
|
243
|
+
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
244
|
+
return true;
|
|
230
245
|
}
|
|
231
246
|
|
|
232
|
-
checkNode(node, context.tokenFallbackOf);
|
|
233
247
|
return false;
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
walkRootValueNodes(parsed, (node, context) => {
|
|
237
|
-
if (node.type === 'function') {
|
|
238
|
-
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
239
|
-
if (skipToken) {
|
|
240
|
-
return skipToken;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
248
|
+
}
|
|
246
249
|
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
if (node.type !== 'word' || checkedBounds.has(node)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
249
253
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
254
|
+
if (!shouldLintMathArgument(context, options)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
253
257
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
checkNode(node, context.tokenFallbackOf);
|
|
259
|
+
return false;
|
|
260
|
+
};
|
|
257
261
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
if (prop === 'transform') {
|
|
263
|
+
walkTransformTranslateNodes(parsed, visit);
|
|
264
|
+
} else {
|
|
265
|
+
walkRootValueNodes(parsed, visit);
|
|
261
266
|
}
|
|
262
267
|
|
|
263
268
|
if (changed) {
|
|
@@ -81,8 +81,9 @@ function resolveTokenReplacement(tokenMap, raw, parsedLength, options) {
|
|
|
81
81
|
if (options.unitStrategy === 'convert') {
|
|
82
82
|
const px = toPx(parsedLength.number, parsedLength.unit, options.baseFontSize);
|
|
83
83
|
if (px !== null) {
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// Formatted, so 1.1rem at a 14px base is "15.4px", not "15.400000000000002px" (issue #275).
|
|
85
|
+
lookups.push([formatLength(px, 'px'), false]);
|
|
86
|
+
if (negative) lookups.push([formatLength(Math.abs(px), 'px'), true]);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -289,60 +290,44 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
289
290
|
return true;
|
|
290
291
|
};
|
|
291
292
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
293
|
+
// The bounds of clamp(), min() and max() are checked in translate
|
|
294
|
+
// functions as anywhere else (issue #272).
|
|
295
|
+
const checkedBounds = new Set();
|
|
296
|
+
const visit = (node, context) => {
|
|
297
|
+
if (node.type === 'function') {
|
|
298
|
+
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
299
|
+
if (skipToken) {
|
|
300
|
+
return skipToken;
|
|
301
|
+
}
|
|
299
302
|
|
|
300
|
-
|
|
301
|
-
|
|
303
|
+
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
304
|
+
for (const bound of collectMathBounds(node)) {
|
|
305
|
+
if (!checkedBounds.has(bound.node)) {
|
|
306
|
+
checkedBounds.add(bound.node);
|
|
307
|
+
checkWordNode(bound.node, context, bound.position);
|
|
308
|
+
}
|
|
302
309
|
}
|
|
303
|
-
|
|
304
|
-
return false;
|
|
305
310
|
}
|
|
306
311
|
|
|
307
|
-
if (node.
|
|
308
|
-
return
|
|
312
|
+
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
313
|
+
return true;
|
|
309
314
|
}
|
|
310
315
|
|
|
311
|
-
checkWordNode(node, context);
|
|
312
316
|
return false;
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
const checkedBounds = new Set();
|
|
316
|
-
walkRootValueNodes(parsed, (node, context) => {
|
|
317
|
-
if (node.type === 'function') {
|
|
318
|
-
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
319
|
-
if (skipToken) {
|
|
320
|
-
return skipToken;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
324
|
-
for (const bound of collectMathBounds(node)) {
|
|
325
|
-
if (!checkedBounds.has(bound.node)) {
|
|
326
|
-
checkedBounds.add(bound.node);
|
|
327
|
-
checkWordNode(bound.node, context, bound.position);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
317
|
+
}
|
|
335
318
|
|
|
336
|
-
|
|
337
|
-
|
|
319
|
+
if (node.type !== 'word' || checkedBounds.has(node)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
338
322
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
323
|
+
checkWordNode(node, context);
|
|
324
|
+
return false;
|
|
325
|
+
};
|
|
342
326
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
327
|
+
if (prop === 'transform') {
|
|
328
|
+
walkTransformTranslateNodes(parsed, visit);
|
|
329
|
+
} else {
|
|
330
|
+
walkRootValueNodes(parsed, visit);
|
|
346
331
|
}
|
|
347
332
|
|
|
348
333
|
writeDeclarationValue(decl, source, parsed);
|
|
@@ -23,7 +23,7 @@ const {
|
|
|
23
23
|
} = require('../../core/time');
|
|
24
24
|
|
|
25
25
|
const { reportValueNode } = require('../report');
|
|
26
|
-
const { validatePrimary } = require('../validate');
|
|
26
|
+
const { validatePrimary, validateSecondaryOptionShapes } = require('../validate');
|
|
27
27
|
const { isNote, normalizeNote, withNote } = require('../../core/options');
|
|
28
28
|
|
|
29
29
|
const ruleName = 'rhythmguard/use-motion-scale';
|
|
@@ -69,7 +69,20 @@ function buildOptions(rawOptions) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Stylelint checks list options per element, so a scalar or an empty list
|
|
73
|
+
// passed and the defaults were used without a word (issue #273).
|
|
74
|
+
const MOTION_SHAPES = Object.freeze({
|
|
75
|
+
durationScale: Object.freeze({ expectsArray: true, nonEmpty: true }),
|
|
76
|
+
durationUnits: Object.freeze({ expectsArray: true, nonEmpty: true }),
|
|
77
|
+
});
|
|
78
|
+
|
|
72
79
|
function validateSecondaryOptions(result, secondaryOptions) {
|
|
80
|
+
const validEntries = validateEntries(result, secondaryOptions);
|
|
81
|
+
const validShapes = validateSecondaryOptionShapes(result, ruleName, secondaryOptions, MOTION_SHAPES);
|
|
82
|
+
return validEntries && validShapes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateEntries(result, secondaryOptions) {
|
|
73
86
|
return stylelint.utils.validateOptions(result, ruleName, {
|
|
74
87
|
actual: secondaryOptions,
|
|
75
88
|
optional: true,
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
nearestText,
|
|
10
10
|
numbersEqual,
|
|
11
11
|
parseLengthToken,
|
|
12
|
+
pxAside,
|
|
12
13
|
pxOnScale,
|
|
13
14
|
toPx,
|
|
14
15
|
} = require('../../core/length');
|
|
@@ -52,17 +53,18 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
|
52
53
|
`Unknown scale preset "${presetName}". Available presets: ${presetNames.join(', ')}.`,
|
|
53
54
|
rejected: (value, lower, upper, note = '', position = null, aside = '', group = null) =>
|
|
54
55
|
`Unexpected off-scale ${position ? `${position} "${value}"${aside}` : `value "${value}"`}. Use ${group ? `${group} ` : ''}scale values (${nearestText(lower, upper)}).${note ? ` ${note}` : ''}`,
|
|
56
|
+
// A value the options forbid outright has no nearest scale value to name (issue #274).
|
|
57
|
+
rejectedNegative: (value, note = '', position = null) =>
|
|
58
|
+
`Unexpected ${position || 'value'} "${value}". Negative values are disabled for this rule.${note ? ` ${note}` : ''}`,
|
|
59
|
+
rejectedPercentage: (value, note = '', position = null) =>
|
|
60
|
+
`Unexpected ${position || 'value'} "${value}". Percentages are disabled for this rule.${note ? ` ${note}` : ''}`,
|
|
55
61
|
});
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
const px = toPx(Math.abs(parsed.number), parsed.unit, baseFontSize);
|
|
64
|
-
return px === null ? '' : ` (${formatLength(parsed.number < 0 ? -px : px, 'px')})`;
|
|
65
|
-
}
|
|
63
|
+
// Passed as `nearest` for a value that `allowNegative` or `allowPercentages` forbids.
|
|
64
|
+
const FORBIDDEN_NEGATIVE = Object.freeze({ forbidden: 'negative' });
|
|
65
|
+
const FORBIDDEN_PERCENTAGE = Object.freeze({ forbidden: 'percentage' });
|
|
66
|
+
|
|
67
|
+
|
|
66
68
|
|
|
67
69
|
/** Decisions from .rhythmguardrc.json; an invalid section is reported once and ignored. */
|
|
68
70
|
function readDecisions(options, { result, root }) {
|
|
@@ -109,7 +111,7 @@ function checkLengthValue({
|
|
|
109
111
|
return false;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
report(node.value, decl, node,
|
|
114
|
+
report(node.value, decl, node, FORBIDDEN_PERCENTAGE);
|
|
113
115
|
return false;
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -118,7 +120,7 @@ function checkLengthValue({
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
if (!options.allowNegative && parsedLength.number < 0) {
|
|
121
|
-
report(node.value, decl, node,
|
|
123
|
+
report(node.value, decl, node, FORBIDDEN_NEGATIVE);
|
|
122
124
|
return false;
|
|
123
125
|
}
|
|
124
126
|
|
|
@@ -278,12 +280,19 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
278
280
|
const getScaleStateForProperty = createPropertyScaleResolver(options);
|
|
279
281
|
|
|
280
282
|
const report = (value, decl, node, nearest, fixedValue = null, nearestUnit = 'px', extraNote = '', position = null) => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
let message;
|
|
284
|
+
if (nearest.forbidden) {
|
|
285
|
+
const note = [fallbackNote, extraNote, options.note].filter(Boolean).join(' ');
|
|
286
|
+
message = nearest.forbidden === 'negative'
|
|
287
|
+
? messages.rejectedNegative(value, note, position)
|
|
288
|
+
: messages.rejectedPercentage(value, note, position);
|
|
289
|
+
} else {
|
|
290
|
+
const tokenNote = tokenHoldsNote(fixedValue, formatLength(nearest.nearest, nearestUnit), String(value).trim().startsWith('-'));
|
|
291
|
+
message = messages.rejected(value, formatLength(nearest.lower, nearestUnit), formatLength(nearest.upper, nearestUnit), [fallbackNote, tokenNote, extraNote, options.note].filter(Boolean).join(' '), position, position ? pxAside(value, options.baseFontSize) : '', getScaleStateForProperty(decl.prop.toLowerCase()).group);
|
|
292
|
+
}
|
|
284
293
|
reportValueNode({
|
|
285
294
|
decl,
|
|
286
|
-
message
|
|
295
|
+
message,
|
|
287
296
|
node,
|
|
288
297
|
replacement: fixedValue,
|
|
289
298
|
result,
|
|
@@ -328,106 +337,70 @@ const ruleFunction = (primary, secondaryOptions) => {
|
|
|
328
337
|
// Operands of a Sass expression that did not resolve are reported, never rewritten (issue #234).
|
|
329
338
|
const sassOperands = sassOperandNodes(parsed, decl.value);
|
|
330
339
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (isMathFunction(node.value) && !options.enforceInsideMathFunctions) {
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return false;
|
|
340
|
+
// The bounds of clamp(), min() and max() are checked in translate
|
|
341
|
+
// functions as anywhere else (issue #272).
|
|
342
|
+
const checkedBounds = new Set();
|
|
343
|
+
const visit = (node, context) => {
|
|
344
|
+
if (node.type === 'function') {
|
|
345
|
+
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
346
|
+
if (skipToken) {
|
|
347
|
+
return skipToken;
|
|
345
348
|
}
|
|
346
349
|
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (isKeyword(node.value, options.ignoreValues)) {
|
|
352
|
-
return false;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (!shouldLintMathArgument(context, options)) {
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
checkLengthValue({
|
|
360
|
-
avoidTokens: context.tokenFallbackOf,
|
|
361
|
-
decl,
|
|
362
|
-
fixable: !sassOperands.has(node),
|
|
363
|
-
node,
|
|
364
|
-
options,
|
|
365
|
-
report,
|
|
366
|
-
scaleByUnit,
|
|
367
|
-
scalePx,
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
return false;
|
|
371
|
-
});
|
|
372
|
-
} else {
|
|
373
|
-
const checkedBounds = new Set();
|
|
374
|
-
walkRootValueNodes(parsed, (node, context) => {
|
|
375
|
-
|
|
376
|
-
if (node.type === 'function') {
|
|
377
|
-
const skipToken = skipTokenFunction(node, options.tokenFunctions, tokenRegex);
|
|
378
|
-
if (skipToken) {
|
|
379
|
-
return skipToken;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
383
|
-
for (const bound of collectMathBounds(node)) {
|
|
384
|
-
if (checkedBounds.has(bound.node)) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
checkedBounds.add(bound.node);
|
|
388
|
-
if (isKeyword(bound.node.value, options.ignoreValues)) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const boundReport = (value, d, n, nearest, fixed, unit, extra) => report(value, d, n, nearest, fixed, unit, extra, bound.position);
|
|
392
|
-
checkLengthValue({ avoidTokens: context.tokenFallbackOf, decl, fixable: !sassOperands.has(bound.node), node: bound.node, options, report: boundReport, scaleByUnit, scalePx });
|
|
350
|
+
if (options.checkMathBounds && isBoundFunction(node)) {
|
|
351
|
+
for (const bound of collectMathBounds(node)) {
|
|
352
|
+
if (checkedBounds.has(bound.node)) {
|
|
353
|
+
continue;
|
|
393
354
|
}
|
|
355
|
+
checkedBounds.add(bound.node);
|
|
356
|
+
if (isKeyword(bound.node.value, options.ignoreValues)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const boundReport = (value, d, n, nearest, fixed, unit, extra) => report(value, d, n, nearest, fixed, unit, extra, bound.position);
|
|
360
|
+
checkLengthValue({ avoidTokens: context.tokenFallbackOf, decl, fixable: !sassOperands.has(bound.node), node: bound.node, options, report: boundReport, scaleByUnit, scalePx });
|
|
394
361
|
}
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
isMathFunction(node.value) &&
|
|
398
|
-
!options.enforceInsideMathFunctions
|
|
399
|
-
) {
|
|
400
|
-
return true;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return false;
|
|
404
362
|
}
|
|
405
363
|
|
|
406
|
-
if (
|
|
407
|
-
|
|
364
|
+
if (
|
|
365
|
+
isMathFunction(node.value) &&
|
|
366
|
+
!options.enforceInsideMathFunctions
|
|
367
|
+
) {
|
|
368
|
+
return true;
|
|
408
369
|
}
|
|
409
370
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
413
373
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
374
|
+
if (node.type !== 'word' || checkedBounds.has(node)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
417
377
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
fixable: !sassOperands.has(node),
|
|
422
|
-
node,
|
|
423
|
-
options,
|
|
424
|
-
report,
|
|
425
|
-
scaleByUnit,
|
|
426
|
-
scalePx,
|
|
427
|
-
});
|
|
378
|
+
if (isKeyword(node.value, options.ignoreValues)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
428
381
|
|
|
382
|
+
if (!shouldLintMathArgument(context, options)) {
|
|
429
383
|
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
checkLengthValue({
|
|
387
|
+
avoidTokens: context.tokenFallbackOf,
|
|
388
|
+
decl,
|
|
389
|
+
fixable: !sassOperands.has(node),
|
|
390
|
+
node,
|
|
391
|
+
options,
|
|
392
|
+
report,
|
|
393
|
+
scaleByUnit,
|
|
394
|
+
scalePx,
|
|
430
395
|
});
|
|
396
|
+
|
|
397
|
+
return false;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
if (prop === 'transform') {
|
|
401
|
+
walkTransformTranslateNodes(parsed, visit);
|
|
402
|
+
} else {
|
|
403
|
+
walkRootValueNodes(parsed, visit);
|
|
431
404
|
}
|
|
432
405
|
|
|
433
406
|
writeDeclarationValue(decl, source, parsed);
|
package/src/rules/validate.js
CHANGED
|
@@ -33,10 +33,18 @@ function validateSecondaryOptionShapes(result, ruleName, secondaryOptions, schem
|
|
|
33
33
|
continue;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const literals = Array.isArray(descriptor.allowLiterals) ? descriptor.allowLiterals : [];
|
|
37
|
+
const literalAllowed = literals.includes(optionValue);
|
|
38
|
+
|
|
39
|
+
// A literal such as "auto" stands for the whole option, so `["auto"]` is
|
|
40
|
+
// invalid, and an empty list where a scale is expected checks nothing
|
|
41
|
+
// (issue #273).
|
|
42
|
+
const invalidArray = Array.isArray(optionValue) && (
|
|
43
|
+
(descriptor.nonEmpty && optionValue.length === 0)
|
|
44
|
+
|| optionValue.some((entry) => literals.includes(entry))
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if ((descriptor.expectsArray && !Array.isArray(optionValue) && !literalAllowed) || invalidArray) {
|
|
40
48
|
valid = false;
|
|
41
49
|
result.warn(
|
|
42
50
|
`Invalid value ${stringifyOptionValue(optionValue)} for option "${optionName}" of rule "${ruleName}"`,
|
|
@@ -126,6 +134,7 @@ function validatePrimary(result, ruleName, primary) {
|
|
|
126
134
|
|
|
127
135
|
module.exports = {
|
|
128
136
|
validatePrimary,
|
|
137
|
+
validateSecondaryOptionShapes,
|
|
129
138
|
validateNoOffscaleTransformSecondaryOptions,
|
|
130
139
|
validatePreferTokenSecondaryOptions,
|
|
131
140
|
validateUseScaleSecondaryOptions,
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
AuditReport,
|
|
10
10
|
RhythmguardRuleOptions,
|
|
11
11
|
RhythmguardStylelintConfig,
|
|
12
|
+
ScalePresetMetadata,
|
|
12
13
|
} from 'stylelint-plugin-rhythmguard';
|
|
13
14
|
import { createAuditReport, toAuditContractReport } from 'stylelint-plugin-rhythmguard/audit';
|
|
14
15
|
import eslintPlugin from 'stylelint-plugin-rhythmguard/eslint';
|
|
@@ -48,6 +49,9 @@ const name: string = useScaleName;
|
|
|
48
49
|
|
|
49
50
|
const presetNames: string[] = listScalePresetNames();
|
|
50
51
|
const preset = getScalePreset('rhythmic-4');
|
|
52
|
+
// Unknown names return null, as the runtime does (#281).
|
|
53
|
+
const missingPreset: readonly number[] | null = getScalePreset('nope');
|
|
54
|
+
const missingMetadata: ScalePresetMetadata | null = presets.getCommunityScaleMetadata('nope');
|
|
51
55
|
const presetsAgain: string[] = presets.listScalePresetNames();
|
|
52
56
|
|
|
53
57
|
const eslintRule = eslint.rules['tailwind-class-use-scale'];
|
|
@@ -55,8 +59,21 @@ const eslintRuleAgain = eslintPlugin.rules['tailwind-class-use-motion-scale'];
|
|
|
55
59
|
|
|
56
60
|
async function runAudit(): Promise<AuditContractReport> {
|
|
57
61
|
const report: AuditReport = await createAuditReport({ dir: './src', noConfig: true });
|
|
58
|
-
const viaPlugin: AuditReport = await audit.createAuditReport({ dir: './src' });
|
|
62
|
+
const viaPlugin: AuditReport = await audit.createAuditReport({ dir: './src', scale: undefined });
|
|
59
63
|
void viaPlugin;
|
|
64
|
+
// The fields the runtime report has (#284).
|
|
65
|
+
const cleanliness: number = report.summary.scaleCleanliness + report.scaleCleanliness;
|
|
66
|
+
const counts: number = report.summary.totalFindings + report.summary.filesWithIssues + report.filesWithIssues;
|
|
67
|
+
const configFile: string | undefined = report.config?.file;
|
|
68
|
+
const firstText: string | undefined = report.findings.css[0]?.text;
|
|
69
|
+
const resolved: number | undefined = report.baseline?.resolvedFindingsCount;
|
|
70
|
+
const contractConfig: { file: string } | null = toAuditContractReport(report).command.config;
|
|
71
|
+
void cleanliness;
|
|
72
|
+
void counts;
|
|
73
|
+
void configFile;
|
|
74
|
+
void firstText;
|
|
75
|
+
void resolved;
|
|
76
|
+
void contractConfig;
|
|
60
77
|
return toAuditContractReport(report);
|
|
61
78
|
}
|
|
62
79
|
|
|
@@ -68,6 +85,8 @@ void docUrl;
|
|
|
68
85
|
void name;
|
|
69
86
|
void presetNames;
|
|
70
87
|
void preset;
|
|
88
|
+
void missingPreset;
|
|
89
|
+
void missingMetadata;
|
|
71
90
|
void presetsAgain;
|
|
72
91
|
void eslintRule;
|
|
73
92
|
void eslintRuleAgain;
|
|
@@ -31,7 +31,15 @@ const flatConfig: Linter.Config = {
|
|
|
31
31
|
rules: rhythmguard.configs.recommended.rules,
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
// configs.recommended registers the plugin, so it stands alone in a config array (#279).
|
|
35
|
+
const standalone = defineConfig([rhythmguard.configs.recommended]);
|
|
36
|
+
const recommendedAsConfig: Linter.Config = companion.configs.recommended;
|
|
37
|
+
const spreadRecommended: Linter.Config = { ...rhythmguard.configs.recommended, files: ['**/*.jsx'] };
|
|
38
|
+
|
|
34
39
|
void config;
|
|
40
|
+
void standalone;
|
|
41
|
+
void recommendedAsConfig;
|
|
42
|
+
void spreadRecommended;
|
|
35
43
|
void asPlugin;
|
|
36
44
|
void companionAsPlugin;
|
|
37
45
|
void flatConfig;
|