@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,107 @@
|
|
|
1
|
+
# ── G1, rule 2 of 3: per-row observer extraction ───────────────────────
|
|
2
|
+
#
|
|
3
|
+
# The MobX list rule from CLAUDE.md: mapping an observable collection with the
|
|
4
|
+
# row body written inline makes the PARENT track every field every row reads, so
|
|
5
|
+
# one item's change invalidates the whole list. Extracting the row into its own
|
|
6
|
+
# `observer` component localises the invalidation.
|
|
7
|
+
#
|
|
8
|
+
# The report's audit found this rule already obeyed everywhere it looked (131
|
|
9
|
+
# `*Row.tsx` files in merkle alone) — so it lands as a ratchet on new code, not
|
|
10
|
+
# as a cleanup campaign.
|
|
11
|
+
#
|
|
12
|
+
# Threshold: the mapped element must contain TWO OR MORE nested JSX elements
|
|
13
|
+
# before this fires. That is what separates a row body from a wrapper, and it is
|
|
14
|
+
# why these stay silent by design:
|
|
15
|
+
#
|
|
16
|
+
# {xs.map(x => <li key={x.id}><Row item={x}/></li>)} one nested element
|
|
17
|
+
# {xs.map(x => <li key={x.id}><a href={x.h}>{x.t}</a></li>)} one nested element
|
|
18
|
+
#
|
|
19
|
+
# Both arguably want extracting too, but this rule is `error`, and an error that
|
|
20
|
+
# fires on a one-element wrapper is an error people suppress. Under-match.
|
|
21
|
+
#
|
|
22
|
+
# Also out of scope by design: a `.map` outside JSX (`const rows = xs.map(…)`
|
|
23
|
+
# assigned first, rendered as `{rows}`). Same defect, no JSX context to key on.
|
|
24
|
+
id: inline-map-row
|
|
25
|
+
language: tsx
|
|
26
|
+
severity: error
|
|
27
|
+
ignores:
|
|
28
|
+
- '**/*.test.tsx'
|
|
29
|
+
- '**/*.spec.tsx'
|
|
30
|
+
- '**/__tests__/**'
|
|
31
|
+
# A gallery/preview harness IS the markup it shows; extracting rows out of it
|
|
32
|
+
# would defeat the point of reading it as one page.
|
|
33
|
+
- '**/showcase/**'
|
|
34
|
+
- '**/*.stories.tsx'
|
|
35
|
+
utils:
|
|
36
|
+
jsx-el:
|
|
37
|
+
any:
|
|
38
|
+
- kind: jsx_element
|
|
39
|
+
- kind: jsx_self_closing_element
|
|
40
|
+
message: |
|
|
41
|
+
An inline row body inside `.map(...)`. The parent is an `observer`, so every
|
|
42
|
+
field this body reads is tracked by the PARENT — one item changing a single
|
|
43
|
+
field re-renders the entire list.
|
|
44
|
+
Extract the body into its own `observer` component and pass the item down:
|
|
45
|
+
{items.map(t => <TaskRow key={t.id} task={t} />)}
|
|
46
|
+
The parent then re-renders only on insert, remove and reorder; each row
|
|
47
|
+
re-renders on its own fields. See CLAUDE.md "MobX lists" and the
|
|
48
|
+
mobx-components skill.
|
|
49
|
+
Doesn't apply when the mapped children are pure data — constants or plain
|
|
50
|
+
props rather than observables.
|
|
51
|
+
rule:
|
|
52
|
+
all:
|
|
53
|
+
- kind: jsx_element
|
|
54
|
+
# Anchor the finding on the row ROOT — the element the callback returns —
|
|
55
|
+
# so a deep body reports once at the line you would edit, instead of once
|
|
56
|
+
# per offending element on the way down. Nested JSX elements are direct
|
|
57
|
+
# children of their parent element, so an unqualified `inside` is exactly
|
|
58
|
+
# the "am I the outermost one" test.
|
|
59
|
+
- not:
|
|
60
|
+
inside:
|
|
61
|
+
kind: jsx_element
|
|
62
|
+
# Two or more nested JSX elements: one that has a JSX sibling before it.
|
|
63
|
+
# `stopBy: end` on `follows` is required — tree-sitter puts `jsx_text`
|
|
64
|
+
# nodes between JSX siblings, so the default immediate-sibling check never
|
|
65
|
+
# sees the element next door.
|
|
66
|
+
- has:
|
|
67
|
+
stopBy: end
|
|
68
|
+
all:
|
|
69
|
+
- matches: jsx-el
|
|
70
|
+
- follows:
|
|
71
|
+
stopBy: end
|
|
72
|
+
matches: jsx-el
|
|
73
|
+
# The enclosing component must BE an observer. This is not a narrowing of
|
|
74
|
+
# convenience — it is the rule's own rationale made into its scope: the
|
|
75
|
+
# defect is that the PARENT'S reactive scope tracks every field every row
|
|
76
|
+
# reads, and a component with no reactive scope has no such defect. It is
|
|
77
|
+
# also the only syntactically decidable form of CLAUDE.md's carve-out
|
|
78
|
+
# ("when the rendered children are pure data … the rule doesn't apply").
|
|
79
|
+
#
|
|
80
|
+
# Measured, not assumed: without this clause the rule fired 50 times in
|
|
81
|
+
# byoklab/components — a UI library and its examples/docs site, which
|
|
82
|
+
# contain no MobX whatsoever — where every hit was a false positive at
|
|
83
|
+
# `error` severity. With it, 0 there.
|
|
84
|
+
#
|
|
85
|
+
# A component that reads observables WITHOUT the wrap is a different defect
|
|
86
|
+
# and belongs to `missing-observer`, not here.
|
|
87
|
+
- inside:
|
|
88
|
+
stopBy: end
|
|
89
|
+
pattern: observer($$$)
|
|
90
|
+
- inside:
|
|
91
|
+
stopBy: end
|
|
92
|
+
all:
|
|
93
|
+
- pattern: $ARR.map($$$CB)
|
|
94
|
+
- inside:
|
|
95
|
+
stopBy: end
|
|
96
|
+
kind: jsx_expression
|
|
97
|
+
# CLAUDE.md's own carve-out: "When the rendered children are pure data
|
|
98
|
+
# (constants, plain props, not MobX observables), the rule doesn't
|
|
99
|
+
# apply." A literal array is the one case that is provably constant —
|
|
100
|
+
# observatory's SessionInfoSkeleton maps `[1, 2, 3, 4]` to draw four
|
|
101
|
+
# placeholder cards, which no amount of extraction improves.
|
|
102
|
+
- not:
|
|
103
|
+
has:
|
|
104
|
+
field: function
|
|
105
|
+
has:
|
|
106
|
+
field: object
|
|
107
|
+
kind: array
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ── rule body adopted from faceless-photo-lib `no-instanceof-map-set` (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
|
+
# the old patterns named only Map and Set; WeakMap and WeakSet have exactly the
|
|
5
|
+
# same cross-realm failure and were invisible.
|
|
6
|
+
#
|
|
7
|
+
# The .tsx half of `instanceof-map-set`.
|
|
8
|
+
#
|
|
9
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
|
|
10
|
+
# at `language: typescript` matches `.ts` and NEVER `.tsx`, so `instanceof-map-set`
|
|
11
|
+
# was blind to every React component file. Verified by effect — byte-identical
|
|
12
|
+
# content in `p.ts` and `p.tsx`, the first flagged and the second silent.
|
|
13
|
+
#
|
|
14
|
+
# Three ways to cover both were tried and two were rejected on measurement:
|
|
15
|
+
# - `language: [typescript, tsx]` — ast-grep refuses to parse the rule.
|
|
16
|
+
# - `languageGlobs: {tsx: ["*.ts"]}` — reassigns `.ts` to the tsx language, so
|
|
17
|
+
# EVERY remaining `language: typescript` rule goes dark. Measured corpus-wide:
|
|
18
|
+
# 5 rules blinded, 6,094 findings lost against 334 gained. It would have
|
|
19
|
+
# looked like a coverage win.
|
|
20
|
+
# - two files with the SAME id — "Error: Duplicate rule id".
|
|
21
|
+
# So: a separate id per language. The trailing `-tsx` is what you suppress with
|
|
22
|
+
# in a .tsx file, and the finding prints the id, so copying from the output is
|
|
23
|
+
# always right.
|
|
24
|
+
#
|
|
25
|
+
# Keep this file's `rule:` block IDENTICAL to `instanceof-map-set.yml`. They are one
|
|
26
|
+
# rule wearing two ids because the tool has no way to say "both languages".
|
|
27
|
+
id: instanceof-map-set-tsx
|
|
28
|
+
language: tsx
|
|
29
|
+
severity: error
|
|
30
|
+
message: |
|
|
31
|
+
Do NOT use `instanceof Map / Set`. Data crossing serialization/immer/MobX boundaries
|
|
32
|
+
loses its runtime type. Duck-type capabilities instead, or use a typed accessor.
|
|
33
|
+
See defensive-errors Rule 3 and Rule 4b.
|
|
34
|
+
Suppressing this one needs the id you see above — `ast-grep-ignore: instanceof-map-set-tsx`.
|
|
35
|
+
`instanceof-map-set` is a DIFFERENT rule (the .ts arm) and does not cover it; ast-grep
|
|
36
|
+
matches suppression ids exactly. A bare `// ast-grep-ignore` suppresses every
|
|
37
|
+
rule on the line, and a wrong id is reported back as `unused-suppression`.
|
|
38
|
+
rule:
|
|
39
|
+
kind: binary_expression
|
|
40
|
+
all:
|
|
41
|
+
- has:
|
|
42
|
+
field: operator
|
|
43
|
+
regex: "^instanceof$"
|
|
44
|
+
- has:
|
|
45
|
+
field: right
|
|
46
|
+
regex: "^(Map|Set|WeakMap|WeakSet)$"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ── rule body adopted from faceless-photo-lib `no-instanceof-map-set` (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
|
+
# the old patterns named only Map and Set; WeakMap and WeakSet have exactly the
|
|
5
|
+
# same cross-realm failure and were invisible.
|
|
6
|
+
#
|
|
7
|
+
id: instanceof-map-set
|
|
8
|
+
language: typescript
|
|
9
|
+
severity: error
|
|
10
|
+
message: |
|
|
11
|
+
Do NOT use `instanceof Map / Set`. Data crossing serialization/immer/MobX boundaries
|
|
12
|
+
loses its runtime type. Duck-type capabilities instead, or use a typed accessor.
|
|
13
|
+
See defensive-errors Rule 3 and Rule 4b.
|
|
14
|
+
rule:
|
|
15
|
+
kind: binary_expression
|
|
16
|
+
all:
|
|
17
|
+
- has:
|
|
18
|
+
field: operator
|
|
19
|
+
regex: "^instanceof$"
|
|
20
|
+
- has:
|
|
21
|
+
field: right
|
|
22
|
+
regex: "^(Map|Set|WeakMap|WeakSet)$"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
id: json-roundtrip
|
|
2
|
+
language: typescript
|
|
3
|
+
severity: warning
|
|
4
|
+
message: |
|
|
5
|
+
JSON.parse(JSON.stringify(x)) loses Maps, Sets, TypedArrays, class instances, circular refs.
|
|
6
|
+
Use a typed serializer with Zod validation at the boundary.
|
|
7
|
+
See defensive-errors Rule 5.
|
|
8
|
+
rule:
|
|
9
|
+
pattern: "JSON.parse(JSON.stringify($X))"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
id: jsx-cond-and
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: warning
|
|
4
|
+
message: |
|
|
5
|
+
`{cond && <JSX/>}` in JSX silently drops the !cond branch (renders falsy).
|
|
6
|
+
Use the `<ShowWhen cond={cond}>{() => <JSX/>}</ShowWhen>` helper — it's an
|
|
7
|
+
observer (MobX-reactive) and communicates intent "show this subtree when cond".
|
|
8
|
+
For branches where both arms render something non-null, use match().exhaustive().
|
|
9
|
+
See defensive-errors Rule 1 & 2.
|
|
10
|
+
rule:
|
|
11
|
+
any:
|
|
12
|
+
# Self-closing: {cond && <Foo />}
|
|
13
|
+
- pattern: $COND && <$TAG />
|
|
14
|
+
# With children: {cond && <Foo>…</Foo>}
|
|
15
|
+
- pattern: $COND && <$TAG>$$$CHILDREN</$TAG>
|
|
16
|
+
- pattern: $COND && <$TAG $$$PROPS>$$$CHILDREN</$TAG>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
id: jsx-ternary
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: warning
|
|
4
|
+
utils:
|
|
5
|
+
# A JSX subtree, however many redundant parens wrap it.
|
|
6
|
+
is-jsx:
|
|
7
|
+
any:
|
|
8
|
+
- kind: jsx_element
|
|
9
|
+
- kind: jsx_self_closing_element
|
|
10
|
+
- all:
|
|
11
|
+
- kind: parenthesized_expression
|
|
12
|
+
- has:
|
|
13
|
+
matches: is-jsx
|
|
14
|
+
message: |
|
|
15
|
+
`cond ? <A/> : <B/>` branches on state with a ternary — the other half of the
|
|
16
|
+
ban `jsx-cond-and` enforces (react.md: "NEVER branch on state with
|
|
17
|
+
`if`/`switch`/ternary or `{cond && <JSX/>}`").
|
|
18
|
+
Both arms render, so nothing leaks into the DOM — what it costs is the build
|
|
19
|
+
break: widen what is being branched on and the second arm silently absorbs
|
|
20
|
+
the new case.
|
|
21
|
+
cond ? <A/> : <B/> → match(value).with(…, () => <A/>).otherwise(() => <B/>)
|
|
22
|
+
or a two-arm .exhaustive() when the union is closed
|
|
23
|
+
xs.length === 0 ? … → match(xs).with([], () => <Empty/>).otherwise(…)
|
|
24
|
+
cond ? <A/> : null → <ShowWhen cond={cond}>{() => <A/>}</ShowWhen>
|
|
25
|
+
Match the value, not a boolean derived from it — that is what makes adding a
|
|
26
|
+
variant a type error. See defensive-errors Rule 1 & 2.
|
|
27
|
+
rule:
|
|
28
|
+
kind: ternary_expression
|
|
29
|
+
any:
|
|
30
|
+
- has:
|
|
31
|
+
field: consequence
|
|
32
|
+
matches: is-jsx
|
|
33
|
+
- has:
|
|
34
|
+
field: alternative
|
|
35
|
+
matches: is-jsx
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
id: kind-if-without-match
|
|
2
|
+
language: typescript
|
|
3
|
+
severity: warning
|
|
4
|
+
message: |
|
|
5
|
+
Discriminated-union branching with if/else silently ignores new variants.
|
|
6
|
+
Use match($X).with({kind: ...}, ...).exhaustive() from ts-pattern.
|
|
7
|
+
See defensive-errors Rule 2 and the pattern-matching skill.
|
|
8
|
+
rule:
|
|
9
|
+
any:
|
|
10
|
+
- pattern: "if ($X.kind === $_) $_"
|
|
11
|
+
- pattern: "if ($X.type === $_) $_"
|
|
12
|
+
- pattern: "if ($X.status === $_) $_"
|
|
13
|
+
- pattern: "if ($X._obj === $_) $_"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# PROMOTED to canon in wave 2 from PPA `r14-literal-union-in-component` (rule-triage).
|
|
2
|
+
#
|
|
3
|
+
# Generic, not PPA-specific: the house store layout already states it — report §8
|
|
4
|
+
# puts «string-literal union'ы вроде `type Tab = "tasks" | "habits"`, которым
|
|
5
|
+
# запрещено жить в файлах компонентов» in the `ui/` tier of the four-level model.
|
|
6
|
+
# The PPA original hard-coded `apps/ppa/components/**` and `apps/ppa/app/**`;
|
|
7
|
+
# the globs below are the same intent stated for any repo.
|
|
8
|
+
#
|
|
9
|
+
# 23 hits across tg-gallery + observatory with the PPA globs stripped, so it is
|
|
10
|
+
# a live rule elsewhere and not a local house style.
|
|
11
|
+
id: literal-union-in-component
|
|
12
|
+
language: typescript
|
|
13
|
+
severity: warning
|
|
14
|
+
files:
|
|
15
|
+
- '**/components/**/*.ts'
|
|
16
|
+
- '**/components/**/*.tsx'
|
|
17
|
+
- '**/screens/**/*.ts'
|
|
18
|
+
- '**/screens/**/*.tsx'
|
|
19
|
+
- '**/app/**/*.ts'
|
|
20
|
+
- '**/app/**/*.tsx'
|
|
21
|
+
ignores:
|
|
22
|
+
- '**/__tests__/**'
|
|
23
|
+
- '**/*.test.ts'
|
|
24
|
+
- '**/*.test.tsx'
|
|
25
|
+
- '**/_fixture.ts'
|
|
26
|
+
- '**/_fixtures.ts'
|
|
27
|
+
message: |
|
|
28
|
+
A string-literal union declared inside a component file. Every component that
|
|
29
|
+
touches the same concept redeclares it, and the copies drift — one file grows
|
|
30
|
+
a `"archived"` member the others never hear about, and nothing fails to build
|
|
31
|
+
because each copy is internally consistent.
|
|
32
|
+
Move it to where the state it describes lives:
|
|
33
|
+
- a UI-tier store, next to the actions that mutate it, when it IS state
|
|
34
|
+
(`type Tab = "tasks" | "habits"` belongs to the store that owns the tab), or
|
|
35
|
+
- a shared module as a typed constant + alias when it is purely lexical
|
|
36
|
+
(file kinds, palette names) with no state attached.
|
|
37
|
+
Component prop types then consume the shared type instead of restating it.
|
|
38
|
+
See the mobx-models skill and report §8.
|
|
39
|
+
rule:
|
|
40
|
+
all:
|
|
41
|
+
- kind: type_alias_declaration
|
|
42
|
+
# Two or more STRING members on the union's own spine. The spine matters:
|
|
43
|
+
# tree-sitter nests unions left-associatively, so a 3-member union is
|
|
44
|
+
# `union_type(union_type(lit, lit), lit)` and a 2-member one is
|
|
45
|
+
# `union_type(lit, lit)` — hence the two arms. Searching with `stopBy: end`
|
|
46
|
+
# instead would reach literals nested INSIDE object members and flag
|
|
47
|
+
# `{ status: "idle" } | { status: "ready" }`, the discriminated union this
|
|
48
|
+
# codebase wants everywhere.
|
|
49
|
+
- has:
|
|
50
|
+
field: value
|
|
51
|
+
kind: union_type
|
|
52
|
+
any:
|
|
53
|
+
# three or more members
|
|
54
|
+
- has:
|
|
55
|
+
kind: union_type
|
|
56
|
+
has:
|
|
57
|
+
kind: literal_type
|
|
58
|
+
has:
|
|
59
|
+
kind: string
|
|
60
|
+
# exactly two members
|
|
61
|
+
- has:
|
|
62
|
+
all:
|
|
63
|
+
- kind: literal_type
|
|
64
|
+
- has:
|
|
65
|
+
kind: string
|
|
66
|
+
- follows:
|
|
67
|
+
stopBy: end
|
|
68
|
+
all:
|
|
69
|
+
- kind: literal_type
|
|
70
|
+
- has:
|
|
71
|
+
kind: string
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
id: match-bool-to-null
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: warning
|
|
4
|
+
message: |
|
|
5
|
+
`match(cond).with(true, ...).with(false, () => null).exhaustive()` is the
|
|
6
|
+
ceremonial form of "maybe render this subtree". Use the purpose-built helper:
|
|
7
|
+
<ShowWhen cond={cond}>{() => <JSX/>}</ShowWhen>
|
|
8
|
+
ShowWhen is an observer (MobX-reactive), lazily evaluates children, and
|
|
9
|
+
communicates intent. If BOTH arms render something non-null, keep match().
|
|
10
|
+
rule:
|
|
11
|
+
pattern: |
|
|
12
|
+
match($COND)
|
|
13
|
+
.with(true, () => $$$A)
|
|
14
|
+
.with(false, () => null)
|
|
15
|
+
.exhaustive()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# ── G1, rule 1 of 3: the observer wrap ─────────────────────────────────
|
|
2
|
+
#
|
|
3
|
+
# `missing-observer` has been listed under "Validation" in mobx-components/SKILL.md
|
|
4
|
+
# for months while existing in no rules/ directory at all. The sketch that was
|
|
5
|
+
# published alongside it (ast-grep/SKILL.md) could not have matched the canonical
|
|
6
|
+
# code even if it had been installed: it required the literal body
|
|
7
|
+
#
|
|
8
|
+
# export const $NAME = () => { $$$ useStore() $$$ }
|
|
9
|
+
#
|
|
10
|
+
# — a bare `useStore()` statement. Every real call site destructures
|
|
11
|
+
# (`const { data, ui } = useStore()`), so the pattern matched nothing anywhere in
|
|
12
|
+
# the park. This is the rule written against the shapes that are actually there.
|
|
13
|
+
#
|
|
14
|
+
# Deliberately UNDER-matching, and at `warning` rather than `error`. "Reads
|
|
15
|
+
# observable data" is not a syntactic property — a component can hold an
|
|
16
|
+
# observable that arrived through any prop under any name. Guessing wider buys
|
|
17
|
+
# false positives, and a rule that cries wolf is a rule people turn off. Two
|
|
18
|
+
# arms only, both of which name the store at the call site:
|
|
19
|
+
#
|
|
20
|
+
# 1. it calls a `use…Store` hook, or
|
|
21
|
+
# 2. it takes a prop literally named `model` or `store` AND dereferences it.
|
|
22
|
+
#
|
|
23
|
+
# What it therefore MISSES, on purpose: a component handed an observable under
|
|
24
|
+
# any other prop name (`session={s}`, `photo={p}`). Those still need the wrap;
|
|
25
|
+
# this rule is not the thing that will tell you.
|
|
26
|
+
#
|
|
27
|
+
# Known false-positive class, accepted at warning tier: a `use…Store` hook can
|
|
28
|
+
# return non-observable services next to the stores — `const { platform } =
|
|
29
|
+
# useStore()` in tg-gallery's ShareButton pulls a platform adapter, and a
|
|
30
|
+
# component reading only that does not strictly need the wrap. Wrapping it is
|
|
31
|
+
# harmless; the rule does not distinguish.
|
|
32
|
+
id: missing-observer
|
|
33
|
+
language: tsx
|
|
34
|
+
severity: warning
|
|
35
|
+
ignores:
|
|
36
|
+
- '**/*.test.tsx'
|
|
37
|
+
- '**/*.spec.tsx'
|
|
38
|
+
- '**/__tests__/**'
|
|
39
|
+
- '**/showcase/**'
|
|
40
|
+
- '**/*.stories.tsx'
|
|
41
|
+
utils:
|
|
42
|
+
# A fragment (`<>…</>`) parses as a `jsx_element` with empty tags in this
|
|
43
|
+
# grammar, so these two kinds cover every JSX return shape.
|
|
44
|
+
jsx-node:
|
|
45
|
+
any:
|
|
46
|
+
- kind: jsx_element
|
|
47
|
+
- kind: jsx_self_closing_element
|
|
48
|
+
# `useStore()`, `useGalleryStore()`, `usePageStore()`, `useRootStore()`.
|
|
49
|
+
store-hook-call:
|
|
50
|
+
kind: call_expression
|
|
51
|
+
has:
|
|
52
|
+
field: function
|
|
53
|
+
kind: identifier
|
|
54
|
+
regex: '^use(Store|[A-Z][A-Za-z0-9]*Store)$'
|
|
55
|
+
# Every name a store hook can introduce, in the three spellings the park uses.
|
|
56
|
+
# Binding these is what lets the rule tell `ui.groupBy` (a plain `UIManifest`
|
|
57
|
+
# prop) apart from `uiStore.x` (the store) — without it the rule fires on any
|
|
58
|
+
# dotted value in the markup, which is how observatory's deliberately-plain
|
|
59
|
+
# `RichEntityListView` got flagged for rendering `groupBy={ui.groupBy}`.
|
|
60
|
+
store-binding:
|
|
61
|
+
any:
|
|
62
|
+
# const { data, ui } = useStore()
|
|
63
|
+
- all:
|
|
64
|
+
- kind: shorthand_property_identifier_pattern
|
|
65
|
+
- inside:
|
|
66
|
+
kind: object_pattern
|
|
67
|
+
inside:
|
|
68
|
+
kind: variable_declarator
|
|
69
|
+
has:
|
|
70
|
+
field: value
|
|
71
|
+
matches: store-hook-call
|
|
72
|
+
# const { ui: uiStore } = useStore() → the local alias
|
|
73
|
+
- all:
|
|
74
|
+
- kind: identifier
|
|
75
|
+
- inside:
|
|
76
|
+
kind: pair_pattern
|
|
77
|
+
field: value
|
|
78
|
+
inside:
|
|
79
|
+
kind: object_pattern
|
|
80
|
+
inside:
|
|
81
|
+
kind: variable_declarator
|
|
82
|
+
has:
|
|
83
|
+
field: value
|
|
84
|
+
matches: store-hook-call
|
|
85
|
+
# const store = useStore()
|
|
86
|
+
- all:
|
|
87
|
+
- kind: identifier
|
|
88
|
+
- inside:
|
|
89
|
+
kind: variable_declarator
|
|
90
|
+
field: name
|
|
91
|
+
has:
|
|
92
|
+
field: value
|
|
93
|
+
matches: store-hook-call
|
|
94
|
+
# Read DURING RENDER — which is what creates the subscription an observer
|
|
95
|
+
# wrap is needed to service — as opposed to touched only from a handler or an
|
|
96
|
+
# effect, neither of which subscribes anything.
|
|
97
|
+
#
|
|
98
|
+
# Stated as an exclusion of the two deferred positions rather than as "appears
|
|
99
|
+
# inside JSX", because real components mostly do NOT dereference the store in
|
|
100
|
+
# the markup: tg-gallery's GalleryView reads `const layoutType =
|
|
101
|
+
# themeStore.layout.type` and `match(galleryStore.detailState)`, observatory's
|
|
102
|
+
# SessionsTreeSection reads `data.sessions.filter(...)` — all render reads,
|
|
103
|
+
# none of them inside a JSX expression. An "inside JSX" test missed every one.
|
|
104
|
+
# The test is structural: a read sitting inside a NESTED function is deferred,
|
|
105
|
+
# a read at the component's own body level runs during render.
|
|
106
|
+
#
|
|
107
|
+
# It works because the component is itself a function. A body-level read has
|
|
108
|
+
# exactly one function ancestor — the component. Anything deferred has two or
|
|
109
|
+
# more: `useEffect(() => …)`, `onOpen={(e) => uiStore.open(e.id)}`, and
|
|
110
|
+
# `const onPress = () => ui.close(id)` all put a second function in between.
|
|
111
|
+
# So "is there a function ancestor that itself has a function ancestor" asks
|
|
112
|
+
# exactly the right question, and asks it without enumerating hook names or
|
|
113
|
+
# handler spellings — an enumerated list missed the local-handler form and the
|
|
114
|
+
# good fixture caught it.
|
|
115
|
+
render-position:
|
|
116
|
+
not:
|
|
117
|
+
inside:
|
|
118
|
+
stopBy: end
|
|
119
|
+
all:
|
|
120
|
+
- any:
|
|
121
|
+
- kind: arrow_function
|
|
122
|
+
- kind: function_expression
|
|
123
|
+
- kind: function_declaration
|
|
124
|
+
- inside:
|
|
125
|
+
stopBy: end
|
|
126
|
+
any:
|
|
127
|
+
- kind: arrow_function
|
|
128
|
+
- kind: function_expression
|
|
129
|
+
- kind: function_declaration
|
|
130
|
+
message: |
|
|
131
|
+
A component that reads observable state without an `observer(...)` wrap. It
|
|
132
|
+
renders once with whatever the store held at mount and then goes quiet —
|
|
133
|
+
nothing subscribes it to the fields it read, so the screen silently shows
|
|
134
|
+
stale data until some ancestor happens to re-render it.
|
|
135
|
+
Wrap the component: `export const X = observer(({ model }: Props) => …)`.
|
|
136
|
+
Scope note: this fires only when the store is named at the call site — a
|
|
137
|
+
`use…Store` hook, or a `model`/`store` prop being dereferenced. A component
|
|
138
|
+
handed an observable under any other prop name still needs the wrap and this
|
|
139
|
+
rule will not catch it. See the mobx-components skill.
|
|
140
|
+
rule:
|
|
141
|
+
all:
|
|
142
|
+
- any:
|
|
143
|
+
- kind: variable_declarator
|
|
144
|
+
- kind: function_declaration
|
|
145
|
+
# PascalCase — a component, not a custom hook. A hook may read the store
|
|
146
|
+
# freely; the wrap belongs on whoever renders with it.
|
|
147
|
+
- has:
|
|
148
|
+
field: name
|
|
149
|
+
regex: '^[A-Z][A-Za-z0-9]*$'
|
|
150
|
+
- has:
|
|
151
|
+
stopBy: end
|
|
152
|
+
matches: jsx-node
|
|
153
|
+
# The component must RENDER a field off something the store gave it. Both
|
|
154
|
+
# halves matter: a component that pulls a store only to call an action from
|
|
155
|
+
# a handler needs no wrap (observatory writes two of those on purpose), and
|
|
156
|
+
# a dotted value in the markup means nothing unless the dotted thing came
|
|
157
|
+
# from a store. `$SLICE` ties the two clauses together.
|
|
158
|
+
- any:
|
|
159
|
+
# Arm 1: a binding introduced by a `use…Store` hook, then rendered.
|
|
160
|
+
- all:
|
|
161
|
+
- has:
|
|
162
|
+
stopBy: end
|
|
163
|
+
all:
|
|
164
|
+
- matches: store-binding
|
|
165
|
+
- pattern: $SLICE
|
|
166
|
+
- has:
|
|
167
|
+
stopBy: end
|
|
168
|
+
all:
|
|
169
|
+
- pattern: $SLICE.$FIELD
|
|
170
|
+
- matches: render-position
|
|
171
|
+
# Arm 2: a prop literally named `model` or `store`, then rendered.
|
|
172
|
+
- all:
|
|
173
|
+
- has:
|
|
174
|
+
stopBy: end
|
|
175
|
+
kind: shorthand_property_identifier_pattern
|
|
176
|
+
regex: '^(model|store)$'
|
|
177
|
+
- has:
|
|
178
|
+
stopBy: end
|
|
179
|
+
all:
|
|
180
|
+
- kind: member_expression
|
|
181
|
+
- has:
|
|
182
|
+
field: object
|
|
183
|
+
kind: identifier
|
|
184
|
+
regex: '^(model|store)$'
|
|
185
|
+
- matches: render-position
|
|
186
|
+
- not:
|
|
187
|
+
has:
|
|
188
|
+
stopBy: end
|
|
189
|
+
pattern: observer($$$)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
id: mobx-effect-observable-dep
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: warning
|
|
4
|
+
message: |
|
|
5
|
+
useEffect whose dependency array reads observable state (`someStore.field`).
|
|
6
|
+
React is being used to react to MobX — but MobX already does that, correctly,
|
|
7
|
+
without a render pass in between. A dependency array cannot track an
|
|
8
|
+
observable it did not read during render, so this drifts.
|
|
9
|
+
Derived value → `computed get`. Side effect on state change → a `reaction()`
|
|
10
|
+
in the store (I/O boundaries only). Load on route change → the page-level
|
|
11
|
+
fetch convention, where the dependency is the route param, not a store field.
|
|
12
|
+
Passing the store itself (`[slug, galleryStore]`) is fine — that is a stable
|
|
13
|
+
reference, not a state read.
|
|
14
|
+
rule:
|
|
15
|
+
all:
|
|
16
|
+
- pattern: useEffect($CB, [$$$DEPS])
|
|
17
|
+
- has:
|
|
18
|
+
field: arguments
|
|
19
|
+
has:
|
|
20
|
+
kind: array
|
|
21
|
+
has:
|
|
22
|
+
kind: member_expression
|
|
23
|
+
regex: '[Ss]tore\.'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
id: mobx-effect-store-write
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: error
|
|
4
|
+
message: |
|
|
5
|
+
useEffect that writes to a store. This is `reaction()`-as-state-sync wearing a
|
|
6
|
+
useEffect costume — the same anti-pattern the mobx-models skill already bans,
|
|
7
|
+
just moved into the component tree where it re-runs on render instead of on
|
|
8
|
+
state change.
|
|
9
|
+
Move it into the store: a flow, an action called at the end of the flow that
|
|
10
|
+
produced the state, or a `computed get` if the value is derived.
|
|
11
|
+
See mobx-components "Anti-pattern: reaction() for state synchronization".
|
|
12
|
+
rule:
|
|
13
|
+
all:
|
|
14
|
+
- pattern: useEffect($$$)
|
|
15
|
+
- has:
|
|
16
|
+
stopBy: end
|
|
17
|
+
kind: assignment_expression
|
|
18
|
+
has:
|
|
19
|
+
field: left
|
|
20
|
+
kind: member_expression
|
|
21
|
+
regex: '[Ss]tore\.'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
id: mobx-usestate-from-store
|
|
2
|
+
language: tsx
|
|
3
|
+
severity: warning
|
|
4
|
+
message: |
|
|
5
|
+
useState seeded from observable state. Now there are two copies of the same
|
|
6
|
+
value and nothing keeps them in sync — which is why the next line is usually a
|
|
7
|
+
useEffect that re-assigns it on every id change (with an eslint/biome ignore
|
|
8
|
+
comment admitting the dependency list is a lie).
|
|
9
|
+
A draft being edited is a real thing — put it in the store (or a small
|
|
10
|
+
UI-state model) so it is observable, or reset it with `key={id}` on the child
|
|
11
|
+
instead of syncing it by effect. Read-only display needs no local copy at all.
|
|
12
|
+
rule:
|
|
13
|
+
all:
|
|
14
|
+
- pattern: useState($INIT)
|
|
15
|
+
- has:
|
|
16
|
+
field: arguments
|
|
17
|
+
has:
|
|
18
|
+
stopBy: end
|
|
19
|
+
kind: member_expression
|
|
20
|
+
regex: '[Ss]tore\.'
|