Skip to content

Commit 7f978ff

Browse files
committed
Document boolean and dict attribute constraints in attr-policy schema.
Show how policies like local = True and execution_requirements entries are encoded via forbidValues and forbidDictEntries.
1 parent a3bbea8 commit 7f978ff

1 file changed

Lines changed: 71 additions & 19 deletions

File tree

docs/attribute-policy-and-test-tuning-design.md

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ whatever CI/automation invokes the tool.
7777

7878
A **single generalized** warning, `attr-policy`, driven entirely by config. It covers
7979
the initial asks (eternal-timeout allow-list, no `exclusive` on tests) and any future
80-
"attribute constrained on rule-kind unless allow-listed" rule **without new Go code**.
80+
"attribute constrained on rule-kind unless allow-listed" rule **without new Go code**
81+
including boolean literals (`local = True`) and dict attributes
82+
(`execution_requirements = {"no-cache": "1"}`).
8183

8284
Rationale for one generalized warning vs. many hardcoded ones: buildifier warnings are
8385
individually toggleable by name, but the *policy content* here is site-specific and
@@ -104,6 +106,21 @@ generic and lets each repo express its own policy.
104106
"attr": "tags",
105107
"forbidListItems": ["exclusive"], // list-membership constraint
106108
"allowlist": []
109+
},
110+
{
111+
"name": "no-local-tests",
112+
"ruleKinds": ["*_test"],
113+
"attr": "local",
114+
"forbidValues": ["True"], // boolean literal constraint (see below)
115+
"message": "Tests must not set local = True; use a hermetic test instead."
116+
},
117+
{
118+
"name": "no-no-cache",
119+
"attr": "execution_requirements",
120+
"forbidDictEntries": { // dict key→value constraint (see below)
121+
"no-cache": "1"
122+
},
123+
"message": "Do not set execution_requirements['no-cache'] = '1'."
107124
}
108125
]
109126
}
@@ -117,18 +134,40 @@ generic and lets each repo express its own policy.
117134
| `name` | string (required) | Stable identifier, included in the finding message. Must be unique. |
118135
| `ruleKinds` | []string | Globs matched against `rule.Kind()`. Empty/absent ⇒ matches all kinds. |
119136
| `attr` | string (required) | Attribute to inspect. |
120-
| `forbidValues` | []string | Scalar attr must not equal any of these. |
137+
| `forbidValues` | []string | Scalar attr must not equal any of these (see scalar matching below). |
121138
| `requireValues` | []string | If attr present, must equal one of these. (If also want "must be present", see `required`.) |
122139
| `forbidListItems` | []string | List attr must not contain any of these items. |
123140
| `requireListItems` | []string | List attr must contain all of these items. |
141+
| `forbidDictEntries` | object (string→string) | Dict attr must not contain any of these key→value pairs. |
142+
| `requireDictEntries` | object (string→string) | Dict attr must contain all of these key→value pairs. |
143+
| `forbidDictKeys` | []string | Dict attr must not contain any of these keys (value ignored). |
124144
| `required` | bool | Attr must be present at all. |
125145
| `allowlist` | []string | Target patterns exempt from this rule (see grammar below). |
126146
| `suppressible` | bool (default `true`) | Whether `# buildifier: disable=attr-policy` silences this rule. `false` = a "hard" rule enforced authoritatively by CI regardless of in-file comments (see §6). |
127147
| `message` | string | Custom message. If absent, a default is synthesized from the constraint. |
128148

149+
**Scalar matching (`forbidValues` / `requireValues`).** Covers string attributes
150+
(`timeout = "eternal"`) and boolean/identifier literals (`local = True`,
151+
`flaky = False`). At check time, read the attribute with `rule.AttrString` first,
152+
then fall back to `rule.AttrLiteral` (which returns `True`/`False` for boolean
153+
literals and identifier names for unquoted tokens). Compare the normalized string
154+
form. An absent attribute does not match `forbidValues`; it only fails
155+
`requireValues` when the attribute is present but wrong.
156+
157+
**Dict matching (`forbidDictEntries` / `requireDictEntries` / `forbidDictKeys`).**
158+
Covers `string_dict` / `label_dict` attributes such as `execution_requirements`.
159+
The attribute must be a `*build.DictExpr`; look up keys with `edit.DictionaryGet`.
160+
Dict values are normalized the same way as scalars (string literal or
161+
`AttrLiteral` on the value node). Example: `execution_requirements = {"no-cache":
162+
"1"}` is flagged by the `no-no-cache` rule above because the dict contains key
163+
`no-cache` with value `"1"`. `forbidDictKeys` is a weaker variant that flags the
164+
presence of a key regardless of its value (useful when any non-default value is
165+
undesirable but the exact value varies).
166+
129167
Exactly one *constraint family* (`forbidValues`/`requireValues` **or**
130-
`forbidListItems`/`requireListItems`) should be set per rule; `required` is
131-
orthogonal. Validation enforces this (see 3.5).
168+
`forbidListItems`/`requireListItems` **or**
169+
`forbidDictEntries`/`requireDictEntries`/`forbidDictKeys`) should be set per
170+
rule; `required` is orthogonal. Validation enforces this (see 3.5).
132171

