blob: bf968f7f147c38711dc0f94ed795e7f4bd939a4f [file] [log] [blame]
// Copyright 2022 Google LLC
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package exporter
import (
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
"go.skia.org/skia/bazel/exporter/build_proto/analysis_v2"
"go.skia.org/skia/bazel/exporter/build_proto/build"
)
const (
ruleOnlyRepoPattern = `^(@\w+)$`
rulePattern = `^(?P<repo>@[^/]+)?/(?P<path>[^:]+)(?P<target>:[^:]+)?$`
locationPattern = `^(?P<path>[^:]+):(?P<line>[^:]+):(?P<pos>[^:]+)$`
)
var (
ruleOnlyRepoRegex = regexp.MustCompile(ruleOnlyRepoPattern)
ruleRegex = regexp.MustCompile(rulePattern)
locRegex = regexp.MustCompile(locationPattern)
)
// Return true if the given rule name represents an external repository.
func isExternalRule(name string) bool {
return name[0] == '@'
}
// Given a Bazel rule name find that rule from within the
// query results. Returns nil if the given rule is not present.
func findRule(qr *analysis_v2.CqueryResult, name string) *build.Rule {
for _, result := range qr.GetResults() {
r := result.GetTarget().GetRule()
if r.GetName() == name {
return r
}
}
return nil
}
// Parse a rule into its constituent parts.
// https://docs.bazel.build/versions/main/guide.html#specifying-targets-to-build
//
// For example, the input rule `//foo/bar:baz` will return:
//
// repo: ""
// path: "/foo/bar"
// target: "baz"
func parseRule(rule string) (repo string, path string, target string, err error) {
match := ruleOnlyRepoRegex.FindStringSubmatch(rule)
if match != nil {
return match[1], "/", strings.TrimPrefix(match[1], "@"), nil
}
match = ruleRegex.FindStringSubmatch(rule)
if match == nil {
return "", "", "", skerr.Fmt(`Unable to match rule %q`, rule)
}
if len(match[3]) > 0 {
target = strings.TrimPrefix(match[3], ":")
} else {
// No explicit target, so use directory name as default target.
target = filepath.Base(match[2])
}
return match[1], match[2], target, nil
}
// Parse a file location into its three constituent parts.
//
// A location is of the form:
//
// /full/path/to/BUILD.bazel:33:20
func parseLocation(location string) (path string, line int, pos int, err error) {
match := locRegex.FindStringSubmatch(location)
if match == nil {
return "", 0, 0, skerr.Fmt(`unable to match file location %q`, location)
}
path = match[1]
line, err = strconv.Atoi(match[2])
if err != nil {
return "", 0, 0, skerr.Fmt(`unable to parse line no. %q`, match[2])
}
pos, err = strconv.Atoi(match[3])
if err != nil {
return "", 0, 0, skerr.Fmt(`unable to parse pos. %q`, match[3])
}
return path, line, pos, nil
}
// Return the directory containing the file in the location string.
func getLocationDir(location string) (string, error) {
filePath, _, _, err := parseLocation(location)
if err != nil {
return "", skerr.Wrap(err)
}
return filepath.Dir(filePath), nil
}
func makeCanonicalRuleName(bazelRuleName string) (string, error) {
repo, path, target, err := parseRule(bazelRuleName)
if err != nil {
return "", skerr.Wrap(err)
}
return fmt.Sprintf("%s/%s:%s", repo, path, target), nil
}
// Determine if a target refers to a file, or a rule. target is of
// the form:
//
// file: //include/private:SingleOwner.h
// rule: //bazel/common_config_settings:has_gpu_backend
func isFileTarget(target string) bool {
_, _, target, err := parseRule(target)
if err != nil {
return false
}
return strings.Contains(target, ".")
}
// Create a string that uniquely identifies the rule and can be used
// in the exported project file as a valid name.
func getRuleSimpleName(bazelRuleName string) (string, error) {
s, err := makeCanonicalRuleName(bazelRuleName)
if err != nil {
return "", skerr.Wrap(err)
}
s = strings.TrimPrefix(s, "//:")
s = strings.TrimPrefix(s, "//")
s = strings.ReplaceAll(s, "//", "_")
s = strings.ReplaceAll(s, "@", "at_")
s = strings.ReplaceAll(s, "/", "_")
s = strings.ReplaceAll(s, ":", "_")
s = strings.ReplaceAll(s, "__", "_")
return s, nil
}
// Append all elements to the slice if not already present in the slice.
func appendUnique(slice []string, elems ...string) []string {
for _, elem := range elems {
if !util.In(elem, slice) {
slice = append(slice, elem)
}
}
return slice
}
// Retrieve (if present) a slice of string attribute values from the given
// rule and attribute name. A nil slice will be returned if the attribute
// does not exist in the rule. A slice of strings (possibly empty) will be
// returned if the attribute is empty. An error will be returned if the
// attribute is not a list type.
func getRuleStringArrayAttribute(r *build.Rule, name string) ([]string, error) {
for _, attrib := range r.Attribute {
if attrib.GetName() != name {
continue
}
if attrib.GetType() != build.Attribute_LABEL_LIST &&
attrib.GetType() != build.Attribute_STRING_LIST {
return nil, skerr.Fmt(`%s in rule %q is not a list`, name, r.GetName())
}
return attrib.GetStringListValue(), nil
}
return nil, nil
}
// Given an input rule target return the workspace relative file path.
// For example, an input of `//src/core:source.cpp` will return
// `src/core/source.cpp`.
func getFilePathFromFileTarget(target string) (string, error) {
_, path, t, err := parseRule(target)
if err != nil {
return "", skerr.Wrap(err)
}
if !isFileTarget(target) {
return "", skerr.Fmt("Target %q is not a file target.", target)
}
return filepath.Join(strings.TrimPrefix(path, "/"), t), nil
}