github.com/wolfd/bazel-gazelle@v0.14.0/internal/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 methods for merging parsed BUILD files.
    17  package merger
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  
    23  	"github.com/bazelbuild/bazel-gazelle/internal/rule"
    24  )
    25  
    26  // Phase indicates which attributes should be merged in matching rules.
    27  //
    28  // The pre-resolve merge is performed before rules are indexed for dependency
    29  // resolution. All attributes not related to dependencies are merged. This
    30  // merge must be performed indexing because attributes related to indexing
    31  // (e.g., srcs, importpath) will be affected.
    32  //
    33  // The post-resolve merge is performed after rules are indexed. All attributes
    34  // related to dependencies are merged.
    35  type Phase int
    36  
    37  const (
    38  	PreResolve Phase = iota
    39  	PostResolve
    40  )
    41  
    42  // MergeFile merges the rules in genRules with matching rules in f and
    43  // adds unmatched rules to the end of the merged file. MergeFile also merges
    44  // rules in empty with matching rules in f and deletes rules that
    45  // are empty after merging. attrs is the set of attributes to merge. Attributes
    46  // not in this set will be left alone if they already exist.
    47  func MergeFile(oldFile *rule.File, emptyRules, genRules []*rule.Rule, phase Phase, kinds map[string]rule.KindInfo) {
    48  	getMergeAttrs := func(r *rule.Rule) map[string]bool {
    49  		if phase == PreResolve {
    50  			return kinds[r.Kind()].MergeableAttrs
    51  		} else {
    52  			return kinds[r.Kind()].ResolveAttrs
    53  		}
    54  	}
    55  
    56  	// Merge empty rules into the file and delete any rules which become empty.
    57  	for _, emptyRule := range emptyRules {
    58  		if oldRule, _ := match(oldFile.Rules, emptyRule, kinds[emptyRule.Kind()]); oldRule != nil {
    59  			if oldRule.ShouldKeep() {
    60  				continue
    61  			}
    62  			rule.MergeRules(emptyRule, oldRule, getMergeAttrs(emptyRule), oldFile.Path)
    63  			if oldRule.IsEmpty(kinds[oldRule.Kind()]) {
    64  				oldRule.Delete()
    65  			}
    66  		}
    67  	}
    68  	oldFile.Sync()
    69  
    70  	// Match generated rules with existing rules in the file. Keep track of
    71  	// rules with non-standard names.
    72  	matchRules := make([]*rule.Rule, len(genRules))
    73  	matchErrors := make([]error, len(genRules))
    74  	substitutions := make(map[string]string)
    75  	for i, genRule := range genRules {
    76  		oldRule, err := match(oldFile.Rules, genRule, kinds[genRule.Kind()])
    77  		if err != nil {
    78  			// TODO(jayconrod): add a verbose mode and log errors. They are too chatty
    79  			// to print by default.
    80  			matchErrors[i] = err
    81  			continue
    82  		}
    83  		matchRules[i] = oldRule
    84  		if oldRule != nil {
    85  			if oldRule.Name() != genRule.Name() {
    86  				substitutions[genRule.Name()] = oldRule.Name()
    87  			}
    88  		}
    89  	}
    90  
    91  	// Rename labels in generated rules that refer to other generated rules.
    92  	if len(substitutions) > 0 {
    93  		for _, genRule := range genRules {
    94  			substituteRule(genRule, substitutions, kinds[genRule.Kind()])
    95  		}
    96  	}
    97  
    98  	// Merge generated rules with existing rules or append to the end of the file.
    99  	for i, genRule := range genRules {
   100  		if matchErrors[i] != nil {
   101  			continue
   102  		}
   103  		if matchRules[i] == nil {
   104  			genRule.Insert(oldFile)
   105  		} else {
   106  			rule.MergeRules(genRule, matchRules[i], getMergeAttrs(genRule), oldFile.Path)
   107  		}
   108  	}
   109  }
   110  
   111  // substituteRule replaces local labels (those beginning with ":", referring to
   112  // targets in the same package) according to a substitution map. This is used
   113  // to update generated rules before merging when the corresponding existing
   114  // rules have different names. If substituteRule replaces a string, it returns
   115  // a new expression; it will not modify the original expression.
   116  func substituteRule(r *rule.Rule, substitutions map[string]string, info rule.KindInfo) {
   117  	for attr := range info.SubstituteAttrs {
   118  		if expr := r.Attr(attr); expr != nil {
   119  			expr = rule.MapExprStrings(expr, func(s string) string {
   120  				if rename, ok := substitutions[strings.TrimPrefix(s, ":")]; ok {
   121  					return ":" + rename
   122  				} else {
   123  					return s
   124  				}
   125  			})
   126  			r.SetAttr(attr, expr)
   127  		}
   128  	}
   129  }
   130  
   131  // match searches for a rule that can be merged with x in rules.
   132  //
   133  // A rule is considered a match if its kind is equal to x's kind AND either its
   134  // name is equal OR at least one of the attributes in matchAttrs is equal.
   135  //
   136  // If there are no matches, nil and nil are returned.
   137  //
   138  // If a rule has the same name but a different kind, nill and an error
   139  // are returned.
   140  //
   141  // If there is exactly one match, the rule and nil are returned.
   142  //
   143  // If there are multiple matches, match will attempt to disambiguate, based on
   144  // the quality of the match (name match is best, then attribute match in the
   145  // order that attributes are listed). If disambiguation is successful,
   146  // the rule and nil are returned. Otherwise, nil and an error are returned.
   147  func match(rules []*rule.Rule, x *rule.Rule, info rule.KindInfo) (*rule.Rule, error) {
   148  	xname := x.Name()
   149  	xkind := x.Kind()
   150  	var nameMatches []*rule.Rule
   151  	var kindMatches []*rule.Rule
   152  	for _, y := range rules {
   153  		if xname == y.Name() {
   154  			nameMatches = append(nameMatches, y)
   155  		}
   156  		if xkind == y.Kind() {
   157  			kindMatches = append(kindMatches, y)
   158  		}
   159  	}
   160  
   161  	if len(nameMatches) == 1 {
   162  		y := nameMatches[0]
   163  		if xkind != y.Kind() {
   164  			return nil, fmt.Errorf("could not merge %s(%s): a rule of the same name has kind %s", xkind, xname, y.Kind())
   165  		}
   166  		return y, nil
   167  	}
   168  	if len(nameMatches) > 1 {
   169  		return nil, fmt.Errorf("could not merge %s(%s): multiple rules have the same name", xkind, xname)
   170  	}
   171  
   172  	for _, key := range info.MatchAttrs {
   173  		var attrMatches []*rule.Rule
   174  		xvalue := x.AttrString(key)
   175  		if xvalue == "" {
   176  			continue
   177  		}
   178  		for _, y := range kindMatches {
   179  			if xvalue == y.AttrString(key) {
   180  				attrMatches = append(attrMatches, y)
   181  			}
   182  		}
   183  		if len(attrMatches) == 1 {
   184  			return attrMatches[0], nil
   185  		} else if len(attrMatches) > 1 {
   186  			return nil, fmt.Errorf("could not merge %s(%s): multiple rules have the same attribute %s = %q", xkind, xname, key, xvalue)
   187  		}
   188  	}
   189  
   190  	if info.MatchAny {
   191  		if len(kindMatches) == 1 {
   192  			return kindMatches[0], nil
   193  		} else if len(kindMatches) > 1 {
   194  			return nil, fmt.Errorf("could not merge %s(%s): multiple rules have the same kind but different names", xkind, xname)
   195  		}
   196  	}
   197  
   198  	return nil, nil
   199  }