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  }