From ad37a7bc98f94aaa081018183ffa5e1eb441eb83 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Wed, 5 May 2021 05:01:30 -0700 Subject: [PATCH] css: remove duplicate rules --- CHANGELOG.md | 2 + internal/css_ast/css_ast.go | 316 +++++++++++++++++++++++-- internal/css_parser/css_parser.go | 42 +++- internal/css_parser/css_parser_test.go | 29 +++ internal/helpers/hash.go | 14 ++ internal/js_parser/js_parser.go | 25 +- 6 files changed, 385 insertions(+), 43 deletions(-) create mode 100644 internal/helpers/hash.go diff --git a/CHANGELOG.md b/CHANGELOG.md index aa212a97358..c9021335468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,8 @@ * The four special pseudo-elements `::before`, `::after`, `::first-line`, and `::first-letter` are allowed to be parsed with one `:` for legacy reasons, so the `::` is now converted to `:` for these pseudo-elements. + * Duplicate CSS rules are now deduplicated. Only the last rule is kept, since that's the only one that has any effect. This applies for both top-level rules and nested rules. + ## 0.11.18 * Add support for OpenBSD on x86-64 ([#1235](https://github.com/evanw/esbuild/issues/1235)) diff --git a/internal/css_ast/css_ast.go b/internal/css_ast/css_ast.go index 51170d51b87..e7549838e2b 100644 --- a/internal/css_ast/css_ast.go +++ b/internal/css_ast/css_ast.go @@ -5,6 +5,7 @@ import ( "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/css_lexer" + "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/logger" ) @@ -69,6 +70,46 @@ type Token struct { Whitespace WhitespaceFlags // 1 byte } +func (a Token) Equal(b Token) bool { + if a.Kind == b.Kind && a.Text == b.Text && a.ImportRecordIndex == b.ImportRecordIndex && a.Whitespace == b.Whitespace { + if a.Children == nil && b.Children == nil { + return true + } + + if a.Children != nil && b.Children != nil && TokensEqual(*a.Children, *b.Children) { + return true + } + } + + return false +} + +func TokensEqual(a []Token, b []Token) bool { + if len(a) != len(b) { + return false + } + for i, c := range a { + if !c.Equal(b[i]) { + return false + } + } + return true +} + +func HashTokens(hash uint32, tokens []Token) uint32 { + hash = helpers.HashCombine(hash, uint32(len(tokens))) + + for _, t := range tokens { + hash = helpers.HashCombine(hash, uint32(t.Kind)) + hash = helpers.HashCombineString(hash, t.Text) + if t.Children != nil { + hash = HashTokens(hash, *t.Children) + } + } + + return hash +} + func (a Token) EqualIgnoringWhitespace(b Token) bool { if a.Kind == b.Kind && a.Text == b.Text && a.ImportRecordIndex == b.ImportRecordIndex { if a.Children == nil && b.Children == nil { @@ -160,21 +201,63 @@ func (t Token) DimensionUnit() string { return t.Text[t.UnitOffset:] } -// This interface is never called. Its purpose is to encode a variant type in -// Go's type system. type R interface { - isRule() + Equal(rule R) bool + Hash() (uint32, bool) +} + +func RulesEqual(a []R, b []R) bool { + if len(a) != len(b) { + return false + } + for i, c := range a { + if !c.Equal(b[i]) { + return false + } + } + return true +} + +func HashRules(hash uint32, rules []R) uint32 { + hash = helpers.HashCombine(hash, uint32(len(rules))) + for _, child := range rules { + if childHash, ok := child.Hash(); ok { + hash = helpers.HashCombine(hash, childHash) + } else { + hash = helpers.HashCombine(hash, 0) + } + } + return hash } type RAtCharset struct { Encoding string } +func (a *RAtCharset) Equal(rule R) bool { + b, ok := rule.(*RAtCharset) + return ok && a.Encoding == b.Encoding +} + +func (r *RAtCharset) Hash() (uint32, bool) { + hash := uint32(1) + hash = helpers.HashCombineString(hash, r.Encoding) + return hash, true +} + type RAtImport struct { ImportRecordIndex uint32 ImportConditions []Token } +func (*RAtImport) Equal(rule R) bool { + return false +} + +func (r *RAtImport) Hash() (uint32, bool) { + return 0, false +} + type RAtKeyframes struct { AtToken string Name string @@ -186,28 +269,163 @@ type KeyframeBlock struct { Rules []R } +func (a *RAtKeyframes) Equal(rule R) bool { + b, ok := rule.(*RAtKeyframes) + if ok && a.AtToken == b.AtToken && a.Name == b.Name && len(a.Blocks) == len(b.Blocks) { + for i, ai := range a.Blocks { + bi := b.Blocks[i] + if len(ai.Selectors) != len(bi.Selectors) { + return false + } + for j, aj := range ai.Selectors { + if aj != bi.Selectors[j] { + return false + } + } + if !RulesEqual(ai.Rules, bi.Rules) { + return false + } + } + return true + } + return false +} + +func (r *RAtKeyframes) Hash() (uint32, bool) { + hash := uint32(2) + hash = helpers.HashCombineString(hash, r.AtToken) + hash = helpers.HashCombineString(hash, r.Name) + hash = helpers.HashCombine(hash, uint32(len(r.Blocks))) + for _, block := range r.Blocks { + hash = helpers.HashCombine(hash, uint32(len(block.Selectors))) + for _, sel := range block.Selectors { + hash = helpers.HashCombineString(hash, sel) + } + hash = HashRules(hash, block.Rules) + } + return hash, true +} + type RKnownAt struct { AtToken string Prelude []Token Rules []R } +func (a *RKnownAt) Equal(rule R) bool { + b, ok := rule.(*RKnownAt) + return ok && a.AtToken == b.AtToken && TokensEqual(a.Prelude, b.Prelude) && RulesEqual(a.Rules, a.Rules) +} + +func (r *RKnownAt) Hash() (uint32, bool) { + hash := uint32(3) + hash = helpers.HashCombineString(hash, r.AtToken) + hash = HashTokens(hash, r.Prelude) + hash = HashRules(hash, r.Rules) + return hash, true +} + type RUnknownAt struct { AtToken string Prelude []Token Block []Token } +func (a *RUnknownAt) Equal(rule R) bool { + b, ok := rule.(*RUnknownAt) + return ok && a.AtToken == b.AtToken && TokensEqual(a.Prelude, b.Prelude) && TokensEqual(a.Block, a.Block) +} + +func (r *RUnknownAt) Hash() (uint32, bool) { + hash := uint32(4) + hash = helpers.HashCombineString(hash, r.AtToken) + hash = HashTokens(hash, r.Prelude) + hash = HashTokens(hash, r.Block) + return hash, true +} + type RSelector struct { Selectors []ComplexSelector Rules []R } +func (a *RSelector) Equal(rule R) bool { + b, ok := rule.(*RSelector) + if ok && len(a.Selectors) == len(b.Selectors) { + for i, ai := range a.Selectors { + bi := b.Selectors[i] + if len(ai.Selectors) != len(bi.Selectors) { + return false + } + + for j, aj := range ai.Selectors { + bj := bi.Selectors[j] + if aj.HasNestPrefix != bj.HasNestPrefix || aj.Combinator != bj.Combinator { + return false + } + + if ats, bts := aj.TypeSelector, bj.TypeSelector; (ats == nil) != (bts == nil) { + return false + } else if ats != nil && bts != nil && !ats.Equal(*bts) { + return false + } + + if len(aj.SubclassSelectors) != len(bj.SubclassSelectors) { + return false + } + for k, ak := range aj.SubclassSelectors { + if !ak.Equal(bj.SubclassSelectors[k]) { + return false + } + } + } + } + + return RulesEqual(a.Rules, b.Rules) + } + + return false +} + +func (r *RSelector) Hash() (uint32, bool) { + hash := uint32(5) + hash = helpers.HashCombine(hash, uint32(len(r.Selectors))) + for _, complex := range r.Selectors { + hash = helpers.HashCombine(hash, uint32(len(complex.Selectors))) + for _, sel := range complex.Selectors { + if sel.TypeSelector != nil { + hash = helpers.HashCombineString(hash, sel.TypeSelector.Name.Text) + } else { + hash = helpers.HashCombine(hash, 0) + } + hash = helpers.HashCombine(hash, uint32(len(sel.SubclassSelectors))) + for _, sub := range sel.SubclassSelectors { + hash = helpers.HashCombine(hash, sub.Hash()) + } + hash = helpers.HashCombineString(hash, sel.Combinator) + } + } + hash = HashRules(hash, r.Rules) + return hash, true +} + type RQualified struct { Prelude []Token Rules []R } +func (a *RQualified) Equal(rule R) bool { + b, ok := rule.(*RQualified) + return ok && TokensEqual(a.Prelude, b.Prelude) && RulesEqual(a.Rules, b.Rules) +} + +func (r *RQualified) Hash() (uint32, bool) { + hash := uint32(6) + hash = HashTokens(hash, r.Prelude) + hash = HashRules(hash, r.Rules) + return hash, true +} + type RDeclaration struct { KeyText string Value []Token @@ -216,19 +434,32 @@ type RDeclaration struct { Important bool } +func (a *RDeclaration) Equal(rule R) bool { + b, ok := rule.(*RDeclaration) + return ok && a.KeyText == b.KeyText && TokensEqual(a.Value, b.Value) && a.Important == b.Important +} + +func (r *RDeclaration) Hash() (uint32, bool) { + hash := uint32(7) + hash = helpers.HashCombine(hash, uint32(r.Key)) + hash = HashTokens(hash, r.Value) + return hash, true +} + type RBadDeclaration struct { Tokens []Token } -func (*RAtCharset) isRule() {} -func (*RAtImport) isRule() {} -func (*RAtKeyframes) isRule() {} -func (*RKnownAt) isRule() {} -func (*RUnknownAt) isRule() {} -func (*RSelector) isRule() {} -func (*RQualified) isRule() {} -func (*RDeclaration) isRule() {} -func (*RBadDeclaration) isRule() {} +func (a *RBadDeclaration) Equal(rule R) bool { + b, ok := rule.(*RBadDeclaration) + return ok && TokensEqual(a.Tokens, b.Tokens) +} + +func (r *RBadDeclaration) Hash() (uint32, bool) { + hash := uint32(8) + hash = HashTokens(hash, r.Tokens) + return hash, true +} type ComplexSelector struct { Selectors []CompoundSelector @@ -254,20 +485,46 @@ type NamespacedName struct { Name NameToken } -// This interface is never called. Its purpose is to encode a variant type in -// Go's type system. +func (a NamespacedName) Equal(b NamespacedName) bool { + return a.Name == b.Name && (a.NamespacePrefix == nil) == (b.NamespacePrefix == nil) && + (a.NamespacePrefix == nil || b.NamespacePrefix == nil || *a.NamespacePrefix == *b.NamespacePrefix) +} + type SS interface { - isSubclassSelector() + Equal(ss SS) bool + Hash() uint32 } type SSHash struct { Name string } +func (a *SSHash) Equal(ss SS) bool { + b, ok := ss.(*SSHash) + return ok && a.Name == b.Name +} + +func (ss *SSHash) Hash() uint32 { + hash := uint32(1) + hash = helpers.HashCombineString(hash, ss.Name) + return hash +} + type SSClass struct { Name string } +func (a *SSClass) Equal(ss SS) bool { + b, ok := ss.(*SSClass) + return ok && a.Name == b.Name +} + +func (ss *SSClass) Hash() uint32 { + hash := uint32(2) + hash = helpers.HashCombineString(hash, ss.Name) + return hash +} + type SSAttribute struct { NamespacedName NamespacedName MatcherOp string @@ -275,13 +532,34 @@ type SSAttribute struct { MatcherModifier byte } +func (a *SSAttribute) Equal(ss SS) bool { + b, ok := ss.(*SSAttribute) + return ok && a.NamespacedName.Equal(b.NamespacedName) && a.MatcherOp == b.MatcherOp && + a.MatcherValue == b.MatcherValue && a.MatcherModifier == b.MatcherModifier +} + +func (ss *SSAttribute) Hash() uint32 { + hash := uint32(3) + hash = helpers.HashCombineString(hash, ss.NamespacedName.Name.Text) + hash = helpers.HashCombineString(hash, ss.MatcherOp) + hash = helpers.HashCombineString(hash, ss.MatcherValue) + return hash +} + type SSPseudoClass struct { Name string Args []Token IsElement bool // If true, this is prefixed by "::" instead of ":" } -func (*SSHash) isSubclassSelector() {} -func (*SSClass) isSubclassSelector() {} -func (*SSAttribute) isSubclassSelector() {} -func (*SSPseudoClass) isSubclassSelector() {} +func (a *SSPseudoClass) Equal(ss SS) bool { + b, ok := ss.(*SSPseudoClass) + return ok && a.Name == b.Name && TokensEqual(a.Args, b.Args) && a.IsElement == b.IsElement +} + +func (ss *SSPseudoClass) Hash() uint32 { + hash := uint32(4) + hash = helpers.HashCombineString(hash, ss.Name) + hash = HashTokens(hash, ss.Args) + return hash +} diff --git a/internal/css_parser/css_parser.go b/internal/css_parser/css_parser.go index 222fd44b980..a44f2ecf630 100644 --- a/internal/css_parser/css_parser.go +++ b/internal/css_parser/css_parser.go @@ -225,7 +225,7 @@ loop: } if p.options.MangleSyntax { - rules = removeEmptyRules(rules) + rules = removeEmptyAndDuplicateRules(rules) } return rules } @@ -239,7 +239,7 @@ func (p *parser) parseListOfDeclarations() (list []css_ast.R) { case css_lexer.TEndOfFile, css_lexer.TCloseBrace: list = p.processDeclarations(list) if p.options.MangleSyntax { - list = removeEmptyRules(list) + list = removeEmptyAndDuplicateRules(list) } return @@ -258,9 +258,20 @@ func (p *parser) parseListOfDeclarations() (list []css_ast.R) { } } -func removeEmptyRules(rules []css_ast.R) []css_ast.R { - end := 0 - for _, rule := range rules { +func removeEmptyAndDuplicateRules(rules []css_ast.R) []css_ast.R { + type hashEntry struct { + indices []uint32 + } + + n := len(rules) + start := n + entries := make(map[uint32]hashEntry) + + // Scan from the back so we keep the last rule +skipRule: + for i := n - 1; i >= 0; i-- { + rule := rules[i] + switch r := rule.(type) { case *css_ast.RAtKeyframes: if len(r.Blocks) == 0 { @@ -278,10 +289,25 @@ func removeEmptyRules(rules []css_ast.R) []css_ast.R { } } - rules[end] = rule - end++ + if hash, ok := rule.Hash(); ok { + entry := entries[hash] + + // For duplicate rules, omit all but the last copy + for _, index := range entry.indices { + if rule.Equal(rules[index]) { + continue skipRule + } + } + + entry.indices = append(entry.indices, uint32(i)) + entries[hash] = entry + } + + start-- + rules[start] = rule } - return rules[:end] + + return rules[start:] } func (p *parser) parseURLOrString() (string, logger.Range, bool) { diff --git a/internal/css_parser/css_parser_test.go b/internal/css_parser/css_parser_test.go index 1bcfd2cfb39..39a6a2429f1 100644 --- a/internal/css_parser/css_parser_test.go +++ b/internal/css_parser/css_parser_test.go @@ -1014,3 +1014,32 @@ func TestBorderRadius(t *testing.T) { expectPrintedMangleMinify(t, "a { border-radius: 1 2 3 4; border-top-right-radius: 5; }", "a{border-radius:1 5 3 4}") expectPrintedMangleMinify(t, "a { border-radius: 1 2 3 4; border-top-right-radius: 5 6; }", "a{border-radius:1 5 3 4/1 6 3 4}") } + +func TestDeduplicateRules(t *testing.T) { + expectPrinted(t, "a { color: red; color: green; color: red }", + "a {\n color: red;\n color: green;\n color: red;\n}\n") + expectPrintedMangle(t, "a { color: red; color: green; color: red }", + "a {\n color: green;\n color: red;\n}\n") + + expectPrinted(t, "a { color: red } a { color: green } a { color: red }", + "a {\n color: red;\n}\na {\n color: green;\n}\na {\n color: red;\n}\n") + expectPrintedMangle(t, "a { color: red } a { color: green } a { color: red }", + "a {\n color: green;\n}\na {\n color: red;\n}\n") + + expectPrintedMangle(t, "@media screen { a { color: red } } @media screen { a { color: red } }", + "@media screen {\n a {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "@media screen { a { color: red } } @media screen { & a { color: red } }", + "@media screen {\n a {\n color: red;\n }\n}\n@media screen {\n & a {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "@media screen { a { color: red } } @media screen { a[x] { color: red } }", + "@media screen {\n a {\n color: red;\n }\n}\n@media screen {\n a[x] {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "@media screen { a { color: red } } @media screen { a.x { color: red } }", + "@media screen {\n a {\n color: red;\n }\n}\n@media screen {\n a.x {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "@media screen { a { color: red } } @media screen { a#x { color: red } }", + "@media screen {\n a {\n color: red;\n }\n}\n@media screen {\n a#x {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "@media screen { a { color: red } } @media screen { a:x { color: red } }", + "@media screen {\n a {\n color: red;\n }\n}\n@media screen {\n a:x {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "@media screen { a:x { color: red } } @media screen { a:x(y) { color: red } }", + "@media screen {\n a:x {\n color: red;\n }\n}\n@media screen {\n a:x(y) {\n color: red;\n }\n}\n") + expectPrintedMangle(t, "@media screen { a b { color: red } } @media screen { a + b { color: red } }", + "@media screen {\n a b {\n color: red;\n }\n}\n@media screen {\n a + b {\n color: red;\n }\n}\n") +} diff --git a/internal/helpers/hash.go b/internal/helpers/hash.go new file mode 100644 index 00000000000..d7028860a5b --- /dev/null +++ b/internal/helpers/hash.go @@ -0,0 +1,14 @@ +package helpers + +// From: http://boost.sourceforge.net/doc/html/boost/hash_combine.html +func HashCombine(seed uint32, hash uint32) uint32 { + return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2)) +} + +func HashCombineString(seed uint32, text string) uint32 { + seed = HashCombine(seed, uint32(len(text))) + for _, c := range text { + seed = HashCombine(seed, uint32(c)) + } + return seed +} diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 4d211eb3f09..ad7b53caf0b 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -11,6 +11,7 @@ import ( "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" + "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/js_ast" "github.com/evanw/esbuild/internal/js_lexer" "github.com/evanw/esbuild/internal/logger" @@ -555,10 +556,6 @@ func (dc *duplicateCaseChecker) check(p *parser, expr js_ast.Expr) { } } -func hashCombine(seed uint32, hash uint32) uint32 { - return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2)) -} - func duplicateCaseHash(expr js_ast.Expr) (uint32, bool) { switch e := expr.Data.(type) { case *js_ast.ENull: @@ -569,44 +566,40 @@ func duplicateCaseHash(expr js_ast.Expr) (uint32, bool) { case *js_ast.EBoolean: if e.Value { - return hashCombine(2, 1), true + return helpers.HashCombine(2, 1), true } - return hashCombine(2, 0), true + return helpers.HashCombine(2, 0), true case *js_ast.ENumber: bits := math.Float64bits(e.Value) - return hashCombine(hashCombine(3, uint32(bits)), uint32(bits>>32)), true + return helpers.HashCombine(helpers.HashCombine(3, uint32(bits)), uint32(bits>>32)), true case *js_ast.EString: hash := uint32(4) for _, c := range e.Value { - hash = hashCombine(hash, uint32(c)) + hash = helpers.HashCombine(hash, uint32(c)) } return hash, true case *js_ast.EBigInt: hash := uint32(5) for _, c := range e.Value { - hash = hashCombine(hash, uint32(c)) + hash = helpers.HashCombine(hash, uint32(c)) } return hash, true case *js_ast.EIdentifier: - return hashCombine(6, e.Ref.InnerIndex), true + return helpers.HashCombine(6, e.Ref.InnerIndex), true case *js_ast.EDot: if target, ok := duplicateCaseHash(e.Target); ok { - hash := hashCombine(7, target) - for _, c := range e.Name { - hash = hashCombine(hash, uint32(c)) - } - return hash, true + return helpers.HashCombineString(helpers.HashCombine(7, target), e.Name), true } case *js_ast.EIndex: if target, ok := duplicateCaseHash(e.Target); ok { if index, ok := duplicateCaseHash(e.Index); ok { - return hashCombine(hashCombine(8, target), index), true + return helpers.HashCombine(helpers.HashCombine(8, target), index), true } } }