• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2022 Google LLC
2//
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5
6package exporter
7
8import (
9	"fmt"
10	"path/filepath"
11	"regexp"
12	"strconv"
13	"strings"
14
15	"go.skia.org/infra/go/skerr"
16	"go.skia.org/infra/go/util"
17	"go.skia.org/skia/bazel/exporter/build_proto/analysis_v2"
18	"go.skia.org/skia/bazel/exporter/build_proto/build"
19)
20
21const (
22	ruleOnlyRepoPattern = `^(@\w+)$`
23	rulePattern         = `^(?P<repo>@[^/]+)?/(?P<path>[^:]+)(?P<target>:[^:]+)?$`
24	locationPattern     = `^(?P<path>[^:]+):(?P<line>[^:]+):(?P<pos>[^:]+)$`
25)
26
27var (
28	ruleOnlyRepoRegex = regexp.MustCompile(ruleOnlyRepoPattern)
29	ruleRegex         = regexp.MustCompile(rulePattern)
30	locRegex          = regexp.MustCompile(locationPattern)
31)
32
33// Return true if the given rule name represents an external repository.
34func isExternalRule(name string) bool {
35	return name[0] == '@'
36}
37
38// Given a Bazel rule name find that rule from within the
39// query results. Returns nil if the given rule is not present.
40func findRule(qr *analysis_v2.CqueryResult, name string) *build.Rule {
41	for _, result := range qr.GetResults() {
42		r := result.GetTarget().GetRule()
43		if r.GetName() == name {
44			return r
45		}
46	}
47	return nil
48}
49
50// Parse a rule into its constituent parts.
51// https://docs.bazel.build/versions/main/guide.html#specifying-targets-to-build
52//
53// For example, the input rule `//foo/bar:baz` will return:
54//
55//	repo: ""
56//	path: "/foo/bar"
57//	target: "baz"
58func parseRule(rule string) (repo string, path string, target string, err error) {
59	match := ruleOnlyRepoRegex.FindStringSubmatch(rule)
60	if match != nil {
61		return match[1], "/", strings.TrimPrefix(match[1], "@"), nil
62	}
63
64	match = ruleRegex.FindStringSubmatch(rule)
65	if match == nil {
66		return "", "", "", skerr.Fmt(`Unable to match rule %q`, rule)
67	}
68
69	if len(match[3]) > 0 {
70		target = strings.TrimPrefix(match[3], ":")
71	} else {
72		// No explicit target, so use directory name as default target.
73		target = filepath.Base(match[2])
74	}
75
76	return match[1], match[2], target, nil
77}
78
79// Parse a file location into its three constituent parts.
80//
81// A location is of the form:
82//
83//	/full/path/to/BUILD.bazel:33:20
84func parseLocation(location string) (path string, line int, pos int, err error) {
85	match := locRegex.FindStringSubmatch(location)
86	if match == nil {
87		return "", 0, 0, skerr.Fmt(`unable to match file location %q`, location)
88	}
89	path = match[1]
90	line, err = strconv.Atoi(match[2])
91	if err != nil {
92		return "", 0, 0, skerr.Fmt(`unable to parse line no. %q`, match[2])
93	}
94	pos, err = strconv.Atoi(match[3])
95	if err != nil {
96		return "", 0, 0, skerr.Fmt(`unable to parse pos. %q`, match[3])
97	}
98	return path, line, pos, nil
99}
100
101// Return the directory containing the file in the location string.
102func getLocationDir(location string) (string, error) {
103	filePath, _, _, err := parseLocation(location)
104	if err != nil {
105		return "", skerr.Wrap(err)
106	}
107	return filepath.Dir(filePath), nil
108}
109
110func makeCanonicalRuleName(bazelRuleName string) (string, error) {
111	repo, path, target, err := parseRule(bazelRuleName)
112	if err != nil {
113		return "", skerr.Wrap(err)
114	}
115	return fmt.Sprintf("%s/%s:%s", repo, path, target), nil
116}
117
118// Determine if a target refers to a file, or a rule. target is of
119// the form:
120//
121// file: //include/private:SingleOwner.h
122// rule: //bazel/common_config_settings:has_gpu_backend
123func isFileTarget(target string) bool {
124	_, _, target, err := parseRule(target)
125	if err != nil {
126		return false
127	}
128	return strings.Contains(target, ".")
129}
130
131// Create a string that uniquely identifies the rule and can be used
132// in the exported project file as a valid name.
133func getRuleSimpleName(bazelRuleName string) (string, error) {
134	s, err := makeCanonicalRuleName(bazelRuleName)
135	if err != nil {
136		return "", skerr.Wrap(err)
137	}
138	s = strings.TrimPrefix(s, "//:")
139	s = strings.TrimPrefix(s, "//")
140	s = strings.ReplaceAll(s, "//", "_")
141	s = strings.ReplaceAll(s, "@", "at_")
142	s = strings.ReplaceAll(s, "/", "_")
143	s = strings.ReplaceAll(s, ":", "_")
144	s = strings.ReplaceAll(s, "__", "_")
145	return s, nil
146}
147
148// Append all elements to the slice if not already present in the slice.
149func appendUnique(slice []string, elems ...string) []string {
150	for _, elem := range elems {
151		if !util.In(elem, slice) {
152			slice = append(slice, elem)
153		}
154	}
155	return slice
156}
157
158// Retrieve (if present) a slice of string attribute values from the given
159// rule and attribute name. A nil slice will be returned if the attribute
160// does not exist in the rule. A slice of strings (possibly empty) will be
161// returned if the attribute is empty. An error will be returned if the
162// attribute is not a list type.
163func getRuleStringArrayAttribute(r *build.Rule, name string) ([]string, error) {
164	for _, attrib := range r.Attribute {
165		if attrib.GetName() != name {
166			continue
167		}
168		if attrib.GetType() != build.Attribute_LABEL_LIST &&
169			attrib.GetType() != build.Attribute_STRING_LIST {
170			return nil, skerr.Fmt(`%s in rule %q is not a list`, name, r.GetName())
171		}
172		return attrib.GetStringListValue(), nil
173	}
174	return nil, nil
175}
176
177// Given an input rule target return the workspace relative file path.
178// For example, an input of `//src/core:source.cpp` will return
179// `src/core/source.cpp`.
180func getFilePathFromFileTarget(target string) (string, error) {
181	_, path, t, err := parseRule(target)
182	if err != nil {
183		return "", skerr.Wrap(err)
184	}
185	if !isFileTarget(target) {
186		return "", skerr.Fmt("Target %q is not a file target.", target)
187	}
188	return filepath.Join(strings.TrimPrefix(path, "/"), t), nil
189}
190