@usefragments/core 1.2.0 → 1.4.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/dist/{chunk-BQQTMGLA.js → chunk-57QDBEHQ.js} +79 -6
- package/dist/chunk-57QDBEHQ.js.map +1 -0
- package/dist/{chunk-DBVTSUMT.js → chunk-CBSNXFOD.js} +20 -6
- package/dist/chunk-CBSNXFOD.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +2 -2
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-DYSirwJu.d.ts → governance-DxdJV6lx.d.ts} +10 -0
- package/dist/index-0lmh0Lbo.d.ts +6981 -0
- package/dist/index.d.ts +65 -29
- package/dist/index.js +397 -127
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +18 -18
- package/dist/schemas/index.d.ts +3 -3377
- package/dist/schemas/index.js +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-format.test.ts +75 -3
- package/src/agent-format.ts +28 -6
- package/src/constants.ts +3 -0
- package/src/evidence.ts +27 -0
- package/src/facts/builders.ts +3 -1
- package/src/facts/fact-index.ts +22 -2
- package/src/facts/facts.test.ts +1 -1
- package/src/facts/types.ts +16 -1
- package/src/index.ts +4 -0
- package/src/rules/__tests__/fix-emission-invariant.test.ts +174 -0
- package/src/rules/__tests__/tokens-require-dual-fallback.test.ts +90 -0
- package/src/rules/components-prefer-library.test.ts +148 -0
- package/src/rules/components-prefer-library.ts +52 -1
- package/src/rules/emit-gate.test.ts +16 -0
- package/src/rules/emit-gate.ts +6 -3
- package/src/rules/finding.ts +3 -0
- package/src/rules/rules.test.ts +2 -1
- package/src/rules/spacing-resolution.ts +5 -8
- package/src/rules/styles-no-raw-color.test.ts +213 -0
- package/src/rules/styles-no-raw-color.ts +157 -23
- package/src/rules/styles-no-raw-dimensions.ts +31 -12
- package/src/rules/styles-no-raw-spacing.ts +33 -11
- package/src/rules/styles-no-raw-typography.ts +37 -15
- package/src/rules/taxonomy.test.ts +13 -0
- package/src/rules/tokens-require-dual-fallback.ts +54 -17
- package/src/rules/utils.ts +51 -0
- package/src/schema.ts +6 -2
- package/src/schemas/index.ts +81 -3
- package/src/token-types.ts +18 -0
- package/src/tokens/__tests__/source-names.test.ts +42 -0
- package/src/tokens/categories.ts +54 -24
- package/src/tokens/design-token-parser.test.ts +24 -5
- package/src/tokens/design-token-parser.ts +40 -8
- package/src/tokens/scss.test.ts +94 -0
- package/src/tokens/scss.ts +11 -4
- package/src/types.ts +11 -0
- package/dist/chunk-BQQTMGLA.js.map +0 -1
- package/dist/chunk-DBVTSUMT.js.map +0 -1
|
@@ -18,11 +18,12 @@ import { makeFinding } from "./finding.js";
|
|
|
18
18
|
import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
|
|
19
19
|
import {
|
|
20
20
|
detectRawColor,
|
|
21
|
+
emitFix,
|
|
21
22
|
indexComponentByNodeId,
|
|
22
|
-
isApplicableTokenReference,
|
|
23
23
|
isExemptColor,
|
|
24
24
|
readUsageNode,
|
|
25
25
|
tokenReference,
|
|
26
|
+
type FixEmission,
|
|
26
27
|
} from "./utils.js";
|
|
27
28
|
|
|
28
29
|
export const RULE_ID = "styles/no-raw-color";
|
|
@@ -36,6 +37,12 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
36
37
|
const preferLabel = policy.prefer === "token" ? "design token" : "CSS variable";
|
|
37
38
|
const findings: Finding[] = [];
|
|
38
39
|
|
|
40
|
+
// Newly-broadened detections (SVG paint attributes, runtime-constructed
|
|
41
|
+
// colors) ship advisory this release so CI gates do not harden on findings
|
|
42
|
+
// teams have not yet had a chance to triage. Existing passes keep the
|
|
43
|
+
// configured severity.
|
|
44
|
+
const advisorySeverity = policy.severity === "error" ? "warn" : policy.severity;
|
|
45
|
+
|
|
39
46
|
for (const decl of ix.byKind("style_declaration")) {
|
|
40
47
|
if (decl.property.startsWith("--") && decl.declaredTokenSource === true) continue;
|
|
41
48
|
const color = detectRawColor(decl.value);
|
|
@@ -43,6 +50,10 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
43
50
|
if (isExemptColor(color, policy.except)) continue;
|
|
44
51
|
|
|
45
52
|
const resolution = resolveColorToken(decl.property, color, colorTokens);
|
|
53
|
+
const fixEmission =
|
|
54
|
+
resolution?.kind === "exact"
|
|
55
|
+
? buildTokenFix(decl.property, decl.value, color, resolution, ix)
|
|
56
|
+
: undefined;
|
|
46
57
|
const evidenceIds = resolution
|
|
47
58
|
? [decl.id, policy.id, resolution.token.id]
|
|
48
59
|
: [decl.id, policy.id];
|
|
@@ -63,10 +74,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
63
74
|
property: decl.property,
|
|
64
75
|
value: color,
|
|
65
76
|
},
|
|
66
|
-
fix:
|
|
67
|
-
resolution?.kind === "exact"
|
|
68
|
-
? buildTokenFix(decl.property, decl.value, color, resolution)
|
|
69
|
-
: undefined,
|
|
77
|
+
fix: fixEmission?.fix,
|
|
70
78
|
attributes: {
|
|
71
79
|
property: decl.property,
|
|
72
80
|
rawValue: decl.value,
|
|
@@ -74,6 +82,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
74
82
|
source: "css",
|
|
75
83
|
suggestedToken: resolution?.token.name,
|
|
76
84
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
85
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
77
86
|
},
|
|
78
87
|
})
|
|
79
88
|
);
|
|
@@ -81,6 +90,43 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
81
90
|
|
|
82
91
|
const componentByNode = indexComponentByNodeId(ix);
|
|
83
92
|
for (const inline of ix.byKind("usage_inline_style")) {
|
|
93
|
+
// #11b — a runtime-constructed color (`` `#${red}` ``, `"rgb(" + r + …`).
|
|
94
|
+
// The extractor already proved the static fragments begin a color literal,
|
|
95
|
+
// so surface it detect-only: the value is partly runtime, so it never
|
|
96
|
+
// carries a deterministic fix and cannot resolve to a token.
|
|
97
|
+
if (inline.valueKind === "dynamic-raw") {
|
|
98
|
+
const node = readUsageNode(ix, inline.nodeId);
|
|
99
|
+
if (!node) continue;
|
|
100
|
+
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
101
|
+
const evidenceIds = componentEvidenceId
|
|
102
|
+
? [node.id, componentEvidenceId, inline.id, policy.id]
|
|
103
|
+
: [node.id, inline.id, policy.id];
|
|
104
|
+
findings.push(
|
|
105
|
+
makeFinding({
|
|
106
|
+
ruleId: RULE_ID,
|
|
107
|
+
ruleVersion: RULE_VERSION,
|
|
108
|
+
severity: advisorySeverity,
|
|
109
|
+
message: `Runtime-constructed raw color \`${inline.value}\` on inline \`${inline.property}\`. Use a ${preferLabel} instead.`,
|
|
110
|
+
location: node.location,
|
|
111
|
+
evidence: ix.evidence(evidenceIds),
|
|
112
|
+
fingerprintIdentity: {
|
|
113
|
+
source: "usage_inline_style_dynamic",
|
|
114
|
+
file: node.file,
|
|
115
|
+
nodePath: node.nodePath,
|
|
116
|
+
element: node.element,
|
|
117
|
+
property: inline.property,
|
|
118
|
+
value: inline.value,
|
|
119
|
+
},
|
|
120
|
+
attributes: {
|
|
121
|
+
property: inline.property,
|
|
122
|
+
rawValue: inline.value,
|
|
123
|
+
source: "jsx",
|
|
124
|
+
dynamic: true,
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
84
130
|
if (inline.valueKind === "css-variable") continue;
|
|
85
131
|
const color = detectRawColor(inline.value);
|
|
86
132
|
if (!color) continue;
|
|
@@ -91,6 +137,10 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
91
137
|
|
|
92
138
|
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
93
139
|
const resolution = resolveColorToken(inline.property, color, colorTokens);
|
|
140
|
+
const fixEmission =
|
|
141
|
+
resolution?.kind === "exact"
|
|
142
|
+
? buildTokenFix(inline.property, inline.value, color, resolution, ix)
|
|
143
|
+
: undefined;
|
|
94
144
|
const baseEvidence = componentEvidenceId
|
|
95
145
|
? [node.id, componentEvidenceId, inline.id, policy.id]
|
|
96
146
|
: [node.id, inline.id, policy.id];
|
|
@@ -112,10 +162,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
112
162
|
property: inline.property,
|
|
113
163
|
value: color,
|
|
114
164
|
},
|
|
115
|
-
fix:
|
|
116
|
-
resolution?.kind === "exact"
|
|
117
|
-
? buildTokenFix(inline.property, inline.value, color, resolution)
|
|
118
|
-
: undefined,
|
|
165
|
+
fix: fixEmission?.fix,
|
|
119
166
|
attributes: {
|
|
120
167
|
property: inline.property,
|
|
121
168
|
rawValue: inline.value,
|
|
@@ -123,6 +170,64 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
123
170
|
source: "jsx",
|
|
124
171
|
suggestedToken: resolution?.token.name,
|
|
125
172
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
173
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// #9d — SVG presentation attributes carry color in JSX markup, not CSS, so
|
|
180
|
+
// they arrive as `usage_prop_resolved` facts the two passes above never read.
|
|
181
|
+
// Scan a fixed paint-attribute allowlist for static raw colors. Data-URI
|
|
182
|
+
// colors (hex inside an encoded `url("data:…")`) are intentionally left out.
|
|
183
|
+
for (const prop of ix.byKind("usage_prop_resolved")) {
|
|
184
|
+
if (prop.resolution !== "static") continue;
|
|
185
|
+
if (typeof prop.value !== "string") continue;
|
|
186
|
+
if (!SVG_PAINT_ATTRS.has(prop.prop)) continue;
|
|
187
|
+
const color = detectRawColor(prop.value);
|
|
188
|
+
if (!color) continue;
|
|
189
|
+
if (isExemptColor(color, policy.except)) continue;
|
|
190
|
+
|
|
191
|
+
const node = readUsageNode(ix, prop.nodeId);
|
|
192
|
+
if (!node) continue;
|
|
193
|
+
|
|
194
|
+
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
195
|
+
const cssProperty = SVG_PAINT_CSS_PROPERTY[prop.prop] ?? prop.prop;
|
|
196
|
+
const resolution = resolveColorToken(cssProperty, color, colorTokens);
|
|
197
|
+
const fixEmission =
|
|
198
|
+
resolution?.kind === "exact"
|
|
199
|
+
? buildTokenFix(cssProperty, prop.value, color, resolution, ix)
|
|
200
|
+
: undefined;
|
|
201
|
+
const baseEvidence = componentEvidenceId
|
|
202
|
+
? [node.id, componentEvidenceId, prop.id, policy.id]
|
|
203
|
+
: [node.id, prop.id, policy.id];
|
|
204
|
+
const evidenceIds = resolution ? [...baseEvidence, resolution.token.id] : baseEvidence;
|
|
205
|
+
|
|
206
|
+
findings.push(
|
|
207
|
+
makeFinding({
|
|
208
|
+
ruleId: RULE_ID,
|
|
209
|
+
ruleVersion: RULE_VERSION,
|
|
210
|
+
severity: advisorySeverity,
|
|
211
|
+
message: colorMessage(color, `\`${prop.prop}\``, resolution, preferLabel),
|
|
212
|
+
location: prop.location ?? node.location,
|
|
213
|
+
evidence: ix.evidence(evidenceIds),
|
|
214
|
+
fingerprintIdentity: {
|
|
215
|
+
source: "usage_prop_resolved",
|
|
216
|
+
file: node.file,
|
|
217
|
+
nodePath: node.nodePath,
|
|
218
|
+
element: node.element,
|
|
219
|
+
property: prop.prop,
|
|
220
|
+
value: color,
|
|
221
|
+
},
|
|
222
|
+
fix: fixEmission?.fix,
|
|
223
|
+
attributes: {
|
|
224
|
+
property: prop.prop,
|
|
225
|
+
rawValue: prop.value,
|
|
226
|
+
color,
|
|
227
|
+
source: "jsx",
|
|
228
|
+
suggestedToken: resolution?.token.name,
|
|
229
|
+
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
230
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
126
231
|
},
|
|
127
232
|
})
|
|
128
233
|
);
|
|
@@ -131,6 +236,31 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
131
236
|
return findings;
|
|
132
237
|
}
|
|
133
238
|
|
|
239
|
+
/**
|
|
240
|
+
* SVG presentation attributes that carry a paint (color) value in JSX markup.
|
|
241
|
+
* Both the raw-SVG kebab spelling and React's camelCase normalization are
|
|
242
|
+
* listed so `stop-color` and `stopColor` both resolve.
|
|
243
|
+
*/
|
|
244
|
+
const SVG_PAINT_ATTRS = new Set([
|
|
245
|
+
"fill",
|
|
246
|
+
"stroke",
|
|
247
|
+
"color",
|
|
248
|
+
"stop-color",
|
|
249
|
+
"stopColor",
|
|
250
|
+
"flood-color",
|
|
251
|
+
"floodColor",
|
|
252
|
+
"lighting-color",
|
|
253
|
+
"lightingColor",
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
/** Map camelCase paint attributes to their CSS property name so the token
|
|
257
|
+
* role-resolver (`colorRoleForProperty`) reasons over a canonical spelling. */
|
|
258
|
+
const SVG_PAINT_CSS_PROPERTY: Record<string, string> = {
|
|
259
|
+
stopColor: "stop-color",
|
|
260
|
+
floodColor: "flood-color",
|
|
261
|
+
lightingColor: "lighting-color",
|
|
262
|
+
};
|
|
263
|
+
|
|
134
264
|
interface TokenMatch {
|
|
135
265
|
token: TokenDefinitionFact;
|
|
136
266
|
/** True when several tokens share the value and none matches the property's
|
|
@@ -298,22 +428,26 @@ function buildTokenFix(
|
|
|
298
428
|
property: string,
|
|
299
429
|
rawValue: string,
|
|
300
430
|
rawColor: string,
|
|
301
|
-
match: TokenMatch
|
|
302
|
-
|
|
431
|
+
match: TokenMatch,
|
|
432
|
+
ix: FactIndex
|
|
433
|
+
): FixEmission<FindingReplaceStyleValueFix> {
|
|
303
434
|
const reference = tokenReference(match.token.name);
|
|
304
435
|
const value = replaceRawColor(rawValue, rawColor, reference);
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
436
|
+
return emitFix(
|
|
437
|
+
{
|
|
438
|
+
kind: "replaceStyleValue",
|
|
439
|
+
title: `Replace ${property} with ${value}`,
|
|
440
|
+
property,
|
|
441
|
+
value,
|
|
442
|
+
deterministic: true,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
symbols: [match.token.name],
|
|
446
|
+
editKind: "replaceStyleValue",
|
|
447
|
+
valuePreserving: !match.ambiguous,
|
|
448
|
+
},
|
|
449
|
+
ix
|
|
450
|
+
);
|
|
317
451
|
}
|
|
318
452
|
|
|
319
453
|
function replaceRawColor(rawValue: string, rawColor: string, replacement: string): string {
|
|
@@ -31,7 +31,13 @@ import type { FactId, FactIndex, TokenDefinitionFact } from "../facts/index.js";
|
|
|
31
31
|
|
|
32
32
|
import { makeFinding } from "./finding.js";
|
|
33
33
|
import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
emitFix,
|
|
36
|
+
indexComponentByNodeId,
|
|
37
|
+
parseLengthValue,
|
|
38
|
+
readUsageNode,
|
|
39
|
+
type FixEmission,
|
|
40
|
+
} from "./utils.js";
|
|
35
41
|
|
|
36
42
|
export const RULE_ID = "styles/no-raw-dimensions";
|
|
37
43
|
export const RULE_VERSION = "1";
|
|
@@ -75,6 +81,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
75
81
|
if (!resolution) continue;
|
|
76
82
|
const tokenIds = resolutionTokenIds(resolution);
|
|
77
83
|
const evidenceIds = tokenIds.length ? [decl.id, policy.id, ...tokenIds] : [decl.id, policy.id];
|
|
84
|
+
const fixEmission = dimensionFix(decl.property, resolution, ix);
|
|
78
85
|
|
|
79
86
|
findings.push(
|
|
80
87
|
makeFinding({
|
|
@@ -92,12 +99,13 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
92
99
|
property: decl.property,
|
|
93
100
|
value: decl.value,
|
|
94
101
|
},
|
|
95
|
-
fix:
|
|
102
|
+
fix: fixEmission?.fix,
|
|
96
103
|
attributes: {
|
|
97
104
|
property: decl.property,
|
|
98
105
|
rawValue: decl.value,
|
|
99
106
|
source: "css",
|
|
100
107
|
suggestedToken: resolutionSuggestedToken(resolution),
|
|
108
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
101
109
|
},
|
|
102
110
|
})
|
|
103
111
|
);
|
|
@@ -121,6 +129,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
121
129
|
: [node.id, inline.id, policy.id];
|
|
122
130
|
const tokenIds = resolutionTokenIds(resolution);
|
|
123
131
|
const evidenceIds = tokenIds.length ? [...baseEvidence, ...tokenIds] : baseEvidence;
|
|
132
|
+
const fixEmission = dimensionFix(inline.property, resolution, ix);
|
|
124
133
|
|
|
125
134
|
findings.push(
|
|
126
135
|
makeFinding({
|
|
@@ -138,12 +147,13 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
138
147
|
property: inline.property,
|
|
139
148
|
value: inline.value,
|
|
140
149
|
},
|
|
141
|
-
fix:
|
|
150
|
+
fix: fixEmission?.fix,
|
|
142
151
|
attributes: {
|
|
143
152
|
property: inline.property,
|
|
144
153
|
rawValue: inline.value,
|
|
145
154
|
source: "jsx",
|
|
146
155
|
suggestedToken: resolutionSuggestedToken(resolution),
|
|
156
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
147
157
|
},
|
|
148
158
|
})
|
|
149
159
|
);
|
|
@@ -219,16 +229,25 @@ function dimensionMessage(
|
|
|
219
229
|
|
|
220
230
|
function dimensionFix(
|
|
221
231
|
property: string,
|
|
222
|
-
resolution: DimensionResolution
|
|
223
|
-
|
|
232
|
+
resolution: DimensionResolution,
|
|
233
|
+
ix: FactIndex
|
|
234
|
+
): FixEmission<FindingReplaceStyleValueFix> | undefined {
|
|
224
235
|
if (resolution.kind === "exact") {
|
|
225
|
-
return
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
return emitFix(
|
|
237
|
+
{
|
|
238
|
+
kind: "replaceStyleValue",
|
|
239
|
+
title: `Replace ${property} with ${resolution.fixValue}`,
|
|
240
|
+
property,
|
|
241
|
+
value: resolution.fixValue,
|
|
242
|
+
deterministic: true,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
symbols: resolution.tokens.map((token) => token.name),
|
|
246
|
+
editKind: "replaceStyleValue",
|
|
247
|
+
valuePreserving: true,
|
|
248
|
+
},
|
|
249
|
+
ix
|
|
250
|
+
);
|
|
232
251
|
}
|
|
233
252
|
// `nearest` deliberately emits NO fix — a value-changing snap must stay a
|
|
234
253
|
// suggestion (see DimensionResolution); only the exact, value-preserving swap
|
|
@@ -42,7 +42,13 @@ import {
|
|
|
42
42
|
type SpacingTokenLookup,
|
|
43
43
|
} from "./spacing-resolution.js";
|
|
44
44
|
import type { Finding } from "./types.js";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
emitFix,
|
|
47
|
+
indexComponentByNodeId,
|
|
48
|
+
readUsageNode,
|
|
49
|
+
tokenSymbolsInText,
|
|
50
|
+
type FixEmission,
|
|
51
|
+
} from "./utils.js";
|
|
46
52
|
|
|
47
53
|
export const RULE_ID = "styles/no-raw-spacing";
|
|
48
54
|
export const RULE_VERSION = "1";
|
|
@@ -104,6 +110,7 @@ function checkDeclaration(
|
|
|
104
110
|
tokens: lookupFor(scale),
|
|
105
111
|
});
|
|
106
112
|
if (!checked) return null;
|
|
113
|
+
const fixEmission = buildSpacingFix(decl.property, checked, ix);
|
|
107
114
|
|
|
108
115
|
return makeFinding({
|
|
109
116
|
ruleId: RULE_ID,
|
|
@@ -120,7 +127,7 @@ function checkDeclaration(
|
|
|
120
127
|
property: decl.property,
|
|
121
128
|
value: decl.value,
|
|
122
129
|
},
|
|
123
|
-
fix:
|
|
130
|
+
fix: fixEmission?.fix,
|
|
124
131
|
attributes: {
|
|
125
132
|
property: decl.property,
|
|
126
133
|
rawValue: decl.value,
|
|
@@ -135,6 +142,7 @@ function checkDeclaration(
|
|
|
135
142
|
assumedRootFontSizePx: checked.assumedRootFontSizePx,
|
|
136
143
|
assumedEmBasePx: checked.assumedEmBasePx,
|
|
137
144
|
source: "css",
|
|
145
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
138
146
|
},
|
|
139
147
|
});
|
|
140
148
|
}
|
|
@@ -162,6 +170,7 @@ function checkInlineStyle(
|
|
|
162
170
|
bareNumberFix: inline.valueKind === "number",
|
|
163
171
|
});
|
|
164
172
|
if (!checked) return null;
|
|
173
|
+
const fixEmission = buildSpacingFix(inline.property, checked, ix);
|
|
165
174
|
|
|
166
175
|
const node = readUsageNode(ix, inline.nodeId);
|
|
167
176
|
if (!node) return null;
|
|
@@ -186,7 +195,7 @@ function checkInlineStyle(
|
|
|
186
195
|
property: inline.property,
|
|
187
196
|
value: inline.value,
|
|
188
197
|
},
|
|
189
|
-
fix:
|
|
198
|
+
fix: fixEmission?.fix,
|
|
190
199
|
attributes: {
|
|
191
200
|
property: inline.property,
|
|
192
201
|
rawValue: inline.value,
|
|
@@ -201,19 +210,32 @@ function checkInlineStyle(
|
|
|
201
210
|
assumedRootFontSizePx: checked.assumedRootFontSizePx,
|
|
202
211
|
assumedEmBasePx: checked.assumedEmBasePx,
|
|
203
212
|
source: "jsx",
|
|
213
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
204
214
|
},
|
|
205
215
|
});
|
|
206
216
|
}
|
|
207
217
|
|
|
208
|
-
function buildSpacingFix(
|
|
218
|
+
function buildSpacingFix(
|
|
219
|
+
property: string,
|
|
220
|
+
checked: CheckedSpacingValue,
|
|
221
|
+
ix: FactIndex
|
|
222
|
+
): FixEmission | undefined {
|
|
209
223
|
if (!checked.fixEmittable || checked.suggestedValue === undefined) return undefined;
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
return emitFix(
|
|
225
|
+
{
|
|
226
|
+
kind: "replaceStyleValue" as const,
|
|
227
|
+
title: `Replace ${property} with ${checked.suggestedValue}`,
|
|
228
|
+
property,
|
|
229
|
+
value: checked.suggestedValue,
|
|
230
|
+
deterministic: true,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
symbols: tokenSymbolsInText(checked.suggestedValue),
|
|
234
|
+
editKind: "replaceStyleValue",
|
|
235
|
+
valuePreserving: checked.deterministicFix,
|
|
236
|
+
},
|
|
237
|
+
ix
|
|
238
|
+
);
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
function spacingMessage(
|
|
@@ -29,13 +29,15 @@ import { makeFinding } from "./finding.js";
|
|
|
29
29
|
import type { Finding } from "./types.js";
|
|
30
30
|
import {
|
|
31
31
|
indexComponentByNodeId,
|
|
32
|
-
|
|
32
|
+
emitFix,
|
|
33
33
|
matchesScale,
|
|
34
34
|
nearestSignedScaleValue,
|
|
35
35
|
normalizeLengthForScale,
|
|
36
36
|
parseLengthValue,
|
|
37
37
|
readUsageNode,
|
|
38
38
|
tokenReference,
|
|
39
|
+
tokenSymbolsInText,
|
|
40
|
+
type FixEmission,
|
|
39
41
|
} from "./utils.js";
|
|
40
42
|
|
|
41
43
|
export const RULE_ID = "styles/no-raw-typography";
|
|
@@ -58,6 +60,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
58
60
|
if (decl.property.toLowerCase() !== "font-size") continue;
|
|
59
61
|
const checked = checkFontSize(decl.value, allowed, scale, tokens);
|
|
60
62
|
if (!checked) continue;
|
|
63
|
+
const fixEmission = buildFix(decl.property, checked, ix);
|
|
61
64
|
findings.push(
|
|
62
65
|
makeFinding({
|
|
63
66
|
ruleId: RULE_ID,
|
|
@@ -74,7 +77,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
74
77
|
property: decl.property,
|
|
75
78
|
value: decl.value,
|
|
76
79
|
},
|
|
77
|
-
fix:
|
|
80
|
+
fix: fixEmission?.fix,
|
|
78
81
|
attributes: {
|
|
79
82
|
property: decl.property,
|
|
80
83
|
rawValue: decl.value,
|
|
@@ -83,6 +86,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
83
86
|
suggestedValue: checked.suggestedValue,
|
|
84
87
|
matchedToken: checked.matchedToken,
|
|
85
88
|
source: "css",
|
|
89
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
86
90
|
},
|
|
87
91
|
})
|
|
88
92
|
);
|
|
@@ -92,8 +96,15 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
92
96
|
for (const inline of ix.byKind("usage_inline_style")) {
|
|
93
97
|
if (inline.valueKind === "css-variable") continue;
|
|
94
98
|
if (!FONT_SIZE_PROPERTIES.has(inline.property.toLowerCase())) continue;
|
|
95
|
-
const checked = checkFontSize(
|
|
99
|
+
const checked = checkFontSize(
|
|
100
|
+
inline.value,
|
|
101
|
+
allowed,
|
|
102
|
+
scale,
|
|
103
|
+
tokens,
|
|
104
|
+
inline.valueKind === "number"
|
|
105
|
+
);
|
|
96
106
|
if (!checked) continue;
|
|
107
|
+
const fixEmission = buildFix(inline.property, checked, ix);
|
|
97
108
|
const node = readUsageNode(ix, inline.nodeId);
|
|
98
109
|
if (!node) continue;
|
|
99
110
|
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
@@ -116,7 +127,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
116
127
|
property: inline.property,
|
|
117
128
|
value: inline.value,
|
|
118
129
|
},
|
|
119
|
-
fix:
|
|
130
|
+
fix: fixEmission?.fix,
|
|
120
131
|
attributes: {
|
|
121
132
|
property: inline.property,
|
|
122
133
|
rawValue: inline.value,
|
|
@@ -125,6 +136,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
125
136
|
suggestedValue: checked.suggestedValue,
|
|
126
137
|
matchedToken: checked.matchedToken,
|
|
127
138
|
source: "jsx",
|
|
139
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
128
140
|
},
|
|
129
141
|
})
|
|
130
142
|
);
|
|
@@ -175,12 +187,10 @@ function checkFontSize(
|
|
|
175
187
|
|
|
176
188
|
if (matchesScale(normalized.value, allowed)) {
|
|
177
189
|
if (!exactToken) return null;
|
|
178
|
-
const applicable =
|
|
179
|
-
isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
|
|
180
190
|
return {
|
|
181
191
|
suggestedValue: tokenReference(exactToken.name),
|
|
182
192
|
fixEmittable: true,
|
|
183
|
-
deterministicFix:
|
|
193
|
+
deterministicFix: normalized.deterministicFix,
|
|
184
194
|
reason: "token-equivalent",
|
|
185
195
|
matchedToken: exactToken.name,
|
|
186
196
|
};
|
|
@@ -206,15 +216,27 @@ function checkFontSize(
|
|
|
206
216
|
};
|
|
207
217
|
}
|
|
208
218
|
|
|
209
|
-
function buildFix(
|
|
219
|
+
function buildFix(
|
|
220
|
+
property: string,
|
|
221
|
+
checked: CheckedFontSize,
|
|
222
|
+
ix: FactIndex
|
|
223
|
+
): FixEmission | undefined {
|
|
210
224
|
if (!checked.fixEmittable || checked.suggestedValue === undefined) return undefined;
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
225
|
+
return emitFix(
|
|
226
|
+
{
|
|
227
|
+
kind: "replaceStyleValue" as const,
|
|
228
|
+
title: `Replace ${property} with ${checked.suggestedValue}`,
|
|
229
|
+
property,
|
|
230
|
+
value: checked.suggestedValue,
|
|
231
|
+
deterministic: true,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
symbols: tokenSymbolsInText(checked.suggestedValue),
|
|
235
|
+
editKind: "replaceStyleValue",
|
|
236
|
+
valuePreserving: checked.deterministicFix,
|
|
237
|
+
},
|
|
238
|
+
ix
|
|
239
|
+
);
|
|
218
240
|
}
|
|
219
241
|
|
|
220
242
|
function typographyMessage(
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
makeContractTokenFact,
|
|
13
13
|
makeStyleCssVarsMustBeDefinedFact,
|
|
14
14
|
makeStyleDeclarationFact,
|
|
15
|
+
makeTokenDefinitionFact,
|
|
15
16
|
makeUsageNodeFact,
|
|
16
17
|
makeUsageTextChildFact,
|
|
17
18
|
RULE_TIER,
|
|
@@ -36,6 +37,7 @@ import type { Fact } from "../index.js";
|
|
|
36
37
|
function rawValueFixture(): Fact[] {
|
|
37
38
|
const file = "src/Widget.module.scss";
|
|
38
39
|
return [
|
|
40
|
+
dualFallbackToken(),
|
|
39
41
|
makeStyleDeclarationFact({
|
|
40
42
|
file,
|
|
41
43
|
selector: ".a",
|
|
@@ -64,6 +66,15 @@ function rawValueFixture(): Fact[] {
|
|
|
64
66
|
];
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
function dualFallbackToken(): Fact {
|
|
70
|
+
return makeTokenDefinitionFact({
|
|
71
|
+
name: "$fui-surface-secondary",
|
|
72
|
+
value: "#f1f5f9",
|
|
73
|
+
category: "color",
|
|
74
|
+
referenceFormat: "scss-var",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
function index(facts: Fact[]): FactIndex {
|
|
68
79
|
const ix = new FactIndex();
|
|
69
80
|
ix.addMany(facts);
|
|
@@ -138,12 +149,14 @@ describe("§2 tokens/require-dual-fallback is Fragments-internal", () => {
|
|
|
138
149
|
it("customer preset = 0, fragments preset = 1", () => {
|
|
139
150
|
const customer = index([
|
|
140
151
|
...compileGlobalGovernanceFacts({ rules: customerDefaultRuleStates() }),
|
|
152
|
+
dualFallbackToken(),
|
|
141
153
|
decl,
|
|
142
154
|
]);
|
|
143
155
|
expect(runRules(customer)).toHaveLength(0);
|
|
144
156
|
|
|
145
157
|
const fragments = index([
|
|
146
158
|
...compileGlobalGovernanceFacts({ rules: fragmentsPresetRuleStates() }),
|
|
159
|
+
dualFallbackToken(),
|
|
147
160
|
decl,
|
|
148
161
|
]);
|
|
149
162
|
const fragmentsFindings = runRules(fragments);
|