github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/merger/merger.go (about) 1 /* Copyright 2016 The Bazel Authors. All rights reserved. 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 // Package merger provides functions for merging generated rules into 17 // existing build files. 18 // 19 // Gazelle's normal workflow is roughly as follows: 20 // 21 // 1. Read metadata from sources. 22 // 23 // 2. Generate new rules. 24 // 25 // 3. Merge newly generated rules with rules in the existing build file 26 // if there is one. 27 // 28 // 4. Build an index of merged library rules for dependency resolution. 29 // 30 // 5. Resolve dependencies (i.e., convert import strings to deps labels). 31 // 32 // 6. Merge the newly resolved dependencies. 33 // 34 // 7. Write the merged file back to disk. 35 // 36 // This package is used for sets 3 and 6 above. 37 package merger 38 39 import ( 40 "fmt" 41 "sort" 42 "strings" 43 44 "github.com/bazelbuild/bazel-gazelle/rule" 45 ) 46 47 // Phase indicates which attributes should be merged in matching rules. 48 type Phase int 49 50 const ( 51 // The pre-resolve merge is performed before rules are indexed for dependency 52 // resolution. All attributes not related to dependencies are merged 53 // (i.e., rule.KindInfo.MergeableAttrs). This merge must be performed 54 // before indexing because attributes related to indexing (e.g., 55 // srcs, importpath) will be affected. 56 PreResolve Phase = iota 57 58 // The post-resolve merge is performed after rules are indexed. All attributes 59 // related to dependencies are merged (i.e., rule.KindInfo.ResolveAttrs). 60 PostResolve 61 ) 62 63 // UnstableInsertIndexKey is the name of an internal attribute that may be set 64 // on newly generated rules. When MergeFile is given a generated rule that 65 // doesn't match any existing rule, MergeFile will insert the rule at the index 66 // indicated by this key instead of at the end of the file. 67 // 68 // This definition is unstable and may be removed in the future. 69 // 70 // TODO(jayconrod): make this stable *or* find a better way to express it. 71 const UnstableInsertIndexKey = "_gazelle_insert_index" 72 73 // MergeFile combines information from newly generated rules with matching 74 // rules in an existing build file. MergeFile can also delete rules which 75 // are empty after merging. 76 // 77 // oldFile is the file to merge. It must not be nil. 78 // 79 // emptyRules is a list of stub rules (with no attributes other than name) 80 // which were not generated. These are merged with matching rules. The merged 81 // rules are deleted if they contain no attributes that make them buildable 82 // (e.g., srcs, deps, anything in rule.KindInfo.NonEmptyAttrs). 83 // 84 // genRules is a list of newly generated rules. These are merged with 85 // matching rules. A rule matches if it has the same kind and name or if 86 // some other attribute in rule.KindInfo.MatchAttrs matches (e.g., 87 // "importpath" in go_library). Elements of genRules that don't match 88 // any existing rule are appended to the end of oldFile. 89 // 90 // phase indicates whether this is a pre- or post-resolve merge. Different 91 // attributes (rule.KindInfo.MergeableAttrs or ResolveAttrs) will be merged. 92 // 93 // kinds maps rule kinds (e.g., "go_library") to metadata that helps merge 94 // rules of that kind. 95 // 96 // When a generated and existing rule are merged, each attribute is merged 97 // separately. If an attribute is mergeable (according to KindInfo), values 98 // from the existing attribute are replaced by values from the generated 99 // attribute. Comments are preserved on values that are present in both 100 // versions of the attribute. If at attribute is not mergeable, the generated 101 // version of the attribute will be added if no existing attribute is present; 102 // otherwise, the existing attribute will be preserved. 103 // 104 // Note that "# keep" comments affect merging. If a value within an existing 105 // attribute is marked with a "# keep" comment, it will not be removed. 106 // If an attribute is marked with a "# keep" comment, it will not be merged. 107 // If a rule is marked with a "# keep" comment, the whole rule will not 108 // be modified. 109 func MergeFile(oldFile *rule.File, emptyRules, genRules []*rule.Rule, phase Phase, kinds map[string]rule.KindInfo) { 110 getMergeAttrs := func(r *rule.Rule) map[string]bool { 111 if phase == PreResolve { 112 return kinds[r.Kind()].MergeableAttrs 113 } else { 114 return kinds[r.Kind()].ResolveAttrs 115 } 116 } 117 118 // Merge empty rules into the file and delete any rules which become empty. 119 for _, emptyRule := range emptyRules { 120 if oldRule, _ := Match(oldFile.Rules, emptyRule, kinds[emptyRule.Kind()]); oldRule != nil { 121 if oldRule.ShouldKeep() { 122 continue 123 } 124 rule.MergeRules(emptyRule, oldRule, getMergeAttrs(emptyRule), oldFile.Path) 125 if oldRule.IsEmpty(kinds[oldRule.Kind()]) { 126 oldRule.Delete() 127 } 128 } 129 } 130 oldFile.Sync() 131 132 // Match generated rules with existing rules in the file. Keep track of 133 // rules with non-standard names. 134 matchRules := make([]*rule.Rule, len(genRules)) 135 matchErrors := make([]error, len(genRules)) 136 substitutions := make(map[string]string) 137 for i, genRule := range genRules { 138 oldRule, err := Match(oldFile.Rules, genRule, kinds[genRule.Kind()]) 139 if err != nil { 140 // TODO(jayconrod): add a verbose mode and log errors. They are too chatty 141 // to print by default. 142 matchErrors[i] = err 143 continue 144 } 145 matchRules[i] = oldRule 146 if oldRule != nil { 147 if oldRule.Name() != genRule.Name() { 148 substitutions[genRule.Name()] = oldRule.Name() 149 } 150 } 151 } 152 153 // Rename labels in generated rules that refer to other generated rules. 154 if len(substitutions) > 0 { 155 for _, genRule := range genRules { 156 substituteRule(genRule, substitutions, kinds[genRule.Kind()]) 157 } 158 } 159 160 // Merge generated rules with existing rules or append to the end of the file. 161 for i, genRule := range genRules { 162 if matchErrors[i] != nil { 163 continue 164 } 165 if matchRules[i] == nil { 166 if index, ok := genRule.PrivateAttr(UnstableInsertIndexKey).(int); ok { 167 genRule.InsertAt(oldFile, index) 168 } else { 169 genRule.Insert(oldFile) 170 } 171 } else { 172 rule.MergeRules(genRule, matchRules[i], getMergeAttrs(genRule), oldFile.Path) 173 } 174 } 175 } 176 177 // substituteRule replaces local labels (those beginning with ":", referring to 178 // targets in the same package) according to a substitution map. This is used 179 // to update generated rules before merging when the corresponding existing 180 // rules have different names. If substituteRule replaces a string, it returns 181 // a new expression; it will not modify the original expression. 182 func substituteRule(r *rule.Rule, substitutions map[string]string, info rule.KindInfo) { 183 for attr := range info.SubstituteAttrs { 184 if expr := r.Attr(attr); expr != nil { 185 expr = rule.MapExprStrings(expr, func(s string) string { 186 if rename, ok := substitutions[strings.TrimPrefix(s, ":")]; ok { 187 return ":" + rename 188 } else { 189 return s 190 } 191 }) 192 r.SetAttr(attr, expr) 193 } 194 } 195 } 196 197 // Match searches for a rule that can be merged with x in rules. 198 // 199 // A rule is considered a match if its kind is equal to x's kind AND either its 200 // name is equal OR at least one of the attributes in matchAttrs is equal. 201 // 202 // If there are no matches, nil and nil are returned. 203 // 204 // If a rule has the same name but a different kind, nill and an error 205 // are returned. 206 // 207 // If there is exactly one match, the rule and nil are returned. 208 // 209 // If there are multiple matches, match will attempt to disambiguate, based on 210 // the quality of the match (name match is best, then attribute match in the 211 // order that attributes are listed). If disambiguation is successful, 212 // the rule and nil are returned. Otherwise, nil and an error are returned. 213 func Match(rules []*rule.Rule, x *rule.Rule, info rule.KindInfo) (*rule.Rule, error) { 214 xname := x.Name() 215 xkind := x.Kind() 216 var nameMatches []*rule.Rule 217 var kindMatches []*rule.Rule 218 for _, y := range rules { 219 if xname == y.Name() { 220 nameMatches = append(nameMatches, y) 221 } 222 if xkind == y.Kind() { 223 kindMatches = append(kindMatches, y) 224 } 225 } 226 227 if len(nameMatches) == 1 { 228 y := nameMatches[0] 229 if xkind != y.Kind() { 230 return nil, fmt.Errorf("could not merge %s(%s): a rule of the same name has kind %s", xkind, xname, y.Kind()) 231 } 232 return y, nil 233 } 234 if len(nameMatches) > 1 { 235 return nil, fmt.Errorf("could not merge %s(%s): multiple rules have the same name", xkind, xname) 236 } 237 238 for _, key := range info.MatchAttrs { 239 var attrMatches []*rule.Rule 240 for _, y := range kindMatches { 241 if attrMatch(x, y, key) { 242 attrMatches = append(attrMatches, y) 243 } 244 } 245 if len(attrMatches) == 1 { 246 return attrMatches[0], nil 247 } else if len(attrMatches) > 1 { 248 return nil, fmt.Errorf("could not merge %s(%s): multiple rules have the same attribute %s", xkind, xname, key) 249 } 250 } 251 252 if info.MatchAny { 253 if len(kindMatches) == 1 { 254 return kindMatches[0], nil 255 } else if len(kindMatches) > 1 { 256 return nil, fmt.Errorf("could not merge %s(%s): multiple rules have the same kind but different names", xkind, xname) 257 } 258 } 259 260 return nil, nil 261 } 262 263 func attrMatch(x, y *rule.Rule, key string) bool { 264 xValue := x.AttrString(key) 265 if xValue != "" && xValue == y.AttrString(key) { 266 return true 267 } 268 xValues := x.AttrStrings(key) 269 yValues := y.AttrStrings(key) 270 if xValues == nil || yValues == nil || len(xValues) != len(yValues) { 271 return false 272 } 273 sort.Strings(xValues) 274 sort.Strings(yValues) 275 for i, v := range xValues { 276 if v != yValues[i] { 277 return false 278 } 279 } 280 return true 281 }