@@ -77,7 +77,9 @@ whatever CI/automation invokes the tool.
7777
7878A ** single generalized** warning, ` attr-policy ` , driven entirely by config. It covers
7979the 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
8284Rationale for one generalized warning vs. many hardcoded ones: buildifier warnings are
8385individually 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+
129167Exactly 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
134173what users already know from ` bazel build ` ):
@@ -167,17 +206,20 @@ type AttrPolicy struct {
167206}
168207
169208type 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