133172
**Allow-list pattern grammar.** Entries are Bazel-style target patterns (a subset of
134173
what users already know from `bazel build`):
@@ -167,17 +206,20 @@ type AttrPolicy struct {
167206
}
168207

169208
type AttrPolicyRule struct {
170-
Name string `json:"name"`
171-
RuleKinds []string `json:"ruleKinds,omitempty"`
172-
Attr string `json:"attr"`
173-
ForbidValues []string `json:"forbidValues,omitempty"`
174-
RequireValues []string `json:"requireValues,omitempty"`
175-
ForbidListItems []string `json:"forbidListItems,omitempty"`
176-
RequireListItems []string `json:"requireListItems,omitempty"`
177-
Required bool `json:"required,omitempty"`
178-
Allowlist []string `json:"allowlist,omitempty"`
179-
Suppressible *bool `json:"suppressible,omitempty"` // pointer so absent ⇒ default true
180-
Message string `json:"message,omitempty"`
209+
Name string `json:"name"`
210+
RuleKinds []string `json:"ruleKinds,omitempty"`
211+
Attr string `json:"attr"`
212+
ForbidValues []string `json:"forbidValues,omitempty"`
213+
RequireValues []string `json:"requireValues,omitempty"`
214+
ForbidListItems []string `json:"forbidListItems,omitempty"`
215+
RequireListItems []string `json:"requireListItems,omitempty"`
216+
ForbidDictEntries map[string]string `json:"forbidDictEntries,omitempty"`
217+
RequireDictEntries map[string]string `json:"requireDictEntries,omitempty"`
218+
ForbidDictKeys []string `json:"forbidDictKeys,omitempty"`
219+
Required bool `json:"required,omitempty"`
220+
Allowlist []string `json:"allowlist,omitempty"`
221+
Suppressible *bool `json:"suppressible,omitempty"` // pointer so absent ⇒ default true
222+
Message string `json:"message,omitempty"`
181223
}
182224
```
183225

@@ -218,7 +260,7 @@ On load, reject:
218260
- duplicate `name`s,
219261
- empty `name` or empty `attr`,
220262
- a rule with no constraint set,
221-
- a rule mixing scalar and list constraint families,
263+
- a rule mixing scalar, list, and dict constraint families,
222264
- malformed globs in `ruleKinds` / malformed target patterns in `allowlist` (must
223265
match the §3.2 grammar; reject a bare `...`, repository-qualified entries, etc.).
224266

@@ -250,8 +292,15 @@ func attrPolicyWarning(f *build.File) []*LinterFinding {
250292

251293
- `p.allowed` runs the precompiled `allowlistMatch` (§3.2 grammar) over the target
252294
label — exact / `:all` / `/...` — not a bare `labels.Equal`.
253-
- `p.check` handles the constraint families using `rule.AttrString` / `rule.AttrStrings`
254-
and `rule.Attr(attr)` for the anchor node; returns `makeLinterFinding(node, msg)`.
295+
- `p.check` handles the constraint families:
296+
- **Scalars:** `attrScalarString(rule, attr)``rule.AttrString`, else
297+
`rule.AttrLiteral`; compare against `forbidValues` / `requireValues`.
298+
- **Lists:** `rule.AttrStrings`.
299+
- **Dicts:** `rule.Attr(attr)` as `*build.DictExpr`; `edit.DictionaryGet` per
300+
key; normalize values like scalars. `forbidDictKeys` flags any listed key
301+
that is present.
302+
Returns `makeLinterFinding(node, msg)` anchored on the offending attr node (or a
303+
specific dict entry's value node when possible).
255304
- Scope: BUILD files only (targets live there). Return `nil` for other file types.
256305
- **Autofix: none initially.** We cannot infer a correct scalar value. The one
257306
mechanically-safe fix (remove a forbidden list item / forbidden tag) is a good
@@ -264,7 +313,8 @@ func attrPolicyWarning(f *build.File) []*LinterFinding {
264313
(recommended, since it no-ops without config anyway — but being config-gated it's
265314
harmless in the default set too; pick opt-in to be conservative).
266315
- Docs: add an entry to `WARNINGS.md` and `warn/docs/warnings.textproto` describing the
267-
warning **and** the `attrPolicy` config block (with the two example rules).
316+
warning **and** the `attrPolicy` config block (with the example rules, including
317+
boolean and dict constraints).
268318
- Add a `-config=example` sample entry so `buildifier -config=example` prints an
269319
`attrPolicy` stub (see `config.Example()`).
270320
- Tests `warn/warn_attr_policy_test.go`:
@@ -273,6 +323,8 @@ func attrPolicyWarning(f *build.File) []*LinterFinding {
273323
- Cases: forbidden scalar value flagged; allow-listed target exempt; recursive
274324
`//pkg/...` allow-list; forbidden list item in `tags`; `ruleKinds` glob match &
275325
non-match; missing-when-`required`; `# buildifier: disable=attr-policy` suppression;
326+
forbidden boolean literal (`local = True`); forbidden dict entry
327+
(`execution_requirements = {"no-cache": "1"}`); `forbidDictKeys` on a dict key;
276328
empty config = no findings.
277329
- Config tests in `buildifier/config/config_test.go`: parse the sample JSON;
278330
validation rejects malformed rules.

0 commit comments

Comments
 (0)