@agentvibes/guardrails 0.1.0
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 +239 -0
- package/candidates/README.md +30 -0
- package/candidates/r10-as-cast.yml +24 -0
- package/candidates/r1d-scattered-numeric-const-in-component.yml +13 -0
- package/candidates/r2-helper-fn-in-tsx.yml +22 -0
- package/candidates/r3-hooks-outside-layout.yml +21 -0
- package/candidates/r6-jsx-comment-smell.yml +14 -0
- package/candidates/r7-raw-hsl-outside-tokens.yml +17 -0
- package/candidates/r8-inline-style-prop.yml +13 -0
- package/candidates/r9-optional-store-arg.yml +24 -0
- package/configs/biome.json +96 -0
- package/configs/tsconfig.base.json +21 -0
- package/dist/astGrep.d.ts +16 -0
- package/dist/astGrep.js +48 -0
- package/dist/astGrep.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +34 -0
- package/dist/deploy.js.map +1 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +102 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fileWalk.d.ts +2 -0
- package/dist/fileWalk.js +45 -0
- package/dist/fileWalk.js.map +1 -0
- package/dist/findings.d.ts +12 -0
- package/dist/findings.js +8 -0
- package/dist/findings.js.map +1 -0
- package/dist/gateRed.d.ts +1 -0
- package/dist/gateRed.js +93 -0
- package/dist/gateRed.js.map +1 -0
- package/dist/gitDiff.d.ts +18 -0
- package/dist/gitDiff.js +90 -0
- package/dist/gitDiff.js.map +1 -0
- package/dist/hookPostedit.d.ts +1 -0
- package/dist/hookPostedit.js +117 -0
- package/dist/hookPostedit.js.map +1 -0
- package/dist/hookStop.d.ts +1 -0
- package/dist/hookStop.js +56 -0
- package/dist/hookStop.js.map +1 -0
- package/dist/hookStopTest.d.ts +1 -0
- package/dist/hookStopTest.js +71 -0
- package/dist/hookStopTest.js.map +1 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +172 -0
- package/dist/init.js.map +1 -0
- package/dist/initTest.d.ts +1 -0
- package/dist/initTest.js +88 -0
- package/dist/initTest.js.map +1 -0
- package/dist/iterate.d.ts +9 -0
- package/dist/iterate.js +79 -0
- package/dist/iterate.js.map +1 -0
- package/dist/iterateTest.d.ts +1 -0
- package/dist/iterateTest.js +91 -0
- package/dist/iterateTest.js.map +1 -0
- package/dist/leakPatterns.d.ts +5 -0
- package/dist/leakPatterns.js +22 -0
- package/dist/leakPatterns.js.map +1 -0
- package/dist/leaks.d.ts +8 -0
- package/dist/leaks.js +211 -0
- package/dist/leaks.js.map +1 -0
- package/dist/leaksRed.d.ts +1 -0
- package/dist/leaksRed.js +62 -0
- package/dist/leaksRed.js.map +1 -0
- package/dist/metrics.d.ts +10 -0
- package/dist/metrics.js +113 -0
- package/dist/metrics.js.map +1 -0
- package/dist/metricsAggregate.d.ts +5 -0
- package/dist/metricsAggregate.js +78 -0
- package/dist/metricsAggregate.js.map +1 -0
- package/dist/metricsBaseline.d.ts +21 -0
- package/dist/metricsBaseline.js +88 -0
- package/dist/metricsBaseline.js.map +1 -0
- package/dist/metricsCollect.d.ts +8 -0
- package/dist/metricsCollect.js +295 -0
- package/dist/metricsCollect.js.map +1 -0
- package/dist/metricsCycle.d.ts +1 -0
- package/dist/metricsCycle.js +79 -0
- package/dist/metricsCycle.js.map +1 -0
- package/dist/metricsTypes.d.ts +102 -0
- package/dist/metricsTypes.js +2 -0
- package/dist/metricsTypes.js.map +1 -0
- package/dist/packagePaths.d.ts +7 -0
- package/dist/packagePaths.js +11 -0
- package/dist/packagePaths.js.map +1 -0
- package/dist/pluginApi.d.ts +24 -0
- package/dist/pluginApi.js +12 -0
- package/dist/pluginApi.js.map +1 -0
- package/dist/pluginResolve.d.ts +6 -0
- package/dist/pluginResolve.js +48 -0
- package/dist/pluginResolve.js.map +1 -0
- package/dist/screenScope.d.ts +21 -0
- package/dist/screenScope.js +81 -0
- package/dist/screenScope.js.map +1 -0
- package/dist/screenScopeTest.d.ts +1 -0
- package/dist/screenScopeTest.js +116 -0
- package/dist/screenScopeTest.js.map +1 -0
- package/dist/severity.d.ts +9 -0
- package/dist/severity.js +62 -0
- package/dist/severity.js.map +1 -0
- package/dist/severityTest.d.ts +1 -0
- package/dist/severityTest.js +63 -0
- package/dist/severityTest.js.map +1 -0
- package/dist/sourceLines.d.ts +9 -0
- package/dist/sourceLines.js +52 -0
- package/dist/sourceLines.js.map +1 -0
- package/dist/structure.d.ts +2 -0
- package/dist/structure.js +98 -0
- package/dist/structure.js.map +1 -0
- package/dist/testRules.d.ts +1 -0
- package/dist/testRules.js +285 -0
- package/dist/testRules.js.map +1 -0
- package/dist/textGrep.d.ts +2 -0
- package/dist/textGrep.js +52 -0
- package/dist/textGrep.js.map +1 -0
- package/dist/tomlTable.d.ts +2 -0
- package/dist/tomlTable.js +26 -0
- package/dist/tomlTable.js.map +1 -0
- package/dist/toolResolve.d.ts +18 -0
- package/dist/toolResolve.js +50 -0
- package/dist/toolResolve.js.map +1 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +49 -0
- package/dist/verify.js.map +1 -0
- package/dist/verifyDiff.d.ts +18 -0
- package/dist/verifyDiff.js +126 -0
- package/dist/verifyDiff.js.map +1 -0
- package/dist/workspaces.d.ts +6 -0
- package/dist/workspaces.js +84 -0
- package/dist/workspaces.js.map +1 -0
- package/mise.toml +9 -0
- package/package.json +62 -0
- package/rules/__fixtures__/as-any-escape/bad.ts +8 -0
- package/rules/__fixtures__/as-any-escape/good.ts +14 -0
- package/rules/__fixtures__/catch-empty/bad.ts +7 -0
- package/rules/__fixtures__/catch-empty/good.ts +14 -0
- package/rules/__fixtures__/direct-store-import/bad.tsx +23 -0
- package/rules/__fixtures__/direct-store-import/good.tsx +34 -0
- package/rules/__fixtures__/direct-store-import/showcase/ignored.tsx +9 -0
- package/rules/__fixtures__/discriminator-ternary/bad.tsx +10 -0
- package/rules/__fixtures__/discriminator-ternary/good.tsx +23 -0
- package/rules/__fixtures__/hardcoded-url-in-component/components/bad.tsx +8 -0
- package/rules/__fixtures__/hardcoded-url-in-component/components/good.tsx +14 -0
- package/rules/__fixtures__/inline-map-row/bad.tsx +72 -0
- package/rules/__fixtures__/inline-map-row/good.tsx +51 -0
- package/rules/__fixtures__/instanceof-map-set/bad.ts +7 -0
- package/rules/__fixtures__/instanceof-map-set/good.ts +8 -0
- package/rules/__fixtures__/literal-union-in-component/components/bad.ts +8 -0
- package/rules/__fixtures__/literal-union-in-component/components/good.ts +23 -0
- package/rules/__fixtures__/missing-observer/bad.tsx +66 -0
- package/rules/__fixtures__/missing-observer/good.tsx +69 -0
- package/rules/__fixtures__/no-local-kit-clone/bad.ts +30 -0
- package/rules/__fixtures__/no-local-kit-clone/bad.tsx +30 -0
- package/rules/__fixtures__/no-local-kit-clone/good.ts +31 -0
- package/rules/__fixtures__/no-local-kit-clone/good.tsx +31 -0
- package/rules/__fixtures__/non-exhaustive-match/bad.ts +8 -0
- package/rules/__fixtures__/non-exhaustive-match/good.ts +14 -0
- package/rules/__fixtures__/page-store-bare-usememo/bad.ts +15 -0
- package/rules/__fixtures__/page-store-bare-usememo/bad.tsx +13 -0
- package/rules/__fixtures__/page-store-bare-usememo/good.ts +17 -0
- package/rules/__fixtures__/page-store-bare-usememo/good.tsx +14 -0
- package/rules/__fixtures__/screen-file-styling/bad.screen.tsx +10 -0
- package/rules/__fixtures__/screen-file-styling/good.screen.tsx +8 -0
- package/rules/__fixtures__/state-loading-boolean-shape/bad.ts +36 -0
- package/rules/__fixtures__/state-loading-boolean-shape/bad.tsx +36 -0
- package/rules/__fixtures__/state-loading-boolean-shape/good.ts +44 -0
- package/rules/__fixtures__/state-loading-boolean-shape/good.tsx +44 -0
- package/rules/__fixtures__/store-async-method/bad.ts +37 -0
- package/rules/__fixtures__/store-async-method/bad.tsx +37 -0
- package/rules/__fixtures__/store-async-method/good.ts +73 -0
- package/rules/__fixtures__/store-async-method/good.tsx +73 -0
- package/rules/__fixtures__/store-context-provider/bad.ts +16 -0
- package/rules/__fixtures__/store-context-provider/bad.tsx +24 -0
- package/rules/__fixtures__/store-context-provider/good.ts +13 -0
- package/rules/__fixtures__/store-context-provider/good.tsx +11 -0
- package/rules/__fixtures__/store-delegation-getter/bad.ts +30 -0
- package/rules/__fixtures__/store-delegation-getter/bad.tsx +30 -0
- package/rules/__fixtures__/store-delegation-getter/good.ts +65 -0
- package/rules/__fixtures__/store-delegation-getter/good.tsx +65 -0
- package/rules/__fixtures__/store-new-map/bad.ts +22 -0
- package/rules/__fixtures__/store-new-map/bad.tsx +22 -0
- package/rules/__fixtures__/store-new-map/good.ts +41 -0
- package/rules/__fixtures__/store-new-map/good.tsx +41 -0
- package/rules/__fixtures__/store-no-runinaction/bad.ts +67 -0
- package/rules/__fixtures__/store-no-runinaction/bad.tsx +67 -0
- package/rules/__fixtures__/store-no-runinaction/good.ts +39 -0
- package/rules/__fixtures__/store-no-runinaction/good.tsx +39 -0
- package/rules/__fixtures__/store-no-setinterval-poll/bad.ts +19 -0
- package/rules/__fixtures__/store-no-setinterval-poll/bad.tsx +19 -0
- package/rules/__fixtures__/store-no-setinterval-poll/good.ts +29 -0
- package/rules/__fixtures__/store-no-setinterval-poll/good.tsx +29 -0
- package/rules/__fixtures__/ts-pattern-dangling-match/bad.ts +20 -0
- package/rules/__fixtures__/ts-pattern-dangling-match/good.ts +35 -0
- package/rules/__fixtures__/ui-imports-app-store/src/ui/bad.tsx +13 -0
- package/rules/__fixtures__/ui-imports-app-store/src/ui/good.tsx +23 -0
- package/rules/__fixtures__/view-file-logic/bad.view.tsx +16 -0
- package/rules/__fixtures__/view-file-logic/good.view.tsx +7 -0
- package/rules/__fixtures__/view-imports-store/bad.view.tsx +13 -0
- package/rules/__fixtures__/view-imports-store/good.view.tsx +23 -0
- package/rules/__fixtures__/zod-optional-nullable/bad.ts +10 -0
- package/rules/__fixtures__/zod-optional-nullable/bad.tsx +17 -0
- package/rules/__fixtures__/zod-optional-nullable/good.ts +16 -0
- package/rules/__fixtures__/zod-optional-nullable/good.tsx +20 -0
- package/rules/as-any-escape-tsx.yml +53 -0
- package/rules/as-any-escape.yml +29 -0
- package/rules/catch-empty-tsx.yml +55 -0
- package/rules/catch-empty.yml +31 -0
- package/rules/classname-not-composed.yml +28 -0
- package/rules/classname-visual-identity.yml +34 -0
- package/rules/demo-mode-by-default-ts.yml +76 -0
- package/rules/demo-mode-by-default.yml +54 -0
- package/rules/direct-store-import.yml +118 -0
- package/rules/discriminator-ternary.yml +70 -0
- package/rules/hardcoded-url-in-component.yml +44 -0
- package/rules/inline-map-row.yml +107 -0
- package/rules/instanceof-map-set-tsx.yml +46 -0
- package/rules/instanceof-map-set.yml +22 -0
- package/rules/json-roundtrip.yml +9 -0
- package/rules/jsx-cond-and.yml +16 -0
- package/rules/jsx-ternary.yml +35 -0
- package/rules/kind-if-without-match.yml +13 -0
- package/rules/literal-union-in-component.yml +71 -0
- package/rules/match-bool-to-null.yml +15 -0
- package/rules/missing-observer.yml +189 -0
- package/rules/mobx-effect-observable-dep.yml +23 -0
- package/rules/mobx-effect-store-write.yml +21 -0
- package/rules/mobx-usestate-from-store.yml +20 -0
- package/rules/no-local-kit-clone-tsx.yml +104 -0
- package/rules/no-local-kit-clone.yml +93 -0
- package/rules/non-exhaustive-match.yml +34 -0
- package/rules/page-store-bare-usememo-tsx.yml +47 -0
- package/rules/page-store-bare-usememo.yml +43 -0
- package/rules/screen-file-styling.yml +29 -0
- package/rules/silent-default-return.yml +17 -0
- package/rules/state-loading-boolean-shape-tsx.yml +102 -0
- package/rules/state-loading-boolean-shape.yml +91 -0
- package/rules/store-async-method-tsx.yml +108 -0
- package/rules/store-async-method.yml +98 -0
- package/rules/store-context-provider-tsx.yml +68 -0
- package/rules/store-context-provider.yml +78 -0
- package/rules/store-delegation-getter-tsx.yml +104 -0
- package/rules/store-delegation-getter.yml +93 -0
- package/rules/store-new-map-tsx.yml +84 -0
- package/rules/store-new-map.yml +73 -0
- package/rules/store-no-runinaction-tsx.yml +71 -0
- package/rules/store-no-runinaction.yml +60 -0
- package/rules/store-no-setinterval-poll-tsx.yml +62 -0
- package/rules/store-no-setinterval-poll.yml +51 -0
- package/rules/ts-pattern-dangling-match-tsx.yml +41 -0
- package/rules/ts-pattern-dangling-match.yml +63 -0
- package/rules/ui-imports-app-store.yml +49 -0
- package/rules/view-file-logic.yml +53 -0
- package/rules/view-imports-store.yml +64 -0
- package/rules/zod-optional-nullable-tsx.yml +56 -0
- package/rules/zod-optional-nullable.yml +29 -0
- package/sgconfig.yml +8 -0
- package/structure/rules/component-decl.yml +48 -0
- package/structure/sgconfig.yml +7 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ── wave 1, store rule 1 of 7 ──────────────────────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# The report's gap number 1: the most-repeated store rule in the corpus had no
|
|
4
|
+
# mechanical check at all, and the one script that did grep for it
|
|
5
|
+
# (mobx-models/scripts/lint-mobx.sh) was wired to nothing and has since been
|
|
6
|
+
# deleted.
|
|
7
|
+
#
|
|
8
|
+
# Scoped by the CLASS, not by the path. `**/stores/**` would have been the
|
|
9
|
+
# obvious glob and it is the wrong one twice over: it misses a store that lives
|
|
10
|
+
# beside its feature, and it catches plain helper classes that happen to sit in
|
|
11
|
+
# the folder. "Has a make(Auto)Observable call in it" is the actual definition
|
|
12
|
+
# of the thing the rule is about.
|
|
13
|
+
#
|
|
14
|
+
# Note the callee is matched as an identifier rather than with
|
|
15
|
+
# `pattern: makeAutoObservable($$$)`. A pattern does not match a call carrying
|
|
16
|
+
# explicit type arguments, and SiteCraft writes
|
|
17
|
+
# `makeAutoObservable<this, '_buildTimer' | 'buildManager'>(this, {…})` — which
|
|
18
|
+
# silently cost the whole of project.store.ts, 7 real call sites, until the
|
|
19
|
+
# count came up short against the grep.
|
|
20
|
+
id: store-no-runinaction
|
|
21
|
+
language: typescript
|
|
22
|
+
severity: error
|
|
23
|
+
ignores:
|
|
24
|
+
# A test may drive a store's state directly to set up a scenario; that is the
|
|
25
|
+
# sanctioned way to test a store without a render. All 6 of tg-gallery's
|
|
26
|
+
# runInAction sites are tests, which is why the report counts it as 0.
|
|
27
|
+
- '**/*.test.ts'
|
|
28
|
+
- '**/*.spec.ts'
|
|
29
|
+
- '**/__tests__/**'
|
|
30
|
+
utils:
|
|
31
|
+
store-class:
|
|
32
|
+
kind: class_declaration
|
|
33
|
+
has:
|
|
34
|
+
stopBy: end
|
|
35
|
+
kind: call_expression
|
|
36
|
+
has:
|
|
37
|
+
field: function
|
|
38
|
+
kind: identifier
|
|
39
|
+
regex: '^make(Auto)?Observable$'
|
|
40
|
+
message: |
|
|
41
|
+
`runInAction` inside a store class. Every method is already an action —
|
|
42
|
+
`makeAutoObservable` made it one — so this wraps an action in an action and
|
|
43
|
+
buys nothing. Where it appears is the tell: after an `await`, because the
|
|
44
|
+
continuation of an async method is NOT covered by the method's action, and
|
|
45
|
+
each `runInAction` is a hand-patched hole in that coverage.
|
|
46
|
+
Convert the method to a `flow`, which resumes inside an action at every yield
|
|
47
|
+
and needs no patching:
|
|
48
|
+
async load() { const x = await f(); runInAction(() => { this.x = x }) }
|
|
49
|
+
→ load = flow(function* (this: Store) { this.x = yield f() })
|
|
50
|
+
See the mobx-models skill.
|
|
51
|
+
rule:
|
|
52
|
+
all:
|
|
53
|
+
- kind: call_expression
|
|
54
|
+
- has:
|
|
55
|
+
field: function
|
|
56
|
+
kind: identifier
|
|
57
|
+
regex: '^runInAction$'
|
|
58
|
+
- inside:
|
|
59
|
+
stopBy: end
|
|
60
|
+
matches: store-class
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ── the .tsx half of `store-no-setinterval-poll` ──
|
|
2
|
+
#
|
|
3
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT: a `language:
|
|
4
|
+
# typescript` rule reads .ts and never .tsx. `demo-mode-by-default-ts.yml`
|
|
5
|
+
# records this repo learning that the hard way, so it is measured here rather
|
|
6
|
+
# than assumed — 30 .tsx files in the park call make(Auto)Observable, and the
|
|
7
|
+
# five twins with hits add 29 findings the .ts halves cannot see.
|
|
8
|
+
#
|
|
9
|
+
# Keep this file's `rule:` block IDENTICAL to `store-no-setinterval-poll.yml`.
|
|
10
|
+
# Suppressing needs THIS id: `ast-grep-ignore: store-no-setinterval-poll-tsx`.
|
|
11
|
+
#
|
|
12
|
+
# ── wave 1, store rule 7 of 7 ──────────────────────────────────────────
|
|
13
|
+
#
|
|
14
|
+
# `setInterval` inside a store class: a hand-rolled poll. Report §14 counts three
|
|
15
|
+
# independent sites rebuilding the same thing, and the failure mode is uniform —
|
|
16
|
+
# the interval outlives what it was polling for, keeps firing while the tab is
|
|
17
|
+
# hidden, and its disposer is either missing or forgotten (ObservableQuery, the
|
|
18
|
+
# reference implementation, shipped with exactly that leak: `dispose()` not
|
|
19
|
+
# removing its `visibilitychange` listener).
|
|
20
|
+
#
|
|
21
|
+
# Matches the CALL, not the identifier, so the common
|
|
22
|
+
# `private timer: ReturnType<typeof setInterval>` field annotation — a type
|
|
23
|
+
# position — stays silent.
|
|
24
|
+
id: store-no-setinterval-poll-tsx
|
|
25
|
+
language: tsx
|
|
26
|
+
severity: warning
|
|
27
|
+
ignores:
|
|
28
|
+
- '**/*.test.tsx'
|
|
29
|
+
- '**/*.spec.tsx'
|
|
30
|
+
- '**/__tests__/**'
|
|
31
|
+
utils:
|
|
32
|
+
store-class:
|
|
33
|
+
kind: class_declaration
|
|
34
|
+
has:
|
|
35
|
+
stopBy: end
|
|
36
|
+
kind: call_expression
|
|
37
|
+
has:
|
|
38
|
+
field: function
|
|
39
|
+
kind: identifier
|
|
40
|
+
regex: '^make(Auto)?Observable$'
|
|
41
|
+
message: |
|
|
42
|
+
A hand-rolled poll inside a store. An interval has to be cancelled on every
|
|
43
|
+
path that ends the thing it polls for, paused when the tab is hidden, and kept
|
|
44
|
+
from overlapping a request still in flight — and the copies in this codebase
|
|
45
|
+
each solve a different two of those three.
|
|
46
|
+
Use the Query object's `refetchInterval`, which owns the disposer, the
|
|
47
|
+
in-flight dedup and the focus/visibility handling in one place.
|
|
48
|
+
CARVE-OUT — process-shaped work (a render, an export, a git sync: something
|
|
49
|
+
with progress and no notion of revalidation) legitimately ticks on a timer.
|
|
50
|
+
There is no manifest glob for that yet, so mark those with a reason:
|
|
51
|
+
// ast-grep-ignore: store-no-setinterval-poll -- progress tick for a render job
|
|
52
|
+
See report §14 and the mobx-models skill.
|
|
53
|
+
rule:
|
|
54
|
+
all:
|
|
55
|
+
- kind: call_expression
|
|
56
|
+
- has:
|
|
57
|
+
field: function
|
|
58
|
+
kind: identifier
|
|
59
|
+
regex: '^setInterval$'
|
|
60
|
+
- inside:
|
|
61
|
+
stopBy: end
|
|
62
|
+
matches: store-class
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ── wave 1, store rule 7 of 7 ──────────────────────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# `setInterval` inside a store class: a hand-rolled poll. Report §14 counts three
|
|
4
|
+
# independent sites rebuilding the same thing, and the failure mode is uniform —
|
|
5
|
+
# the interval outlives what it was polling for, keeps firing while the tab is
|
|
6
|
+
# hidden, and its disposer is either missing or forgotten (ObservableQuery, the
|
|
7
|
+
# reference implementation, shipped with exactly that leak: `dispose()` not
|
|
8
|
+
# removing its `visibilitychange` listener).
|
|
9
|
+
#
|
|
10
|
+
# Matches the CALL, not the identifier, so the common
|
|
11
|
+
# `private timer: ReturnType<typeof setInterval>` field annotation — a type
|
|
12
|
+
# position — stays silent.
|
|
13
|
+
id: store-no-setinterval-poll
|
|
14
|
+
language: typescript
|
|
15
|
+
severity: warning
|
|
16
|
+
ignores:
|
|
17
|
+
- '**/*.test.ts'
|
|
18
|
+
- '**/*.spec.ts'
|
|
19
|
+
- '**/__tests__/**'
|
|
20
|
+
utils:
|
|
21
|
+
store-class:
|
|
22
|
+
kind: class_declaration
|
|
23
|
+
has:
|
|
24
|
+
stopBy: end
|
|
25
|
+
kind: call_expression
|
|
26
|
+
has:
|
|
27
|
+
field: function
|
|
28
|
+
kind: identifier
|
|
29
|
+
regex: '^make(Auto)?Observable$'
|
|
30
|
+
message: |
|
|
31
|
+
A hand-rolled poll inside a store. An interval has to be cancelled on every
|
|
32
|
+
path that ends the thing it polls for, paused when the tab is hidden, and kept
|
|
33
|
+
from overlapping a request still in flight — and the copies in this codebase
|
|
34
|
+
each solve a different two of those three.
|
|
35
|
+
Use the Query object's `refetchInterval`, which owns the disposer, the
|
|
36
|
+
in-flight dedup and the focus/visibility handling in one place.
|
|
37
|
+
CARVE-OUT — process-shaped work (a render, an export, a git sync: something
|
|
38
|
+
with progress and no notion of revalidation) legitimately ticks on a timer.
|
|
39
|
+
There is no manifest glob for that yet, so mark those with a reason:
|
|
40
|
+
// ast-grep-ignore: store-no-setinterval-poll -- progress tick for a render job
|
|
41
|
+
See report §14 and the mobx-models skill.
|
|
42
|
+
rule:
|
|
43
|
+
all:
|
|
44
|
+
- kind: call_expression
|
|
45
|
+
- has:
|
|
46
|
+
field: function
|
|
47
|
+
kind: identifier
|
|
48
|
+
regex: '^setInterval$'
|
|
49
|
+
- inside:
|
|
50
|
+
stopBy: end
|
|
51
|
+
matches: store-class
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# PROMOTED to canon in wave 2 from faceless-photo-lib/.ast-grep (rule-triage).
|
|
2
|
+
# No canon equivalent existed: `non-exhaustive-match` is about PREFERRING
|
|
3
|
+
# `.exhaustive()` over `.otherwise()`, which is a style call the owner has
|
|
4
|
+
# explicitly deferred (decision #4). This is the different, non-negotiable
|
|
5
|
+
# half — a chain with NO terminator evaluates to a builder object, so every
|
|
6
|
+
# arm body is dead code and the truthy builder keeps the caller compiling.
|
|
7
|
+
# Verified on real code: took a terminated chain out of observatory, deleted
|
|
8
|
+
# `.exhaustive()` -> fires; left it -> silent; a dangling chain nested in a
|
|
9
|
+
# callback of an OUTER terminated chain -> fires on the inner one only;
|
|
10
|
+
# `"abc".match(/b/)` -> silent.
|
|
11
|
+
#
|
|
12
|
+
# TSX twin of ts-pattern-dangling-match (ast-grep rules are single-language, so
|
|
13
|
+
# .tsx components get their own rule). Same policy — see
|
|
14
|
+
# ts-pattern-dangling-match.yml for the full rationale. This twin carries most of
|
|
15
|
+
# the repo's JSX-embedded `{match(state)...}` render branches.
|
|
16
|
+
id: ts-pattern-dangling-match-tsx
|
|
17
|
+
language: tsx
|
|
18
|
+
severity: error
|
|
19
|
+
message: "ts-pattern `match()` chain has no terminator — it evaluates to a builder, not a value. Add `.exhaustive()` (project.md §4)."
|
|
20
|
+
note: "`.otherwise()` and `.run()` also terminate a chain; preferring `.exhaustive()` over `.otherwise()` is the separate ts-pattern-prefer-exhaustive rule."
|
|
21
|
+
rule:
|
|
22
|
+
all:
|
|
23
|
+
- kind: call_expression
|
|
24
|
+
has:
|
|
25
|
+
field: function
|
|
26
|
+
kind: identifier
|
|
27
|
+
regex: "^match$"
|
|
28
|
+
- not:
|
|
29
|
+
inside:
|
|
30
|
+
stopBy:
|
|
31
|
+
not:
|
|
32
|
+
any:
|
|
33
|
+
- kind: member_expression
|
|
34
|
+
- kind: call_expression
|
|
35
|
+
kind: call_expression
|
|
36
|
+
has:
|
|
37
|
+
field: function
|
|
38
|
+
kind: member_expression
|
|
39
|
+
has:
|
|
40
|
+
field: property
|
|
41
|
+
regex: "^(exhaustive|otherwise|run)$"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# PROMOTED to canon in wave 2 from faceless-photo-lib/.ast-grep (rule-triage).
|
|
2
|
+
# No canon equivalent existed: `non-exhaustive-match` is about PREFERRING
|
|
3
|
+
# `.exhaustive()` over `.otherwise()`, which is a style call the owner has
|
|
4
|
+
# explicitly deferred (decision #4). This is the different, non-negotiable
|
|
5
|
+
# half — a chain with NO terminator evaluates to a builder object, so every
|
|
6
|
+
# arm body is dead code and the truthy builder keeps the caller compiling.
|
|
7
|
+
# Verified on real code: took a terminated chain out of observatory, deleted
|
|
8
|
+
# `.exhaustive()` -> fires; left it -> silent; a dangling chain nested in a
|
|
9
|
+
# callback of an OUTER terminated chain -> fires on the inner one only;
|
|
10
|
+
# `"abc".match(/b/)` -> silent.
|
|
11
|
+
#
|
|
12
|
+
# ts-pattern-dangling-match (convention-rules-ci task 2.4; project.md §3 and §4
|
|
13
|
+
# "`match().exhaustive()` everywhere").
|
|
14
|
+
#
|
|
15
|
+
# A `match(...)` chain that is never terminated does not evaluate anything — it
|
|
16
|
+
# leaves a ts-pattern *builder object* where the caller expects a value. That is
|
|
17
|
+
# always a bug, and a silent one: the builder is truthy, so `if`/JSX/`return`
|
|
18
|
+
# paths keep working while every branch body is dead code.
|
|
19
|
+
#
|
|
20
|
+
# This rule is the hard gate: it fires only when a chain has NO terminator at
|
|
21
|
+
# all (`.exhaustive()`, `.otherwise()`, or `.run()`). Preferring `.exhaustive()`
|
|
22
|
+
# over `.otherwise()` is the separate, advisory `ts-pattern-prefer-exhaustive`
|
|
23
|
+
# rule, because `.otherwise()` is legitimate when matching an OPEN domain (an
|
|
24
|
+
# external boundary string, say) that no union can enumerate.
|
|
25
|
+
#
|
|
26
|
+
# Implementation notes:
|
|
27
|
+
# - The target is a call_expression whose callee is the bare identifier
|
|
28
|
+
# `match`, so `someString.match(re)` (the RegExp method) is never confused
|
|
29
|
+
# for ts-pattern.
|
|
30
|
+
# - The terminator search walks ONLY the member/call spine of this chain — the
|
|
31
|
+
# `stopBy` halts at the first ancestor that is neither, so an enclosing
|
|
32
|
+
# terminated chain cannot mask a dangling chain nested in one of its
|
|
33
|
+
# callbacks.
|
|
34
|
+
# - Terminators are matched structurally (`kind` + `regex`) rather than with a
|
|
35
|
+
# surface `pattern:`, because ast-grep's default `smart` strictness also
|
|
36
|
+
# compares interleaved `comment` nodes — and these chains carry per-arm
|
|
37
|
+
# comments constantly, which would produce false positives.
|
|
38
|
+
id: ts-pattern-dangling-match
|
|
39
|
+
language: typescript
|
|
40
|
+
severity: error
|
|
41
|
+
message: "ts-pattern `match()` chain has no terminator — it evaluates to a builder, not a value. Add `.exhaustive()` (project.md §4)."
|
|
42
|
+
note: "`.otherwise()` and `.run()` also terminate a chain; preferring `.exhaustive()` over `.otherwise()` is the separate ts-pattern-prefer-exhaustive rule."
|
|
43
|
+
rule:
|
|
44
|
+
all:
|
|
45
|
+
- kind: call_expression
|
|
46
|
+
has:
|
|
47
|
+
field: function
|
|
48
|
+
kind: identifier
|
|
49
|
+
regex: "^match$"
|
|
50
|
+
- not:
|
|
51
|
+
inside:
|
|
52
|
+
stopBy:
|
|
53
|
+
not:
|
|
54
|
+
any:
|
|
55
|
+
- kind: member_expression
|
|
56
|
+
- kind: call_expression
|
|
57
|
+
kind: call_expression
|
|
58
|
+
has:
|
|
59
|
+
field: function
|
|
60
|
+
kind: member_expression
|
|
61
|
+
has:
|
|
62
|
+
field: property
|
|
63
|
+
regex: "^(exhaustive|otherwise|run)$"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
id: ui-imports-app-store
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: error
|
|
4
|
+
message: |
|
|
5
|
+
A reusable UI component importing an app store. The moment it knows where the
|
|
6
|
+
data comes from it stops being reusable — and the boundary is the whole point:
|
|
7
|
+
a UI component consumes an interface (props, or a view-model interface the
|
|
8
|
+
store implements), never the store itself.
|
|
9
|
+
Take the values as props and let the app component do the reading. See the
|
|
10
|
+
ui-components skill.
|
|
11
|
+
files:
|
|
12
|
+
- '**/packages/ui/**'
|
|
13
|
+
- '**/components/ui/**'
|
|
14
|
+
- '**/src/ui/**'
|
|
15
|
+
utils:
|
|
16
|
+
# A type-only import: erased at build time, so it creates no dependency on the
|
|
17
|
+
# store — only on its shape. Covers all four spellings TypeScript allows.
|
|
18
|
+
type-only-import:
|
|
19
|
+
any:
|
|
20
|
+
- pattern: import type { $$$S } from $SRC
|
|
21
|
+
- pattern: import type $D from $SRC
|
|
22
|
+
- pattern: import type * as $NS from $SRC
|
|
23
|
+
# `import { type A, type B }` — every named specifier inline-typed.
|
|
24
|
+
- all:
|
|
25
|
+
- kind: import_statement
|
|
26
|
+
- has:
|
|
27
|
+
stopBy: end
|
|
28
|
+
kind: named_imports
|
|
29
|
+
- not:
|
|
30
|
+
has:
|
|
31
|
+
stopBy: end
|
|
32
|
+
all:
|
|
33
|
+
- kind: import_specifier
|
|
34
|
+
- not:
|
|
35
|
+
regex: '^type\s'
|
|
36
|
+
rule:
|
|
37
|
+
all:
|
|
38
|
+
- kind: import_statement
|
|
39
|
+
- has:
|
|
40
|
+
stopBy: end
|
|
41
|
+
kind: string_fragment
|
|
42
|
+
regex: '(^|/)stores?(/|$)'
|
|
43
|
+
# X7 (report §2): the view-model pattern this rule exists to protect tells a
|
|
44
|
+
# UI component to type itself against `IXxxModel` — which lives in the store
|
|
45
|
+
# module. Firing at `error` on that import made the rule contradict the
|
|
46
|
+
# pattern it enforces, so a compliant component could not be written without
|
|
47
|
+
# a suppression comment. A type import is the compliant shape, not a breach.
|
|
48
|
+
- not:
|
|
49
|
+
matches: type-only-import
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ── simple-components, rule 3 of 4 ─────────────────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# The view half of the two-tier split (decision D8, report §9): a `*.view.tsx`
|
|
4
|
+
# is props in, JSX out. No hooks, no `match`, no ternary, no data transformation.
|
|
5
|
+
# That contract is what makes a view the file a cheap model writes without
|
|
6
|
+
# getting it wrong, and what lets its test mount it with nothing around it.
|
|
7
|
+
#
|
|
8
|
+
# WARNING TIER ON PURPOSE. D8 ships the split as a recommendation first, with a
|
|
9
|
+
# ratchet on the share of view files — not as a hard gate. Raising this to error
|
|
10
|
+
# would decide a question the owner deliberately parked. The store half of the
|
|
11
|
+
# same contract is the canon's `view-imports-store` (error), which is a narrower
|
|
12
|
+
# claim: no store access anywhere in the app's view files.
|
|
13
|
+
#
|
|
14
|
+
# `useTheme` / `useTokens` / `useStyles` / `useColorScheme` are exempt: the house
|
|
15
|
+
# styling idiom is RN inline objects read from a theme hook (report §10), so
|
|
16
|
+
# banning that hook would ban styling from the file that owns styling.
|
|
17
|
+
id: view-file-logic
|
|
18
|
+
language: tsx
|
|
19
|
+
severity: warning
|
|
20
|
+
files:
|
|
21
|
+
- '**/*.view.tsx'
|
|
22
|
+
message: |
|
|
23
|
+
Logic in a `*.view.tsx`. A view renders the props it was handed and decides
|
|
24
|
+
nothing: no hooks, no `match`, no ternary, no formatting, no icon-picking.
|
|
25
|
+
Move the decision up to the logic component or the model, and hand the view
|
|
26
|
+
the ANSWER — `tone="danger"`, `formattedDate`, `icon` — not the input it would
|
|
27
|
+
have to branch on. Passing `status` and mapping it to colours inside the view
|
|
28
|
+
is the same branch, laundered.
|
|
29
|
+
Exempt: `useTheme` / `useTokens` / `useStyles` / `useColorScheme` — styling is
|
|
30
|
+
what a view owns.
|
|
31
|
+
See the simple-components skill (§two-tier) and report §9.
|
|
32
|
+
rule:
|
|
33
|
+
any:
|
|
34
|
+
# Any hook call that is not a styling hook.
|
|
35
|
+
- all:
|
|
36
|
+
- kind: call_expression
|
|
37
|
+
- has:
|
|
38
|
+
field: function
|
|
39
|
+
kind: identifier
|
|
40
|
+
regex: '^use[A-Z]'
|
|
41
|
+
- not:
|
|
42
|
+
has:
|
|
43
|
+
field: function
|
|
44
|
+
kind: identifier
|
|
45
|
+
regex: '^use(Theme|Tokens|Styles|ColorScheme|Colors)$'
|
|
46
|
+
# Branching that belongs to the logic tier.
|
|
47
|
+
- all:
|
|
48
|
+
- kind: call_expression
|
|
49
|
+
- has:
|
|
50
|
+
field: function
|
|
51
|
+
kind: identifier
|
|
52
|
+
regex: '^match$'
|
|
53
|
+
- kind: ternary_expression
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# ── G1, rule 3 of 3 (second half): stores are banned outright in views ─
|
|
2
|
+
#
|
|
3
|
+
# Split from `direct-store-import.yml` for one mechanical reason: ast-grep scopes
|
|
4
|
+
# `files:` per rule file, and the two halves need different globs. This is not a
|
|
5
|
+
# fourth rule so much as the `*.view.tsx` arm of the third.
|
|
6
|
+
#
|
|
7
|
+
# Report §9 fixes the view contract as props → JSX and nothing else: no hooks, no
|
|
8
|
+
# `match`, no data transformation, no store. That is stricter than the rest of
|
|
9
|
+
# `.tsx`, where decision D1 makes a `rootStore` import legal at screen level — a
|
|
10
|
+
# view is never a screen, so `rootStore` is banned here too, and so is `useStore`.
|
|
11
|
+
#
|
|
12
|
+
# The only store-path import a view may keep is a type: C40 has the view take
|
|
13
|
+
# `model={store}` typed by `IXxxModel`, which lives in the store module and
|
|
14
|
+
# erases at build time.
|
|
15
|
+
#
|
|
16
|
+
# Expected hit count today: zero. `*.view.tsx` does not exist anywhere in the
|
|
17
|
+
# park yet (the two-tier split ships as a recommendation first, report §9 /
|
|
18
|
+
# decision #8). This is a guardrail waiting at the convention's door, not a
|
|
19
|
+
# finding about current code.
|
|
20
|
+
id: view-imports-store
|
|
21
|
+
language: tsx
|
|
22
|
+
severity: error
|
|
23
|
+
files:
|
|
24
|
+
- '**/*.view.tsx'
|
|
25
|
+
utils:
|
|
26
|
+
type-only-import:
|
|
27
|
+
any:
|
|
28
|
+
- pattern: import type { $$$S } from $SRC
|
|
29
|
+
- pattern: import type $D from $SRC
|
|
30
|
+
- pattern: import type * as $NS from $SRC
|
|
31
|
+
- all:
|
|
32
|
+
- kind: import_statement
|
|
33
|
+
- has:
|
|
34
|
+
stopBy: end
|
|
35
|
+
kind: named_imports
|
|
36
|
+
- not:
|
|
37
|
+
has:
|
|
38
|
+
stopBy: end
|
|
39
|
+
all:
|
|
40
|
+
- kind: import_specifier
|
|
41
|
+
- not:
|
|
42
|
+
regex: '^type\s'
|
|
43
|
+
message: |
|
|
44
|
+
A `*.view.tsx` importing from a stores directory. A view is props in, JSX out
|
|
45
|
+
— it has no hooks, no branching and no data access, which is exactly what
|
|
46
|
+
makes it the file a cheap model writes without getting it wrong, and what
|
|
47
|
+
lets its test mount it with nothing around it.
|
|
48
|
+
Move the read up to the logic component and hand the view what it renders.
|
|
49
|
+
Unlike the rest of the app, the global `rootStore` is not an exception here:
|
|
50
|
+
a view is never a screen.
|
|
51
|
+
`import type { IXxxModel }` IS allowed — typing the view against the model
|
|
52
|
+
interface it renders is the prescribed shape (C40). See report §9 and the
|
|
53
|
+
mobx-components skill.
|
|
54
|
+
rule:
|
|
55
|
+
all:
|
|
56
|
+
- kind: import_statement
|
|
57
|
+
- has:
|
|
58
|
+
field: source
|
|
59
|
+
has:
|
|
60
|
+
stopBy: end
|
|
61
|
+
kind: string_fragment
|
|
62
|
+
regex: '(^|/)stores?(/|$)'
|
|
63
|
+
- not:
|
|
64
|
+
matches: type-only-import
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ── rule body adopted from faceless-photo-lib `zod-no-optional-nullable` (wave 2 triage) ──
|
|
2
|
+
# The two rules were duplicates; the faceless implementation is strictly better,
|
|
3
|
+
# so the canon id keeps its message and takes that body. Why it is better here:
|
|
4
|
+
# `z.$T().optional()` only matched a DIRECT call on `z` — it missed both
|
|
5
|
+
# `UserSchema.optional()` and any chained builder (`z.string().min(1).optional()`).
|
|
6
|
+
# The receiver anchor keeps it to real zod schemas. Measured: +14 findings, 0 lost.
|
|
7
|
+
#
|
|
8
|
+
# The .tsx half of `zod-optional-nullable`.
|
|
9
|
+
#
|
|
10
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
|
|
11
|
+
# at `language: typescript` matches `.ts` and NEVER `.tsx`, so
|
|
12
|
+
# `zod-optional-nullable` was blind to every React component file — exactly
|
|
13
|
+
# where a form's validation schema often lives, right next to the component
|
|
14
|
+
# that uses it. Verified by effect — byte-identical content in `p.ts` and
|
|
15
|
+
# `p.tsx`, the first flagged and the second silent.
|
|
16
|
+
#
|
|
17
|
+
# Three ways to cover both were tried and two were rejected on measurement:
|
|
18
|
+
# - `language: [typescript, tsx]` — ast-grep refuses to parse the rule.
|
|
19
|
+
# - `languageGlobs: {tsx: ["*.ts"]}` — reassigns `.ts` to the tsx language, so
|
|
20
|
+
# EVERY remaining `language: typescript` rule goes dark. Measured corpus-wide:
|
|
21
|
+
# 5 rules blinded, 6,094 findings lost against 334 gained. It would have
|
|
22
|
+
# looked like a coverage win.
|
|
23
|
+
# - two files with the SAME id — "Error: Duplicate rule id".
|
|
24
|
+
# So: a separate id per language. The trailing `-tsx` is what you suppress with
|
|
25
|
+
# in a .tsx file, and the finding prints the id, so copying from the output is
|
|
26
|
+
# always right.
|
|
27
|
+
#
|
|
28
|
+
# Keep this file's `rule:` block IDENTICAL to `zod-optional-nullable.yml`. They
|
|
29
|
+
# are one rule wearing two ids because the tool has no way to say "both
|
|
30
|
+
# languages".
|
|
31
|
+
id: zod-optional-nullable-tsx
|
|
32
|
+
language: tsx
|
|
33
|
+
severity: warning
|
|
34
|
+
message: |
|
|
35
|
+
`z.X().optional()` / `z.X().nullable()` makes invalid states representable.
|
|
36
|
+
Prefer a discriminated union with explicit states so the absent case is a named variant.
|
|
37
|
+
See zod-schemas skill and defensive-errors philosophy.
|
|
38
|
+
Suppressing this one needs the id you see above — `ast-grep-ignore: zod-optional-nullable-tsx`.
|
|
39
|
+
`zod-optional-nullable` is a DIFFERENT rule (the .ts arm) and does not cover it; ast-grep
|
|
40
|
+
matches suppression ids exactly. A bare `// ast-grep-ignore` suppresses every
|
|
41
|
+
rule on the line, and a wrong id is reported back as `unused-suppression`.
|
|
42
|
+
rule:
|
|
43
|
+
all:
|
|
44
|
+
- kind: call_expression
|
|
45
|
+
has:
|
|
46
|
+
field: function
|
|
47
|
+
kind: member_expression
|
|
48
|
+
has:
|
|
49
|
+
field: property
|
|
50
|
+
regex: "^(optional|nullable|nullish)$"
|
|
51
|
+
# Anchor: the receiver chain mentions `z` or a `*Schema` binding, i.e. this
|
|
52
|
+
# really is a zod schema and not an unrelated builder with the same method.
|
|
53
|
+
- has:
|
|
54
|
+
stopBy: end
|
|
55
|
+
kind: identifier
|
|
56
|
+
regex: "^(z|.*Schema)$"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# ── rule body adopted from faceless-photo-lib `zod-no-optional-nullable` (wave 2 triage) ──
|
|
2
|
+
# The two rules were duplicates; the faceless implementation is strictly better,
|
|
3
|
+
# so the canon id keeps its message and takes that body. Why it is better here:
|
|
4
|
+
# `z.$T().optional()` only matched a DIRECT call on `z` — it missed both
|
|
5
|
+
# `UserSchema.optional()` and any chained builder (`z.string().min(1).optional()`).
|
|
6
|
+
# The receiver anchor keeps it to real zod schemas. Measured: +14 findings, 0 lost.
|
|
7
|
+
#
|
|
8
|
+
id: zod-optional-nullable
|
|
9
|
+
language: typescript
|
|
10
|
+
severity: warning
|
|
11
|
+
message: |
|
|
12
|
+
`z.X().optional()` / `z.X().nullable()` makes invalid states representable.
|
|
13
|
+
Prefer a discriminated union with explicit states so the absent case is a named variant.
|
|
14
|
+
See zod-schemas skill and defensive-errors philosophy.
|
|
15
|
+
rule:
|
|
16
|
+
all:
|
|
17
|
+
- kind: call_expression
|
|
18
|
+
has:
|
|
19
|
+
field: function
|
|
20
|
+
kind: member_expression
|
|
21
|
+
has:
|
|
22
|
+
field: property
|
|
23
|
+
regex: "^(optional|nullable|nullish)$"
|
|
24
|
+
# Anchor: the receiver chain mentions `z` or a `*Schema` binding, i.e. this
|
|
25
|
+
# really is a zod schema and not an unrelated builder with the same method.
|
|
26
|
+
- has:
|
|
27
|
+
stopBy: end
|
|
28
|
+
kind: identifier
|
|
29
|
+
regex: "^(z|.*Schema)$"
|
package/sgconfig.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# ast-grep config for the @agentvibes/guardrails rule canon.
|
|
2
|
+
# Invoke directly: ast-grep scan -c <abs path to this file> <target>
|
|
3
|
+
# Or via the CLI: guardrails verify [paths]
|
|
4
|
+
#
|
|
5
|
+
# candidates/ is deliberately NOT listed: those rules are un-triaged imports
|
|
6
|
+
# (see candidates/README.md) and load nowhere until promoted into rules/.
|
|
7
|
+
ruleDirs:
|
|
8
|
+
- rules
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
id: component-decl
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: info
|
|
4
|
+
# Same out-of-scope surface every canon rule declares: tests, stories, and
|
|
5
|
+
# showcase files are legitimate multi-component / oversized territory (a
|
|
6
|
+
# showcase screen IS a bundle of demo components). Found by the first real
|
|
7
|
+
# adoption (observatory): without this, a reformat touching a showcase file's
|
|
8
|
+
# declaration lines turned the verify-diff ratchet red on fixture code.
|
|
9
|
+
# Metrics already excluded these paths on its own; now the structure findings
|
|
10
|
+
# agree with it.
|
|
11
|
+
ignores:
|
|
12
|
+
- '**/*.test.tsx'
|
|
13
|
+
- '**/*.spec.tsx'
|
|
14
|
+
- '**/__tests__/**'
|
|
15
|
+
- '**/showcase/**'
|
|
16
|
+
- '**/*.stories.tsx'
|
|
17
|
+
message: |
|
|
18
|
+
Marker rule — matches every top-level React component declaration so
|
|
19
|
+
check-structure.sh can count them per file and measure their spans.
|
|
20
|
+
Never reported directly (severity info is invisible to the post-edit hook
|
|
21
|
+
and to the CI diff gate, both of which only look at error/warning tiers).
|
|
22
|
+
rule:
|
|
23
|
+
all:
|
|
24
|
+
# `const Foo = <anything>` or `function Foo(...) { ... }`
|
|
25
|
+
- any:
|
|
26
|
+
- pattern: const $N = $INIT
|
|
27
|
+
- pattern: |
|
|
28
|
+
function $N($$$) { $$$ }
|
|
29
|
+
# ...that renders JSX somewhere inside. Excludes `const StoreContext =
|
|
30
|
+
# createContext(...)`, capitalised constant maps, class instances, etc.
|
|
31
|
+
- has:
|
|
32
|
+
stopBy: end
|
|
33
|
+
any:
|
|
34
|
+
- kind: jsx_element
|
|
35
|
+
- kind: jsx_self_closing_element
|
|
36
|
+
# Only top-level declarations count. A component defined inside another
|
|
37
|
+
# function is a different (worse) problem — see noNestedComponentDefinitions.
|
|
38
|
+
not:
|
|
39
|
+
inside:
|
|
40
|
+
stopBy: end
|
|
41
|
+
any:
|
|
42
|
+
- kind: arrow_function
|
|
43
|
+
- kind: function_declaration
|
|
44
|
+
- kind: function_expression
|
|
45
|
+
- kind: method_definition
|
|
46
|
+
constraints:
|
|
47
|
+
N:
|
|
48
|
+
regex: '^[A-Z][A-Za-z0-9]*$'
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# ast-grep config for the React structure check (guardrails verify / verify-diff).
|
|
2
|
+
# Separate from ../sgconfig.yml on purpose: this dir holds a marker rule, not
|
|
3
|
+
# a reportable finding — the reportable findings (react-multi-component,
|
|
4
|
+
# react-component-too-long, react-component-needs-folder) are synthesised by
|
|
5
|
+
# the CLI from the marker's per-file counts and line spans.
|
|
6
|
+
ruleDirs:
|
|
7
|
+
- rules
|