@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,98 @@
|
|
|
1
|
+
# ── wave 1, store rule 2 of 7 ──────────────────────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# The other half of `store-no-runinaction`, and the reason that rule has so much
|
|
4
|
+
# to find: an `async` method's continuation after each `await` is NOT covered by
|
|
5
|
+
# the action `makeAutoObservable` created. Every assignment past the first await
|
|
6
|
+
# is an unbatched mutation outside an action.
|
|
7
|
+
#
|
|
8
|
+
# Deliberately does NOT fire when the method already contains `runInAction` —
|
|
9
|
+
# that method is reported by `store-no-runinaction` at `error`, and the fix is
|
|
10
|
+
# the same conversion. Two findings prescribing one change read as two problems,
|
|
11
|
+
# and the error-tier one is the one worth showing.
|
|
12
|
+
#
|
|
13
|
+
# So this rule catches the quieter half: the async store method that mutates
|
|
14
|
+
# without even the hand-patched `runInAction`, where nothing marks the hole.
|
|
15
|
+
id: store-async-method
|
|
16
|
+
language: typescript
|
|
17
|
+
severity: warning
|
|
18
|
+
ignores:
|
|
19
|
+
- '**/*.test.ts'
|
|
20
|
+
- '**/*.spec.ts'
|
|
21
|
+
- '**/__tests__/**'
|
|
22
|
+
utils:
|
|
23
|
+
store-class:
|
|
24
|
+
kind: class_declaration
|
|
25
|
+
has:
|
|
26
|
+
stopBy: end
|
|
27
|
+
kind: call_expression
|
|
28
|
+
has:
|
|
29
|
+
field: function
|
|
30
|
+
kind: identifier
|
|
31
|
+
regex: '^make(Auto)?Observable$'
|
|
32
|
+
contains-runinaction:
|
|
33
|
+
has:
|
|
34
|
+
stopBy: end
|
|
35
|
+
kind: call_expression
|
|
36
|
+
has:
|
|
37
|
+
field: function
|
|
38
|
+
kind: identifier
|
|
39
|
+
regex: '^runInAction$'
|
|
40
|
+
# An observable MUTATION — which is the whole defect. Without this clause the
|
|
41
|
+
# rule fires on any async method, and in SiteCraft 10 of its 12 hits were
|
|
42
|
+
# private I/O helpers (`walkDir`, `collectAllFiles`, `uploadFile`) that await
|
|
43
|
+
# a filesystem and never touch a field. Those have no action coverage to lose,
|
|
44
|
+
# so at warning tier they were pure noise.
|
|
45
|
+
mutates-this:
|
|
46
|
+
has:
|
|
47
|
+
stopBy: end
|
|
48
|
+
any:
|
|
49
|
+
# this.field = … / this.field += …
|
|
50
|
+
- all:
|
|
51
|
+
- any:
|
|
52
|
+
- kind: assignment_expression
|
|
53
|
+
- kind: augmented_assignment_expression
|
|
54
|
+
- has:
|
|
55
|
+
field: left
|
|
56
|
+
kind: member_expression
|
|
57
|
+
has:
|
|
58
|
+
field: object
|
|
59
|
+
kind: this
|
|
60
|
+
# this.list.push(…), this.map.set(…), … — mutation through a method.
|
|
61
|
+
- all:
|
|
62
|
+
- kind: call_expression
|
|
63
|
+
- has:
|
|
64
|
+
field: function
|
|
65
|
+
kind: member_expression
|
|
66
|
+
all:
|
|
67
|
+
- has:
|
|
68
|
+
field: property
|
|
69
|
+
regex: '^(push|pop|shift|unshift|splice|sort|reverse|set|delete|clear|add)$'
|
|
70
|
+
- has:
|
|
71
|
+
field: object
|
|
72
|
+
kind: member_expression
|
|
73
|
+
has:
|
|
74
|
+
field: object
|
|
75
|
+
kind: this
|
|
76
|
+
message: |
|
|
77
|
+
An `async` method on a store class. The action that `makeAutoObservable`
|
|
78
|
+
wrapped around it ends at the first `await` — everything after resumes
|
|
79
|
+
OUTSIDE it, so each assignment there is an unbatched mutation and observers
|
|
80
|
+
see the half-updated store between them.
|
|
81
|
+
Use a `flow`, which resumes inside an action at every yield:
|
|
82
|
+
async load() { this.x = await f(); this.y = await g() }
|
|
83
|
+
→ load = flow(function* (this: Store) { this.x = yield f(); this.y = yield g() })
|
|
84
|
+
Scope: this fires only when the method actually MUTATES an observable —
|
|
85
|
+
assigns `this.field` or calls `push`/`set`/`delete` on one. A private async
|
|
86
|
+
helper that only does I/O and returns has no action coverage to lose and stays
|
|
87
|
+
silent.
|
|
88
|
+
See the mobx-models skill.
|
|
89
|
+
rule:
|
|
90
|
+
all:
|
|
91
|
+
- kind: method_definition
|
|
92
|
+
- regex: '^((private|public|protected|static|override|abstract)\s+)*async\s'
|
|
93
|
+
- inside:
|
|
94
|
+
stopBy: end
|
|
95
|
+
matches: store-class
|
|
96
|
+
- matches: mutates-this
|
|
97
|
+
- not:
|
|
98
|
+
matches: contains-runinaction
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# ADOPTION NOTE (canon promotion): brownfield-heavy — ~48-105 whole-tree hits
|
|
2
|
+
# per useStore-era repo (tg-gallery/observatory/ppa). Run it DIFF-SCOPED
|
|
3
|
+
# (verify-diff / hook) until a repo's useStore migration campaign lands;
|
|
4
|
+
# whole-tree `verify` output will be loud there by design, not by accident.
|
|
5
|
+
# ── simple-components, rule 1 of 4 — the .tsx half ─────────────────────
|
|
6
|
+
#
|
|
7
|
+
# ast-grep's `typescript` and `tsx` languages are disjoint, not nested: a rule at
|
|
8
|
+
# `language: typescript` never sees a `.tsx` file. So this is the same rule
|
|
9
|
+
# wearing a second id, plus one arm that only exists in JSX (`<StoreContext.Provider>`).
|
|
10
|
+
# Suppress with the id the finding PRINTS — `-tsx` in a `.tsx` file.
|
|
11
|
+
# Keep the two `rule:` blocks in sync.
|
|
12
|
+
id: store-context-provider-tsx
|
|
13
|
+
language: tsx
|
|
14
|
+
severity: error
|
|
15
|
+
ignores:
|
|
16
|
+
- '**/*.test.tsx'
|
|
17
|
+
- '**/*.spec.tsx'
|
|
18
|
+
- '**/__tests__/**'
|
|
19
|
+
message: |
|
|
20
|
+
A store reached through React context. Decision D1: MobX already gives every
|
|
21
|
+
component a live channel to the data — a second one buys nothing and costs a
|
|
22
|
+
provider, a null-check and a hook on every file that reads state.
|
|
23
|
+
Do instead:
|
|
24
|
+
- one module-level `rootStore` in the canonical store module, imported
|
|
25
|
+
DIRECTLY and only by screen-level files;
|
|
26
|
+
- platform/DI dependencies through `initRootStore(deps)`, called once from
|
|
27
|
+
the entry file before the first render;
|
|
28
|
+
- page-scoped state through `usePageStore(() => new XxxPageStore(...), [k])`;
|
|
29
|
+
- everything below a screen receives `model={...}` as a prop.
|
|
30
|
+
Tests stop needing a provider at all: `new XxxStore(fakeDeps)` directly, and
|
|
31
|
+
`*.view.tsx` files mount with nothing around them.
|
|
32
|
+
See the simple-components skill (§store access) and report §8.
|
|
33
|
+
rule:
|
|
34
|
+
any:
|
|
35
|
+
- all:
|
|
36
|
+
- kind: call_expression
|
|
37
|
+
- has:
|
|
38
|
+
field: function
|
|
39
|
+
kind: identifier
|
|
40
|
+
regex: '^useStore$'
|
|
41
|
+
- all:
|
|
42
|
+
- any:
|
|
43
|
+
- kind: function_declaration
|
|
44
|
+
- kind: variable_declarator
|
|
45
|
+
- has:
|
|
46
|
+
field: name
|
|
47
|
+
regex: '^useStore$'
|
|
48
|
+
- all:
|
|
49
|
+
- kind: call_expression
|
|
50
|
+
- has:
|
|
51
|
+
field: function
|
|
52
|
+
kind: identifier
|
|
53
|
+
regex: '^createContext$'
|
|
54
|
+
- inside:
|
|
55
|
+
stopBy: end
|
|
56
|
+
kind: variable_declarator
|
|
57
|
+
regex: '[Ss]tore'
|
|
58
|
+
- all:
|
|
59
|
+
- kind: call_expression
|
|
60
|
+
- has:
|
|
61
|
+
field: function
|
|
62
|
+
kind: identifier
|
|
63
|
+
regex: '^useContext$'
|
|
64
|
+
- regex: '[Ss]tore'
|
|
65
|
+
# JSX-only arm: `<XxxStoreContext.Provider value={…}>`.
|
|
66
|
+
- all:
|
|
67
|
+
- kind: jsx_opening_element
|
|
68
|
+
- regex: '[Ss]tore[A-Za-z]*\.Provider'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# ADOPTION NOTE (canon promotion): brownfield-heavy — ~48-105 whole-tree hits
|
|
2
|
+
# per useStore-era repo (tg-gallery/observatory/ppa). Run it DIFF-SCOPED
|
|
3
|
+
# (verify-diff / hook) until a repo's useStore migration campaign lands;
|
|
4
|
+
# whole-tree `verify` output will be loud there by design, not by accident.
|
|
5
|
+
# ── simple-components, rule 1 of 4 ─────────────────────────────────────
|
|
6
|
+
#
|
|
7
|
+
# Decision D1: a MobX project has NO store context. One module-level `rootStore`,
|
|
8
|
+
# imported at screen level only; everything below gets models through props.
|
|
9
|
+
#
|
|
10
|
+
# The canon rule `direct-store-import` covers the other half — where a store
|
|
11
|
+
# import is legal. Nothing in the canon bans the context CHANNEL itself, which is
|
|
12
|
+
# what this rule is for. Audit basis (report §8): all four store-bearing apps
|
|
13
|
+
# route through a `useStore()` context hook (215/97/69/46 call sites) and not one
|
|
14
|
+
# of them uses it for anything context is needed for: in one of them the provider
|
|
15
|
+
# hands out a module-level singleton unconditionally, and its own tests bypass
|
|
16
|
+
# the provider entirely.
|
|
17
|
+
#
|
|
18
|
+
# NOT a candidate for blanket adoption in a repo that has not migrated: on
|
|
19
|
+
# today's park this fires hundreds of times. Run it diff-scoped (the skill's
|
|
20
|
+
# check-simple.sh --changed, or `guardrails verify-diff`) until the migration
|
|
21
|
+
# lands.
|
|
22
|
+
id: store-context-provider
|
|
23
|
+
language: typescript
|
|
24
|
+
severity: error
|
|
25
|
+
ignores:
|
|
26
|
+
- '**/*.test.ts'
|
|
27
|
+
- '**/*.spec.ts'
|
|
28
|
+
- '**/__tests__/**'
|
|
29
|
+
message: |
|
|
30
|
+
A store reached through React context. Decision D1: MobX already gives every
|
|
31
|
+
component a live channel to the data — a second one buys nothing and costs a
|
|
32
|
+
provider, a null-check and a hook on every file that reads state.
|
|
33
|
+
Do instead:
|
|
34
|
+
- one module-level `rootStore` in the canonical store module, imported
|
|
35
|
+
DIRECTLY and only by screen-level files;
|
|
36
|
+
- platform/DI dependencies through `initRootStore(deps)`, called once from
|
|
37
|
+
the entry file before the first render;
|
|
38
|
+
- page-scoped state through `usePageStore(() => new XxxPageStore(...), [k])`;
|
|
39
|
+
- everything below a screen receives `model={...}` as a prop.
|
|
40
|
+
Tests stop needing a provider at all: `new XxxStore(fakeDeps)` directly, and
|
|
41
|
+
`*.view.tsx` files mount with nothing around them.
|
|
42
|
+
See the simple-components skill (§store access) and report §8.
|
|
43
|
+
rule:
|
|
44
|
+
any:
|
|
45
|
+
# `useStore()` — the call...
|
|
46
|
+
- all:
|
|
47
|
+
- kind: call_expression
|
|
48
|
+
- has:
|
|
49
|
+
field: function
|
|
50
|
+
kind: identifier
|
|
51
|
+
regex: '^useStore$'
|
|
52
|
+
# ...and its declaration, which is the thing that enables every call site.
|
|
53
|
+
- all:
|
|
54
|
+
- any:
|
|
55
|
+
- kind: function_declaration
|
|
56
|
+
- kind: variable_declarator
|
|
57
|
+
- has:
|
|
58
|
+
field: name
|
|
59
|
+
regex: '^useStore$'
|
|
60
|
+
# `createContext` whose declarator mentions a store.
|
|
61
|
+
- all:
|
|
62
|
+
- kind: call_expression
|
|
63
|
+
- has:
|
|
64
|
+
field: function
|
|
65
|
+
kind: identifier
|
|
66
|
+
regex: '^createContext$'
|
|
67
|
+
- inside:
|
|
68
|
+
stopBy: end
|
|
69
|
+
kind: variable_declarator
|
|
70
|
+
regex: '[Ss]tore'
|
|
71
|
+
# `useContext(SomethingStoreContext)`.
|
|
72
|
+
- all:
|
|
73
|
+
- kind: call_expression
|
|
74
|
+
- has:
|
|
75
|
+
field: function
|
|
76
|
+
kind: identifier
|
|
77
|
+
regex: '^useContext$'
|
|
78
|
+
- regex: '[Ss]tore'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# ── the .tsx half of `store-delegation-getter` ──
|
|
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-delegation-getter.yml`.
|
|
10
|
+
# Suppressing needs THIS id: `ast-grep-ignore: store-delegation-getter-tsx`.
|
|
11
|
+
#
|
|
12
|
+
# ── wave 1, store rule 5 of 7 ──────────────────────────────────────────
|
|
13
|
+
#
|
|
14
|
+
# A getter whose entire body is `return this.someStore.prop` — a store aliasing
|
|
15
|
+
# another store's field. It adds a second name for one piece of state, so call
|
|
16
|
+
# sites disagree about which name is canonical, and the alias silently outlives
|
|
17
|
+
# the field it forwards to.
|
|
18
|
+
#
|
|
19
|
+
# THE NEGATIVE IS THE WHOLE DESIGN OF THIS RULE. The identical shape,
|
|
20
|
+
# `return this.data.x`, is the house data-wrapper pattern and is completely
|
|
21
|
+
# legal: a `Model` class holding a plain record and exposing its fields as
|
|
22
|
+
# observable getters. Measured across SiteCraft, observatory, byok and metro,
|
|
23
|
+
# single-line `return this.X.y` getters break down as:
|
|
24
|
+
#
|
|
25
|
+
# 89 × this.data.* byok Model, metro domain stores — LEGAL
|
|
26
|
+
# 2 × this.connectionStore.* — the real store→store alias
|
|
27
|
+
#
|
|
28
|
+
# So the discriminator is the intermediate NAME, and the rule matches only
|
|
29
|
+
# `Store`-suffixed ones — the same `[Ss]tore` convention mobx-effect-observable-dep
|
|
30
|
+
# and mobx-usestate-from-store already key on.
|
|
31
|
+
#
|
|
32
|
+
# KNOWN AND DELIBERATE MISS: a rootStore reference conventionally named `root`
|
|
33
|
+
# (WireDrill annotates `{ root: false }`), so `return this.root.user` is a real
|
|
34
|
+
# store→store alias this rule does not catch. Widening to `root` would put
|
|
35
|
+
# `this.data`-shaped names back in scope by degrees; the good fixture pins the
|
|
36
|
+
# miss so it stays a decision rather than becoming a surprise.
|
|
37
|
+
id: store-delegation-getter-tsx
|
|
38
|
+
language: tsx
|
|
39
|
+
severity: warning
|
|
40
|
+
ignores:
|
|
41
|
+
- '**/*.test.tsx'
|
|
42
|
+
- '**/*.spec.tsx'
|
|
43
|
+
- '**/__tests__/**'
|
|
44
|
+
utils:
|
|
45
|
+
store-class:
|
|
46
|
+
kind: class_declaration
|
|
47
|
+
has:
|
|
48
|
+
stopBy: end
|
|
49
|
+
kind: call_expression
|
|
50
|
+
has:
|
|
51
|
+
field: function
|
|
52
|
+
kind: identifier
|
|
53
|
+
regex: '^make(Auto)?Observable$'
|
|
54
|
+
message: |
|
|
55
|
+
A getter that only forwards another store's field. Now the value has two
|
|
56
|
+
names, and every call site has to know which one is current — the alias keeps
|
|
57
|
+
compiling long after the field behind it moves or changes meaning.
|
|
58
|
+
Read it where it lives (`rootStore.userStore.name`), or, if this store really
|
|
59
|
+
owns the concept, hold the value rather than a pointer to someone else's.
|
|
60
|
+
A computed that DERIVES something (combines, filters, formats) is the point of
|
|
61
|
+
computeds and is not this — this fires only on a body that is a bare
|
|
62
|
+
`return this.otherStore.field`.
|
|
63
|
+
Not this rule: `return this.data.x` on a model wrapping a plain record. That
|
|
64
|
+
is the sanctioned data-wrapper pattern and stays silent.
|
|
65
|
+
See the mobx-models skill.
|
|
66
|
+
rule:
|
|
67
|
+
all:
|
|
68
|
+
- kind: method_definition
|
|
69
|
+
- regex: '^((private|public|protected|static|override)\s+)*get\s'
|
|
70
|
+
- inside:
|
|
71
|
+
stopBy: end
|
|
72
|
+
matches: store-class
|
|
73
|
+
- has:
|
|
74
|
+
field: body
|
|
75
|
+
all:
|
|
76
|
+
- kind: statement_block
|
|
77
|
+
- has:
|
|
78
|
+
kind: return_statement
|
|
79
|
+
has:
|
|
80
|
+
kind: member_expression
|
|
81
|
+
has:
|
|
82
|
+
field: object
|
|
83
|
+
all:
|
|
84
|
+
- kind: member_expression
|
|
85
|
+
- has:
|
|
86
|
+
field: object
|
|
87
|
+
kind: this
|
|
88
|
+
- has:
|
|
89
|
+
field: property
|
|
90
|
+
regex: '[Ss]tore$'
|
|
91
|
+
# Body is ONLY that return — anything computed on the way makes it a
|
|
92
|
+
# real derivation rather than an alias.
|
|
93
|
+
- not:
|
|
94
|
+
has:
|
|
95
|
+
any:
|
|
96
|
+
- kind: if_statement
|
|
97
|
+
- kind: lexical_declaration
|
|
98
|
+
- kind: variable_declaration
|
|
99
|
+
- kind: expression_statement
|
|
100
|
+
- kind: for_statement
|
|
101
|
+
- kind: for_in_statement
|
|
102
|
+
- kind: while_statement
|
|
103
|
+
- kind: switch_statement
|
|
104
|
+
- kind: try_statement
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# ── wave 1, store rule 5 of 7 ──────────────────────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# A getter whose entire body is `return this.someStore.prop` — a store aliasing
|
|
4
|
+
# another store's field. It adds a second name for one piece of state, so call
|
|
5
|
+
# sites disagree about which name is canonical, and the alias silently outlives
|
|
6
|
+
# the field it forwards to.
|
|
7
|
+
#
|
|
8
|
+
# THE NEGATIVE IS THE WHOLE DESIGN OF THIS RULE. The identical shape,
|
|
9
|
+
# `return this.data.x`, is the house data-wrapper pattern and is completely
|
|
10
|
+
# legal: a `Model` class holding a plain record and exposing its fields as
|
|
11
|
+
# observable getters. Measured across SiteCraft, observatory, byok and metro,
|
|
12
|
+
# single-line `return this.X.y` getters break down as:
|
|
13
|
+
#
|
|
14
|
+
# 89 × this.data.* byok Model, metro domain stores — LEGAL
|
|
15
|
+
# 2 × this.connectionStore.* — the real store→store alias
|
|
16
|
+
#
|
|
17
|
+
# So the discriminator is the intermediate NAME, and the rule matches only
|
|
18
|
+
# `Store`-suffixed ones — the same `[Ss]tore` convention mobx-effect-observable-dep
|
|
19
|
+
# and mobx-usestate-from-store already key on.
|
|
20
|
+
#
|
|
21
|
+
# KNOWN AND DELIBERATE MISS: a rootStore reference conventionally named `root`
|
|
22
|
+
# (WireDrill annotates `{ root: false }`), so `return this.root.user` is a real
|
|
23
|
+
# store→store alias this rule does not catch. Widening to `root` would put
|
|
24
|
+
# `this.data`-shaped names back in scope by degrees; the good fixture pins the
|
|
25
|
+
# miss so it stays a decision rather than becoming a surprise.
|
|
26
|
+
id: store-delegation-getter
|
|
27
|
+
language: typescript
|
|
28
|
+
severity: warning
|
|
29
|
+
ignores:
|
|
30
|
+
- '**/*.test.ts'
|
|
31
|
+
- '**/*.spec.ts'
|
|
32
|
+
- '**/__tests__/**'
|
|
33
|
+
utils:
|
|
34
|
+
store-class:
|
|
35
|
+
kind: class_declaration
|
|
36
|
+
has:
|
|
37
|
+
stopBy: end
|
|
38
|
+
kind: call_expression
|
|
39
|
+
has:
|
|
40
|
+
field: function
|
|
41
|
+
kind: identifier
|
|
42
|
+
regex: '^make(Auto)?Observable$'
|
|
43
|
+
message: |
|
|
44
|
+
A getter that only forwards another store's field. Now the value has two
|
|
45
|
+
names, and every call site has to know which one is current — the alias keeps
|
|
46
|
+
compiling long after the field behind it moves or changes meaning.
|
|
47
|
+
Read it where it lives (`rootStore.userStore.name`), or, if this store really
|
|
48
|
+
owns the concept, hold the value rather than a pointer to someone else's.
|
|
49
|
+
A computed that DERIVES something (combines, filters, formats) is the point of
|
|
50
|
+
computeds and is not this — this fires only on a body that is a bare
|
|
51
|
+
`return this.otherStore.field`.
|
|
52
|
+
Not this rule: `return this.data.x` on a model wrapping a plain record. That
|
|
53
|
+
is the sanctioned data-wrapper pattern and stays silent.
|
|
54
|
+
See the mobx-models skill.
|
|
55
|
+
rule:
|
|
56
|
+
all:
|
|
57
|
+
- kind: method_definition
|
|
58
|
+
- regex: '^((private|public|protected|static|override)\s+)*get\s'
|
|
59
|
+
- inside:
|
|
60
|
+
stopBy: end
|
|
61
|
+
matches: store-class
|
|
62
|
+
- has:
|
|
63
|
+
field: body
|
|
64
|
+
all:
|
|
65
|
+
- kind: statement_block
|
|
66
|
+
- has:
|
|
67
|
+
kind: return_statement
|
|
68
|
+
has:
|
|
69
|
+
kind: member_expression
|
|
70
|
+
has:
|
|
71
|
+
field: object
|
|
72
|
+
all:
|
|
73
|
+
- kind: member_expression
|
|
74
|
+
- has:
|
|
75
|
+
field: object
|
|
76
|
+
kind: this
|
|
77
|
+
- has:
|
|
78
|
+
field: property
|
|
79
|
+
regex: '[Ss]tore$'
|
|
80
|
+
# Body is ONLY that return — anything computed on the way makes it a
|
|
81
|
+
# real derivation rather than an alias.
|
|
82
|
+
- not:
|
|
83
|
+
has:
|
|
84
|
+
any:
|
|
85
|
+
- kind: if_statement
|
|
86
|
+
- kind: lexical_declaration
|
|
87
|
+
- kind: variable_declaration
|
|
88
|
+
- kind: expression_statement
|
|
89
|
+
- kind: for_statement
|
|
90
|
+
- kind: for_in_statement
|
|
91
|
+
- kind: while_statement
|
|
92
|
+
- kind: switch_statement
|
|
93
|
+
- kind: try_statement
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ── the .tsx half of `store-new-map` ──
|
|
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-new-map.yml`.
|
|
10
|
+
# Suppressing needs THIS id: `ast-grep-ignore: store-new-map-tsx`.
|
|
11
|
+
#
|
|
12
|
+
# ── wave 1, store rule 3 of 7 ──────────────────────────────────────────
|
|
13
|
+
#
|
|
14
|
+
# A plain `new Map()` held as a store field. `makeAutoObservable` makes the FIELD
|
|
15
|
+
# observable — reassigning it notifies — but not the map's contents: `.set()`
|
|
16
|
+
# and `.delete()` mutate in place and no observer wakes up. The usual symptom is
|
|
17
|
+
# a cache that updates and a UI that does not, "fixed" by reassigning a fresh
|
|
18
|
+
# Map on every write.
|
|
19
|
+
#
|
|
20
|
+
# The negative that matters is the legal one, and it is why this rule reads the
|
|
21
|
+
# annotation object rather than just the initializer: a field explicitly
|
|
22
|
+
# annotated `{ _cache: false }` is deliberately NOT observable — a private
|
|
23
|
+
# memo/dedup cache nothing renders from. That is a correct pattern and must stay
|
|
24
|
+
# silent. A shared `$FIELD` metavariable ties the field's name to the annotation
|
|
25
|
+
# entry, so the check is per-field rather than per-class.
|
|
26
|
+
id: store-new-map-tsx
|
|
27
|
+
language: tsx
|
|
28
|
+
severity: warning
|
|
29
|
+
ignores:
|
|
30
|
+
- '**/*.test.tsx'
|
|
31
|
+
- '**/*.spec.tsx'
|
|
32
|
+
- '**/__tests__/**'
|
|
33
|
+
utils:
|
|
34
|
+
store-class:
|
|
35
|
+
kind: class_declaration
|
|
36
|
+
has:
|
|
37
|
+
stopBy: end
|
|
38
|
+
kind: call_expression
|
|
39
|
+
has:
|
|
40
|
+
field: function
|
|
41
|
+
kind: identifier
|
|
42
|
+
regex: '^make(Auto)?Observable$'
|
|
43
|
+
message: |
|
|
44
|
+
A plain `Map` as an observable store field. The field reference is
|
|
45
|
+
observable; the map's CONTENTS are not — `.set()` and `.delete()` mutate in
|
|
46
|
+
place and notify nobody, so anything rendering from this map silently stops
|
|
47
|
+
updating.
|
|
48
|
+
Use `observable.map()`, whose mutations are tracked:
|
|
49
|
+
entries = new Map<string, Row>() → entries = observable.map<string, Row>()
|
|
50
|
+
If this is a private cache nothing renders from, say so in the annotation and
|
|
51
|
+
the rule steps aside:
|
|
52
|
+
makeAutoObservable(this, { _cache: false })
|
|
53
|
+
See the mobx-models skill.
|
|
54
|
+
rule:
|
|
55
|
+
all:
|
|
56
|
+
- kind: public_field_definition
|
|
57
|
+
- has:
|
|
58
|
+
field: name
|
|
59
|
+
pattern: $FIELD
|
|
60
|
+
- has:
|
|
61
|
+
field: value
|
|
62
|
+
kind: new_expression
|
|
63
|
+
has:
|
|
64
|
+
field: constructor
|
|
65
|
+
kind: identifier
|
|
66
|
+
regex: '^Map$'
|
|
67
|
+
- inside:
|
|
68
|
+
stopBy: end
|
|
69
|
+
matches: store-class
|
|
70
|
+
# Annotated non-observable for this exact field name — the legal escape.
|
|
71
|
+
- not:
|
|
72
|
+
inside:
|
|
73
|
+
stopBy: end
|
|
74
|
+
kind: class_declaration
|
|
75
|
+
has:
|
|
76
|
+
stopBy: end
|
|
77
|
+
all:
|
|
78
|
+
- kind: pair
|
|
79
|
+
- has:
|
|
80
|
+
field: key
|
|
81
|
+
pattern: $FIELD
|
|
82
|
+
- has:
|
|
83
|
+
field: value
|
|
84
|
+
regex: '^false$'
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ── wave 1, store rule 3 of 7 ──────────────────────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# A plain `new Map()` held as a store field. `makeAutoObservable` makes the FIELD
|
|
4
|
+
# observable — reassigning it notifies — but not the map's contents: `.set()`
|
|
5
|
+
# and `.delete()` mutate in place and no observer wakes up. The usual symptom is
|
|
6
|
+
# a cache that updates and a UI that does not, "fixed" by reassigning a fresh
|
|
7
|
+
# Map on every write.
|
|
8
|
+
#
|
|
9
|
+
# The negative that matters is the legal one, and it is why this rule reads the
|
|
10
|
+
# annotation object rather than just the initializer: a field explicitly
|
|
11
|
+
# annotated `{ _cache: false }` is deliberately NOT observable — a private
|
|
12
|
+
# memo/dedup cache nothing renders from. That is a correct pattern and must stay
|
|
13
|
+
# silent. A shared `$FIELD` metavariable ties the field's name to the annotation
|
|
14
|
+
# entry, so the check is per-field rather than per-class.
|
|
15
|
+
id: store-new-map
|
|
16
|
+
language: typescript
|
|
17
|
+
severity: warning
|
|
18
|
+
ignores:
|
|
19
|
+
- '**/*.test.ts'
|
|
20
|
+
- '**/*.spec.ts'
|
|
21
|
+
- '**/__tests__/**'
|
|
22
|
+
utils:
|
|
23
|
+
store-class:
|
|
24
|
+
kind: class_declaration
|
|
25
|
+
has:
|
|
26
|
+
stopBy: end
|
|
27
|
+
kind: call_expression
|
|
28
|
+
has:
|
|
29
|
+
field: function
|
|
30
|
+
kind: identifier
|
|
31
|
+
regex: '^make(Auto)?Observable$'
|
|
32
|
+
message: |
|
|
33
|
+
A plain `Map` as an observable store field. The field reference is
|
|
34
|
+
observable; the map's CONTENTS are not — `.set()` and `.delete()` mutate in
|
|
35
|
+
place and notify nobody, so anything rendering from this map silently stops
|
|
36
|
+
updating.
|
|
37
|
+
Use `observable.map()`, whose mutations are tracked:
|
|
38
|
+
entries = new Map<string, Row>() → entries = observable.map<string, Row>()
|
|
39
|
+
If this is a private cache nothing renders from, say so in the annotation and
|
|
40
|
+
the rule steps aside:
|
|
41
|
+
makeAutoObservable(this, { _cache: false })
|
|
42
|
+
See the mobx-models skill.
|
|
43
|
+
rule:
|
|
44
|
+
all:
|
|
45
|
+
- kind: public_field_definition
|
|
46
|
+
- has:
|
|
47
|
+
field: name
|
|
48
|
+
pattern: $FIELD
|
|
49
|
+
- has:
|
|
50
|
+
field: value
|
|
51
|
+
kind: new_expression
|
|
52
|
+
has:
|
|
53
|
+
field: constructor
|
|
54
|
+
kind: identifier
|
|
55
|
+
regex: '^Map$'
|
|
56
|
+
- inside:
|
|
57
|
+
stopBy: end
|
|
58
|
+
matches: store-class
|
|
59
|
+
# Annotated non-observable for this exact field name — the legal escape.
|
|
60
|
+
- not:
|
|
61
|
+
inside:
|
|
62
|
+
stopBy: end
|
|
63
|
+
kind: class_declaration
|
|
64
|
+
has:
|
|
65
|
+
stopBy: end
|
|
66
|
+
all:
|
|
67
|
+
- kind: pair
|
|
68
|
+
- has:
|
|
69
|
+
field: key
|
|
70
|
+
pattern: $FIELD
|
|
71
|
+
- has:
|
|
72
|
+
field: value
|
|
73
|
+
regex: '^false$'
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ── the .tsx half of `store-no-runinaction` ──
|
|
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-runinaction.yml`.
|
|
10
|
+
# Suppressing needs THIS id: `ast-grep-ignore: store-no-runinaction-tsx`.
|
|
11
|
+
#
|
|
12
|
+
# ── wave 1, store rule 1 of 7 ──────────────────────────────────────────
|
|
13
|
+
#
|
|
14
|
+
# The report's gap number 1: the most-repeated store rule in the corpus had no
|
|
15
|
+
# mechanical check at all, and the one script that did grep for it
|
|
16
|
+
# (mobx-models/scripts/lint-mobx.sh) was wired to nothing and has since been
|
|
17
|
+
# deleted.
|
|
18
|
+
#
|
|
19
|
+
# Scoped by the CLASS, not by the path. `**/stores/**` would have been the
|
|
20
|
+
# obvious glob and it is the wrong one twice over: it misses a store that lives
|
|
21
|
+
# beside its feature, and it catches plain helper classes that happen to sit in
|
|
22
|
+
# the folder. "Has a make(Auto)Observable call in it" is the actual definition
|
|
23
|
+
# of the thing the rule is about.
|
|
24
|
+
#
|
|
25
|
+
# Note the callee is matched as an identifier rather than with
|
|
26
|
+
# `pattern: makeAutoObservable($$$)`. A pattern does not match a call carrying
|
|
27
|
+
# explicit type arguments, and SiteCraft writes
|
|
28
|
+
# `makeAutoObservable<this, '_buildTimer' | 'buildManager'>(this, {…})` — which
|
|
29
|
+
# silently cost the whole of project.store.ts, 7 real call sites, until the
|
|
30
|
+
# count came up short against the grep.
|
|
31
|
+
id: store-no-runinaction-tsx
|
|
32
|
+
language: tsx
|
|
33
|
+
severity: error
|
|
34
|
+
ignores:
|
|
35
|
+
# A test may drive a store's state directly to set up a scenario; that is the
|
|
36
|
+
# sanctioned way to test a store without a render. All 6 of tg-gallery's
|
|
37
|
+
# runInAction sites are tests, which is why the report counts it as 0.
|
|
38
|
+
- '**/*.test.tsx'
|
|
39
|
+
- '**/*.spec.tsx'
|
|
40
|
+
- '**/__tests__/**'
|
|
41
|
+
utils:
|
|
42
|
+
store-class:
|
|
43
|
+
kind: class_declaration
|
|
44
|
+
has:
|
|
45
|
+
stopBy: end
|
|
46
|
+
kind: call_expression
|
|
47
|
+
has:
|
|
48
|
+
field: function
|
|
49
|
+
kind: identifier
|
|
50
|
+
regex: '^make(Auto)?Observable$'
|
|
51
|
+
message: |
|
|
52
|
+
`runInAction` inside a store class. Every method is already an action —
|
|
53
|
+
`makeAutoObservable` made it one — so this wraps an action in an action and
|
|
54
|
+
buys nothing. Where it appears is the tell: after an `await`, because the
|
|
55
|
+
continuation of an async method is NOT covered by the method's action, and
|
|
56
|
+
each `runInAction` is a hand-patched hole in that coverage.
|
|
57
|
+
Convert the method to a `flow`, which resumes inside an action at every yield
|
|
58
|
+
and needs no patching:
|
|
59
|
+
async load() { const x = await f(); runInAction(() => { this.x = x }) }
|
|
60
|
+
→ load = flow(function* (this: Store) { this.x = yield f() })
|
|
61
|
+
See the mobx-models skill.
|
|
62
|
+
rule:
|
|
63
|
+
all:
|
|
64
|
+
- kind: call_expression
|
|
65
|
+
- has:
|
|
66
|
+
field: function
|
|
67
|
+
kind: identifier
|
|
68
|
+
regex: '^runInAction$'
|
|
69
|
+
- inside:
|
|
70
|
+
stopBy: end
|
|
71
|
+
matches: store-class
|