diff --git a/tools/nx/check-font-weights.mjs b/tools/nx/check-font-weights.mjs index f17a7ed82..50355caa0 100644 --- a/tools/nx/check-font-weights.mjs +++ b/tools/nx/check-font-weights.mjs @@ -757,6 +757,11 @@ function capOnly(term) { return Boolean(term.cap) && !offScale; } +/** What a finding reports (see `describeFinding`): one key, one message. */ +function findingKey({ file, line, name, value, cap, computed }) { + return JSON.stringify([file, line, name, value, cap, computed]); +} + /** A string literal in code, whole: its quote and its text. */ const STRING_LITERAL = /^\s*(['"`])([\s\S]*)\1\s*$/; @@ -2364,7 +2369,13 @@ export function findIndirectWeights(scans) { ); } } - return findings; + // A definition several references reach (two rules reading one Sass + // variable, or both readings of a keyframe) is reported once. + return [ + ...new Map( + findings.map((finding) => [findingKey(finding), finding]) + ).values(), + ]; } /** @@ -2450,13 +2461,11 @@ export function findWorkspaceWeights(sources) { ...scans.flatMap((scan) => scan.findings), ...findIndirectWeights(scans), ]; - const keyOf = ({ file, line, name, value, cap, computed }) => - JSON.stringify([file, line, name, value, cap, computed]); - const seen = new Set(all.filter((f) => !f.landed).map(keyOf)); + const seen = new Set(all.filter((f) => !f.landed).map(findingKey)); const findings = []; for (const { landed, ...finding } of all) { if (landed) { - const key = keyOf(finding); + const key = findingKey(finding); if (seen.has(key)) continue; seen.add(key); } diff --git a/tools/nx/check-font-weights.test.mjs b/tools/nx/check-font-weights.test.mjs index fa40ebcfc..a06503085 100644 --- a/tools/nx/check-font-weights.test.mjs +++ b/tools/nx/check-font-weights.test.mjs @@ -197,6 +197,27 @@ test('follows variables a weight refers to, through chains and files', () => { ); }); +test('reports a definition once however many references reach it', () => { + const mono = "'JetBrains Mono'"; + for (const [source, expected] of [ + ['$w: 650; .a { font-weight: $w; } .b { font-weight: $w; }', '650'], + [ + '$w: calc(400 + 100); .a { font-weight: $w; } .b { font-weight: $w; }', + 'calc(400 + 100)', + ], + [ + `$w: calc(400 + 100); .a { font-family: ${mono}; font-weight: $w; } .b { font-weight: $w; }`, + 'calc(400 + 100)', + ], + ]) { + assert.deepEqual( + offScale('libs/a.scss', source), + [`1 $w: ${expected}`], + source + ); + } +}); + test('checks Angular style bindings and literal DOM writes', () => { const template = [ '

a

', @@ -4944,7 +4965,8 @@ test('follows a weight variable in either reading of a keyframe that does not ho const roboto = '@keyframes k { to { font-family: Roboto; } }'; const report = (body) => findOffScaleWeights('libs/s11/g.scss', body).findings.map( - ({ name, value, cap }) => `${name}: ${value} cap ${cap}` + ({ name, value, cap }) => + `${name}: ${value}${cap ? ` cap ${cap}` : ''}` ); for (const [source, expected] of [ // Mono once it has run: the variable meets the cap where it is set. @@ -4960,6 +4982,11 @@ test('follows a weight variable in either reading of a keyframe that does not ho `$w: 700; ${roboto} .x { animation: k 1ms; font: $w 12px/1 ${mono}; }`, ['$w: 700 cap 500'], ], + // A computed weight is one finding, whichever reading reaches it. + [ + `$w: calc(400 + 100); ${roboto} .x { font-family: ${mono}; animation: k 1ms; font-weight: $w; }`, + ['$w: calc(400 + 100)'], + ], // Mono only while it runs. [ `$w: 700; .x { font-family: Roboto; animation: k 1ms; font-weight: $w; } @keyframes k { to { font-family: ${mono}; } }`,