Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion cli/adopt/src/scaffold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,53 @@ pub fn plan_init_files_at(root: &Path, module_name: &str) -> Result<Vec<Scaffold
} else {
DEFAULT_BRANCH_FALLBACK.to_owned()
};
plan_init_files_full(module_name, &branch)
let mut files = plan_init_files_full(module_name, &branch)?;
if root.is_dir() && needs_empty_ruff_config(root) {
files.push(ScaffoldFile {
path: "ruff.toml".to_owned(),
content: String::new(),
kind: FileKind::WriteOnce,
});
}
Ok(files)
}

/// Whether `root` has Python sources and no ruff config anywhere.
fn needs_empty_ruff_config(root: &Path) -> bool {
let mut python = false;
let mut pending = vec![root.to_path_buf()];
while let Some(dir) = pending.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
let skip = name.starts_with('.')
|| name.starts_with("bazel-")
|| matches!(name.as_str(), "node_modules" | "target" | "site-packages");
if !skip {
pending.push(entry.path());
}
} else if name == ".ruff.toml" || name == "ruff.toml" {
return false;
} else if name == "pyproject.toml" {
let text = std::fs::read_to_string(entry.path()).unwrap_or_default();
let has_ruff = text
.parse::<toml::Table>()
.is_ok_and(|table| table.get("tool").and_then(|t| t.get("ruff")).is_some());
if has_ruff {
return false;
}
} else if name.ends_with(".py") {
python = true;
}
}
}
python
}

fn plan_init_files_full(module_name: &str, branch: &str) -> Result<Vec<ScaffoldFile>, AdoptError> {
Expand Down Expand Up @@ -881,6 +927,28 @@ mod tests {
);
}

