• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2023 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
15package python
16
17import (
18	"fmt"
19	"log"
20	"os"
21	"path/filepath"
22	"strings"
23
24	"github.com/bazelbuild/bazel-gazelle/config"
25	"github.com/bazelbuild/bazel-gazelle/label"
26	"github.com/bazelbuild/bazel-gazelle/repo"
27	"github.com/bazelbuild/bazel-gazelle/resolve"
28	"github.com/bazelbuild/bazel-gazelle/rule"
29	bzl "github.com/bazelbuild/buildtools/build"
30	"github.com/emirpasic/gods/sets/treeset"
31	godsutils "github.com/emirpasic/gods/utils"
32
33	"github.com/bazelbuild/rules_python/gazelle/pythonconfig"
34)
35
36const languageName = "py"
37
38const (
39	// resolvedDepsKey is the attribute key used to pass dependencies that don't
40	// need to be resolved by the dependency resolver in the Resolver step.
41	resolvedDepsKey = "_gazelle_python_resolved_deps"
42)
43
44// Resolver satisfies the resolve.Resolver interface. It resolves dependencies
45// in rules generated by this extension.
46type Resolver struct{}
47
48// Name returns the name of the language. This is the prefix of the kinds of
49// rules generated. E.g. py_library and py_binary.
50func (*Resolver) Name() string { return languageName }
51
52// Imports returns a list of ImportSpecs that can be used to import the rule
53// r. This is used to populate RuleIndex.
54//
55// If nil is returned, the rule will not be indexed. If any non-nil slice is
56// returned, including an empty slice, the rule will be indexed.
57func (py *Resolver) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
58	cfgs := c.Exts[languageName].(pythonconfig.Configs)
59	cfg := cfgs[f.Pkg]
60	srcs := r.AttrStrings("srcs")
61	provides := make([]resolve.ImportSpec, 0, len(srcs)+1)
62	for _, src := range srcs {
63		ext := filepath.Ext(src)
64		if ext == ".py" {
65			pythonProjectRoot := cfg.PythonProjectRoot()
66			provide := importSpecFromSrc(pythonProjectRoot, f.Pkg, src)
67			provides = append(provides, provide)
68		}
69	}
70	if len(provides) == 0 {
71		return nil
72	}
73	return provides
74}
75
76// importSpecFromSrc determines the ImportSpec based on the target that contains the src so that
77// the target can be indexed for import statements that match the calculated src relative to the its
78// Python project root.
79func importSpecFromSrc(pythonProjectRoot, bzlPkg, src string) resolve.ImportSpec {
80	pythonPkgDir := filepath.Join(bzlPkg, filepath.Dir(src))
81	relPythonPkgDir, err := filepath.Rel(pythonProjectRoot, pythonPkgDir)
82	if err != nil {
83		panic(fmt.Errorf("unexpected failure: %v", err))
84	}
85	if relPythonPkgDir == "." {
86		relPythonPkgDir = ""
87	}
88	pythonPkg := strings.ReplaceAll(relPythonPkgDir, "/", ".")
89	filename := filepath.Base(src)
90	if filename == pyLibraryEntrypointFilename {
91		if pythonPkg != "" {
92			return resolve.ImportSpec{
93				Lang: languageName,
94				Imp:  pythonPkg,
95			}
96		}
97	}
98	moduleName := strings.TrimSuffix(filename, ".py")
99	var imp string
100	if pythonPkg == "" {
101		imp = moduleName
102	} else {
103		imp = fmt.Sprintf("%s.%s", pythonPkg, moduleName)
104	}
105	return resolve.ImportSpec{
106		Lang: languageName,
107		Imp:  imp,
108	}
109}
110
111// Embeds returns a list of labels of rules that the given rule embeds. If
112// a rule is embedded by another importable rule of the same language, only
113// the embedding rule will be indexed. The embedding rule will inherit
114// the imports of the embedded rule.
115func (py *Resolver) Embeds(r *rule.Rule, from label.Label) []label.Label {
116	// TODO(f0rmiga): implement.
117	return make([]label.Label, 0)
118}
119
120// Resolve translates imported libraries for a given rule into Bazel
121// dependencies. Information about imported libraries is returned for each
122// rule generated by language.GenerateRules in
123// language.GenerateResult.Imports. Resolve generates a "deps" attribute (or
124// the appropriate language-specific equivalent) for each import according to
125// language-specific rules and heuristics.
126func (py *Resolver) Resolve(
127	c *config.Config,
128	ix *resolve.RuleIndex,
129	rc *repo.RemoteCache,
130	r *rule.Rule,
131	modulesRaw interface{},
132	from label.Label,
133) {
134	// TODO(f0rmiga): may need to be defensive here once this Gazelle extension
135	// join with the main Gazelle binary with other rules. It may conflict with
136	// other generators that generate py_* targets.
137	deps := treeset.NewWith(godsutils.StringComparator)
138	if modulesRaw != nil {
139		cfgs := c.Exts[languageName].(pythonconfig.Configs)
140		cfg := cfgs[from.Pkg]
141		pythonProjectRoot := cfg.PythonProjectRoot()
142		modules := modulesRaw.(*treeset.Set)
143		it := modules.Iterator()
144		explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
145		hasFatalError := false
146	MODULES_LOOP:
147		for it.Next() {
148			mod := it.Value().(module)
149			moduleParts := strings.Split(mod.Name, ".")
150			possibleModules := []string{mod.Name}
151			for len(moduleParts) > 1 {
152				// Iterate back through the possible imports until
153				// a match is found.
154				// For example, "from foo.bar import baz" where bar is a variable, we should try
155				// `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py`
156				// in the directory `foo/bar`.
157				// Or, the import could be variable `bar` in file `foo/bar.py`.
158				// The import could also be from a standard module, e.g. `six.moves`, where
159				// the dependency is actually `six`.
160				moduleParts = moduleParts[:len(moduleParts)-1]
161				possibleModules = append(possibleModules, strings.Join(moduleParts, "."))
162			}
163			errs := []error{}
164		POSSIBLE_MODULE_LOOP:
165			for _, moduleName := range possibleModules {
166				imp := resolve.ImportSpec{Lang: languageName, Imp: moduleName}
167				if override, ok := resolve.FindRuleWithOverride(c, imp, languageName); ok {
168					if override.Repo == "" {
169						override.Repo = from.Repo
170					}
171					if !override.Equal(from) {
172						if override.Repo == from.Repo {
173							override.Repo = ""
174						}
175						dep := override.String()
176						deps.Add(dep)
177						if explainDependency == dep {
178							log.Printf("Explaining dependency (%s): "+
179								"in the target %q, the file %q imports %q at line %d, "+
180								"which resolves using the \"gazelle:resolve\" directive.\n",
181								explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
182						}
183						continue MODULES_LOOP
184					}
185				} else {
186					if dep, ok := cfg.FindThirdPartyDependency(moduleName); ok {
187						deps.Add(dep)
188						if explainDependency == dep {
189							log.Printf("Explaining dependency (%s): "+
190								"in the target %q, the file %q imports %q at line %d, "+
191								"which resolves from the third-party module %q from the wheel %q.\n",
192								explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber, mod.Name, dep)
193						}
194						continue MODULES_LOOP
195					} else {
196						matches := ix.FindRulesByImportWithConfig(c, imp, languageName)
197						if len(matches) == 0 {
198							// Check if the imported module is part of the standard library.
199							if isStd, err := isStdModule(module{Name: moduleName}); err != nil {
200								log.Println("Error checking if standard module: ", err)
201								hasFatalError = true
202								continue POSSIBLE_MODULE_LOOP
203							} else if isStd {
204								continue MODULES_LOOP
205							} else if cfg.ValidateImportStatements() {
206								err := fmt.Errorf(
207									"%[1]q at line %[2]d from %[3]q is an invalid dependency: possible solutions:\n"+
208										"\t1. Add it as a dependency in the requirements.txt file.\n"+
209										"\t2. Instruct Gazelle to resolve to a known dependency using the gazelle:resolve directive.\n"+
210										"\t3. Ignore it with a comment '# gazelle:ignore %[1]s' in the Python file.\n",
211									moduleName, mod.LineNumber, mod.Filepath,
212								)
213								errs = append(errs, err)
214								continue POSSIBLE_MODULE_LOOP
215							}
216						}
217						filteredMatches := make([]resolve.FindResult, 0, len(matches))
218						for _, match := range matches {
219							if match.IsSelfImport(from) {
220								// Prevent from adding itself as a dependency.
221								continue MODULES_LOOP
222							}
223							filteredMatches = append(filteredMatches, match)
224						}
225						if len(filteredMatches) == 0 {
226							continue POSSIBLE_MODULE_LOOP
227						}
228						if len(filteredMatches) > 1 {
229							sameRootMatches := make([]resolve.FindResult, 0, len(filteredMatches))
230							for _, match := range filteredMatches {
231								if strings.HasPrefix(match.Label.Pkg, pythonProjectRoot) {
232									sameRootMatches = append(sameRootMatches, match)
233								}
234							}
235							if len(sameRootMatches) != 1 {
236								err := fmt.Errorf(
237									"multiple targets (%s) may be imported with %q at line %d in %q "+
238										"- this must be fixed using the \"gazelle:resolve\" directive",
239									targetListFromResults(filteredMatches), moduleName, mod.LineNumber, mod.Filepath)
240								errs = append(errs, err)
241								continue POSSIBLE_MODULE_LOOP
242							}
243							filteredMatches = sameRootMatches
244						}
245						matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg)
246						dep := matchLabel.String()
247						deps.Add(dep)
248						if explainDependency == dep {
249							log.Printf("Explaining dependency (%s): "+
250								"in the target %q, the file %q imports %q at line %d, "+
251								"which resolves from the first-party indexed labels.\n",
252								explainDependency, from.String(), mod.Filepath, moduleName, mod.LineNumber)
253						}
254						continue MODULES_LOOP
255					}
256				}
257			} // End possible modules loop.
258			if len(errs) > 0 {
259				// If, after trying all possible modules, we still haven't found anything, error out.
260				joinedErrs := ""
261				for _, err := range errs {
262					joinedErrs = fmt.Sprintf("%s%s\n", joinedErrs, err)
263				}
264				log.Printf("ERROR: failed to validate dependencies for target %q: %v\n", from.String(), joinedErrs)
265				hasFatalError = true
266			}
267		}
268		if hasFatalError {
269			os.Exit(1)
270		}
271	}
272	resolvedDeps := r.PrivateAttr(resolvedDepsKey).(*treeset.Set)
273	if !resolvedDeps.Empty() {
274		it := resolvedDeps.Iterator()
275		for it.Next() {
276			deps.Add(it.Value())
277		}
278	}
279	if !deps.Empty() {
280		r.SetAttr("deps", convertDependencySetToExpr(deps))
281	}
282}
283
284// targetListFromResults returns a string with the human-readable list of
285// targets contained in the given results.
286func targetListFromResults(results []resolve.FindResult) string {
287	list := make([]string, len(results))
288	for i, result := range results {
289		list[i] = result.Label.String()
290	}
291	return strings.Join(list, ", ")
292}
293
294// convertDependencySetToExpr converts the given set of dependencies to an
295// expression to be used in the deps attribute.
296func convertDependencySetToExpr(set *treeset.Set) bzl.Expr {
297	deps := make([]bzl.Expr, set.Size())
298	it := set.Iterator()
299	for it.Next() {
300		dep := it.Value().(string)
301		deps[it.Index()] = &bzl.StringExpr{Value: dep}
302	}
303	return &bzl.ListExpr{List: deps}
304}
305