@usefragments/core 1.0.0 → 1.3.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-R3X7UBHU.js → chunk-57QDBEHQ.js} +101 -6
- package/dist/chunk-57QDBEHQ.js.map +1 -0
- package/dist/{chunk-243QYRUF.js → chunk-MLRDNSSA.js} +88 -47
- package/dist/chunk-MLRDNSSA.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- 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-BOa3KyiJ.d.ts → governance-DYSirwJu.d.ts} +22 -1
- package/dist/index-h_yWj15D.d.ts +6981 -0
- package/dist/index.d.ts +113 -97
- package/dist/index.js +387 -113
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +36 -36
- package/dist/schemas/index.d.ts +3 -2584
- package/dist/schemas/index.js +9 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-format.test.ts +144 -2
- package/src/agent-format.ts +34 -7
- package/src/codes/__tests__/codes.test.ts +57 -7
- package/src/codes/codes.ts +44 -42
- package/src/conform-prove.ts +4 -8
- package/src/contract/index.ts +3 -0
- package/src/contract/preimage.test.ts +50 -0
- package/src/contract/preimage.ts +56 -0
- package/src/evidence.ts +27 -0
- package/src/facts/builders.ts +4 -0
- package/src/facts/fact-index.ts +25 -1
- package/src/facts/facts.test.ts +49 -0
- package/src/facts/types.ts +3 -0
- package/src/governance-integrity.test.ts +2 -0
- package/src/governance-integrity.ts +7 -1
- package/src/index.ts +13 -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.ts +165 -7
- package/src/rules/emit-gate.test.ts +40 -2
- package/src/rules/emit-gate.ts +21 -10
- package/src/rules/finding.ts +3 -0
- package/src/rules/fix-availability.ts +33 -0
- package/src/rules/rules.test.ts +180 -24
- package/src/rules/spacing-resolution.ts +5 -8
- package/src/rules/styles-no-raw-color.ts +32 -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 +19 -3
- package/src/rules/tiers.ts +1 -1
- package/src/rules/tokens-require-dual-fallback.ts +54 -17
- package/src/rules/utils.ts +51 -0
- package/src/schemas/index.ts +106 -4
- package/src/token-types.ts +6 -0
- package/src/tokens/__tests__/source-names.test.ts +42 -0
- package/src/tokens/design-token-parser.test.ts +24 -5
- package/src/tokens/design-token-parser.ts +40 -8
- package/dist/chunk-243QYRUF.js.map +0 -1
- package/dist/chunk-R3X7UBHU.js.map +0 -1
package/src/rules/rules.test.ts
CHANGED
|
@@ -1509,9 +1509,10 @@ describe("tokens/require-dual-fallback", () => {
|
|
|
1509
1509
|
);
|
|
1510
1510
|
ix.addMany([
|
|
1511
1511
|
makeTokenDefinitionFact({
|
|
1512
|
-
name: "
|
|
1512
|
+
name: "$fui-color-brand",
|
|
1513
1513
|
value: "#123456",
|
|
1514
1514
|
category: "color",
|
|
1515
|
+
referenceFormat: "scss-var",
|
|
1515
1516
|
}),
|
|
1516
1517
|
makeStyleDeclarationFact({
|
|
1517
1518
|
file: "src/button.scss",
|
|
@@ -1775,7 +1776,7 @@ describe("components/prefer-library", () => {
|
|
|
1775
1776
|
expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
|
|
1776
1777
|
});
|
|
1777
1778
|
|
|
1778
|
-
it("
|
|
1779
|
+
it("keeps same-name local import redirects advisory without a verified source contract", () => {
|
|
1779
1780
|
const ix = new FactIndex();
|
|
1780
1781
|
ix.addMany(
|
|
1781
1782
|
compileGlobalGovernanceFacts({
|
|
@@ -1812,13 +1813,57 @@ describe("components/prefer-library", () => {
|
|
|
1812
1813
|
expect(findings[0]).toMatchObject({
|
|
1813
1814
|
ruleId: "components/prefer-library",
|
|
1814
1815
|
code: "FUI1004",
|
|
1816
|
+
fix: {
|
|
1817
|
+
kind: "replaceImport",
|
|
1818
|
+
from: "../components/Card",
|
|
1819
|
+
to: "@acme/ui",
|
|
1820
|
+
deterministic: false,
|
|
1821
|
+
},
|
|
1815
1822
|
attributes: {
|
|
1816
1823
|
suggestedComponent: "Card",
|
|
1817
1824
|
suggestedImport: "@acme/ui",
|
|
1825
|
+
advisory: true,
|
|
1826
|
+
contractProof: "missing",
|
|
1818
1827
|
},
|
|
1819
1828
|
});
|
|
1820
1829
|
});
|
|
1821
1830
|
|
|
1831
|
+
it("does not mistake TanStack Router Link for the canonical Link by name alone", () => {
|
|
1832
|
+
const ix = new FactIndex();
|
|
1833
|
+
ix.addMany(
|
|
1834
|
+
compileGlobalGovernanceFacts({
|
|
1835
|
+
rules: {
|
|
1836
|
+
"components/prefer-library": {
|
|
1837
|
+
enabled: true,
|
|
1838
|
+
severity: "error",
|
|
1839
|
+
options: {
|
|
1840
|
+
canonicalSources: [{ kind: "npm", specifier: "@acme/ui", include: ["Link"] }],
|
|
1841
|
+
},
|
|
1842
|
+
},
|
|
1843
|
+
},
|
|
1844
|
+
})
|
|
1845
|
+
);
|
|
1846
|
+
ix.add(
|
|
1847
|
+
makeUsageNodeFact({
|
|
1848
|
+
file: "src/app.tsx",
|
|
1849
|
+
nodePath: "0:0",
|
|
1850
|
+
element: "Link",
|
|
1851
|
+
location: { file: "src/app.tsx", line: 5, column: 4 },
|
|
1852
|
+
})
|
|
1853
|
+
);
|
|
1854
|
+
ix.add(
|
|
1855
|
+
makeUsageImportFact({
|
|
1856
|
+
file: "src/app.tsx",
|
|
1857
|
+
local: "Link",
|
|
1858
|
+
imported: "Link",
|
|
1859
|
+
source: "@tanstack/react-router",
|
|
1860
|
+
location: { file: "src/app.tsx", line: 1, column: 0 },
|
|
1861
|
+
})
|
|
1862
|
+
);
|
|
1863
|
+
|
|
1864
|
+
expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1822
1867
|
it("recognizes a current import through legacy npm source and mapping spellings", () => {
|
|
1823
1868
|
const ix = new FactIndex();
|
|
1824
1869
|
ix.addMany(
|
|
@@ -2159,7 +2204,7 @@ describe("components/prefer-library", () => {
|
|
|
2159
2204
|
expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
|
|
2160
2205
|
});
|
|
2161
2206
|
|
|
2162
|
-
it("
|
|
2207
|
+
it("keeps a complete mapped replacement guided until the conform AST preflight", () => {
|
|
2163
2208
|
const ix = new FactIndex();
|
|
2164
2209
|
ix.addMany(
|
|
2165
2210
|
compileGlobalGovernanceFacts({
|
|
@@ -2183,6 +2228,15 @@ describe("components/prefer-library", () => {
|
|
|
2183
2228
|
},
|
|
2184
2229
|
})
|
|
2185
2230
|
);
|
|
2231
|
+
ix.add(
|
|
2232
|
+
makeUsageImportFact({
|
|
2233
|
+
file: "src/app.tsx",
|
|
2234
|
+
local: "Button",
|
|
2235
|
+
imported: "Button",
|
|
2236
|
+
source: "@acme/ui",
|
|
2237
|
+
location: { file: "src/app.tsx", line: 1, column: 0 },
|
|
2238
|
+
})
|
|
2239
|
+
);
|
|
2186
2240
|
ix.add(
|
|
2187
2241
|
makeUsageNodeFact({
|
|
2188
2242
|
file: "src/app.tsx",
|
|
@@ -2200,14 +2254,102 @@ describe("components/prefer-library", () => {
|
|
|
2200
2254
|
kind: "replaceComponent",
|
|
2201
2255
|
from: "button",
|
|
2202
2256
|
to: "Button",
|
|
2203
|
-
deterministic:
|
|
2257
|
+
deterministic: false,
|
|
2204
2258
|
},
|
|
2205
2259
|
attributes: {
|
|
2206
2260
|
suggestedComponent: "Button",
|
|
2207
2261
|
suggestedImport: "@acme/ui",
|
|
2208
2262
|
propMapping: [{ rawProp: "className", canonicalProp: "variant" }],
|
|
2263
|
+
propCompatibility: "observed-complete",
|
|
2209
2264
|
},
|
|
2210
2265
|
});
|
|
2266
|
+
expect(findings[0].attributes).not.toHaveProperty("canonicalMappingConfirmed");
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
it("withholds a native-element fix when a native-only prop has no confirmed mapping", () => {
|
|
2270
|
+
const ix = new FactIndex();
|
|
2271
|
+
ix.addMany(
|
|
2272
|
+
compileGlobalGovernanceFacts({
|
|
2273
|
+
rules: {
|
|
2274
|
+
"components/prefer-library": {
|
|
2275
|
+
enabled: true,
|
|
2276
|
+
severity: "warning",
|
|
2277
|
+
options: {
|
|
2278
|
+
canonicalMappings: [
|
|
2279
|
+
{
|
|
2280
|
+
name: "Button",
|
|
2281
|
+
canonical: "Button",
|
|
2282
|
+
htmlEquivalent: "button",
|
|
2283
|
+
importPath: "@acme/ui",
|
|
2284
|
+
},
|
|
2285
|
+
],
|
|
2286
|
+
},
|
|
2287
|
+
},
|
|
2288
|
+
},
|
|
2289
|
+
})
|
|
2290
|
+
);
|
|
2291
|
+
ix.add(
|
|
2292
|
+
makeUsageImportFact({
|
|
2293
|
+
file: "src/app.tsx",
|
|
2294
|
+
local: "Button",
|
|
2295
|
+
imported: "Button",
|
|
2296
|
+
source: "@acme/ui",
|
|
2297
|
+
location: { file: "src/app.tsx", line: 1, column: 0 },
|
|
2298
|
+
})
|
|
2299
|
+
);
|
|
2300
|
+
addRawNode(ix, {
|
|
2301
|
+
element: "button",
|
|
2302
|
+
nodePath: "0:0",
|
|
2303
|
+
props: [{ prop: "formAction", value: "/save" }],
|
|
2304
|
+
});
|
|
2305
|
+
|
|
2306
|
+
const findings = ruleComponentsPreferLibrary(ix);
|
|
2307
|
+
expect(findings).toHaveLength(1);
|
|
2308
|
+
expect(findings[0]).toMatchObject({
|
|
2309
|
+
fix: { kind: "replaceComponent", deterministic: false },
|
|
2310
|
+
attributes: {
|
|
2311
|
+
propCompatibility: "incomplete",
|
|
2312
|
+
unprovenProps: ["formAction"],
|
|
2313
|
+
},
|
|
2314
|
+
});
|
|
2315
|
+
});
|
|
2316
|
+
|
|
2317
|
+
it("keeps replacement metadata but marks it non-deterministic when its import is absent", () => {
|
|
2318
|
+
const ix = new FactIndex();
|
|
2319
|
+
ix.addMany(
|
|
2320
|
+
compileGlobalGovernanceFacts({
|
|
2321
|
+
rules: {
|
|
2322
|
+
"components/prefer-library": {
|
|
2323
|
+
enabled: true,
|
|
2324
|
+
severity: "warning",
|
|
2325
|
+
options: {
|
|
2326
|
+
canonicalMappings: [
|
|
2327
|
+
{
|
|
2328
|
+
name: "Button",
|
|
2329
|
+
canonical: "Button",
|
|
2330
|
+
htmlEquivalent: "button",
|
|
2331
|
+
importPath: "@acme/ui",
|
|
2332
|
+
},
|
|
2333
|
+
],
|
|
2334
|
+
},
|
|
2335
|
+
},
|
|
2336
|
+
},
|
|
2337
|
+
})
|
|
2338
|
+
);
|
|
2339
|
+
addRawNode(ix, { element: "button", nodePath: "0:0", text: "Save" });
|
|
2340
|
+
|
|
2341
|
+
const findings = ruleComponentsPreferLibrary(ix);
|
|
2342
|
+
expect(findings).toHaveLength(1);
|
|
2343
|
+
expect(findings[0].attributes).toMatchObject({
|
|
2344
|
+
suggestedComponent: "Button",
|
|
2345
|
+
suggestedImport: "@acme/ui",
|
|
2346
|
+
});
|
|
2347
|
+
expect(findings[0].fix).toMatchObject({
|
|
2348
|
+
kind: "replaceComponent",
|
|
2349
|
+
from: "button",
|
|
2350
|
+
to: "Button",
|
|
2351
|
+
deterministic: false,
|
|
2352
|
+
});
|
|
2211
2353
|
});
|
|
2212
2354
|
|
|
2213
2355
|
it("does not flag generic <div>/<span> from an inferred component primitive (anti-flood)", () => {
|
|
@@ -2561,29 +2703,38 @@ describe("components/prefer-library", () => {
|
|
|
2561
2703
|
expect(findings.filter((f) => f.attributes?.suggestedComponent === "Hint")).toHaveLength(0);
|
|
2562
2704
|
});
|
|
2563
2705
|
|
|
2564
|
-
it("STILL fires the legit name-based import
|
|
2706
|
+
it("STILL fires the legit local name-based import redirect (<Modal> → import @acme/ui)", () => {
|
|
2565
2707
|
// Regression guard: removing the canonical selector must not kill the
|
|
2566
2708
|
// real name-match import-redirect. <Modal> (the component's own name)
|
|
2567
2709
|
// imported from the wrong path is genuine drift.
|
|
2568
2710
|
const findings = importedFindings(
|
|
2569
2711
|
[{ name: "Modal", canonical: "Dialog", importPath: "@acme/ui" }],
|
|
2570
2712
|
"Modal",
|
|
2571
|
-
"
|
|
2713
|
+
"../Modal"
|
|
2572
2714
|
);
|
|
2573
2715
|
expect(findings).toHaveLength(1);
|
|
2574
2716
|
expect(findings[0].fix?.kind).toBe("replaceImport");
|
|
2575
2717
|
expect(findings[0].attributes?.suggestedComponent).toBe("Modal");
|
|
2576
2718
|
});
|
|
2577
2719
|
|
|
2578
|
-
it("STILL fires when name === canonical (<Dialog> for name 'Dialog')
|
|
2720
|
+
it("STILL fires for a local import when name === canonical (<Dialog> for name 'Dialog')", () => {
|
|
2579
2721
|
const findings = importedFindings(
|
|
2580
2722
|
[{ name: "Dialog", canonical: "Dialog", importPath: "@acme/ui" }],
|
|
2581
2723
|
"Dialog",
|
|
2582
|
-
"
|
|
2724
|
+
"../Dialog"
|
|
2583
2725
|
);
|
|
2584
2726
|
expect(findings).toHaveLength(1);
|
|
2585
2727
|
expect(findings[0].attributes?.suggestedComponent).toBe("Dialog");
|
|
2586
2728
|
});
|
|
2729
|
+
|
|
2730
|
+
it("does not redirect a package-owned same-name Link without an explicit import policy", () => {
|
|
2731
|
+
const findings = importedFindings(
|
|
2732
|
+
[{ name: "Link", canonical: "Link", importPath: "@acme/ui" }],
|
|
2733
|
+
"Link",
|
|
2734
|
+
"@tanstack/react-router"
|
|
2735
|
+
);
|
|
2736
|
+
expect(findings).toHaveLength(0);
|
|
2737
|
+
});
|
|
2587
2738
|
});
|
|
2588
2739
|
|
|
2589
2740
|
describe("directory canonical source (unfiltered include — dogfood shape)", () => {
|
|
@@ -2672,7 +2823,7 @@ describe("components/prefer-library", () => {
|
|
|
2672
2823
|
expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
|
|
2673
2824
|
});
|
|
2674
2825
|
|
|
2675
|
-
it("flags raw <button> → Button when Button is
|
|
2826
|
+
it("flags raw <button> → Button without advertising it as deterministic when Button is not imported", () => {
|
|
2676
2827
|
const ix = new FactIndex();
|
|
2677
2828
|
ix.addMany(
|
|
2678
2829
|
compileGlobalGovernanceFacts({
|
|
@@ -2704,18 +2855,18 @@ describe("components/prefer-library", () => {
|
|
|
2704
2855
|
expect(findings).toHaveLength(1);
|
|
2705
2856
|
expect(findings[0]).toMatchObject({
|
|
2706
2857
|
ruleId: "components/prefer-library",
|
|
2707
|
-
fix: {
|
|
2708
|
-
kind: "replaceComponent",
|
|
2709
|
-
from: "button",
|
|
2710
|
-
to: "Button",
|
|
2711
|
-
deterministic: true,
|
|
2712
|
-
},
|
|
2713
2858
|
attributes: {
|
|
2714
2859
|
rawValue: "button",
|
|
2715
2860
|
suggestedComponent: "Button",
|
|
2716
2861
|
suggestedImportSourceKind: "directory",
|
|
2717
2862
|
},
|
|
2718
2863
|
});
|
|
2864
|
+
expect(findings[0].fix).toMatchObject({
|
|
2865
|
+
kind: "replaceComponent",
|
|
2866
|
+
from: "button",
|
|
2867
|
+
to: "Button",
|
|
2868
|
+
deterministic: false,
|
|
2869
|
+
});
|
|
2719
2870
|
});
|
|
2720
2871
|
|
|
2721
2872
|
it("flags a raw element by canonical vocab id when display name diverges (Modal → Dialog)", () => {
|
|
@@ -2825,7 +2976,9 @@ describe("components/prefer-library", () => {
|
|
|
2825
2976
|
|
|
2826
2977
|
const findings = ruleComponentsPreferLibrary(ix);
|
|
2827
2978
|
|
|
2828
|
-
//
|
|
2979
|
+
// Type-specific mappings win over the generic tag-level one; text falls
|
|
2980
|
+
// back. No canonical bindings exist in this fixture, so suggestions remain
|
|
2981
|
+
// useful but are not advertised as deterministic edits.
|
|
2829
2982
|
expect(findings.map((finding) => finding.attributes?.suggestedComponent)).toEqual([
|
|
2830
2983
|
"Checkbox",
|
|
2831
2984
|
"RadioGroup",
|
|
@@ -2836,7 +2989,7 @@ describe("components/prefer-library", () => {
|
|
|
2836
2989
|
"RadioGroup",
|
|
2837
2990
|
"TextField",
|
|
2838
2991
|
]);
|
|
2839
|
-
expect(findings.every((finding) => finding.fix?.deterministic ===
|
|
2992
|
+
expect(findings.every((finding) => finding.fix?.deterministic === false)).toBe(true);
|
|
2840
2993
|
});
|
|
2841
2994
|
|
|
2842
2995
|
it("does not let a type-constrained mapping match a different or untyped input", () => {
|
|
@@ -2872,7 +3025,7 @@ describe("components/prefer-library", () => {
|
|
|
2872
3025
|
expect(ruleComponentsPreferLibrary(ix)).toHaveLength(0);
|
|
2873
3026
|
});
|
|
2874
3027
|
|
|
2875
|
-
it("
|
|
3028
|
+
it("keeps a same-name local import redirect advisory without source-contract proof", () => {
|
|
2876
3029
|
const ix = new FactIndex();
|
|
2877
3030
|
ix.addMany(
|
|
2878
3031
|
compileGlobalGovernanceFacts({
|
|
@@ -2918,9 +3071,10 @@ describe("components/prefer-library", () => {
|
|
|
2918
3071
|
kind: "replaceImport",
|
|
2919
3072
|
from: "../Button",
|
|
2920
3073
|
to: "@acme/ui",
|
|
2921
|
-
deterministic:
|
|
3074
|
+
deterministic: false,
|
|
2922
3075
|
},
|
|
2923
3076
|
location: { line: 1, column: 0 },
|
|
3077
|
+
attributes: { advisory: true, contractProof: "missing" },
|
|
2924
3078
|
});
|
|
2925
3079
|
});
|
|
2926
3080
|
|
|
@@ -2967,13 +3121,14 @@ describe("components/prefer-library", () => {
|
|
|
2967
3121
|
suggestedComponent: "Alert",
|
|
2968
3122
|
matchedRole: "status",
|
|
2969
3123
|
confidence: 0.9,
|
|
3124
|
+
advisory: true,
|
|
2970
3125
|
},
|
|
2971
3126
|
});
|
|
2972
3127
|
// Role matches are advisory: no deterministic tag→component rewrite.
|
|
2973
3128
|
expect(findings[0].fix).toBeUndefined();
|
|
2974
3129
|
});
|
|
2975
3130
|
|
|
2976
|
-
it("flags input type primitives config-free
|
|
3131
|
+
it("flags input type primitives config-free without unsafe missing-import fixes", () => {
|
|
2977
3132
|
const ix = preferLibraryIndex();
|
|
2978
3133
|
addRawNode(ix, {
|
|
2979
3134
|
element: "input",
|
|
@@ -3003,7 +3158,7 @@ describe("components/prefer-library", () => {
|
|
|
3003
3158
|
"input-type",
|
|
3004
3159
|
"input-type",
|
|
3005
3160
|
]);
|
|
3006
|
-
expect(findings.
|
|
3161
|
+
expect(findings.every((finding) => finding.fix?.deterministic === false)).toBe(true);
|
|
3007
3162
|
});
|
|
3008
3163
|
|
|
3009
3164
|
it("does not infer generic text inputs as deterministic primitives", () => {
|
|
@@ -3093,7 +3248,7 @@ describe("components/prefer-library", () => {
|
|
|
3093
3248
|
kind: "replaceComponent",
|
|
3094
3249
|
from: "button",
|
|
3095
3250
|
to: "Button",
|
|
3096
|
-
deterministic:
|
|
3251
|
+
deterministic: false,
|
|
3097
3252
|
});
|
|
3098
3253
|
});
|
|
3099
3254
|
|
|
@@ -3139,13 +3294,13 @@ describe("components/prefer-library", () => {
|
|
|
3139
3294
|
kind: "replaceComponent",
|
|
3140
3295
|
from: "button",
|
|
3141
3296
|
to: "Button",
|
|
3142
|
-
deterministic:
|
|
3297
|
+
deterministic: false,
|
|
3143
3298
|
});
|
|
3144
3299
|
expect(findings[2].fix).toMatchObject({
|
|
3145
3300
|
kind: "replaceComponent",
|
|
3146
3301
|
from: "button",
|
|
3147
3302
|
to: "Button",
|
|
3148
|
-
deterministic:
|
|
3303
|
+
deterministic: false,
|
|
3149
3304
|
});
|
|
3150
3305
|
});
|
|
3151
3306
|
|
|
@@ -3430,6 +3585,7 @@ describe("styles/no-raw-typography (BUG 10)", () => {
|
|
|
3430
3585
|
);
|
|
3431
3586
|
const findings = ruleStylesNoRawTypography(ix);
|
|
3432
3587
|
expect(findings).toHaveLength(1);
|
|
3588
|
+
expect(findings[0].code).toBe("FUI2016");
|
|
3433
3589
|
expect(findings[0].fix).toMatchObject({
|
|
3434
3590
|
value: "var(--font-size-md)",
|
|
3435
3591
|
deterministic: true,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { TokenDefinitionFact } from "../facts/index.js";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
isApplicableTokenReference,
|
|
5
4
|
matchesScale,
|
|
6
5
|
nearestSignedScaleValue,
|
|
7
6
|
normalizeLengthForScale,
|
|
@@ -75,7 +74,7 @@ export function resolveSpacingValue(input: {
|
|
|
75
74
|
let reliableSuggestion = true;
|
|
76
75
|
// Deterministic only when every violating part is a value-preserving,
|
|
77
76
|
// verbatim-resolvable token swap.
|
|
78
|
-
let
|
|
77
|
+
let allValuePreserving = true;
|
|
79
78
|
|
|
80
79
|
for (const part of parts) {
|
|
81
80
|
const normalized = normalizeLengthForScale(part.parsed, input.scale);
|
|
@@ -93,9 +92,7 @@ export function resolveSpacingValue(input: {
|
|
|
93
92
|
// On-grid literal that equals a token: value-preserving swap.
|
|
94
93
|
anyViolation = true;
|
|
95
94
|
const reference = tokenReference(exactToken.name);
|
|
96
|
-
|
|
97
|
-
isApplicableTokenReference(exactToken.referenceFormat) && normalized.deterministicFix;
|
|
98
|
-
if (!applicable) allApplicableValuePreserving = false;
|
|
95
|
+
if (!normalized.deterministicFix) allValuePreserving = false;
|
|
99
96
|
suggestedParts.push(reference);
|
|
100
97
|
if (!firstViolation) {
|
|
101
98
|
firstViolation = {
|
|
@@ -103,7 +100,7 @@ export function resolveSpacingValue(input: {
|
|
|
103
100
|
suggestedValue: reference,
|
|
104
101
|
suggestedToken: exactToken.name,
|
|
105
102
|
fixEmittable: true,
|
|
106
|
-
deterministicFix:
|
|
103
|
+
deterministicFix: normalized.deterministicFix,
|
|
107
104
|
reason: "token-equivalent",
|
|
108
105
|
matchedToken: exactToken.name,
|
|
109
106
|
assumedRootFontSizePx: normalized.assumedRootFontSizePx,
|
|
@@ -117,7 +114,7 @@ export function resolveSpacingValue(input: {
|
|
|
117
114
|
// one exists. Snapping changes rendered value, so this is not deterministic.
|
|
118
115
|
anyViolation = true;
|
|
119
116
|
anyOffScale = true;
|
|
120
|
-
|
|
117
|
+
allValuePreserving = false;
|
|
121
118
|
|
|
122
119
|
const nearest = nearestSignedScaleValue(normalized.value, input.allowed);
|
|
123
120
|
const snapsToZero = nearest === 0 && normalized.value !== 0;
|
|
@@ -158,7 +155,7 @@ export function resolveSpacingValue(input: {
|
|
|
158
155
|
const joined = suggestedParts.join(" ");
|
|
159
156
|
const changes = joined !== input.raw.trim().replace(/\s+/g, " ");
|
|
160
157
|
const fixEmittable = canFixAll && reliableSuggestion && changes;
|
|
161
|
-
const deterministic = fixEmittable && !anyOffScale &&
|
|
158
|
+
const deterministic = fixEmittable && !anyOffScale && allValuePreserving;
|
|
162
159
|
|
|
163
160
|
return {
|
|
164
161
|
...firstViolation,
|
|
@@ -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";
|
|
@@ -43,6 +44,10 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
43
44
|
if (isExemptColor(color, policy.except)) continue;
|
|
44
45
|
|
|
45
46
|
const resolution = resolveColorToken(decl.property, color, colorTokens);
|
|
47
|
+
const fixEmission =
|
|
48
|
+
resolution?.kind === "exact"
|
|
49
|
+
? buildTokenFix(decl.property, decl.value, color, resolution, ix)
|
|
50
|
+
: undefined;
|
|
46
51
|
const evidenceIds = resolution
|
|
47
52
|
? [decl.id, policy.id, resolution.token.id]
|
|
48
53
|
: [decl.id, policy.id];
|
|
@@ -63,10 +68,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
63
68
|
property: decl.property,
|
|
64
69
|
value: color,
|
|
65
70
|
},
|
|
66
|
-
fix:
|
|
67
|
-
resolution?.kind === "exact"
|
|
68
|
-
? buildTokenFix(decl.property, decl.value, color, resolution)
|
|
69
|
-
: undefined,
|
|
71
|
+
fix: fixEmission?.fix,
|
|
70
72
|
attributes: {
|
|
71
73
|
property: decl.property,
|
|
72
74
|
rawValue: decl.value,
|
|
@@ -74,6 +76,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
74
76
|
source: "css",
|
|
75
77
|
suggestedToken: resolution?.token.name,
|
|
76
78
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
79
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
77
80
|
},
|
|
78
81
|
})
|
|
79
82
|
);
|
|
@@ -91,6 +94,10 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
91
94
|
|
|
92
95
|
const componentEvidenceId = componentByNode.get(node.id)?.id;
|
|
93
96
|
const resolution = resolveColorToken(inline.property, color, colorTokens);
|
|
97
|
+
const fixEmission =
|
|
98
|
+
resolution?.kind === "exact"
|
|
99
|
+
? buildTokenFix(inline.property, inline.value, color, resolution, ix)
|
|
100
|
+
: undefined;
|
|
94
101
|
const baseEvidence = componentEvidenceId
|
|
95
102
|
? [node.id, componentEvidenceId, inline.id, policy.id]
|
|
96
103
|
: [node.id, inline.id, policy.id];
|
|
@@ -112,10 +119,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
112
119
|
property: inline.property,
|
|
113
120
|
value: color,
|
|
114
121
|
},
|
|
115
|
-
fix:
|
|
116
|
-
resolution?.kind === "exact"
|
|
117
|
-
? buildTokenFix(inline.property, inline.value, color, resolution)
|
|
118
|
-
: undefined,
|
|
122
|
+
fix: fixEmission?.fix,
|
|
119
123
|
attributes: {
|
|
120
124
|
property: inline.property,
|
|
121
125
|
rawValue: inline.value,
|
|
@@ -123,6 +127,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
|
|
|
123
127
|
source: "jsx",
|
|
124
128
|
suggestedToken: resolution?.token.name,
|
|
125
129
|
...(resolution ? { tokenMatch: resolution.kind } : {}),
|
|
130
|
+
...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
|
|
126
131
|
},
|
|
127
132
|
})
|
|
128
133
|
);
|
|
@@ -298,22 +303,26 @@ function buildTokenFix(
|
|
|
298
303
|
property: string,
|
|
299
304
|
rawValue: string,
|
|
300
305
|
rawColor: string,
|
|
301
|
-
match: TokenMatch
|
|
302
|
-
|
|
306
|
+
match: TokenMatch,
|
|
307
|
+
ix: FactIndex
|
|
308
|
+
): FixEmission<FindingReplaceStyleValueFix> {
|
|
303
309
|
const reference = tokenReference(match.token.name);
|
|
304
310
|
const value = replaceRawColor(rawValue, rawColor, reference);
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
311
|
+
return emitFix(
|
|
312
|
+
{
|
|
313
|
+
kind: "replaceStyleValue",
|
|
314
|
+
title: `Replace ${property} with ${value}`,
|
|
315
|
+
property,
|
|
316
|
+
value,
|
|
317
|
+
deterministic: true,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
symbols: [match.token.name],
|
|
321
|
+
editKind: "replaceStyleValue",
|
|
322
|
+
valuePreserving: !match.ambiguous,
|
|
323
|
+
},
|
|
324
|
+
ix
|
|
325
|
+
);
|
|
317
326
|
}
|
|
318
327
|
|
|
319
328
|
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
|