@usefragments/core 1.5.2 → 1.6.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-BAHCOAVG.js → chunk-WVFNDPM4.js} +423 -189
- package/dist/chunk-WVFNDPM4.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-pKrfh517.d.ts → governance-DxFipN5V.d.ts} +609 -20
- package/dist/index.d.ts +638 -43
- package/dist/index.js +304 -37
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/policy-exclude.test.ts +180 -0
- package/src/canonical-bridge.ts +69 -1
- package/src/canonical-direction.test.ts +118 -0
- package/src/canonical-direction.ts +43 -2
- package/src/codes/__tests__/codes.test.ts +14 -1
- package/src/codes/codes.ts +40 -0
- package/src/facts/builders.ts +35 -0
- package/src/facts/compile.ts +135 -21
- package/src/facts/fact-index.ts +15 -2
- package/src/facts/facts.test.ts +6 -2
- package/src/facts/index.ts +9 -6
- package/src/facts/types.ts +45 -9
- package/src/governance-integrity.test.ts +174 -3
- package/src/governance-integrity.ts +305 -5
- package/src/governance.ts +87 -1
- package/src/index.ts +38 -1
- package/src/policy-exclude.ts +113 -0
- package/src/rules/families.test.ts +69 -0
- package/src/rules/families.ts +52 -0
- package/src/rules/index.ts +6 -0
- package/src/rules/jsx-preferred-import-path.ts +29 -11
- package/src/rules/rules.test.ts +125 -1
- package/src/rules/styles-no-raw-color.ts +13 -4
- package/src/rules/styles-no-raw-dimensions.ts +13 -4
- package/src/rules/styles-no-raw-spacing.ts +12 -4
- package/src/rules/styles-no-raw-typography.ts +13 -4
- package/src/rules/utils.ts +39 -0
- package/dist/chunk-BAHCOAVG.js.map +0 -1
package/src/rules/rules.test.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
makeUsageNodeFact,
|
|
18
18
|
makeUsagePropResolvedFact,
|
|
19
19
|
makeUsageTextChildFact,
|
|
20
|
+
projectSupersededImportPathPreferences,
|
|
20
21
|
ruleA11yRequiredAccessibleName,
|
|
21
22
|
ruleComponentsForbiddenPropValue,
|
|
22
23
|
ruleComponentsPreferLibrary,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
ruleTokensRequireDualFallback,
|
|
33
34
|
runRules,
|
|
34
35
|
} from "../index.js";
|
|
35
|
-
import type { Fact, FactId } from "../index.js";
|
|
36
|
+
import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
|
|
36
37
|
|
|
37
38
|
type ButtonProps = {
|
|
38
39
|
variant?: "primary" | "secondary" | "ghost" | "link";
|
|
@@ -625,6 +626,129 @@ describe("preferred JSX imports and components", () => {
|
|
|
625
626
|
expect(ruleJsxPreferredImportPath(ix)).toHaveLength(0);
|
|
626
627
|
});
|
|
627
628
|
|
|
629
|
+
// Report #2 B6 (brownfield-canonical brief 03): a confirmed bridge
|
|
630
|
+
// supersedes any non-bridge preference over the same underlying import —
|
|
631
|
+
// otherwise both emit with the same fingerprint and the hand rule flags the
|
|
632
|
+
// wrapper's own implementation file with a self-import fix.
|
|
633
|
+
describe("bridge supersession of non-bridge import preferences", () => {
|
|
634
|
+
const B6_BRIDGE: CanonicalBridgeV1 = {
|
|
635
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
636
|
+
local: {
|
|
637
|
+
componentKey: "src/components/Button.tsx#Button",
|
|
638
|
+
moduleSpecifier: "@/components",
|
|
639
|
+
exportName: "Button",
|
|
640
|
+
implementationFiles: ["src/components/Button.tsx"],
|
|
641
|
+
},
|
|
642
|
+
decision: { state: "confirmed", source: "authored" },
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
it("emits only the bridge finding when a hand-authored record duplicates it", () => {
|
|
646
|
+
const ix = new FactIndex();
|
|
647
|
+
ix.addMany(
|
|
648
|
+
compileGlobalGovernanceFacts({
|
|
649
|
+
jsx: [g.jsx.importPath().prefer("@mui/material", "@/components", { imported: "Button" })],
|
|
650
|
+
canonicalBridges: [B6_BRIDGE],
|
|
651
|
+
})
|
|
652
|
+
);
|
|
653
|
+
ix.add(
|
|
654
|
+
makeUsageImportFact({
|
|
655
|
+
file: "src/app/page.tsx",
|
|
656
|
+
local: "MuiButton",
|
|
657
|
+
imported: "Button",
|
|
658
|
+
source: "@mui/material",
|
|
659
|
+
location: { file: "src/app/page.tsx", line: 1, column: 0 },
|
|
660
|
+
})
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const findings = ruleJsxPreferredImportPath(ix);
|
|
664
|
+
expect(findings).toHaveLength(1);
|
|
665
|
+
expect(findings[0].attributes).toMatchObject({ canonicalBridge: true });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("keeps the hand rule silent inside the wrapper implementation file", () => {
|
|
669
|
+
const ix = new FactIndex();
|
|
670
|
+
ix.addMany(
|
|
671
|
+
compileGlobalGovernanceFacts({
|
|
672
|
+
jsx: [g.jsx.importPath().prefer("@mui/material", "@/components", { imported: "Button" })],
|
|
673
|
+
canonicalBridges: [B6_BRIDGE],
|
|
674
|
+
})
|
|
675
|
+
);
|
|
676
|
+
ix.add(
|
|
677
|
+
makeUsageImportFact({
|
|
678
|
+
file: "src/components/Button.tsx",
|
|
679
|
+
local: "MuiButton",
|
|
680
|
+
imported: "Button",
|
|
681
|
+
source: "@mui/material",
|
|
682
|
+
location: { file: "src/components/Button.tsx", line: 1, column: 0 },
|
|
683
|
+
})
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
expect(ruleJsxPreferredImportPath(ix)).toHaveLength(0);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("leaves the hand rule firing on imports the bridge does not govern", () => {
|
|
690
|
+
const ix = new FactIndex();
|
|
691
|
+
ix.addMany(
|
|
692
|
+
compileGlobalGovernanceFacts({
|
|
693
|
+
jsx: [g.jsx.importPath().prefer("@mui/material", "@/components")],
|
|
694
|
+
canonicalBridges: [B6_BRIDGE],
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
ix.add(
|
|
698
|
+
makeUsageImportFact({
|
|
699
|
+
file: "src/app/page.tsx",
|
|
700
|
+
local: "Stepper",
|
|
701
|
+
imported: "Stepper",
|
|
702
|
+
source: "@mui/material",
|
|
703
|
+
location: { file: "src/app/page.tsx", line: 1, column: 0 },
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
const findings = ruleJsxPreferredImportPath(ix);
|
|
708
|
+
expect(findings).toHaveLength(1);
|
|
709
|
+
expect(findings[0].attributes).toMatchObject({ imported: "Stepper" });
|
|
710
|
+
expect(findings[0].attributes?.canonicalBridge).toBeUndefined();
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("names each superseded record through the compile-side projection", () => {
|
|
714
|
+
const superseded = projectSupersededImportPathPreferences({
|
|
715
|
+
jsx: [
|
|
716
|
+
g.jsx.importPath().prefer("@mui/material", "@/components", { imported: "Button" }),
|
|
717
|
+
g.jsx.importPath().prefer("@mui/material/Button", "@/components"),
|
|
718
|
+
g.jsx.importPath().prefer("@mui/material", "@/inputs", { imported: "TextField" }),
|
|
719
|
+
g.jsx.importPath().prefer("@legacy/ui", "@usefragments/ui"),
|
|
720
|
+
],
|
|
721
|
+
canonicalBridges: [B6_BRIDGE],
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
expect(superseded).toEqual([
|
|
725
|
+
{
|
|
726
|
+
from: "@mui/material",
|
|
727
|
+
imported: "Button",
|
|
728
|
+
to: "@/components",
|
|
729
|
+
packageName: "@mui/material",
|
|
730
|
+
underlyingExportName: "Button",
|
|
731
|
+
localExportName: "Button",
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
from: "@mui/material/Button",
|
|
735
|
+
to: "@/components",
|
|
736
|
+
packageName: "@mui/material",
|
|
737
|
+
underlyingExportName: "Button",
|
|
738
|
+
localExportName: "Button",
|
|
739
|
+
},
|
|
740
|
+
]);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("projects nothing without bridges", () => {
|
|
744
|
+
expect(
|
|
745
|
+
projectSupersededImportPathPreferences({
|
|
746
|
+
jsx: [g.jsx.importPath().prefer("@mui/material", "@/components")],
|
|
747
|
+
})
|
|
748
|
+
).toEqual([]);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
628
752
|
it("does not exempt sibling helpers, stories, barrels, or consumers", () => {
|
|
629
753
|
const ix = new FactIndex();
|
|
630
754
|
ix.addMany(
|
|
@@ -20,6 +20,7 @@ import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
|
|
|
20
20
|
import {
|
|
21
21
|
detectRawColor,
|
|
22
22
|
emitFix,
|
|
23
|
+
hoppedValue,
|
|
23
24
|
indexComponentByNodeId,
|
|
24
25
|
isExemptColor,
|
|
25
26
|
readUsageNode,
|
|
@@ -51,8 +52,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
51
52
|
if (isExemptColor(color, policy.except)) continue;
|
|
52
53
|
|
|
53
54
|
const resolution = resolveColorToken(decl.property, color, colorTokens);
|
|
55
|
+
const hopped = hoppedValue(decl);
|
|
54
56
|
const fixEmission =
|
|
55
|
-
resolution?.kind === "exact"
|
|
57
|
+
resolution?.kind === "exact" && !hopped
|
|
56
58
|
? buildTokenFix(decl.property, decl.value, color, resolution, ix)
|
|
57
59
|
: undefined;
|
|
58
60
|
const evidenceIds = resolution
|
|
@@ -64,7 +66,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
64
66
|
ruleId: RULE_ID,
|
|
65
67
|
ruleVersion: RULE_VERSION,
|
|
66
68
|
severity: policy.severity,
|
|
67
|
-
message:
|
|
69
|
+
message:
|
|
70
|
+
colorMessage(color, `\`${decl.property}\``, resolution, preferLabel) +
|
|
71
|
+
(hopped?.suffix ?? ""),
|
|
68
72
|
location: decl.location,
|
|
69
73
|
evidence: ix.evidence(evidenceIds),
|
|
70
74
|
fingerprintIdentity: {
|
|
@@ -84,6 +88,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
84
88
|
suggestedToken: resolution?.token.name,
|
|
85
89
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
86
90
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
91
|
+
...(hopped?.attributes ?? {}),
|
|
87
92
|
},
|
|
88
93
|
})
|
|
89
94
|
);
|
|
@@ -138,8 +143,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
138
143
|
|
|
139
144
|
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
140
145
|
const resolution = resolveColorToken(inline.property, color, colorTokens);
|
|
146
|
+
const hopped = hoppedValue(inline);
|
|
141
147
|
const fixEmission =
|
|
142
|
-
resolution?.kind === "exact"
|
|
148
|
+
resolution?.kind === "exact" && !hopped
|
|
143
149
|
? buildTokenFix(inline.property, inline.value, color, resolution, ix)
|
|
144
150
|
: undefined;
|
|
145
151
|
const baseEvidence = componentEvidenceId
|
|
@@ -152,7 +158,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
152
158
|
ruleId: RULE_ID,
|
|
153
159
|
ruleVersion: RULE_VERSION,
|
|
154
160
|
severity: policy.severity,
|
|
155
|
-
message:
|
|
161
|
+
message:
|
|
162
|
+
colorMessage(color, `inline \`${inline.property}\``, resolution, preferLabel) +
|
|
163
|
+
(hopped?.suffix ?? ""),
|
|
156
164
|
location: node.location,
|
|
157
165
|
evidence: ix.evidence(evidenceIds),
|
|
158
166
|
fingerprintIdentity: {
|
|
@@ -172,6 +180,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
172
180
|
suggestedToken: resolution?.token.name,
|
|
173
181
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
174
182
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
183
|
+
...(hopped?.attributes ?? {}),
|
|
175
184
|
},
|
|
176
185
|
})
|
|
177
186
|
);
|
|
@@ -34,6 +34,7 @@ import { localTokenCandidates } from "./token-candidates.js";
|
|
|
34
34
|
import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
|
|
35
35
|
import {
|
|
36
36
|
emitFix,
|
|
37
|
+
hoppedValue,
|
|
37
38
|
indexComponentByNodeId,
|
|
38
39
|
parseLengthValue,
|
|
39
40
|
readUsageNode,
|
|
@@ -83,14 +84,17 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
83
84
|
if (!resolution) continue;
|
|
84
85
|
const tokenIds = resolutionTokenIds(resolution);
|
|
85
86
|
const evidenceIds = tokenIds.length ? [decl.id, policy.id, ...tokenIds] : [decl.id, policy.id];
|
|
86
|
-
const
|
|
87
|
+
const hopped = hoppedValue(decl);
|
|
88
|
+
const fixEmission = hopped ? undefined : dimensionFix(decl.property, resolution, ix);
|
|
87
89
|
|
|
88
90
|
findings.push(
|
|
89
91
|
makeFinding({
|
|
90
92
|
ruleId: RULE_ID,
|
|
91
93
|
ruleVersion: RULE_VERSION,
|
|
92
94
|
severity: policy.severity,
|
|
93
|
-
message:
|
|
95
|
+
message:
|
|
96
|
+
dimensionMessage(decl.value, decl.property, preferLabel, resolution, false) +
|
|
97
|
+
(hopped?.suffix ?? ""),
|
|
94
98
|
location: decl.location,
|
|
95
99
|
evidence: ix.evidence(evidenceIds),
|
|
96
100
|
fingerprintIdentity: {
|
|
@@ -108,6 +112,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
108
112
|
source: "css",
|
|
109
113
|
suggestedToken: resolutionSuggestedToken(resolution),
|
|
110
114
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
115
|
+
...(hopped?.attributes ?? {}),
|
|
111
116
|
},
|
|
112
117
|
})
|
|
113
118
|
);
|
|
@@ -131,14 +136,17 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
131
136
|
: [node.id, inline.id, policy.id];
|
|
132
137
|
const tokenIds = resolutionTokenIds(resolution);
|
|
133
138
|
const evidenceIds = tokenIds.length ? [...baseEvidence, ...tokenIds] : baseEvidence;
|
|
134
|
-
const
|
|
139
|
+
const hopped = hoppedValue(inline);
|
|
140
|
+
const fixEmission = hopped ? undefined : dimensionFix(inline.property, resolution, ix);
|
|
135
141
|
|
|
136
142
|
findings.push(
|
|
137
143
|
makeFinding({
|
|
138
144
|
ruleId: RULE_ID,
|
|
139
145
|
ruleVersion: RULE_VERSION,
|
|
140
146
|
severity: policy.severity,
|
|
141
|
-
message:
|
|
147
|
+
message:
|
|
148
|
+
dimensionMessage(inline.value, inline.property, preferLabel, resolution, true) +
|
|
149
|
+
(hopped?.suffix ?? ""),
|
|
142
150
|
location: node.location,
|
|
143
151
|
evidence: ix.evidence(evidenceIds),
|
|
144
152
|
fingerprintIdentity: {
|
|
@@ -156,6 +164,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
|
|
|
156
164
|
source: "jsx",
|
|
157
165
|
suggestedToken: resolutionSuggestedToken(resolution),
|
|
158
166
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
167
|
+
...(hopped?.attributes ?? {}),
|
|
159
168
|
},
|
|
160
169
|
})
|
|
161
170
|
);
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import type { Finding } from "./types.js";
|
|
46
46
|
import {
|
|
47
47
|
emitFix,
|
|
48
|
+
hoppedValue,
|
|
48
49
|
indexComponentByNodeId,
|
|
49
50
|
readUsageNode,
|
|
50
51
|
tokenSymbolsInText,
|
|
@@ -111,13 +112,15 @@ function checkDeclaration(
|
|
|
111
112
|
tokens: lookupFor(scale),
|
|
112
113
|
});
|
|
113
114
|
if (!checked) return null;
|
|
114
|
-
const
|
|
115
|
+
const hopped = hoppedValue(decl);
|
|
116
|
+
const fixEmission = hopped ? undefined : buildSpacingFix(decl.property, checked, ix);
|
|
115
117
|
|
|
116
118
|
return makeFinding({
|
|
117
119
|
ruleId: RULE_ID,
|
|
118
120
|
ruleVersion: RULE_VERSION,
|
|
119
121
|
severity: policy.severity,
|
|
120
|
-
message:
|
|
122
|
+
message:
|
|
123
|
+
spacingMessage(decl.property, decl.value, allowed, scale, checked) + (hopped?.suffix ?? ""),
|
|
121
124
|
location: decl.location,
|
|
122
125
|
evidence: ix.evidence([decl.id, policy.id, scale.id]),
|
|
123
126
|
fingerprintIdentity: {
|
|
@@ -144,6 +147,7 @@ function checkDeclaration(
|
|
|
144
147
|
assumedEmBasePx: checked.assumedEmBasePx,
|
|
145
148
|
source: "css",
|
|
146
149
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
150
|
+
...(hopped?.attributes ?? {}),
|
|
147
151
|
},
|
|
148
152
|
});
|
|
149
153
|
}
|
|
@@ -171,7 +175,8 @@ function checkInlineStyle(
|
|
|
171
175
|
bareNumberFix: inline.valueKind === "number",
|
|
172
176
|
});
|
|
173
177
|
if (!checked) return null;
|
|
174
|
-
const
|
|
178
|
+
const hopped = hoppedValue(inline);
|
|
179
|
+
const fixEmission = hopped ? undefined : buildSpacingFix(inline.property, checked, ix);
|
|
175
180
|
|
|
176
181
|
const node = readUsageNode(ix, inline.nodeId);
|
|
177
182
|
if (!node) return null;
|
|
@@ -185,7 +190,9 @@ function checkInlineStyle(
|
|
|
185
190
|
ruleId: RULE_ID,
|
|
186
191
|
ruleVersion: RULE_VERSION,
|
|
187
192
|
severity: policy.severity,
|
|
188
|
-
message:
|
|
193
|
+
message:
|
|
194
|
+
spacingMessage(inline.property, inline.value, allowed, scale, checked) +
|
|
195
|
+
(hopped?.suffix ?? ""),
|
|
189
196
|
location: node.location,
|
|
190
197
|
evidence: ix.evidence(evidenceIds),
|
|
191
198
|
fingerprintIdentity: {
|
|
@@ -212,6 +219,7 @@ function checkInlineStyle(
|
|
|
212
219
|
assumedEmBasePx: checked.assumedEmBasePx,
|
|
213
220
|
source: "jsx",
|
|
214
221
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
222
|
+
...(hopped?.attributes ?? {}),
|
|
215
223
|
},
|
|
216
224
|
});
|
|
217
225
|
}
|
|
@@ -31,6 +31,7 @@ import type { Finding } from "./types.js";
|
|
|
31
31
|
import {
|
|
32
32
|
indexComponentByNodeId,
|
|
33
33
|
emitFix,
|
|
34
|
+
hoppedValue,
|
|
34
35
|
matchesScale,
|
|
35
36
|
nearestSignedScaleValue,
|
|
36
37
|
normalizeLengthForScale,
|
|
@@ -61,13 +62,16 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
61
62
|
if (decl.property.toLowerCase() !== "font-size") continue;
|
|
62
63
|
const checked = checkFontSize(decl.value, allowed, scale, tokens);
|
|
63
64
|
if (!checked) continue;
|
|
64
|
-
const
|
|
65
|
+
const hopped = hoppedValue(decl);
|
|
66
|
+
const fixEmission = hopped ? undefined : buildFix(decl.property, checked, ix);
|
|
65
67
|
findings.push(
|
|
66
68
|
makeFinding({
|
|
67
69
|
ruleId: RULE_ID,
|
|
68
70
|
ruleVersion: RULE_VERSION,
|
|
69
71
|
severity: policy.severity,
|
|
70
|
-
message:
|
|
72
|
+
message:
|
|
73
|
+
typographyMessage(decl.property, decl.value, allowed, scale.unit, checked) +
|
|
74
|
+
(hopped?.suffix ?? ""),
|
|
71
75
|
location: decl.location,
|
|
72
76
|
evidence: ix.evidence([decl.id, policy.id, scale.id]),
|
|
73
77
|
fingerprintIdentity: {
|
|
@@ -88,6 +92,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
88
92
|
matchedToken: checked.matchedToken,
|
|
89
93
|
source: "css",
|
|
90
94
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
95
|
+
...(hopped?.attributes ?? {}),
|
|
91
96
|
},
|
|
92
97
|
})
|
|
93
98
|
);
|
|
@@ -105,7 +110,8 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
105
110
|
inline.valueKind === "number"
|
|
106
111
|
);
|
|
107
112
|
if (!checked) continue;
|
|
108
|
-
const
|
|
113
|
+
const hopped = hoppedValue(inline);
|
|
114
|
+
const fixEmission = hopped ? undefined : buildFix(inline.property, checked, ix);
|
|
109
115
|
const node = readUsageNode(ix, inline.nodeId);
|
|
110
116
|
if (!node) continue;
|
|
111
117
|
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
@@ -117,7 +123,9 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
117
123
|
ruleId: RULE_ID,
|
|
118
124
|
ruleVersion: RULE_VERSION,
|
|
119
125
|
severity: policy.severity,
|
|
120
|
-
message:
|
|
126
|
+
message:
|
|
127
|
+
typographyMessage(inline.property, inline.value, allowed, scale.unit, checked) +
|
|
128
|
+
(hopped?.suffix ?? ""),
|
|
121
129
|
location: node.location,
|
|
122
130
|
evidence: ix.evidence(evidenceIds),
|
|
123
131
|
fingerprintIdentity: {
|
|
@@ -138,6 +146,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
|
|
|
138
146
|
matchedToken: checked.matchedToken,
|
|
139
147
|
source: "jsx",
|
|
140
148
|
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
149
|
+
...(hopped?.attributes ?? {}),
|
|
141
150
|
},
|
|
142
151
|
})
|
|
143
152
|
);
|
package/src/rules/utils.ts
CHANGED
|
@@ -232,6 +232,45 @@ export function readStyleDeclaration(ix: FactIndex, id: FactId): StyleDeclaratio
|
|
|
232
232
|
return fact && fact.kind === "style_declaration" ? fact : undefined;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/** What a raw-value rule needs to report a value that arrived through a hop. */
|
|
236
|
+
export interface HoppedValue {
|
|
237
|
+
/** Appended to the message so the use site names the declaration a reader
|
|
238
|
+
* cannot see from the finding's own line. */
|
|
239
|
+
suffix: string;
|
|
240
|
+
/** Merged into the finding's attributes for JSON/SARIF consumers. */
|
|
241
|
+
attributes: {
|
|
242
|
+
valueFrom: "const-binding";
|
|
243
|
+
declaredAs: string;
|
|
244
|
+
declaredAt: string;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Shared reading of `valueFrom` provenance for the raw-value style rules
|
|
250
|
+
* (color, spacing, dimensions, typography). One helper so a const-resolved
|
|
251
|
+
* value reports identically everywhere instead of becoming a color special
|
|
252
|
+
* case.
|
|
253
|
+
*
|
|
254
|
+
* Callers must also drop their fix when this returns a value: the authored text
|
|
255
|
+
* at the finding's location is an identifier, so replacing "the value" there
|
|
256
|
+
* would rewrite code the user never wrote.
|
|
257
|
+
*/
|
|
258
|
+
export function hoppedValue(fact: {
|
|
259
|
+
valueFrom?: { kind: "const-binding"; name: string; location: { file: string; line: number } };
|
|
260
|
+
}): HoppedValue | undefined {
|
|
261
|
+
const from = fact.valueFrom;
|
|
262
|
+
if (!from) return undefined;
|
|
263
|
+
const declaredAt = `${from.location.file}:${from.location.line}`;
|
|
264
|
+
return {
|
|
265
|
+
suffix: ` Value comes from \`${from.name}\`, declared at ${declaredAt}.`,
|
|
266
|
+
attributes: {
|
|
267
|
+
valueFrom: "const-binding",
|
|
268
|
+
declaredAs: from.name,
|
|
269
|
+
declaredAt,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
235
274
|
/**
|
|
236
275
|
* The set of CSS custom-property names a token vocabulary defines, normalized to
|
|
237
276
|
* `--`-prefixed form. Shared by the token-vocabulary rules
|