#[test]
fn empty_ruff_config_only_when_python_has_none() {
let root = std::env::temp_dir().join(format!("dx-adopt-ruff-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("src")).expect("temp dir");
assert!(!super::needs_empty_ruff_config(&root));
std::fs::write(root.join("src/app.py"), "").expect("source");
assert!(super::needs_empty_ruff_config(&root));
std::fs::write(root.join("pyproject.toml"), "[project]\nname = \"a\"\n").expect("toml");
assert!(super::needs_empty_ruff_config(&root));
std::fs::write(
root.join("pyproject.toml"),
"[tool.ruff]\nline-length = 100\n",
)
.expect("toml");
assert!(!super::needs_empty_ruff_config(&root));
std::fs::remove_file(root.join("pyproject.toml")).expect("remove");
std::fs::write(root.join("src/.ruff.toml"), "").expect("ruff");
assert!(!super::needs_empty_ruff_config(&root));
let _ = std::fs::remove_dir_all(&root);
}

#[test]
fn caller_leaves_reusable_defaults_out() {
let caller = render_consumer_caller();
Expand Down
3 changes: 3 additions & 0 deletions gazelle/dispatch/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ var conformanceRows = map[string]conformanceRow{
kind: "python_library", target: "demo", defs: "python/rules:defs.bzl",
symbols: []string{"python_library", "python_test", "python_binary"},
inert: []string{"notes.txt", "orphan.pyi"},
extraLoads: map[string][]string{
"@rules_dx//quality:native_config.bzl": {"ruff_config"},
},
},
"ruby": {
files: map[string]string{"demo.rb": "module Demo\nend\n"},
Expand Down
5 changes: 5 additions & 0 deletions gazelle/python/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ go_library(
"lang.go",
"naming.go",
"parser.go",
"ruff.go",
"stdlib.go",
],
importpath = "github.com/devhusdk/rules_dx/gazelle/python",
Expand All @@ -20,6 +21,8 @@ go_library(
"//gazelle/common/treesitter",
"@com_github_odvcencio_gotreesitter//:gotreesitter",
"@com_github_odvcencio_gotreesitter//grammars/python",
"@com_github_pelletier_go_toml_v2//:go-toml",
"@gazelle//config",
"@gazelle//language",
"@gazelle//v2/rule",
],
Expand Down Expand Up @@ -62,7 +65,9 @@ real_source_target(
aspect_hints = ["//quality:corpus_vale_config"],
markdown_srcs = [
"testdata/entries/README.md",
"testdata/pyproject_no_ruff/README.md",
"testdata/pytest_plugins/README.md",
"testdata/ruff_pyproject/README.md",
"testdata/source_only/README.md",
],
tags = ["corpus"],
Expand Down
42 changes: 33 additions & 9 deletions gazelle/python/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var pythonKinds = map[string]rule.KindInfo{
libraryKind: kindInfo(),
testKind: kindInfo(),
binaryKind: binaryKindInfo(),
ruffKind: ruffKindInfo(),
}

func kindInfo() rule.KindInfo {
Expand All @@ -30,19 +31,29 @@ func kindInfo() rule.KindInfo {
NonEmptyAttrs: map[string]bool{"srcs": true},
MergeableAttrs: map[string]bool{
"srcs": true,
"imports": true,
"deps": true,
"imports": true,
"deps": true,
"aspect_hints": true,
},
ResolveAttrs: map[string]bool{"deps": true},
}
}

func ruffKindInfo() rule.KindInfo {
return rule.KindInfo{
MatchAttrs: []string{"src"},
NonEmptyAttrs: map[string]bool{"src": true},
MergeableAttrs: map[string]bool{"src": true},
}
}

func binaryKindInfo() rule.KindInfo {
return rule.KindInfo{
MatchAttrs: []string{"main"},
MergeableAttrs: map[string]bool{
"imports": true,
"deps": true,
"imports": true,
"deps": true,
"aspect_hints": true,
},
ResolveAttrs: map[string]bool{"deps": true},
}
Expand All @@ -54,7 +65,9 @@ func newLang() *common.SingleLang {
return common.NewSingleLang(common.SingleLangSpec{
LanguageName: languageName,
LibraryKind: libraryKind,
ExtraKinds: []string{testKind, binaryKind},
ExtraKinds: []string{testKind, binaryKind, ruffKind},
KindDefs: map[string]string{ruffKind: nativeDefs},
ConfigureExt: configureRuff,
Kinds: pythonKinds,
DefsPackage: "python/rules:defs.bzl",
ImportAttr: "srcs",
Expand All @@ -79,8 +92,14 @@ func generateRules(args language.GenerateArgs, _ map[string]rule.KindInfo, rep c
}
}
sort.Strings(sources)
ruff := ruffAt(args.Config)
var result language.GenerateResult
if ruff != nil && ruff.rel == args.Rel {
result.Gen = append(result.Gen, ruffRule(ruff, args.Rel))
result.Imports = append(result.Imports, common.WrapImportSet(nil))
}
if len(sources) == 0 {
return mergeStale(args.File, language.GenerateResult{})
return mergeStale(args.File, result)
}

type plan struct {
Expand Down Expand Up @@ -140,7 +159,6 @@ func generateRules(args language.GenerateArgs, _ map[string]rule.KindInfo, rep c
}

plain := common.CustomRunnerTests(args.File, args.OtherGen, testKind, "pytest")
var result language.GenerateResult
for _, p := range plans {
kind := libraryKind
if p.test {
Expand All @@ -167,8 +185,14 @@ func generateRules(args language.GenerateArgs, _ map[string]rule.KindInfo, rep c
result.Imports = append(result.Imports, common.WrapImportSet(nil))
}
}
if isFixturePath(args.Rel) {
for _, r := range result.Gen {
for _, r := range result.Gen {
if r.Kind() == ruffKind {
continue
}
if hints := withRuffHint(existingRule(args.File, r.Kind(), r.Name()), ruff, args.Rel); len(hints) > 0 {
r.SetAttr("aspect_hints", hints)
}
if isFixturePath(args.Rel) {
r.SetAttr("testonly", true)
}
}
Expand Down
103 changes: 103 additions & 0 deletions gazelle/python/ruff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package python

import (
"os"
"path/filepath"

"github.com/bazel-contrib/bazel-gazelle/v2/rule"
"github.com/bazelbuild/bazel-gazelle/config"
"github.com/devhusdk/rules_dx/gazelle/common"
"github.com/pelletier/go-toml/v2"
)

const (
ruffKind = "ruff_config"
ruffName = "ruff_config"
ruffExt = "python_ruff"
nativeDefs = "quality:native_config.bzl"
)

// ruffState is the nearest ruff config at or above a directory.
type ruffState struct {
rel string
file string
}

func (s *ruffState) label() string { return "//" + s.rel + ":" + ruffName }

func ruffFile(dir string) string {
for _, name := range []string{".ruff.toml", "ruff.toml"} {
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
return name
}
}
content, err := os.ReadFile(filepath.Join(dir, "pyproject.toml"))
if err != nil {
return ""
}
var raw struct {
Tool map[string]any `toml:"tool"`
}
if toml.Unmarshal(common.StripBOM(content), &raw) != nil {
return ""
}
if _, ok := raw.Tool["ruff"]; ok {
return "pyproject.toml"
}
return ""
}

func configureRuff(c *config.Config, rel string, _ *rule.File, _ common.Reporter) {
if file := ruffFile(filepath.Join(c.RepoRoot, filepath.FromSlash(rel))); file != "" {
c.Exts[ruffExt] = &ruffState{rel: rel, file: file}
}
}

func ruffAt(c *config.Config) *ruffState {
if c == nil {
return nil
}
state, _ := c.Exts[ruffExt].(*ruffState)
return state
}

func ruffRule(state *ruffState, rel string) *rule.Rule {
r := rule.NewRule(ruffKind, ruffName)
r.SetAttr("src", state.file)
r.SetAttr("visibility", []string{"//" + rel + ":__subpackages__"})
return r
}

// withRuffHint returns the user's aspect_hints plus the ruff config label.
func withRuffHint(existing *rule.Rule, state *ruffState, rel string) []string {
var hints []string
if existing != nil {
hints = existing.AttrStrings("aspect_hints")
}
if state == nil {
return hints
}
label := state.label()
if state.rel == rel {
label = ":" + ruffName
}
for _, hint := range hints {
if hint == label || hint == state.label() {
return hints
}
}
return append(hints, label)
}

func existingRule(file *rule.File, kind, name string) *rule.Rule {
if file == nil {
return nil
}
for _, r := range file.Rules {
if r.Name() == name && r.Kind() == kind {
return r
}
}
return nil
}

4 changes: 4 additions & 0 deletions gazelle/python/testdata/pyproject_no_ruff/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pyproject.toml Without Ruff

Verifies a `pyproject.toml` with no `[tool.ruff]` table generates no `ruff_config`
and no `aspect_hints`.
Empty file.
7 changes: 7 additions & 0 deletions gazelle/python/testdata/pyproject_no_ruff/pkg/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("@rules_dx//python/rules:defs.bzl", "python_library")

python_library(
name = "lib",
srcs = ["lib.py"],
imports = ["."],
)
1 change: 1 addition & 0 deletions gazelle/python/testdata/pyproject_no_ruff/pkg/lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VALUE = 1
2 changes: 2 additions & 0 deletions gazelle/python/testdata/pyproject_no_ruff/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[project]
name = "demo"
7 changes: 7 additions & 0 deletions gazelle/python/testdata/ruff_pyproject/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("@rules_dx//quality:native_config.bzl", "ruff_config")

ruff_config(
name = "ruff_config",
src = "pyproject.toml",
visibility = ["//:__subpackages__"],
)
4 changes: 4 additions & 0 deletions gazelle/python/testdata/ruff_pyproject/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ruff Config From pyproject.toml

Verifies a `[tool.ruff]` table yields a `ruff_config` that generated rules hint at,
keeping the hints a user already wrote.
Empty file.
8 changes: 8 additions & 0 deletions gazelle/python/testdata/ruff_pyproject/pkg/BUILD.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("@rules_dx//python/rules:defs.bzl", "python_library")

python_library(
name = "lib",
srcs = ["lib.py"],
aspect_hints = ["//:user_hint"],
imports = ["."],
)
19 changes: 19 additions & 0 deletions gazelle/python/testdata/ruff_pyproject/pkg/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("@rules_dx//python/rules:defs.bzl", "python_library", "python_test")

python_library(
name = "lib",
srcs = ["lib.py"],
aspect_hints = [
"//:user_hint",
"//:ruff_config",
],
imports = ["."],
)

python_test(
name = "lib_test",
srcs = ["lib_test.py"],
aspect_hints = ["//:ruff_config"],
imports = ["."],
deps = [":lib"],
)
1 change: 1 addition & 0 deletions gazelle/python/testdata/ruff_pyproject/pkg/lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VALUE = 1
1 change: 1 addition & 0 deletions gazelle/python/testdata/ruff_pyproject/pkg/lib_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import lib
2 changes: 2 additions & 0 deletions gazelle/python/testdata/ruff_pyproject/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tool.ruff]
line-length = 100
Loading