@neralabs/rerail-pack-python 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,14 +19,17 @@ checked by `npx vitest run test/engine/catalog/packs.test.ts`.
19
19
  | `licenses/<tool>/` | The tool's licence and `UPSTREAM.md`, listing every copied file |
20
20
  | `candidates/rules/` | Ports still being evaluated (98 Ruff and Bandit ports): never shipped or listed in `pack.json`. 96 have a `conformance.json` port and are scored against the recordings without gating; A005 (GPL-derived fixtures only) and INP001 (tested only with a `namespace-packages` option) are not recorded |
21
21
 
22
- Re-recording runs offline against the pinned tool versions, under CPython 3.14 (Bandit itself runs from its own
23
- install, on CPython 3.13):
22
+ Re-recording is a maintainer deriving the pack's test data from its pins and archiving it
23
+ ([CONTRIBUTING.md](../../CONTRIBUTING.md#pack-test-data)); it is never committed. The recipe
24
+ (`scripts/pack-data/recipes/python.ts`) runs each tool's `generate.py`, in `fixtures` mode and then `corpus` mode over
25
+ django, against the pinned tool versions, under CPython 3.14 (Bandit itself runs from its own install, on CPython
26
+ 3.13). The cases Rerail wrote (`fixtures/rerail/`) are authored: a change to them goes in your pull request with
27
+ `git add -f`.
24
28
 
25
29
  ```sh
26
- python3.14 upstream/ruff/generate.py fixtures <ruff checkout at 0.16.9> <ruff 0.16.9 executable>
27
- python3.14 upstream/ruff/generate.py corpus <ruff 0.16.9 executable> django=<django checkout>
28
- python3.14 upstream/bandit/generate.py fixtures <bandit checkout at 1.9.4> <bandit 1.9.4 executable>
29
- python3.14 upstream/bandit/generate.py corpus <bandit 1.9.4 executable> django=<django checkout>
30
+ npm run fixtures:derive -- --pack python # every tool
31
+ npm run fixtures:derive -- --pack python --tool ruff # one
32
+ npm run fixtures:push -- --pack python --tool ruff # on your own branch: archive it and rewrite its test-data.lock
30
33
  ```
31
34
 
32
35
  Scoring one rule with every false positive and false negative printed:
@@ -85,7 +88,7 @@ Only matches `xml.sax.parse/parseString/make_parser` calls that are direct child
85
88
 
86
89
  ### `bandit-b406-import-xml-sax`
87
90
 
88
- Port of Bandit 1.9.4's B406 import_xml_sax. Divergence: an import of only xml.sax.saxutils (`from xml.sax.saxutils import ...`, `from xml.sax import saxutils`, aliased or parenthesised) is not reported. saxutils holds XML writing and escaping helpers (escape, quoteattr, unescape, XMLGenerator) and no parser, so defusedxml has nothing to replace it with. `import xml.sax.saxutils` is still reported, since it also binds xml.sax and its parse functions (NER-1827).
91
+ Port of Bandit 1.9.4's B406 import_xml_sax. Divergence: an import of only xml.sax.saxutils (`from xml.sax.saxutils import ...`, `from xml.sax import saxutils`, aliased or parenthesised) is not reported. saxutils holds XML writing and escaping helpers (escape, quoteattr, unescape, XMLGenerator) and no parser, so defusedxml has nothing to replace it with. `import xml.sax.saxutils` is still reported, since it also binds xml.sax and its parse functions.
89
92
 
90
93
  ### `bandit-b501-request-with-no-cert-validation`
91
94
 
@@ -109,7 +112,7 @@ A `shell=` keyword counts as truthy per Bandit's has_shell: anything but a falsy
109
112
 
110
113
  ### `bandit-b610-django-extra-used`
111
114
 
112
- Port of Bandit 1.9.4's B610 django_extra_used: a call of `extra` whose `where` or `tables` is not a list of plain strings, or whose `select` is not a dict of plain strings, bound to QuerySet.extra's six positional slots. Port of the kernel test b610_extra_with_a_non_literal_where_tables_or_select. `# nosec` honoured. Divergence: Bandit reports a call of any function or method named `extra`; the port reports only a method call whose receiver looks like a Django QuerySet or Manager: a name or attribute named `objects`, `qs`, `QS`, `queryset`, `querysets`, `manager` or `managers`, alone or after an underscore-joined prefix (`User.objects`, `self.queryset`, `base_qs`, `_default_manager`); a call of `get_queryset`; a method call on such a receiver (`User.objects.filter(...)`); or `self` or `super()` inside a class with a base whose name ends in `QuerySet` or is `Manager`, `BaseManager` or `BaseUserManager`, optionally dotted (`models.QuerySet`); a base that only ends in `Manager` (`AbstractContextManager`, `PluginManager`, `TreeManager`) does not count. A bare `extra(...)` call and `extra` on any other receiver are not reported (NER-1827).
115
+ Port of Bandit 1.9.4's B610 django_extra_used: a call of `extra` whose `where` or `tables` is not a list of plain strings, or whose `select` is not a dict of plain strings, bound to QuerySet.extra's six positional slots. Port of the kernel test b610_extra_with_a_non_literal_where_tables_or_select. `# nosec` honoured. Divergence: Bandit reports a call of any function or method named `extra`; the port reports only a method call whose receiver looks like a Django QuerySet or Manager: a name or attribute named `objects`, `qs`, `QS`, `queryset`, `querysets`, `manager` or `managers`, alone or after an underscore-joined prefix (`User.objects`, `self.queryset`, `base_qs`, `_default_manager`); a call of `get_queryset`; a method call on such a receiver (`User.objects.filter(...)`); or `self` or `super()` inside a class with a base whose name ends in `QuerySet` or is `Manager`, `BaseManager` or `BaseUserManager`, optionally dotted (`models.QuerySet`); a base that only ends in `Manager` (`AbstractContextManager`, `PluginManager`, `TreeManager`) does not count. A bare `extra(...)` call and `extra` on any other receiver are not reported.
113
116
 
114
117
  ### `bandit-b613-trojansource`
115
118
 
@@ -505,7 +508,7 @@ Port of Ruff's F707 (default-except-not-last) with default settings: a bare `exc
505
508
 
506
509
  ### `ruff-f722-forward-annotation-syntax-error`
507
510
 
508
- Port of Ruff's F722 (forward-annotation-syntax-error) with default settings: a string Ruff parses as a type definition (a string annotation, a `type` value, a `cast`/`TypeVar` argument, a typing generic's `"..."` slice), read with the `annotation` grammar. Three checks: a string Ruff's parser refuses (the parse is one `ERROR` node), reported on the string; each semantic syntax error Ruff's checker reports in a string it parsed (`await` or `yield` outside a function, a walrus rebinding a comprehension variable, duplicate arguments, `__debug__` stored, a name used before its `global`, ...: an `ERROR` node after the expression), reported at the offending node; and Ruff's second error on an `await` evaluated outside any function ("`await` statement outside of a function": the string's host is not in a function body with no class between them, and the `await` is not in a lambda's body or a generator expression's element, conditions or later iterables), at the `await`. Strings nested in one (`List['a b']` inside a string annotation) are read too. Not inside a function decorated with a `no_type_check` that resolves to `typing`, `_typeshed` or `typing_extensions` (Ruff's match_typing_expr): its parameters, defaults, return annotation and body. Ported from the Ruff conformance suite (rerail/conformance/ruff/rules/F722.json at 3f102adcb), with Ruff's `# noqa` honoured as the Ruff CLI honours it. Divergence: a string after the first element of a subscript whose generic resolves to a jaxtyping import (`Float[Array, "b n"]`, `jaxtyping.Float[...]`, `jt.Shaped[...]`) is a jaxtyping shape, which jaxtyping parses with its own grammar, not a forward annotation, so it is not reported. Ruff 0.16.9 reports it (NER-1827). A shape inside a string annotation (`"Float[Array, 'b n']"`) is still reported.
511
+ Port of Ruff's F722 (forward-annotation-syntax-error) with default settings: a string Ruff parses as a type definition (a string annotation, a `type` value, a `cast`/`TypeVar` argument, a typing generic's `"..."` slice), read with the `annotation` grammar. Three checks: a string Ruff's parser refuses (the parse is one `ERROR` node), reported on the string; each semantic syntax error Ruff's checker reports in a string it parsed (`await` or `yield` outside a function, a walrus rebinding a comprehension variable, duplicate arguments, `__debug__` stored, a name used before its `global`, ...: an `ERROR` node after the expression), reported at the offending node; and Ruff's second error on an `await` evaluated outside any function ("`await` statement outside of a function": the string's host is not in a function body with no class between them, and the `await` is not in a lambda's body or a generator expression's element, conditions or later iterables), at the `await`. Strings nested in one (`List['a b']` inside a string annotation) are read too. Not inside a function decorated with a `no_type_check` that resolves to `typing`, `_typeshed` or `typing_extensions` (Ruff's match_typing_expr): its parameters, defaults, return annotation and body. Ported from the Ruff conformance suite (rerail/conformance/ruff/rules/F722.json at 3f102adcb), with Ruff's `# noqa` honoured as the Ruff CLI honours it. Divergence: a string after the first element of a subscript whose generic resolves to a jaxtyping import (`Float[Array, "b n"]`, `jaxtyping.Float[...]`, `jt.Shaped[...]`) is a jaxtyping shape, which jaxtyping parses with its own grammar, not a forward annotation, so it is not reported. Ruff 0.16.9 reports it. A shape inside a string annotation (`"Float[Array, 'b n']"`) is still reported.
509
512
 
510
513
  ### `ruff-f811-redefined-while-unused`
511
514
 
@@ -387,8 +387,8 @@ Paths are relative to the pack directory, `packs/python/`.
387
387
  | `upstream/ruff/fixtures/syntax_errors/yield_scope.py` | `crates/ruff_linter/resources/test/fixtures/syntax_errors/yield_scope.py` | verbatim |
388
388
 
389
389
  > `recording.json` is a port readiness recording, and no file of this pack, so it is recorded here rather than in the
390
- > table above: the tool-pack's `recording.lock` names it and a GitHub release holds the bytes (docs/port-readiness.md).
391
- > It carries excerpts of this upstream's tests (`crates/ruff_linter/src/rules/*/mod.rs`,
390
+ > table above: the tool-pack's `recording.lock` names it by its sha256, and the bytes are an object in S3, not a file in
391
+ > git (docs/port-readiness.md). It carries excerpts of this upstream's tests (`crates/ruff_linter/src/rules/*/mod.rs`,
392
392
  > `crates/ruff_linter/src/linter.rs`: their test modules' string literals), redistributed under the licence above,
393
- > together with Rerail's Apache-2.0; the LICENSE beside this file is this upstream's own, at the pinned commit.
393
+ > together with Rerail's Apache-2.0; `LICENSE` beside this file is this upstream's own, at the pinned commit.
394
394
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neralabs/rerail-pack-python",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Rerail rule pack: Ruff and Bandit rules ported to Rerail code rules.",
5
5
  "license": "Apache-2.0 AND MIT",
6
6
  "repository": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-async210-blocking-http-call-in-async-function",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Don't make blocking HTTP calls with requests, httpx, urllib or urllib3 inside async functions; they stall the event loop.",
7
7
  "scope": {
@@ -147,7 +147,7 @@
147
147
  "violation": {
148
148
  "anchor": "witness.span",
149
149
  "code": "ASYNC210",
150
- "message": "Replace this blocking HTTP call with an awaited async client call, such as `httpx.AsyncClient` or `aiohttp`.",
150
+ "message": "Use an existing async client, or `await asyncio.to_thread(helper, ...)` for the request and response handling; preserve the API and options.",
151
151
  "suppress": {
152
152
  "noqa": {
153
153
  "codes": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-async222-wait-for-process-in-async-function",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Don't call os.wait, os.waitpid or the other blocking os.wait functions inside async functions; they block the event loop.",
7
7
  "scope": {
@@ -130,7 +130,7 @@
130
130
  "violation": {
131
131
  "anchor": "witness.span",
132
132
  "code": "ASYNC222",
133
- "message": "Replace this blocking `os.wait` call by awaiting the async process object, such as `await proc.wait()`.",
133
+ "message": "Offload the wait with `await asyncio.to_thread(wait_fn, ...)`; preserve arguments and status. Await `proc.wait()` only for an async process.",
134
134
  "suppress": {
135
135
  "noqa": {
136
136
  "codes": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-b905-zip-without-explicit-strict",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Pass strict= explicitly to zip() over two or more iterables on Python 3.10+; otherwise mismatched lengths are truncated silently.",
7
7
  "scope": {
@@ -415,7 +415,7 @@
415
415
  "violation": {
416
416
  "anchor": "witness.span",
417
417
  "code": "B905",
418
- "message": "`zip()` without an explicit `strict=` parameter",
418
+ "message": "Pass `strict=` explicitly: `strict=True` when lengths must match, `strict=False` when truncation is intended.",
419
419
  "suppress": {
420
420
  "noqa": {
421
421
  "codes": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-e711-none-comparison",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Use is None or is not None instead of == or != to compare with None; equality can be overridden and gives surprising results.",
7
7
  "scope": {
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "clauses": [
14
14
  {
15
- "id": "none-comparison",
15
+ "id": "none-comparison-equal",
16
16
  "forbid": {
17
17
  "ast": {
18
18
  "language": "python",
@@ -113,7 +113,7 @@
113
113
  {
114
114
  "precedes": {
115
115
  "stopBy": "neighbor",
116
- "regex": "^(==|!=)$",
116
+ "regex": "^==$",
117
117
  "precedes": {
118
118
  "stopBy": "neighbor",
119
119
  "not": {
@@ -127,7 +127,7 @@
127
127
  {
128
128
  "follows": {
129
129
  "stopBy": "neighbor",
130
- "regex": "^(==|!=)$",
130
+ "regex": "^==$",
131
131
  "follows": {
132
132
  "stopBy": "neighbor",
133
133
  "not": {
@@ -185,7 +185,191 @@
185
185
  "violation": {
186
186
  "anchor": "witness.span",
187
187
  "code": "E711",
188
- "message": "Comparison to `None` should be `cond is None`",
188
+ "message": "Use `is None` instead of `== None` for this comparison.",
189
+ "suppress": {
190
+ "noqa": {
191
+ "codes": [
192
+ "E711"
193
+ ]
194
+ }
195
+ }
196
+ }
197
+ },
198
+ {
199
+ "id": "none-comparison-not-equal",
200
+ "forbid": {
201
+ "ast": {
202
+ "language": "python",
203
+ "utils": {
204
+ "constant": {
205
+ "any": [
206
+ {
207
+ "kind": "string",
208
+ "not": {
209
+ "has": {
210
+ "stopBy": "end",
211
+ "kind": "interpolation"
212
+ }
213
+ }
214
+ },
215
+ {
216
+ "kind": "concatenated_string",
217
+ "not": {
218
+ "has": {
219
+ "stopBy": "end",
220
+ "kind": "interpolation"
221
+ }
222
+ }
223
+ },
224
+ {
225
+ "kind": "integer"
226
+ },
227
+ {
228
+ "kind": "float"
229
+ },
230
+ {
231
+ "kind": "none"
232
+ },
233
+ {
234
+ "kind": "true"
235
+ },
236
+ {
237
+ "kind": "false"
238
+ },
239
+ {
240
+ "kind": "ellipsis"
241
+ },
242
+ {
243
+ "kind": "parenthesized_expression",
244
+ "has": {
245
+ "stopBy": "neighbor",
246
+ "matches": "constant"
247
+ }
248
+ },
249
+ {
250
+ "kind": "tuple",
251
+ "not": {
252
+ "has": {
253
+ "stopBy": "neighbor",
254
+ "not": {
255
+ "any": [
256
+ {
257
+ "matches": "constant"
258
+ },
259
+ {
260
+ "regex": "^[(),]$"
261
+ }
262
+ ]
263
+ }
264
+ }
265
+ }
266
+ }
267
+ ]
268
+ },
269
+ "constant-non-singleton": {
270
+ "all": [
271
+ {
272
+ "matches": "constant"
273
+ },
274
+ {
275
+ "not": {
276
+ "matches": "singleton"
277
+ }
278
+ }
279
+ ]
280
+ },
281
+ "compared-operand": {
282
+ "inside": {
283
+ "stopBy": "neighbor",
284
+ "kind": "comparison_operator"
285
+ },
286
+ "any": [
287
+ {
288
+ "all": [
289
+ {
290
+ "not": {
291
+ "follows": {
292
+ "stopBy": "neighbor",
293
+ "regex": "(?s)."
294
+ }
295
+ }
296
+ },
297
+ {
298
+ "precedes": {
299
+ "stopBy": "neighbor",
300
+ "regex": "^!=$",
301
+ "precedes": {
302
+ "stopBy": "neighbor",
303
+ "not": {
304
+ "matches": "constant-non-singleton"
305
+ }
306
+ }
307
+ }
308
+ }
309
+ ]
310
+ },
311
+ {
312
+ "follows": {
313
+ "stopBy": "neighbor",
314
+ "regex": "^!=$",
315
+ "follows": {
316
+ "stopBy": "neighbor",
317
+ "not": {
318
+ "matches": "constant-non-singleton"
319
+ }
320
+ }
321
+ }
322
+ }
323
+ ]
324
+ },
325
+ "operand": {
326
+ "any": [
327
+ {
328
+ "matches": "compared-operand"
329
+ },
330
+ {
331
+ "inside": {
332
+ "stopBy": "neighbor",
333
+ "kind": "parenthesized_expression",
334
+ "matches": "operand"
335
+ }
336
+ }
337
+ ]
338
+ },
339
+ "singleton": {
340
+ "any": [
341
+ {
342
+ "kind": "none"
343
+ },
344
+ {
345
+ "kind": "true"
346
+ },
347
+ {
348
+ "kind": "false"
349
+ },
350
+ {
351
+ "kind": "ellipsis"
352
+ },
353
+ {
354
+ "kind": "parenthesized_expression",
355
+ "has": {
356
+ "stopBy": "neighbor",
357
+ "matches": "singleton"
358
+ }
359
+ }
360
+ ]
361
+ }
362
+ },
363
+ "rule": {
364
+ "kind": "none",
365
+ "matches": "operand"
366
+ }
367
+ }
368
+ },
369
+ "violation": {
370
+ "anchor": "witness.span",
371
+ "code": "E711",
372
+ "message": "Use `is not None` instead of `!= None` for this comparison.",
189
373
  "suppress": {
190
374
  "noqa": {
191
375
  "codes": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-f541-f-string-missing-placeholders",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Remove the f prefix from Python f-strings that have no placeholders; it suggests interpolation that never happens.",
7
7
  "scope": { "include": ["**/*"], "exclude": [] },
@@ -38,7 +38,7 @@
38
38
  "violation": {
39
39
  "anchor": "witness.span",
40
40
  "code": "F541",
41
- "message": "Remove the `f` prefix from this string; it has no placeholders.",
41
+ "message": "Remove the `f` prefix and unescape doubled braces to keep this string's runtime value unchanged.",
42
42
  "suppress": {
43
43
  "noqa": {
44
44
  "codes": [
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-f632-is-literal",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
- "description": "Use == instead of is to compare against string, number, tuple or container literals; the identity of such values is unreliable.",
6
+ "description": "Use == or != instead of is or is not to compare literal values; the identity of such values is unreliable.",
7
7
  "scope": {
8
8
  "include": [
9
9
  "**/*"
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "clauses": [
14
14
  {
15
- "id": "is-literal",
15
+ "id": "is-literal-is",
16
16
  "forbid": {
17
17
  "ast": {
18
18
  "language": "python",
@@ -150,7 +150,7 @@
150
150
  ]
151
151
  },
152
152
  "is-literal-op": {
153
- "regex": "^is(?:[\\s\\\\]+not)?$",
153
+ "regex": "^is$",
154
154
  "any": [
155
155
  {
156
156
  "follows": {
@@ -217,7 +217,224 @@
217
217
  "violation": {
218
218
  "anchor": "capture.C",
219
219
  "code": "F632",
220
- "message": "Use `==` to compare constant literals",
220
+ "message": "Use `==` instead of `is` to compare this literal value.",
221
+ "each": "O",
222
+ "suppress": {
223
+ "noqa": {
224
+ "codes": [
225
+ "F632"
226
+ ]
227
+ }
228
+ }
229
+ }
230
+ },
231
+ {
232
+ "id": "is-literal-is-not",
233
+ "forbid": {
234
+ "ast": {
235
+ "language": "python",
236
+ "utils": {
237
+ "constant": {
238
+ "any": [
239
+ {
240
+ "kind": "string",
241
+ "not": {
242
+ "has": {
243
+ "stopBy": "end",
244
+ "kind": "interpolation"
245
+ }
246
+ }
247
+ },
248
+ {
249
+ "kind": "concatenated_string",
250
+ "not": {
251
+ "has": {
252
+ "stopBy": "end",
253
+ "kind": "interpolation"
254
+ }
255
+ }
256
+ },
257
+ {
258
+ "kind": "integer"
259
+ },
260
+ {
261
+ "kind": "float"
262
+ },
263
+ {
264
+ "kind": "none"
265
+ },
266
+ {
267
+ "kind": "true"
268
+ },
269
+ {
270
+ "kind": "false"
271
+ },
272
+ {
273
+ "kind": "ellipsis"
274
+ },
275
+ {
276
+ "kind": "parenthesized_expression",
277
+ "has": {
278
+ "stopBy": "neighbor",
279
+ "matches": "constant"
280
+ }
281
+ },
282
+ {
283
+ "kind": "tuple",
284
+ "not": {
285
+ "has": {
286
+ "stopBy": "neighbor",
287
+ "not": {
288
+ "any": [
289
+ {
290
+ "matches": "constant"
291
+ },
292
+ {
293
+ "regex": "^[(),]$"
294
+ }
295
+ ]
296
+ }
297
+ }
298
+ }
299
+ }
300
+ ]
301
+ },
302
+ "singleton": {
303
+ "any": [
304
+ {
305
+ "kind": "none"
306
+ },
307
+ {
308
+ "kind": "true"
309
+ },
310
+ {
311
+ "kind": "false"
312
+ },
313
+ {
314
+ "kind": "ellipsis"
315
+ },
316
+ {
317
+ "kind": "parenthesized_expression",
318
+ "has": {
319
+ "stopBy": "neighbor",
320
+ "matches": "singleton"
321
+ }
322
+ }
323
+ ]
324
+ },
325
+ "constant-non-singleton": {
326
+ "all": [
327
+ {
328
+ "matches": "constant"
329
+ },
330
+ {
331
+ "not": {
332
+ "matches": "singleton"
333
+ }
334
+ }
335
+ ]
336
+ },
337
+ "literal-operand": {
338
+ "any": [
339
+ {
340
+ "matches": "constant-non-singleton"
341
+ },
342
+ {
343
+ "kind": "set"
344
+ },
345
+ {
346
+ "kind": "set_comprehension"
347
+ },
348
+ {
349
+ "kind": "list"
350
+ },
351
+ {
352
+ "kind": "list_comprehension"
353
+ },
354
+ {
355
+ "kind": "dictionary"
356
+ },
357
+ {
358
+ "kind": "dictionary_comprehension"
359
+ },
360
+ {
361
+ "kind": "parenthesized_expression",
362
+ "has": {
363
+ "stopBy": "neighbor",
364
+ "matches": "literal-operand"
365
+ }
366
+ }
367
+ ]
368
+ },
369
+ "is-literal-op": {
370
+ "regex": "^is[\\s\\\\]+not$",
371
+ "any": [
372
+ {
373
+ "follows": {
374
+ "stopBy": "neighbor",
375
+ "matches": "literal-operand"
376
+ }
377
+ },
378
+ {
379
+ "precedes": {
380
+ "stopBy": "neighbor",
381
+ "matches": "literal-operand"
382
+ }
383
+ }
384
+ ]
385
+ },
386
+ "is-literal-in-string": {
387
+ "any": [
388
+ {
389
+ "kind": "string"
390
+ },
391
+ {
392
+ "kind": "concatenated_string"
393
+ }
394
+ ],
395
+ "parse": {
396
+ "grammar": "annotation",
397
+ "has": {
398
+ "stopBy": "end",
399
+ "any": [
400
+ {
401
+ "kind": "comparison_operator",
402
+ "has": {
403
+ "stopBy": "neighbor",
404
+ "matches": "is-literal-op",
405
+ "capture": "O"
406
+ },
407
+ "capture": "C"
408
+ },
409
+ {
410
+ "matches": "is-literal-in-string"
411
+ }
412
+ ]
413
+ }
414
+ }
415
+ }
416
+ },
417
+ "rule": {
418
+ "any": [
419
+ {
420
+ "matches": "is-literal-op",
421
+ "inside": {
422
+ "stopBy": "neighbor",
423
+ "kind": "comparison_operator",
424
+ "pattern": "$C"
425
+ }
426
+ },
427
+ {
428
+ "matches": "is-literal-in-string"
429
+ }
430
+ ]
431
+ }
432
+ }
433
+ },
434
+ "violation": {
435
+ "anchor": "capture.C",
436
+ "code": "F632",
437
+ "message": "Use `!=` instead of `is not` to compare this literal value.",
221
438
  "each": "O",
222
439
  "suppress": {
223
440
  "noqa": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-f707-default-except-not-last",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Put a bare except handler last in its try statement; Python rejects any handler after it.",
7
7
  "scope": {
@@ -36,7 +36,7 @@
36
36
  "violation": {
37
37
  "anchor": "witness.span",
38
38
  "code": "F707",
39
- "message": "An `except` block as not the last exception handler",
39
+ "message": "Move this bare `except:` handler after all typed handlers; a bare handler must be last.",
40
40
  "suppress": {
41
41
  "noqa": {
42
42
  "codes": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-sim112-uncapitalized-environment-variables",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Use uppercase names when reading environment variables through os.environ or os.getenv; the lowercase proxy variables are exempt.",
7
7
  "scope": {
@@ -154,7 +154,7 @@
154
154
  "violation": {
155
155
  "anchor": "witness.span",
156
156
  "code": "SIM112",
157
- "message": "Write this environment variable name in uppercase, and set the variable under that name wherever it is defined.",
157
+ "message": "Uppercase only if the deployment uses that name; preserve intentional lowercase keys because lookups are case-sensitive on POSIX.",
158
158
  "suppress": {
159
159
  "noqa": {
160
160
  "codes": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "format": "rerail.policy/v1",
3
3
  "id": "ruff-sim300-yoda-conditions",
4
- "revision": 1,
4
+ "revision": 2,
5
5
  "mode": "steer",
6
6
  "description": "Put the variable first and the constant or literal second in comparisons, avoiding Yoda conditions; it reads the way you would say it.",
7
7
  "scope": {
@@ -473,7 +473,7 @@
473
473
  "violation": {
474
474
  "anchor": "witness.span",
475
475
  "code": "SIM300",
476
- "message": "Swap the comparison so the variable comes first and the constant second, as in `x == 1`.",
476
+ "message": "Put the variable first; reverse `<`/`>` and `<=`/`>=` when swapping operands, and keep equality operators unchanged.",
477
477
  "suppress": {
478
478
  "noqa": {
479
479
  "codes": [