Skip to content
Open
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
28 changes: 28 additions & 0 deletions internal/agent/preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,34 @@ func TestWhyExcluded_ExtensionFilter(t *testing.T) {
},
expected: ExcludeNone,
},
{
name: "supported Starlark bzl extension",
diff: model.Diff{
NewPath: "rules/library.bzl",
},
expected: ExcludeNone,
},
{
name: "supported Starlark star extension",
diff: model.Diff{
NewPath: "config.star",
},
expected: ExcludeNone,
},
{
name: "supported Bazel extension",
diff: model.Diff{
NewPath: "MODULE.bazel",
},
expected: ExcludeNone,
},
{
name: "extensionless Starlark build file",
diff: model.Diff{
NewPath: "pkg/BUILD",
},
expected: ExcludeNone,
},
{
name: "file without extension",
diff: model.Diff{
Expand Down
6 changes: 6 additions & 0 deletions internal/config/allowlist/allowed_ext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ func TestIsAllowedExt(t *testing.T) {
{".THRIFT", true},
{".capnp", true},
{".CAPNP", true},
{".bzl", true},
{".BZL", true},
{".bazel", true},
{".BAZEL", true},
{".star", true},
{".STAR", true},
{".sol", true},
{".SOL", true},
{".vy", true},
Expand Down
3 changes: 3 additions & 0 deletions internal/config/allowlist/supported_file_types.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
".zig",
".thrift",
".capnp",
".bzl",
".bazel",
".star",
".sol",
".vy"
]
226 changes: 226 additions & 0 deletions internal/config/rules/rule_docs/starlark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Starlark Code Review Guide

> Guidance for AI reviewers. Starlark is a deterministic, hermetic configuration
> language (a dialect of Python) used primarily by Bazel. It is **not** Python:
> the syntax is a strict subset, but the semantics differ, and many Python
> features are intentionally absent. Unless noted, this guide assumes Bazel's
> Starlark dialect.

## Obvious Typos or Spelling Errors

- Spelling errors in variable, function, provider, rule, attribute, and target
names **at their declaration sites**; do not report spelling errors at call
sites.
- Spelling errors in `fail()` or `print()` messages, comments, and docstrings
that affect readability.
- Typos in `load()` symbol names and label strings (these can cause load or
analysis failures).
- Misspelled attribute names in rule or macro calls (for example `sorce` instead
of `srcs`), because they are usually a runtime error or are silently ignored.

## Determinism and Purity

Starlark is designed to be deterministic and side-effect free. Review code that
could break hermeticity.

**Key checks:**

- No reliance on wall-clock time, randomness, host filesystem state,
environment variables, or network access in ordinary `.bzl` or `BUILD` code.
- Rule implementations must produce the same result for the same inputs and
`ctx`.
- Repository rules may perform network or filesystem access, but their result
must still be deterministic for a given set of inputs.
- No `print()` in production code (see Error Reporting).

## Recursion and Termination

**Key checks:**

- No direct or mutual recursion; Starlark rejects recursion at runtime.
- No `while` loops; Starlark has only `for` loops over finite sequences.
- Starlark `for` loops always terminate because they iterate over finite
collections; `break` and `continue` cannot make a loop non-terminating, so
flag excessive iteration cost instead (see Depsets and Efficiency).
- Avoid deeply nested comprehensions that make termination or cost hard to
reason about.

## Frozen Values and Mutation

Starlark favors immutability. Only `list` and `dict` are mutable, and only
inside the evaluation context that created them. Bazel 8.1 also added a
mutable core `set` type, so `set` values are subject to the same rules.

**Key checks:**

- Do not mutate a value loaded from another `.bzl` file, a rule attribute, or a
value returned by a rule or provider; those values are frozen.
- Do not mutate a collection while iterating over it (a dynamic error).
- Avoid mutable default arguments; defaults are evaluated once and shared
across calls.
- Do not rely on object identity; Starlark has no `is` operator.

**Example:**

```python
# Bad: mutates a frozen value
# other.bzl
load(":defs.bzl", "registry")

def use():
registry.append("x") # runtime error: registry is frozen

# Good: the function owns the mutable value; loaded module globals are frozen
# after evaluation and cannot be mutated from another file
def register(name):
registry = []
registry.append(name)
return registry
```

```python
# Bad: mutable default argument is shared between calls
def add(name, names=[]):
names.append(name)
return names

# Good: use None and create the list inside
def add(name, names=None):
names = names or []
names.append(name)
return names
```

## Safe Data Handling (Python Differences)

**Key checks:**

- Ordered comparisons (`<`, `<=`, `>`, `>=`) are defined only within a single
value type; `==` and `!=` may compare across types.
- Strings are not iterable; use `s.elems()` to iterate over characters.
- Dictionary literals cannot contain duplicate keys.
- Dictionary iteration order is deterministic.
- There is no implicit string concatenation; use `+`.
- In Bazel, `int` is limited to 32-bit signed values and overflow is an error.
- Use `struct()` instead of `class`; use `provider()` for rule outputs.
- Define named functions with `def`; Bazel's dialect has no `lambda`.
- There is no `import`; use `load()`, placed at the top of the file.
- There is no `global` or `nonlocal`.

**Example:**

```python
# Bad: Python-style exception handling does not exist
def lookup(data, key):
try:
return data[key]
except KeyError:
return None

# Good: check first and fail with a clear message
def lookup(data, key):
if key not in data:
fail("missing key %r in %r" % (key, data))
return data[key]
```

## Depsets and Efficiency

**Key checks:**

- Aggregate transitive information with `depset`; do not flatten transitive
depsets with `to_list()` inside a loop, which is O(N^2) over the dependency
graph.
- Avoid `depset.to_list()` except for debugging.
- Build a depset in a single call; do not construct depsets inside a loop.
- Use `ctx.actions.args()` for command lines instead of flattening depsets or
building giant strings.
- Pass depsets directly as action inputs; do not convert them to lists.

**Example:**

```python
# Bad: repeatedly flattening depsets with to_list() is O(N^2)
files = []
for dep in ctx.attr.deps:
files += dep[MyProvider].files.to_list()

# Good: keep the depsets nested and merge once
files = depset(
direct = ctx.files.srcs,
transitive = [dep[MyProvider].files for dep in ctx.attr.deps],
)
```

## Error Reporting

**Key checks:**

- Use `fail()` to report validation errors and unrecoverable conditions.
- `print()` is for debugging only; it must not appear in production code, or it
must be gated behind a hardcoded `DEBUG` flag.
- Error messages should name the offending value and the expected condition.
- Do not silently swallow errors, and do not return `None` where the caller
expects a value without documenting that behavior.

## Loads, Symbols, and Encapsulation

**Key checks:**

- Use `load()` (not `import`), with all loads at the top of the file.
- Minimize exported symbols per `.bzl` file; mark file-private top-level
symbols with a leading `_`.
- Remove unused loads, variables, and parameters.
- Prefer rules over macros; macros expand before graph analysis and hide the
real build graph.
- Flag macro-generated helper targets that are not prefixed with the main
target's `name` (for example `name_bar` or `_name_bar`).
- Flag helper targets that lack `visibility = ["//visibility:private"]` or a
`manual` tag; without `manual`, wildcard patterns such as `:all` and `:...`
expand them.
- Keep `BUILD` files simple and explicit (DAMP over DRY); avoid top-level list
comprehensions and shared `deps` variables.

## Naming Conventions

**Requirements:**

- Variables, functions, rules, attributes, and targets use `snake_case`.
- Constants use `UPPER_SNAKE_CASE`.
- File-private top-level symbols start with `_`.
- Rule and macro implementation functions are named `_<name>_impl`.
- Rule names are nouns describing the produced artifact; common suffixes
include `_library`, `_binary`, `_test`, and `_import`.
- Common attributes follow standard names and types: `srcs` is a `label_list`
(typically source files), `deps` is a `label_list` (typically *not* files),
`data` is a `label_list` (runtime/test data), and `runtime_deps` is a
`label_list` for dependencies not needed at compile time.
- A macro's `name` parameter is first, and generated targets are prefixed with
`name` plus `_`.
- Call rules and macros with keyword arguments, spaces around `=`, and `name`
first.

## Documentation

- Add a docstring at the top of each `.bzl` file and for each public function.
- Document rules, aspects, providers, and attributes using the `doc` argument.
- Rules pass data through well-defined providers; flag provider fields that
are undeclared or undocumented.
- Use comments to explain *why*, not what the code does.

## Formatting

- Formatting must match `buildifier`; do not nitpick formatting it would
produce.
- Prefer double quotes for strings.
- Use four-space indentation.
- Use a single blank line between top-level definitions.

## Do Not Report

- Code that already avoids Python-isms correctly, such as using `==` instead of
`is`, or `fail()` instead of `try`/`except`.
- Formatting that `buildifier` is responsible for.
- Spelling errors at call sites rather than declaration sites.
- The absence of features Starlark intentionally omits (`class`, `while`,
`import`, recursion, and so on).
1 change: 1 addition & 0 deletions internal/config/rules/system_rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"**/*.zig": "zig.md",
"**/*.thrift": "thrift.md",
"**/*.capnp": "capnp.md",
"**/{BUILD,WORKSPACE,*.bzl,*.bazel,*.star}": "starlark.md",
"**/*.m": "matlab.md",
"**/*.sol": "solidity.md",
"**/*.vy": "vyper.md"
Expand Down
5 changes: 5 additions & 0 deletions internal/config/rules/system_rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ func TestResolve_DefaultRules(t *testing.T) {
{"if/common.thrift", "Field IDs and Wire Compatibility"},
{"schema/addressbook.capnp", "Ordinals and Wire Compatibility"},
{"src/rpc.capnp", "Ordinals and Wire Compatibility"},
{"rules/library.bzl", "Starlark Code Review Guide"},
{"config.star", "Starlark Code Review Guide"},
{"MODULE.bazel", "Starlark Code Review Guide"},
{"pkg/BUILD", "Starlark Code Review Guide"},
{"WORKSPACE", "Starlark Code Review Guide"},
{"Models/main.m", "Indexing, Shapes, and Implicit Expansion"},
{"src/Counter.sol", "Checks-Effects-Interactions"},
{"contracts/Vault.sol", "Delegatecall and Proxy Upgradeability"},
Expand Down
5 changes: 5 additions & 0 deletions internal/scan/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ func TestWhyExcluded_AllBranches(t *testing.T) {
item: model.ScanItem{Path: "main.go", Content: "x"},
want: model.ExcludeNone,
},
{
name: "Starlark file passes",
item: model.ScanItem{Path: "rules/library.bzl", Content: "x"},
want: model.ExcludeNone,
},
}

for _, tt := range tests {
Expand Down
4 changes: 2 additions & 2 deletions internal/tool/file_find.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (p *FileFindProvider) listWalkFiles(ctx context.Context) ([]string, error)
// shouldSkipFile returns true if a git ls-files output path should be skipped.
// Keeps only widely useful files (those with recognizable extensions).
func shouldSkipFile(path string) bool {
// Keep extensionless build/config files like Makefile, Dockerfile, LICENSE
// Keep well-known extensionless build/config files.
base := path
if idx := strings.LastIndex(path, "/"); idx != -1 {
base = path[idx+1:]
Expand All @@ -211,7 +211,7 @@ func shouldSkipFile(path string) bool {
if !hasExt {
// Allow well-known extensionless files
switch base {
case "Makefile", "Dockerfile", "LICENSE", "Vagrantfile", "Containerfile":
case "Makefile", "Dockerfile", "LICENSE", "Vagrantfile", "Containerfile", "BUILD", "WORKSPACE":
return false
}
return true // skip other extensionless files
Expand Down
29 changes: 29 additions & 0 deletions internal/tool/file_find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func setupFileFindRepo(t *testing.T) string {
write("Makefile", "all:\n")
write("Dockerfile", "FROM scratch\n")
write("LICENSE", "MIT\n")
write("pkg/BUILD", "filegroup(name = \"all\")\n")
write("WORKSPACE", "workspace(name = \"example\")\n")
write("data_binary", "binary\n")

run("git", "add", ".")
Expand All @@ -138,6 +140,30 @@ func TestFileFind_GitRepo_WorkspaceMode(t *testing.T) {
}
}

func TestFileFind_GitRepo_StarlarkBuildFiles(t *testing.T) {
dir := setupFileFindRepo(t)
p := NewFileFind(&FileReader{RepoDir: dir, Mode: ModeWorkspace})

tests := []struct {
query string
want string
}{
{query: "BUILD", want: "pkg/BUILD"},
{query: "WORKSPACE", want: "WORKSPACE"},
}
for _, tt := range tests {
t.Run(tt.query, func(t *testing.T) {
got, err := p.Execute(context.Background(), map[string]any{"query_name": tt.query})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(got, tt.want) {
t.Errorf("expected %s, got: %s", tt.want, got)
}
})
}
}

func TestFileFind_GitRepo_CommitMode(t *testing.T) {
dir := setupFileFindRepo(t)
cmd := exec.Command("git", "rev-parse", "HEAD")
Expand Down Expand Up @@ -287,6 +313,9 @@ func TestShouldSkipFile(t *testing.T) {
{"LICENSE", false},
{"Vagrantfile", false},
{"Containerfile", false},
{"BUILD", false},
{"pkg/BUILD", false},
{"WORKSPACE", false},
{"some_binary", true},
{"dir/unknown_file", true},
}
Expand Down
1 change: 1 addition & 0 deletions pages/src/content/docs/en/review-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ matching order:
| `**/*.{jsonnet,libsonnet}` | `jsonnet.md` — Jsonnet configuration templates and libraries. |
| `**/*.thrift` | `thrift.md` — Apache Thrift IDL wire compatibility. |
| `**/*.capnp` | `capnp.md` — Cap'n Proto schema wire compatibility. |
| `**/{BUILD,WORKSPACE,*.bzl,*.bazel,*.star}` | `starlark.md` — Starlark and Bazel build, macro, and rule design. |
| `**/*.m` | `matlab.md` (or `objc.md` via [content sniffing](#content-sniffing-for-m-files)) |
| `**/*.sol` | `solidity.md` — Solidity smart contracts. |
| `**/*.vy` | `vyper.md` — Vyper smart contracts. |
Expand Down
Loading
Loading