github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/label/label.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 label provides utilities for parsing and manipulating
    17  // Bazel labels. See
    18  // https://docs.bazel.build/versions/master/build-ref.html#labels
    19  // for more information.
    20  package label
    21  
    22  import (
    23  	"fmt"
    24  	"log"
    25  	"path"
    26  	"regexp"
    27  	"strings"
    28  
    29  	"github.com/bazelbuild/bazel-gazelle/pathtools"
    30  	bzl "github.com/bazelbuild/buildtools/build"
    31  )
    32  
    33  // A Label represents a label of a build target in Bazel. Labels have three
    34  // parts: a repository name, a package name, and a target name, formatted
    35  // as @repo//pkg:target.
    36  type Label struct {
    37  	// Repo is the repository name. If omitted, the label refers to a target
    38  	// in the current repository.
    39  	Repo string
    40  
    41  	// Pkg is the package name, which is usually the directory that contains
    42  	// the target. If both Repo and Pkg are omitted, the label is relative.
    43  	Pkg string
    44  
    45  	// Name is the name of the target the label refers to. Name must not be empty.
    46  	// Note that the name may be omitted from a label string if it is equal to
    47  	// the last component of the package name ("//x" is equivalent to "//x:x"),
    48  	// but in either case, Name should be set here.
    49  	Name string
    50  
    51  	// Relative indicates whether the label refers to a target in the current
    52  	// package. Relative is true if and only if Repo and Pkg are both omitted.
    53  	Relative bool
    54  }
    55  
    56  // New constructs a new label from components.
    57  func New(repo, pkg, name string) Label {
    58  	return Label{Repo: repo, Pkg: pkg, Name: name}
    59  }
    60  
    61  // NoLabel is the zero value of Label. It is not a valid label and may be
    62  // returned when an error occurs.
    63  var NoLabel = Label{}
    64  
    65  var (
    66  	// This was taken from https://github.com/bazelbuild/bazel/blob/71fb1e4188b01e582a308cfe4bcbf1c730eded1b/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryName.java#L159C1-L164
    67  	labelRepoRegexp = regexp.MustCompile(`^@$|^[A-Za-z0-9_.-][A-Za-z0-9_.~-]*$`)
    68  	// This was taken from https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/cmdline/LabelValidator.java
    69  	// Package names may contain all 7-bit ASCII characters except:
    70  	// 0-31 (control characters)
    71  	// 58 ':' (colon) - target name separator
    72  	// 92 '\' (backslash) - directory separator (on Windows); may be allowed in the future
    73  	// 127 (delete)
    74  	// Target names may contain the same characters
    75  	labelPkgRegexp  = regexp.MustCompile(`^[\x20-\x39\x3B-\x5B\x5D-\x7E]*$`)
    76  	labelNameRegexp = labelPkgRegexp
    77  )
    78  
    79  // Parse reads a label from a string.
    80  // See https://docs.bazel.build/versions/master/build-ref.html#lexi.
    81  func Parse(s string) (Label, error) {
    82  	origStr := s
    83  
    84  	relative := true
    85  	var repo string
    86  	// if target name begins @@ drop the first @
    87  	if strings.HasPrefix(s, "@@") {
    88  		s = s[len("@"):]
    89  	}
    90  	if strings.HasPrefix(s, "@") {
    91  		relative = false
    92  		endRepo := strings.Index(s, "//")
    93  		if endRepo > len("@") {
    94  			repo = s[len("@"):endRepo]
    95  			s = s[endRepo:]
    96  			// If the label begins with "@//...", set repo = "@"
    97  			// to remain distinct from "//...", where repo = ""
    98  		} else if endRepo == len("@") {
    99  			repo = s[:len("@")]
   100  			s = s[len("@"):]
   101  		} else {
   102  			repo = s[len("@"):]
   103  			s = "//:" + repo
   104  		}
   105  		if !labelRepoRegexp.MatchString(repo) {
   106  			return NoLabel, fmt.Errorf("label parse error: repository has invalid characters: %q", origStr)
   107  		}
   108  	}
   109  
   110  	var pkg string
   111  	if strings.HasPrefix(s, "//") {
   112  		relative = false
   113  		endPkg := strings.Index(s, ":")
   114  		if endPkg < 0 {
   115  			pkg = s[len("//"):]
   116  			s = ""
   117  		} else {
   118  			pkg = s[len("//"):endPkg]
   119  			s = s[endPkg:]
   120  		}
   121  		if !labelPkgRegexp.MatchString(pkg) {
   122  			return NoLabel, fmt.Errorf("label parse error: package has invalid characters: %q", origStr)
   123  		}
   124  	}
   125  
   126  	if s == ":" {
   127  		return NoLabel, fmt.Errorf("label parse error: empty name: %q", origStr)
   128  	}
   129  	name := strings.TrimPrefix(s, ":")
   130  	if !labelNameRegexp.MatchString(name) {
   131  		return NoLabel, fmt.Errorf("label parse error: name has invalid characters: %q", origStr)
   132  	}
   133  
   134  	if pkg == "" && name == "" {
   135  		return NoLabel, fmt.Errorf("label parse error: empty package and name: %q", origStr)
   136  	}
   137  	if name == "" {
   138  		name = path.Base(pkg)
   139  	}
   140  
   141  	return Label{
   142  		Repo:     repo,
   143  		Pkg:      pkg,
   144  		Name:     name,
   145  		Relative: relative,
   146  	}, nil
   147  }
   148  
   149  func (l Label) String() string {
   150  	if l.Relative {
   151  		return fmt.Sprintf(":%s", l.Name)
   152  	}
   153  
   154  	var repo string
   155  	if l.Repo != "" && l.Repo != "@" {
   156  		repo = fmt.Sprintf("@%s", l.Repo)
   157  	} else {
   158  		// if l.Repo == "", the label string will begin with "//"
   159  		// if l.Repo == "@", the label string will begin with "@//"
   160  		repo = l.Repo
   161  	}
   162  
   163  	if path.Base(l.Pkg) == l.Name {
   164  		return fmt.Sprintf("%s//%s", repo, l.Pkg)
   165  	}
   166  	return fmt.Sprintf("%s//%s:%s", repo, l.Pkg, l.Name)
   167  }
   168  
   169  // Abs computes an absolute label (one with a repository and package name)
   170  // from this label. If this label is already absolute, it is returned
   171  // unchanged.
   172  func (l Label) Abs(repo, pkg string) Label {
   173  	if !l.Relative {
   174  		return l
   175  	}
   176  	return Label{Repo: repo, Pkg: pkg, Name: l.Name}
   177  }
   178  
   179  // Rel attempts to compute a relative label from this label. If this label
   180  // is already relative or is in a different package, this label may be
   181  // returned unchanged.
   182  func (l Label) Rel(repo, pkg string) Label {
   183  	if l.Relative || l.Repo != repo {
   184  		return l
   185  	}
   186  	if l.Pkg == pkg {
   187  		return Label{Name: l.Name, Relative: true}
   188  	}
   189  	return Label{Pkg: l.Pkg, Name: l.Name}
   190  }
   191  
   192  // Equal returns whether two labels are exactly the same. It does not return
   193  // true for different labels that refer to the same target.
   194  func (l Label) Equal(other Label) bool {
   195  	return l.Repo == other.Repo &&
   196  		l.Pkg == other.Pkg &&
   197  		l.Name == other.Name &&
   198  		l.Relative == other.Relative
   199  }
   200  
   201  // Contains returns whether other is contained by the package of l or a
   202  // sub-package. Neither label may be relative.
   203  func (l Label) Contains(other Label) bool {
   204  	if l.Relative {
   205  		log.Panicf("l must not be relative: %s", l)
   206  	}
   207  	if other.Relative {
   208  		log.Panicf("other must not be relative: %s", other)
   209  	}
   210  	result := l.Repo == other.Repo && pathtools.HasPrefix(other.Pkg, l.Pkg)
   211  	return result
   212  }
   213  
   214  func (l Label) BzlExpr() bzl.Expr {
   215  	return &bzl.StringExpr {
   216  	    Value: l.String(),
   217  	}
   218  }
   219  
   220  var nonWordRe = regexp.MustCompile(`\W+`)
   221  
   222  // ImportPathToBazelRepoName converts a Go import path into a bazel repo name
   223  // following the guidelines in http://bazel.io/docs/be/functions.html#workspace
   224  func ImportPathToBazelRepoName(importpath string) string {
   225  	importpath = strings.ToLower(importpath)
   226  	components := strings.Split(importpath, "/")
   227  	labels := strings.Split(components[0], ".")
   228  	reversed := make([]string, 0, len(labels)+len(components)-1)
   229  	for i := range labels {
   230  		l := labels[len(labels)-i-1]
   231  		reversed = append(reversed, l)
   232  	}
   233  	repo := strings.Join(append(reversed, components[1:]...), ".")
   234  	return nonWordRe.ReplaceAllString(repo, "_")
   235  }