Skip to content

Commit bee2e68

Browse files
committed
Wire predicates analyzer into constable command
Make the new check runnable via constable alongside existing analyzers, with CLI coverage for failing and passing cases.
1 parent 7c638f1 commit bee2e68

4 files changed

Lines changed: 100 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,24 @@ constable -complexity.limit=15 ./...
7878

7979
The limit defaults to 10 and is configured with `-complexity.limit`.
8080

81+
## Predicates
82+
83+
The predicates analyzer reports `&&` and `||` operands, except the
84+
first, that are not predicate-function calls. The first operand always
85+
executes, so coverage observes short-circuiting through the rest.
86+
87+
```go
88+
if a && b { // reported: use a && isB()
89+
}
90+
91+
if a && isB() { // allowed
92+
}
93+
```
94+
95+
The check also covers `for` conditions and bool assignments such as
96+
`c := a && b`, including negated variables, field and map access,
97+
and comparisons.
98+
8199
## Development
82100

83101
Run all fixers,

‎SKILL.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ func (c Count) Increment() Count {
111111
}
112112
```
113113

114+
## Predicates
115+
116+
The predicates analyzer reports `&&` and `||` operands, except the
117+
first, that are not predicate-function calls. The first operand always
118+
executes, so coverage observes short-circuiting through the rest.
119+
120+
```go
121+
func F(a, b bool) {
122+
if a && b { // reported: use a && isB()
123+
}
124+
}
125+
126+
func isB() bool { return false }
127+
128+
func G(a bool) {
129+
if a && isB() { // allowed
130+
}
131+
}
132+
```
133+
134+
The check covers `if` and `for` conditions and bool assignments such
135+
as `c := a && b`, including `!a`, field and map access, and
136+
comparisons. Operands that are already calls, compound expressions,
137+
or `true`/`false`/`nil` are allowed.
138+
114139
## Agent workflow
115140

116141
1. Identify functions and methods whose API contract should not mutate caller-owned data.

‎cmd/constable/main.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"github.com/nnutter/constable/internal/analysis/complexity"
55
"github.com/nnutter/constable/internal/analysis/methodical"
66
"github.com/nnutter/constable/internal/analysis/nonmutating"
7+
"github.com/nnutter/constable/internal/analysis/predicates"
78
"github.com/nnutter/constable/internal/analysis/testify"
89
"github.com/nnutter/constable/internal/driver"
910
)
@@ -14,5 +15,5 @@ import (
1415
var version string
1516

1617
func main() {
17-
driver.Main(version, nonmutating.Analyzer, methodical.Analyzer, testify.Analyzer, complexity.Analyzer)
18+
driver.Main(version, nonmutating.Analyzer, methodical.Analyzer, predicates.Analyzer, testify.Analyzer, complexity.Analyzer)
1819
}

‎cmd/constable/main_test.go‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,61 @@ func TestCLIComplexityFail(t *testing.T) {
226226
assert.NotContains(t, output, moduleDir)
227227
}
228228

229+
func TestCLIPredicatesFail(t *testing.T) {
230+
binary := buildBinary(t)
231+
moduleDir := writeModule(t, module{
232+
GoMod: []string{
233+
"module example.com/predicates",
234+
"",
235+
"go 1.26",
236+
},
237+
Files: map[string][]string{"a.go": {
238+
"package predicates",
239+
"",
240+
"func F(a, b bool) {",
241+
"\tif a && b {",
242+
"\t\tprintln(\"both\")",
243+
"\t}",
244+
"}",
245+
}},
246+
})
247+
248+
output, err := runConstable(t, binary, moduleDir)
249+
250+
require.Error(t, err)
251+
assert.Contains(t, output, `use predicate function instead of "b" in compound conditional`)
252+
assert.NotContains(t, output, moduleDir)
253+
}
254+
255+
func TestCLIPredicatesPass(t *testing.T) {
256+
binary := buildBinary(t)
257+
moduleDir := writeModule(t, module{
258+
GoMod: []string{
259+
"module example.com/predicatespass",
260+
"",
261+
"go 1.26",
262+
},
263+
Files: map[string][]string{"a.go": {
264+
"package predicates",
265+
"",
266+
"func isA() bool { return true }",
267+
"",
268+
"func isB() bool { return false }",
269+
"",
270+
"func F() {",
271+
"\tif isA() && isB() {",
272+
"\t\tprintln(\"both\")",
273+
"\t}",
274+
"}",
275+
}},
276+
})
277+
278+
output, err := runConstable(t, binary, moduleDir)
279+
280+
require.NoError(t, err, output)
281+
assert.Empty(t, output)
282+
}
283+
229284
func TestCLIComplexityPass(t *testing.T) {
230285
binary := buildBinary(t)
231286

0 commit comments

Comments
 (0)