mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(tools): follow weight variables in both readings of a keyframe (#1850)
* fix(tools): follow weight variables in both readings of a keyframe A keyframe that sets a font only while it runs makes scanWeights scan the file twice: once with the keyframe over the rule that runs it, once with the rule after it. The wrapper merged only the findings and the deferred weights of the second pass, so a weight read through a variable (`font-weight: $w`, `var(--w)`, `font: $w ...`) that meets JetBrains Mono only after a non-holding animation was never followed to its definition. Merge the references too; findIndirectWeights already dedupes a reference both passes record. Definitions, call sites, loads and declaration counts do not depend on the reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(tools): cover a keyframe run by another module's mixin Since #1795 the second reading also starts when an included mixin from another module runs a keyframe that does not hold its frame. Its references went through the same wrapper and were dropped the same way; cover that path too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(tools): report a weight definition once however it is reached findIndirectWeights follows each Sass reference from its own read site, so a variable two rules read was reported twice at its definition. Both readings of a non-holding keyframe now reach it too, with different caps, so a computed weight (`$w: calc(400 + 100)`) got the same warning twice. Deduplicate its findings by what they report, the key findWorkspaceWeights already used for landed findings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6c8f5028c7
commit
f3c576d182
2 files changed
+94
-5
No files matched your search
@@ -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*$/;
|
||||
|
||||
@@ -940,6 +945,8 @@ export function scanWeights(file, written, modules = {}) {
|
||||
return {
|
||||
...running,
|
||||
findings: union(running.findings, after.findings),
|
||||
// A variable a weight reads is followed from either reading.
|
||||
references: union(running.references, after.references),
|
||||
deferred: union(running.deferred, after.deferred),
|
||||
// A call meets the families of either reading.
|
||||
includeCalls: new Map(
|
||||
@@ -2362,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(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2448,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);
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
'<p [style.font-weight]="active() ? 650 : 400">a</p>',
|
||||
@@ -4939,6 +4960,63 @@ test('reads a keyframe where a rule runs it, over its own', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('follows a weight variable in either reading of a keyframe that does not hold', () => {
|
||||
const mono = "'JetBrains Mono'";
|
||||
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 ${cap}` : ''}`
|
||||
);
|
||||
for (const [source, expected] of [
|
||||
// Mono once it has run: the variable meets the cap where it is set.
|
||||
[
|
||||
`$w: 700; ${roboto} .x { font-family: ${mono}; animation: k 1ms; font-weight: $w; }`,
|
||||
['$w: 700 cap 500'],
|
||||
],
|
||||
[
|
||||
`:root { --w: 700; } ${roboto} .x { font-family: ${mono}; animation: k 1ms; font-weight: var(--w); }`,
|
||||
['--w: 700 cap 500'],
|
||||
],
|
||||
[
|
||||
`$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}; } }`,
|
||||
['$w: 700 cap 500'],
|
||||
],
|
||||
// One that holds its frame keeps Roboto after it.
|
||||
[
|
||||
`$w: 700; ${roboto} .x { font-family: ${mono}; animation: k 1ms forwards; font-weight: $w; }`,
|
||||
[],
|
||||
],
|
||||
]) {
|
||||
assert.deepEqual(report(source), expected, source);
|
||||
}
|
||||
// And where another module's mixin runs it.
|
||||
for (const [run, expected] of [
|
||||
['k 1ms', ['libs/s11/c.scss:2 $w: 700']],
|
||||
['k 1ms forwards', []],
|
||||
]) {
|
||||
const face = `@mixin face { font-family: ${mono}; animation: ${run}; }`;
|
||||
assert.deepEqual(
|
||||
workspace({
|
||||
'libs/s11/_type.scss': `${roboto}\n${face}`,
|
||||
'libs/s11/c.scss': `@use 'type';\n$w: 700;\n.x { @include type.face; font-weight: $w; }`,
|
||||
}),
|
||||
expected,
|
||||
run
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('reads an `@at-root` rule where Sass writes it out', () => {
|
||||
const mono = "'JetBrains Mono'";
|
||||
const media = '@media (min-width: 1px)';
|
||||
|
||||
Reference in new issue
Block a user