From afd499f022642fdb72b042865dfde844ff89e75c Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Mon, 26 Oct 2026 17:04:31 +0100 Subject: [PATCH 1/2] refactor: clean up superfluous code --- src/cli/pool.js | 2 +- src/cli/render.js | 2 -- src/consolidate.js | 6 ++---- src/lib/hacks.js | 2 +- src/lib/normalization.js | 12 ++++++------ src/lib/scopes.js | 4 ++-- src/lib/selectors.js | 2 +- src/lib/shorthands.js | 2 +- src/lib/transaction.js | 7 ++----- src/merge.js | 20 ++++++++++---------- src/plugin.js | 4 ++-- 11 files changed, 28 insertions(+), 35 deletions(-) diff --git a/src/cli/pool.js b/src/cli/pool.js index 7909ef1..dfc3201 100644 --- a/src/cli/pool.js +++ b/src/cli/pool.js @@ -156,7 +156,7 @@ export function runPool(slots, settings, onOutcome) { // Every worker starts before anything is dispatched, so a pool that can’t // start at all has yet to write a file or print a line - const size = Math.min(poolSize(queue.length), queue.length); + const size = poolSize(queue.length); try { for (let i = 0; i < size; i++) { const worker = new Worker(new URL(import.meta.url), { diff --git a/src/cli/render.js b/src/cli/render.js index 9b981b6..664481d 100644 --- a/src/cli/render.js +++ b/src/cli/render.js @@ -157,7 +157,6 @@ function renderFixPass(payload, { isStdin, label, multi, flags, savingsOnly }) { exitFailure: skipped.length > 0 || Boolean(withheld), errored: false, stats: { - findings: null, applied, skipped: skipped.length, bytesBefore: bytes.before, @@ -297,7 +296,6 @@ function printOverallFixSummary(ok, savingsOnly) { filesGrowLen: filesGrow.length, growTotal, totalBefore: totalBeforeAll, - skipFlag: '--fix --savings-only', }); if (outcome) { for (const line of outcome) console.log(line); diff --git a/src/consolidate.js b/src/consolidate.js index b6ec3af..31bf20a 100644 --- a/src/consolidate.js +++ b/src/consolidate.js @@ -117,10 +117,8 @@ function consolidateRoot(root, options = {}) { const initiallyEmpty = new Set(); const aggressive = options.aggressive ?? false; const conditionBlocks = aggressive ? collectConditionBlocks(root) : []; - if (aggressive) { - for (const atrule of conditionBlocks) { - if (atrule.nodes && !atrule.nodes.length) initiallyEmpty.add(atrule); - } + for (const atrule of conditionBlocks) { + if (atrule.nodes && !atrule.nodes.length) initiallyEmpty.add(atrule); } // Bringing the style sheet to the state it would ship in. Idempotent, so the diff --git a/src/lib/hacks.js b/src/lib/hacks.js index 28a9495..f6d73e5 100644 --- a/src/lib/hacks.js +++ b/src/lib/hacks.js @@ -3,7 +3,7 @@ // into a shared selector list is risky: Browsers that don’t recognize it can // drop the entire rule, not just that one selector. Legacy hacks that rely on // a selector being invalid/valid in specific browsers have the same problem. -export const DEFAULT_IGNORE_PATTERNS = [ +const DEFAULT_IGNORE_PATTERNS = [ /::?-(?:moz|webkit|ms|o)-/i, // vendor-prefixed pseudo-classes/elements /\*\s*html/i, // IE6 star-html hack /\*\s*\+\s*html/i, // IE7 star-plus-html hack diff --git a/src/lib/normalization.js b/src/lib/normalization.js index 8b08ff7..3d1b06c 100644 --- a/src/lib/normalization.js +++ b/src/lib/normalization.js @@ -21,14 +21,14 @@ const ZERO_PERCENT_SENSITIVE_PROPS = new Set([ // Collapses a decimal number’s redundant leading/trailing zeros, so `0.5`, // `.5`, and `0.50` compare equal, as do `1.0` and `1` -const RE_DECIMAL = /(-?)(\d*)\.(\d+)/g; +const RE_DECIMAL = /(\d*)\.(\d+)/g; function normalizeDecimals(value) { - return value.replace(RE_DECIMAL, (_match, sign, intPart, fracPart) => { + return value.replace(RE_DECIMAL, (_match, intPart, fracPart) => { const trimmedFrac = fracPart.replace(/0+$/, ''); const int = intPart === '' ? '0' : intPart; - if (trimmedFrac === '') return `${sign}${int}`; - return `${sign}${int === '0' ? '' : int}.${trimmedFrac}`; + if (trimmedFrac === '') return int; + return `${int === '0' ? '' : int}.${trimmedFrac}`; }); } @@ -292,7 +292,7 @@ const PROPERTY_ALIASES = { 'grid-column-gap': 'column-gap', }; -// Per run (see `src/caches.js`). One cache per mode rather than one keyed by +// Per run (see `caches.js`). One cache per mode rather than one keyed by // mode plus name: building that composite key would allocate per lookup. const propCacheDefault = new Map(); const propCacheAggressive = new Map(); @@ -375,7 +375,7 @@ export function normalizeValue(prop, rawValue, aggressive = false) { value = value.replace(RE_OPAQUE_PLACEHOLDER, (_match, index) => opaques[index]); - if (ZERO_IS_NONE_PROPS.has(propNormalized) && (value === '0' || value === 'none')) { + if (ZERO_IS_NONE_PROPS.has(propNormalized) && value === '0') { value = 'none'; } diff --git a/src/lib/scopes.js b/src/lib/scopes.js index 85a6590..d19523c 100644 --- a/src/lib/scopes.js +++ b/src/lib/scopes.js @@ -64,7 +64,7 @@ export function describeScope(container) { // and merging those two boundaries would report duplicates across unrelated // scopes. `JSON.stringify` over the segment array can’t be spelled by // the segments. -export function scopeKey(container) { +function scopeKey(container) { if (container.type === 'root') return 'root'; return JSON.stringify(scopeSegments(container)); } @@ -156,7 +156,7 @@ export function atRuleLabel(atrule) { // whitespace-insensitive—`.b, .a` matches the same elements with the same // specificities as `.a, .b`. Whitespace is only collapsed outside quotes: // `[data-x="a b"]` and `[data-x="a b"]` are different selectors. -export function selectorSetKey(rule) { +function selectorSetKey(rule) { return splitSelectors(rule.selector) .map(selector => selector.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|\s+/g, (match, quoted) => quoted ?? ' ')) .sort() diff --git a/src/lib/selectors.js b/src/lib/selectors.js index 11dc50d..44d643e 100644 --- a/src/lib/selectors.js +++ b/src/lib/selectors.js @@ -3,7 +3,7 @@ import { memoized } from './util.js'; // One depth/quote/escape-aware scan of a selector list, answering both // questions anything here asks of it: where its top-level commas are, and // whether the first is followed by whitespace. Memoized per run (see -// `src/caches.js`)—the same handful of selector strings get split repeatedly +// caches.js)—the same handful of selector strings get split repeatedly // within a run (eligibility filtering, merge-safety scans, occurrence // reporting), but selector text is unbounded across runs. // diff --git a/src/lib/shorthands.js b/src/lib/shorthands.js index 9bfe235..3067011 100644 --- a/src/lib/shorthands.js +++ b/src/lib/shorthands.js @@ -3,7 +3,7 @@ // `margin-inline-start`): which physical side a logical longhand resolves to // depends on writing mode, which static CSS can’t tell. Not exhaustive; extend // as needed. -export const SHORTHAND_LONGHANDS = { +const SHORTHAND_LONGHANDS = { margin: [ 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', 'margin-block', 'margin-block-start', 'margin-block-end', diff --git a/src/lib/transaction.js b/src/lib/transaction.js index 28d487d..9e8e819 100644 --- a/src/lib/transaction.js +++ b/src/lib/transaction.js @@ -179,7 +179,7 @@ export function snapshot(root, scope, rules) { // itself is state a rollback has to restore scopeRules: scope.rules.slice(), containers: affectedContainers(rules).map(container => ({ container, nodes: container.nodes.slice() })), - rules: rules.map(rule => ({ entry: captureNode(rule), parent: rule.parent })), + rules: rules.map(captureNode), }; } @@ -280,10 +280,7 @@ export function rollback(snap, scope) { forgetSeparator(container); } - for (const { entry, parent } of snap.rules) { - restoreNode(entry); - entry.node.parent = parent; - } + for (const entry of snap.rules) restoreNode(entry); scope.rules.length = 0; scope.rules.push(...snap.scopeRules); diff --git a/src/merge.js b/src/merge.js index 8cec8d0..e774fe2 100644 --- a/src/merge.js +++ b/src/merge.js @@ -73,7 +73,7 @@ export function removeRedundantDuplicates(ctx, container, scopeLabel, selectors) const last = decls.at(-1); const value = shortestValue(decls); - if (last.value !== value) last.value = value; + last.value = value; for (const decl of decls) { if (decl !== last) decl.remove(); @@ -104,7 +104,7 @@ function isOverlappingExtra(ctx, node, propNormalized, excludeKey) { // // `exemptRules` covers the group’s whole entangled cluster—a fellow member is // being absorbed into the same coordinated merge, not staying behind. -export function findBlockingRule(ctx, scope, distinctRules, exemptRules, firstIndex, lastIndex, propNormalized) { +function findBlockingRule(ctx, scope, distinctRules, exemptRules, firstIndex, lastIndex, propNormalized) { // Only rules actually inside the span can block; walking the whole scope to // skip everything outside it made this quadratic in the scope’s rule count. // The group’s own selector list is likewise only needed once something in @@ -204,7 +204,7 @@ export function foldSameSelectorRules(ctx, scope) { // declarations always need to move, overlapping or not, while a non-target // occurrence’s extra only needs to move if it overlaps the shared property and // was declared after it. -export function mergeSoloGroup(ctx, scope, group) { +function mergeSoloGroup(ctx, scope, group) { const { key, occurrences, distinctRules, propNormalized } = group; const target = distinctRules.at(-1); @@ -238,7 +238,7 @@ export function mergeSoloGroup(ctx, scope, group) { const value = shortestValue(occurrenceDecls(occurrences)); const targetDecl = declFor(occurrences, target); - if (targetDecl.value !== value) targetDecl.value = value; + targetDecl.value = value; for (const rule of distinctRules) { if (rule === target) continue; @@ -294,7 +294,7 @@ export function mergeSoloGroup(ctx, scope, group) { // // Solo groups only—in a multi-group cluster a partial merge could relocate a // shared rule’s selector out from under the other groups. -export function mergePartialGroup(ctx, scope, group, reason) { +function mergePartialGroup(ctx, scope, group, reason) { const { key, occurrences, distinctRules, propNormalized } = group; const exempt = new Set(distinctRules); @@ -362,7 +362,7 @@ function mergeTwinRules(ctx, scope, cluster, ruleKeyCounts) { const key = ctx.keyOf(decl); const group = cluster.find(candidate => candidate.key === key); const value = shortestValue(occurrenceDecls(group.occurrences)); - if (decl.value !== value) decl.value = value; + decl.value = value; ctx.applied.push({ scope: scope.label, key, selectors: mergedSelectors, value }); } @@ -433,7 +433,7 @@ function mergeClusterGroupRuns(ctx, scope, group) { mergedRule.selector = joinSelectors(mergedSelectors, lastRule, ctx.multilineSelectors, ctx.spacedCommas); const lastDecl = declFor(runOccurrences, lastRule); lastDecl.remove(); - if (lastDecl.value !== value) lastDecl.value = value; + lastDecl.value = value; mergedRule.append(lastDecl); lastRule.after(mergedRule); recordInsertion(mergedRule); @@ -501,7 +501,7 @@ function splitStarHub(ctx, scope, cluster, hub) { )); const value = shortestValue(occurrenceDecls(occurrences)); - if (sharedDecl.value !== value) sharedDecl.value = value; + sharedDecl.value = value; const anchorRule = hub.clone({ nodes: [] }); anchorRule.selector = joinSelectors(mergedSelectors, hub, ctx.multilineSelectors, ctx.spacedCommas); @@ -587,7 +587,7 @@ function mergeCluster(ctx, scope, cluster) { hubCandidates++; } } - const isStar = hub !== null && hubCandidates === 1 && [...ruleKeyCounts.values()].every(keysHere => ( + const isStar = hubCandidates === 1 && [...ruleKeyCounts.values()].every(keysHere => ( keysHere.size === clusterSize || keysHere.size === 1 )); @@ -661,7 +661,7 @@ function gated(ctx, scope, keys, rules, apply) { // block behind. ctx.settle(); - const snap = snapshot(ctx.root, scope, [...new Set(rules)]); + const snap = snapshot(ctx.root, scope, rules); const appliedBefore = ctx.applied.length; apply(); diff --git a/src/plugin.js b/src/plugin.js index 6022216..05d2c4a 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -15,7 +15,7 @@ export default function cssdedup(options = {}) { root.warn(result, `${withheld.count} merge${withheld.count !== 1 ? 's' : ''} withheld (\`savingsOnly\`): applying ${withheld.count !== 1 ? 'them' : 'it'} would make the style sheet ${Math.abs(withheld.bytes.saved)} bytes bigger`); } for (const item of skipped) { - root.warn(result, `Duplicate \`${item.key}\` left unmerged (${item.scope === 'root' ? 'root' : item.scope}): ${item.reason}`); + root.warn(result, `Duplicate \`${item.key}\` left unmerged (${item.scope}): ${item.reason}`); } return; } @@ -23,7 +23,7 @@ export default function cssdedup(options = {}) { const { findings } = analyzeRoot(root, options); for (const finding of findings) { if (finding.repeated) { - root.warn(result, `Selector \`${finding.key}\` written ${finding.occurrences.length} times in its scope (${finding.scope === 'root' ? 'root' : finding.scope})`); + root.warn(result, `Selector \`${finding.key}\` written ${finding.occurrences.length} times in its scope (${finding.scope})`); continue; } From a6b894467998b96973a122dfd045a1c0c983fbd4 Mon Sep 17 00:00:00 2001 From: Jens Oliver Meiert Date: Mon, 26 Oct 2026 17:11:49 +0100 Subject: [PATCH 2/2] docs: add note about superfluous-code removal --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 995173c..07e8622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to CSS Dedup are documented in this file, which is (mostly) The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +* Removed dead and no-op code across the engine, CLI, and plugin (redundant guards, unread fields and arguments, identical-branch conditionals, unused exports) + ## [1.13.3] - 2026-09-24 ### Changed