@ashstack/lint 0.1.0 → 0.2.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 +1 -1
- package/dist/core/index.js +1 -222
- package/dist/core/rules/core/components-tsx-only.js +1 -37
- package/dist/core/rules/core/hoist-intl.js +1 -32
- package/dist/core/rules/core/index.js +1 -18
- package/dist/core/rules/core/no-comments.js +1 -194
- package/dist/core/rules/core/no-naming-convention.js +1 -143
- package/dist/core/rules/core/use-design-system.js +1 -147
- package/dist/core/rules/zod/index.js +1 -12
- package/dist/core/rules/zod/prefer-enum.js +1 -44
- package/dist/index.js +1 -3
- package/dist/lib/ast.js +1 -162
- package/dist/lib/detect.js +1 -42
- package/dist/lib/merge.js +1 -18
- package/dist/lib/module.js +1 -49
- package/dist/lib/types.js +1 -1
- package/dist/react/index.js +1 -126
- package/dist/react/rules/i18n/index.js +1 -16
- package/dist/react/rules/i18n/no-bare-attrs.js +1 -38
- package/dist/react/rules/i18n/no-bare-text.js +1 -26
- package/dist/react/rules/i18n/no-bare-toast.js +1 -38
- package/dist/react/rules/query/index.js +1 -20
- package/dist/react/rules/query/next-page-param-undefined.js +1 -26
- package/dist/react/rules/query/no-deprecated-filters.js +1 -63
- package/dist/react/rules/query/no-fetch-in-query-fn.js +1 -22
- package/dist/react/rules/query/no-inline-keys.js +1 -107
- package/dist/react/rules/query/require-destructured-hooks.js +1 -41
- package/dist/react/rules/query/shared.js +1 -10
- package/dist/react/rules/zustand/index.js +1 -12
- package/dist/react/rules/zustand/require-selector.js +1 -62
- package/dist/react-native/index.js +1 -113
- package/dist/react-native/rules/keyboard/index.js +1 -18
- package/dist/react-native/rules/legend-list/index.js +1 -51
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +1 -77
- package/dist/react-native/rules/legend-list/no-index-key-extractor.js +1 -26
- package/dist/react-native/rules/legend-list/no-inline-data.js +1 -25
- package/dist/react-native/rules/legend-list/no-inline-extra-data.js +1 -21
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +1 -35
- package/dist/react-native/rules/legend-list/no-mixed-children.js +1 -24
- package/dist/react-native/rules/legend-list/no-remount-key.js +1 -18
- package/dist/react-native/rules/legend-list/no-scrollview-map.js +1 -27
- package/dist/react-native/rules/legend-list/no-unsupported-props.js +1 -29
- package/dist/react-native/rules/legend-list/required-props.js +1 -23
- package/dist/react-native/rules/legend-list/shared.js +1 -25
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +1 -49
- package/dist/react-native/rules/legend-state/index.js +1 -35
- package/dist/react-native/rules/legend-state/naming.js +1 -26
- package/dist/react-native/rules/legend-state/no-assignment.js +1 -31
- package/dist/react-native/rules/legend-state/no-nested-observable.js +1 -23
- package/dist/react-native/rules/legend-state/no-object-selector.js +1 -47
- package/dist/react-native/rules/legend-state/no-observable-in-component.js +1 -46
- package/dist/react-native/rules/legend-state/no-peek-in-selector.js +1 -67
- package/dist/react-native/rules/legend-state/no-react-mirror.js +1 -33
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +1 -52
- package/dist/react-native/rules/legend-state/shared.js +1 -23
- package/dist/react-native/rules/react-native/hoist-stateless-function.js +1 -59
- package/dist/react-native/rules/react-native/index.js +1 -50
- package/dist/react-native/rules/react-native/no-conditional-style-array.js +1 -23
- package/dist/react-native/rules/react-native/no-dynamic-import.js +1 -18
- package/dist/react-native/rules/react-native/no-keyboard-will-events.js +1 -18
- package/dist/react-native/rules/react-native/no-leaked-render.js +1 -43
- package/dist/react-native/rules/react-native/no-manual-memo.js +1 -87
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +1 -36
- package/dist/react-native/rules/react-native/no-rn-image-network-source.js +1 -58
- package/dist/react-native/rules/react-native/no-rn-namespace-import.js +1 -38
- package/dist/react-native/rules/react-native/no-scroll-position-state.js +1 -26
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +1 -62
- package/dist/react-native/rules/reanimated/animated-reaction-safety.js +1 -110
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +1 -72
- package/dist/react-native/rules/reanimated/animated-updater-purity.js +1 -38
- package/dist/react-native/rules/reanimated/gpu-properties-only.js +1 -80
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +1 -43
- package/dist/react-native/rules/reanimated/index.js +1 -51
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +1 -41
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +1 -86
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +1 -112
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +1 -24
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +1 -21
- package/dist/react-native/rules/reanimated/shared-value-usage.js +1 -105
- package/dist/react-native/rules/reanimated/shared.js +1 -13
- package/dist/react-native/rules/skia/canvas-opaque.js +1 -49
- package/dist/react-native/rules/skia/index.js +1 -21
- package/dist/react-native/rules/turbo-image/index.js +1 -14
- package/dist/react-native/rules/turbo-image/require-cache-policy.js +1 -21
- package/dist/react-native/rules/turbo-image/require-resize.js +1 -22
- package/dist/react-native/rules/unistyles/animated-theme.js +1 -37
- package/dist/react-native/rules/unistyles/content-container.js +1 -73
- package/dist/react-native/rules/unistyles/in-sheet.js +1 -116
- package/dist/react-native/rules/unistyles/index.js +1 -58
- package/dist/react-native/rules/unistyles/insets.js +1 -92
- package/dist/react-native/rules/unistyles/no-hardcoded-color.js +1 -30
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +1 -78
- package/dist/react-native/rules/unistyles/no-margin.js +1 -19
- package/dist/react-native/rules/unistyles/no-style-spread.js +1 -41
- package/dist/react-native/rules/unistyles/no-unused-styles.js +1 -99
- package/dist/react-native/rules/unistyles/rtl-style-call.js +1 -42
- package/dist/react-native/rules/unistyles/shared.js +1 -70
- package/dist/react-native/rules/unistyles/theme-screen-component.js +1 -22
- package/dist/react-native/rules/unistyles/theme-style-attr.js +1 -18
- package/dist/react-native/stylesheet.js +1 -44
- package/package.json +3 -4
- package/RULES.md +0 -2881
package/dist/react/index.js
CHANGED
|
@@ -1,126 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import core, { coreModules } from "../core/index.js";
|
|
3
|
-
import { mergeConfigs } from "../lib/merge.js";
|
|
4
|
-
import { composeModules } from "../lib/module.js";
|
|
5
|
-
import i18nModule from "./rules/i18n/index.js";
|
|
6
|
-
import queryModule from "./rules/query/index.js";
|
|
7
|
-
import zustandModule from "./rules/zustand/index.js";
|
|
8
|
-
export const reactModules = [queryModule, zustandModule, i18nModule];
|
|
9
|
-
const ALLOW_EMPTY_NOOP_HANDLERS = {
|
|
10
|
-
"eslint/no-empty": "off",
|
|
11
|
-
"eslint/no-empty-function": "off",
|
|
12
|
-
};
|
|
13
|
-
const REACT_COMPILER_RULES = {
|
|
14
|
-
"react/error-boundaries": "error",
|
|
15
|
-
"react/globals": "error",
|
|
16
|
-
"react/immutability": "error",
|
|
17
|
-
"react/incompatible-library": "error",
|
|
18
|
-
"react/preserve-manual-memoization": "error",
|
|
19
|
-
"react/purity": "error",
|
|
20
|
-
"react/refs": "error",
|
|
21
|
-
"react/set-state-in-effect": "error",
|
|
22
|
-
"react/set-state-in-render": "error",
|
|
23
|
-
"react/static-components": "error",
|
|
24
|
-
"react/use-memo": "error",
|
|
25
|
-
"react/void-use-memo": "error",
|
|
26
|
-
"react/capitalized-calls": "error",
|
|
27
|
-
"react/exhaustive-effect-dependencies": "error",
|
|
28
|
-
"react/hooks": "error",
|
|
29
|
-
"react/memo-dependencies": "error",
|
|
30
|
-
"react/no-deriving-state-in-effects": "error",
|
|
31
|
-
"react/invariant": "error",
|
|
32
|
-
"react/rule-suppression": "error",
|
|
33
|
-
"react/syntax": "error",
|
|
34
|
-
"react/todo": "error",
|
|
35
|
-
"react/unsupported-syntax": "error",
|
|
36
|
-
};
|
|
37
|
-
const FILE_BASED_ROUTER_FILES = ["**/routes/**", "**/src/app/**", "**/app/**/_layout.tsx", "**/app/**/+*.tsx"];
|
|
38
|
-
const REACT_RULES = {
|
|
39
|
-
...ALLOW_EMPTY_NOOP_HANDLERS,
|
|
40
|
-
"max-lines-per-function": ["error", { max: 120, skipBlankLines: true, skipComments: true }],
|
|
41
|
-
"react/react-in-jsx-scope": "off",
|
|
42
|
-
"react/style-prop-object": "off",
|
|
43
|
-
"react/only-export-components": "off",
|
|
44
|
-
"react/rules-of-hooks": "error",
|
|
45
|
-
"react/exhaustive-deps": "error",
|
|
46
|
-
"react/button-has-type": "error",
|
|
47
|
-
"react/checked-requires-onchange-or-readonly": "error",
|
|
48
|
-
"react/iframe-missing-sandbox": "error",
|
|
49
|
-
"react/jsx-boolean-value": "error",
|
|
50
|
-
"react/jsx-key": "error",
|
|
51
|
-
"react/jsx-no-comment-textnodes": "error",
|
|
52
|
-
"react/jsx-no-constructed-context-values": "error",
|
|
53
|
-
"react/jsx-no-duplicate-props": "error",
|
|
54
|
-
"react/jsx-no-target-blank": "error",
|
|
55
|
-
"react/jsx-no-useless-fragment": "error",
|
|
56
|
-
"react/no-array-index-key": "error",
|
|
57
|
-
"react/no-children-prop": "error",
|
|
58
|
-
"react/no-danger": "error",
|
|
59
|
-
"react/no-danger-with-children": "error",
|
|
60
|
-
"react/no-direct-mutation-state": "error",
|
|
61
|
-
"react/no-is-mounted": "error",
|
|
62
|
-
"react/no-render-return-value": "error",
|
|
63
|
-
"react/no-string-refs": "error",
|
|
64
|
-
"react/no-this-in-sfc": "error",
|
|
65
|
-
"react/no-unsafe": "error",
|
|
66
|
-
"react/no-unescaped-entities": "error",
|
|
67
|
-
"react/no-unstable-nested-components": ["error", { allowAsProps: true }],
|
|
68
|
-
"react/require-render-return": "error",
|
|
69
|
-
"react/self-closing-comp": "error",
|
|
70
|
-
"react/void-dom-elements-no-children": "error",
|
|
71
|
-
...REACT_COMPILER_RULES,
|
|
72
|
-
"jsx-a11y/alt-text": "error",
|
|
73
|
-
"jsx-a11y/anchor-has-content": "error",
|
|
74
|
-
"jsx-a11y/anchor-is-valid": "error",
|
|
75
|
-
"jsx-a11y/click-events-have-key-events": "off",
|
|
76
|
-
"jsx-a11y/control-has-associated-label": "off",
|
|
77
|
-
"jsx-a11y/heading-has-content": "error",
|
|
78
|
-
"jsx-a11y/img-redundant-alt": "error",
|
|
79
|
-
"jsx-a11y/interactive-supports-focus": "error",
|
|
80
|
-
"jsx-a11y/label-has-associated-control": "error",
|
|
81
|
-
"jsx-a11y/no-aria-hidden-on-focusable": "error",
|
|
82
|
-
"jsx-a11y/no-autofocus": ["error", { ignoreNonDOM: true }],
|
|
83
|
-
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
|
84
|
-
"jsx-a11y/no-noninteractive-tabindex": "error",
|
|
85
|
-
"jsx-a11y/no-redundant-roles": "error",
|
|
86
|
-
"jsx-a11y/no-static-element-interactions": "error",
|
|
87
|
-
"jsx-a11y/prefer-tag-over-role": "off",
|
|
88
|
-
"jsx-a11y/role-has-required-aria-props": "error",
|
|
89
|
-
"jsx-a11y/role-supports-aria-props": "error",
|
|
90
|
-
"react-effect/no-derived-state": "error",
|
|
91
|
-
"react-effect/no-chain-state-updates": "error",
|
|
92
|
-
"react-effect/no-event-handler": "error",
|
|
93
|
-
"react-effect/no-adjust-state-on-prop-change": "error",
|
|
94
|
-
"react-effect/no-reset-all-state-on-prop-change": "error",
|
|
95
|
-
"react-effect/no-pass-live-state-to-parent": "error",
|
|
96
|
-
"react-effect/no-pass-data-to-parent": "error",
|
|
97
|
-
"react-effect/no-external-store-subscription": "error",
|
|
98
|
-
"react-effect/no-initialize-state": "error",
|
|
99
|
-
"unicorn/filename-case": ["error", { cases: { kebabCase: true, pascalCase: true } }],
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* React (web) entry — everything in core plus react, jsx-a11y, React Compiler
|
|
103
|
-
* diagnostics, you-might-not-need-an-effect, and the auto-detected library
|
|
104
|
-
* modules (query, zustand, i18n).
|
|
105
|
-
*/
|
|
106
|
-
const react = (options = {}) => {
|
|
107
|
-
const composed = composeModules([...coreModules, ...reactModules], options);
|
|
108
|
-
return mergeConfigs(core(options), {
|
|
109
|
-
plugins: ["react", "jsx-a11y"],
|
|
110
|
-
jsPlugins: [
|
|
111
|
-
{
|
|
112
|
-
name: "react-effect",
|
|
113
|
-
specifier: fileURLToPath(import.meta.resolve("eslint-plugin-react-you-might-not-need-an-effect")),
|
|
114
|
-
},
|
|
115
|
-
...composed.jsPlugins,
|
|
116
|
-
],
|
|
117
|
-
rules: { ...REACT_RULES, ...composed.rules },
|
|
118
|
-
overrides: [
|
|
119
|
-
{
|
|
120
|
-
files: FILE_BASED_ROUTER_FILES,
|
|
121
|
-
rules: { "unicorn/filename-case": "off" },
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
export default react;
|
|
1
|
+
import{fileURLToPath as o}from"node:url";import t,{coreModules as a}from"../core/index.js";import{mergeConfigs as n}from"../lib/merge.js";import{composeModules as s}from"../lib/module.js";import c from"./rules/i18n/index.js";import i from"./rules/query/index.js";import l from"./rules/zustand/index.js";export const reactModules=[i,l,c];const f={"eslint/no-empty":"off","eslint/no-empty-function":"off"},p={"react/error-boundaries":"error","react/globals":"error","react/immutability":"error","react/incompatible-library":"error","react/preserve-manual-memoization":"error","react/purity":"error","react/refs":"error","react/set-state-in-effect":"error","react/set-state-in-render":"error","react/static-components":"error","react/use-memo":"error","react/void-use-memo":"error","react/capitalized-calls":"error","react/exhaustive-effect-dependencies":"error","react/hooks":"error","react/memo-dependencies":"error","react/no-deriving-state-in-effects":"error","react/invariant":"error","react/rule-suppression":"error","react/syntax":"error","react/todo":"error","react/unsupported-syntax":"error"},u=["**/routes/**","**/src/app/**","**/app/**/_layout.tsx","**/app/**/+*.tsx"],d={...f,"max-lines-per-function":["error",{max:120,skipBlankLines:!0,skipComments:!0}],"react/react-in-jsx-scope":"off","react/style-prop-object":"off","react/only-export-components":"off","react/rules-of-hooks":"error","react/exhaustive-deps":"error","react/button-has-type":"error","react/checked-requires-onchange-or-readonly":"error","react/iframe-missing-sandbox":"error","react/jsx-boolean-value":"error","react/jsx-key":"error","react/jsx-no-comment-textnodes":"error","react/jsx-no-constructed-context-values":"error","react/jsx-no-duplicate-props":"error","react/jsx-no-target-blank":"error","react/jsx-no-useless-fragment":"error","react/no-array-index-key":"error","react/no-children-prop":"error","react/no-danger":"error","react/no-danger-with-children":"error","react/no-direct-mutation-state":"error","react/no-is-mounted":"error","react/no-render-return-value":"error","react/no-string-refs":"error","react/no-this-in-sfc":"error","react/no-unsafe":"error","react/no-unescaped-entities":"error","react/no-unstable-nested-components":["error",{allowAsProps:!0}],"react/require-render-return":"error","react/self-closing-comp":"error","react/void-dom-elements-no-children":"error",...p,"jsx-a11y/alt-text":"error","jsx-a11y/anchor-has-content":"error","jsx-a11y/anchor-is-valid":"error","jsx-a11y/click-events-have-key-events":"off","jsx-a11y/control-has-associated-label":"off","jsx-a11y/heading-has-content":"error","jsx-a11y/img-redundant-alt":"error","jsx-a11y/interactive-supports-focus":"error","jsx-a11y/label-has-associated-control":"error","jsx-a11y/no-aria-hidden-on-focusable":"error","jsx-a11y/no-autofocus":["error",{ignoreNonDOM:!0}],"jsx-a11y/no-noninteractive-element-interactions":"off","jsx-a11y/no-noninteractive-tabindex":"error","jsx-a11y/no-redundant-roles":"error","jsx-a11y/no-static-element-interactions":"error","jsx-a11y/prefer-tag-over-role":"off","jsx-a11y/role-has-required-aria-props":"error","jsx-a11y/role-supports-aria-props":"error","react-effect/no-derived-state":"error","react-effect/no-chain-state-updates":"error","react-effect/no-event-handler":"error","react-effect/no-adjust-state-on-prop-change":"error","react-effect/no-reset-all-state-on-prop-change":"error","react-effect/no-pass-live-state-to-parent":"error","react-effect/no-pass-data-to-parent":"error","react-effect/no-external-store-subscription":"error","react-effect/no-initialize-state":"error","unicorn/filename-case":["error",{cases:{kebabCase:!0,pascalCase:!0}}]},m=(r={})=>{const e=s([...a,...reactModules],r);return n(t(r),{plugins:["react","jsx-a11y"],jsPlugins:[{name:"react-effect",specifier:o(import.meta.resolve("eslint-plugin-react-you-might-not-need-an-effect"))},...e.jsPlugins],rules:{...d,...e.rules},overrides:[{files:u,rules:{"unicorn/filename-case":"off"}}]})};export default m;
|
|
@@ -1,16 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { noBareAttrs } from "./no-bare-attrs.js";
|
|
3
|
-
import { noBareText } from "./no-bare-text.js";
|
|
4
|
-
import { noBareToast } from "./no-bare-toast.js";
|
|
5
|
-
export default defineModule({
|
|
6
|
-
meta: { name: "@ashstack/i18n" },
|
|
7
|
-
url: import.meta.url,
|
|
8
|
-
packages: ["i18next", "react-i18next", "@lingui/core", "react-intl", "use-intl", "next-intl", "expo-localization"],
|
|
9
|
-
option: "i18n",
|
|
10
|
-
docsWhen: "auto-enabled by `react()` when an i18n library (i18next, lingui, react-intl, use-intl, next-intl, expo-localization) is a dependency",
|
|
11
|
-
rules: {
|
|
12
|
-
"no-bare-text": noBareText,
|
|
13
|
-
"no-bare-attrs": noBareAttrs,
|
|
14
|
-
"no-bare-toast": noBareToast,
|
|
15
|
-
},
|
|
16
|
-
});
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{noBareAttrs as t}from"./no-bare-attrs.js";import{noBareText as n}from"./no-bare-text.js";import{noBareToast as a}from"./no-bare-toast.js";export default e({meta:{name:"@ashstack/i18n"},url:import.meta.url,packages:["i18next","react-i18next","@lingui/core","react-intl","use-intl","next-intl","expo-localization"],option:"i18n",docsWhen:"auto-enabled by `react()` when an i18n library (i18next, lingui, react-intl, use-intl, next-intl, expo-localization) is a dependency",rules:{"no-bare-text":n,"no-bare-attrs":t,"no-bare-toast":a}});
|
|
@@ -1,38 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
const BARE_JSX_ATTRIBUTE = 'Pass `t("<key>")` as this attribute value and add the key to every locale file.';
|
|
3
|
-
const NATIVE_TRANSLATABLE_ATTRIBUTES = ["placeholder", "accessibilityLabel", "accessibilityHint", "title"];
|
|
4
|
-
export const noBareAttrs = {
|
|
5
|
-
meta: {
|
|
6
|
-
type: "problem",
|
|
7
|
-
docs: {
|
|
8
|
-
description: "Disallow a plain string literal on a configurable list of user-visible JSX attributes, defaulting to placeholder, accessibilityLabel, accessibilityHint and title.",
|
|
9
|
-
},
|
|
10
|
-
schema: [
|
|
11
|
-
{
|
|
12
|
-
type: "object",
|
|
13
|
-
properties: { attributes: { type: "array", items: { type: "string" }, minItems: 1 } },
|
|
14
|
-
additionalProperties: false,
|
|
15
|
-
},
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
createOnce(context) {
|
|
19
|
-
const attributes = new Set();
|
|
20
|
-
return {
|
|
21
|
-
before() {
|
|
22
|
-
attributes.clear();
|
|
23
|
-
const configured = optionsOf(context, {}).attributes;
|
|
24
|
-
for (const attribute of configured ?? NATIVE_TRANSLATABLE_ATTRIBUTES) {
|
|
25
|
-
attributes.add(attribute);
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
JSXAttribute(node) {
|
|
29
|
-
if (!attributes.has(attributeName(node)))
|
|
30
|
-
return;
|
|
31
|
-
const { value } = node;
|
|
32
|
-
if (value?.type !== "Literal" || typeof value.value !== "string" || value.value.length === 0)
|
|
33
|
-
return;
|
|
34
|
-
context.report({ node, message: BARE_JSX_ATTRIBUTE });
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
};
|
|
1
|
+
import{attributeName as r,optionsOf as s}from"../../../lib/ast.js";const l='Pass `t("<key>")` as this attribute value and add the key to every locale file.',o=["placeholder","accessibilityLabel","accessibilityHint","title"];export const noBareAttrs={meta:{type:"problem",docs:{description:"Disallow a plain string literal on a configurable list of user-visible JSX attributes, defaulting to placeholder, accessibilityLabel, accessibilityHint and title."},schema:[{type:"object",properties:{attributes:{type:"array",items:{type:"string"},minItems:1}},additionalProperties:!1}]},createOnce(a){const i=new Set;return{before(){i.clear();const t=s(a,{}).attributes;for(const e of t??o)i.add(e)},JSXAttribute(t){if(!i.has(r(t)))return;const{value:e}=t;if(e?.type!=="Literal"||typeof e.value!=="string"||e.value.length===0)return;a.report({node:t,message:l})}}}};
|
|
@@ -1,26 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
const BARE_JSX_TEXT = 'Wrap this text in `t("<key>")`, or `<Trans>` when it holds markup, and add the key to every locale file.';
|
|
3
|
-
const I18N_COMPONENTS = new Set(["Trans", "Plural", "Select"]);
|
|
4
|
-
const BARE_TEXT = /^[A-Za-z][^<{}]{2,}$/;
|
|
5
|
-
export const noBareText = problem("Disallow a JSX element that carries no attributes and whose single child is plain literal text.", {
|
|
6
|
-
createOnce(context) {
|
|
7
|
-
return {
|
|
8
|
-
JSXElement(node) {
|
|
9
|
-
const opening = node.openingElement;
|
|
10
|
-
if (opening.attributes.length > 0)
|
|
11
|
-
return;
|
|
12
|
-
const children = node.children.filter(child => child.type !== "JSXText" || child.value.trim() !== "");
|
|
13
|
-
if (children.length !== 1)
|
|
14
|
-
return;
|
|
15
|
-
const [only] = children;
|
|
16
|
-
if (only?.type !== "JSXText")
|
|
17
|
-
return;
|
|
18
|
-
if (!BARE_TEXT.test(only.value.trim()))
|
|
19
|
-
return;
|
|
20
|
-
if (I18N_COMPONENTS.has(tagIdentifier(opening.name)))
|
|
21
|
-
return;
|
|
22
|
-
context.report({ node: only, message: BARE_JSX_TEXT });
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
});
|
|
1
|
+
import{problem as l,tagIdentifier as o}from"../../../lib/ast.js";const s='Wrap this text in `t("<key>")`, or `<Trans>` when it holds markup, and add the key to every locale file.',c=new Set(["Trans","Plural","Select"]),u=/^[A-Za-z][^<{}]{2,}$/;export const noBareText=l("Disallow a JSX element that carries no attributes and whose single child is plain literal text.",{createOnce(a){return{JSXElement(t){const n=t.openingElement;if(n.attributes.length>0)return;const r=t.children.filter((i)=>i.type!=="JSXText"||i.value.trim()!=="");if(r.length!==1)return;const[e]=r;if(e?.type!=="JSXText")return;if(!u.test(e.value.trim()))return;if(c.has(o(n.name)))return;a.report({node:e,message:s})}}}});
|
|
@@ -1,38 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
const BARE_TOAST = 'Pass `t("<key>")` to this toast instead of the literal, and add the key to every locale file.';
|
|
3
|
-
const TOAST_METHODS = new Set(["success", "error", "info", "warning", "loading", "message"]);
|
|
4
|
-
const isToastCall = (node) => {
|
|
5
|
-
if (node.type !== "CallExpression")
|
|
6
|
-
return false;
|
|
7
|
-
const { callee } = node;
|
|
8
|
-
if (callee.type !== "MemberExpression")
|
|
9
|
-
return false;
|
|
10
|
-
if (callee.computed)
|
|
11
|
-
return false;
|
|
12
|
-
const { object, property } = callee;
|
|
13
|
-
if (object.type !== "Identifier" || object.name !== "toast")
|
|
14
|
-
return false;
|
|
15
|
-
return property.type === "Identifier" && TOAST_METHODS.has(property.name);
|
|
16
|
-
};
|
|
17
|
-
const onlyStringArgument = (node) => {
|
|
18
|
-
if (node.type !== "CallExpression" || node.arguments.length !== 1)
|
|
19
|
-
return undefined;
|
|
20
|
-
const [argument] = node.arguments;
|
|
21
|
-
if (argument?.type !== "Literal" || typeof argument.value !== "string")
|
|
22
|
-
return undefined;
|
|
23
|
-
return argument;
|
|
24
|
-
};
|
|
25
|
-
export const noBareToast = problem("Disallow a `toast.*` call whose only argument is a string literal.", {
|
|
26
|
-
createOnce(context) {
|
|
27
|
-
return {
|
|
28
|
-
CallExpression(node) {
|
|
29
|
-
if (!isToastCall(node))
|
|
30
|
-
return;
|
|
31
|
-
const argument = onlyStringArgument(node);
|
|
32
|
-
if (argument === undefined)
|
|
33
|
-
return;
|
|
34
|
-
context.report({ node: argument, message: BARE_TOAST });
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
});
|
|
1
|
+
import{problem as s}from"../../../lib/ast.js";const a='Pass `t("<key>")` to this toast instead of the literal, and add the key to every locale file.',o=new Set(["success","error","info","warning","loading","message"]),i=(t)=>{if(t.type!=="CallExpression")return!1;const{callee:e}=t;if(e.type!=="MemberExpression")return!1;if(e.computed)return!1;const{object:r,property:n}=e;if(r.type!=="Identifier"||r.name!=="toast")return!1;return n.type==="Identifier"&&o.has(n.name)},l=(t)=>{if(t.type!=="CallExpression"||t.arguments.length!==1)return;const[e]=t.arguments;if(e?.type!=="Literal"||typeof e.value!=="string")return;return e};export const noBareToast=s("Disallow a `toast.*` call whose only argument is a string literal.",{createOnce(t){return{CallExpression(e){if(!i(e))return;const r=l(e);if(r===void 0)return;t.report({node:r,message:a})}}}});
|
|
@@ -1,20 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { nextPageParamUndefined } from "./next-page-param-undefined.js";
|
|
3
|
-
import { noDeprecatedFilters } from "./no-deprecated-filters.js";
|
|
4
|
-
import { noFetchInQueryFn } from "./no-fetch-in-query-fn.js";
|
|
5
|
-
import { noInlineKeys } from "./no-inline-keys.js";
|
|
6
|
-
import { requireDestructuredHooks } from "./require-destructured-hooks.js";
|
|
7
|
-
export default defineModule({
|
|
8
|
-
meta: { name: "@ashstack/query" },
|
|
9
|
-
url: import.meta.url,
|
|
10
|
-
packages: ["@tanstack/react-query"],
|
|
11
|
-
option: "query",
|
|
12
|
-
docsWhen: "auto-enabled by `react()` when `@tanstack/react-query` is a dependency",
|
|
13
|
-
rules: {
|
|
14
|
-
"no-inline-keys": noInlineKeys,
|
|
15
|
-
"no-deprecated-filters": noDeprecatedFilters,
|
|
16
|
-
"require-destructured-hooks": requireDestructuredHooks,
|
|
17
|
-
"no-fetch-in-query-fn": noFetchInQueryFn,
|
|
18
|
-
"next-page-param-undefined": nextPageParamUndefined,
|
|
19
|
-
},
|
|
20
|
-
});
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{nextPageParamUndefined as r}from"./next-page-param-undefined.js";import{noDeprecatedFilters as t}from"./no-deprecated-filters.js";import{noFetchInQueryFn as n}from"./no-fetch-in-query-fn.js";import{noInlineKeys as o}from"./no-inline-keys.js";import{requireDestructuredHooks as a}from"./require-destructured-hooks.js";export default e({meta:{name:"@ashstack/query"},url:import.meta.url,packages:["@tanstack/react-query"],option:"query",docsWhen:"auto-enabled by `react()` when `@tanstack/react-query` is a dependency",rules:{"no-inline-keys":o,"no-deprecated-filters":t,"require-destructured-hooks":a,"no-fetch-in-query-fn":n,"next-page-param-undefined":r}});
|
|
@@ -1,26 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { propertyKeyName } from "./shared.js";
|
|
3
|
-
const NEXT_PAGE_PARAM_NULL = "Return `undefined` to signal there are no more pages. `null` is a valid page param, so returning it reads as a real next cursor and the list keeps fetching forever.";
|
|
4
|
-
const returnsNull = (node) => node.type === "ReturnStatement" && node.argument?.type === "Literal" && node.argument.value === null;
|
|
5
|
-
/** `() => null` returns null without a ReturnStatement to find. */
|
|
6
|
-
const arrowReturningNull = (node) => node.type === "ArrowFunctionExpression" && node.body.type === "Literal" && node.body.value === null
|
|
7
|
-
? node.body
|
|
8
|
-
: null;
|
|
9
|
-
export const nextPageParamUndefined = problem("Disallow `return null` in the body of a `getNextPageParam`.", {
|
|
10
|
-
createOnce(context) {
|
|
11
|
-
return {
|
|
12
|
-
before() {
|
|
13
|
-
return gate(context, "getNextPageParam");
|
|
14
|
-
},
|
|
15
|
-
Property(node) {
|
|
16
|
-
if (propertyKeyName(node) !== "getNextPageParam")
|
|
17
|
-
return;
|
|
18
|
-
const { value } = node;
|
|
19
|
-
const found = arrowReturningNull(value) ?? findInSubtree(value, returnsNull);
|
|
20
|
-
if (!found || crossesFunctionBefore(found, value, FUNCTION_TYPES))
|
|
21
|
-
return;
|
|
22
|
-
context.report({ node: value, message: NEXT_PAGE_PARAM_NULL });
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
});
|
|
1
|
+
import{FUNCTION_TYPES as n,crossesFunctionBefore as o,findInSubtree as u,gate as l,problem as s}from"../../../lib/ast.js";import{propertyKeyName as i}from"./shared.js";const g="Return `undefined` to signal there are no more pages. `null` is a valid page param, so returning it reads as a real next cursor and the list keeps fetching forever.",p=(e)=>e.type==="ReturnStatement"&&e.argument?.type==="Literal"&&e.argument.value===null,m=(e)=>e.type==="ArrowFunctionExpression"&&e.body.type==="Literal"&&e.body.value===null?e.body:null;export const nextPageParamUndefined=s("Disallow `return null` in the body of a `getNextPageParam`.",{createOnce(e){return{before(){return l(e,"getNextPageParam")},Property(t){if(i(t)!=="getNextPageParam")return;const{value:r}=t,a=m(r)??u(r,p);if(!a||o(a,r,n))return;e.report({node:r,message:g})}}}});
|
|
@@ -1,63 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const FILTER_METHODS = new Set([
|
|
3
|
-
"invalidateQueries",
|
|
4
|
-
"removeQueries",
|
|
5
|
-
"refetchQueries",
|
|
6
|
-
"cancelQueries",
|
|
7
|
-
"resetQueries",
|
|
8
|
-
]);
|
|
9
|
-
const isFilterMethodCall = (node) => {
|
|
10
|
-
if (node.type !== "CallExpression")
|
|
11
|
-
return false;
|
|
12
|
-
const { callee } = node;
|
|
13
|
-
if (callee.type !== "MemberExpression" || callee.computed)
|
|
14
|
-
return false;
|
|
15
|
-
const { property } = callee;
|
|
16
|
-
return property.type === "Identifier" && FILTER_METHODS.has(property.name);
|
|
17
|
-
};
|
|
18
|
-
const onlyArgument = (node) => {
|
|
19
|
-
if (node.type !== "CallExpression" || node.arguments.length !== 1)
|
|
20
|
-
return undefined;
|
|
21
|
-
return node.arguments[0];
|
|
22
|
-
};
|
|
23
|
-
const isStringLiteral = (node) => node.type === "Literal" && typeof node.value === "string";
|
|
24
|
-
const filterObjectFor = (context, argument) => {
|
|
25
|
-
if (argument.type === "ArrayExpression")
|
|
26
|
-
return `{ queryKey: ${context.sourceCode.getText(argument)} }`;
|
|
27
|
-
if (isStringLiteral(argument))
|
|
28
|
-
return `{ queryKey: [${context.sourceCode.getText(argument)}] }`;
|
|
29
|
-
return null;
|
|
30
|
-
};
|
|
31
|
-
export const noDeprecatedFilters = {
|
|
32
|
-
meta: {
|
|
33
|
-
type: "problem",
|
|
34
|
-
docs: {
|
|
35
|
-
description: "Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form.",
|
|
36
|
-
},
|
|
37
|
-
hasSuggestions: true,
|
|
38
|
-
},
|
|
39
|
-
createOnce(context) {
|
|
40
|
-
return {
|
|
41
|
-
CallExpression(node) {
|
|
42
|
-
if (!isFilterMethodCall(node))
|
|
43
|
-
return;
|
|
44
|
-
const argument = onlyArgument(node);
|
|
45
|
-
if (argument === undefined)
|
|
46
|
-
return;
|
|
47
|
-
const filterObject = filterObjectFor(context, argument);
|
|
48
|
-
if (filterObject === null)
|
|
49
|
-
return;
|
|
50
|
-
context.report({
|
|
51
|
-
node: argument,
|
|
52
|
-
message: DEPRECATED_FILTERS,
|
|
53
|
-
suggest: [
|
|
54
|
-
{
|
|
55
|
-
desc: "Wrap in a filter object",
|
|
56
|
-
fix: fixer => fixer.replaceText(argument, filterObject),
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
});
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
};
|
|
1
|
+
const n="Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })`. TanStack Query v5 removed the positional `(queryKey)` form.",o=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries"]),a=(e)=>{if(e.type!=="CallExpression")return!1;const{callee:r}=e;if(r.type!=="MemberExpression"||r.computed)return!1;const{property:t}=r;return t.type==="Identifier"&&o.has(t.name)},l=(e)=>{if(e.type!=="CallExpression"||e.arguments.length!==1)return;return e.arguments[0]},u=(e)=>e.type==="Literal"&&typeof e.value==="string",c=(e,r)=>{if(r.type==="ArrayExpression")return`{ queryKey: ${e.sourceCode.getText(r)} }`;if(u(r))return`{ queryKey: [${e.sourceCode.getText(r)}] }`;return null};export const noDeprecatedFilters={meta:{type:"problem",docs:{description:"Disallow the positional key argument that TanStack Query v5 removed from `invalidateQueries` and its sibling methods. The suggestion rewrites it to the filter-object form."},hasSuggestions:!0},createOnce(e){return{CallExpression(r){if(!a(r))return;const t=l(r);if(t===void 0)return;const s=c(e,t);if(s===null)return;e.report({node:t,message:n,suggest:[{desc:"Wrap in a filter object",fix:(i)=>i.replaceText(t,s)}]})}}}};
|
|
@@ -1,22 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { propertyKeyName } from "./shared.js";
|
|
3
|
-
const FETCH_IN_QUERY_FN = "Call a typed function from the feature's `*.requests.ts` module here instead of `fetch`. That client is what sends the auth header, applies the timeout, retries a 429 or 5xx, and throws the app's error type.";
|
|
4
|
-
const QUERY_FN_KEYS = new Set(["queryFn", "mutationFn"]);
|
|
5
|
-
/** The global `fetch`, not a typed client's `api.fetch()`. */
|
|
6
|
-
const isBareFetch = (node) => node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "fetch";
|
|
7
|
-
export const noFetchInQueryFn = problem("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.", {
|
|
8
|
-
createOnce(context) {
|
|
9
|
-
return {
|
|
10
|
-
before() {
|
|
11
|
-
return gate(context, "queryFn", "mutationFn");
|
|
12
|
-
},
|
|
13
|
-
Property(node) {
|
|
14
|
-
if (!QUERY_FN_KEYS.has(propertyKeyName(node)))
|
|
15
|
-
return;
|
|
16
|
-
if (!subtreeHas(node.value, isBareFetch))
|
|
17
|
-
return;
|
|
18
|
-
context.report({ node: node.value, message: FETCH_IN_QUERY_FN });
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
});
|
|
1
|
+
import{gate as r,problem as a,subtreeHas as n}from"../../../lib/ast.js";import{propertyKeyName as s}from"./shared.js";const o="Call a typed function from the feature's `*.requests.ts` module here instead of `fetch`. That client is what sends the auth header, applies the timeout, retries a 429 or 5xx, and throws the app's error type.",i=new Set(["queryFn","mutationFn"]),u=(e)=>e.type==="CallExpression"&&e.callee.type==="Identifier"&&e.callee.name==="fetch";export const noFetchInQueryFn=a("Disallow a bare `fetch(` inside a `queryFn` or `mutationFn`.",{createOnce(e){return{before(){return r(e,"queryFn","mutationFn")},Property(t){if(!i.has(s(t)))return;if(!n(t.value,u))return;e.report({node:t.value,message:o})}}}});
|
|
@@ -1,107 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { propertyKeyName } from "./shared.js";
|
|
3
|
-
const MESSAGES = {
|
|
4
|
-
inlineQueryKey: "Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`, so invalidation elsewhere matches the same key.",
|
|
5
|
-
inlineGetQueryData: "Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",
|
|
6
|
-
inlineSetQueryData: "Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`.",
|
|
7
|
-
};
|
|
8
|
-
const QUERY_KEY_HOOKS = new Set([
|
|
9
|
-
"useQuery",
|
|
10
|
-
"useSuspenseQuery",
|
|
11
|
-
"useInfiniteQuery",
|
|
12
|
-
"useSuspenseInfiniteQuery",
|
|
13
|
-
"useQueries",
|
|
14
|
-
]);
|
|
15
|
-
const QUERY_KEY_METHODS = new Set([
|
|
16
|
-
"invalidateQueries",
|
|
17
|
-
"removeQueries",
|
|
18
|
-
"refetchQueries",
|
|
19
|
-
"cancelQueries",
|
|
20
|
-
"resetQueries",
|
|
21
|
-
"fetchQuery",
|
|
22
|
-
"prefetchQuery",
|
|
23
|
-
"ensureQueryData",
|
|
24
|
-
"getQueriesData",
|
|
25
|
-
"setQueriesData",
|
|
26
|
-
]);
|
|
27
|
-
const methodName = (node) => {
|
|
28
|
-
if (node.type !== "CallExpression")
|
|
29
|
-
return "";
|
|
30
|
-
const { callee } = node;
|
|
31
|
-
if (callee.type !== "MemberExpression" || callee.computed)
|
|
32
|
-
return "";
|
|
33
|
-
const { property } = callee;
|
|
34
|
-
return property.type === "Identifier" ? property.name : "";
|
|
35
|
-
};
|
|
36
|
-
const firstArgumentArray = (node) => {
|
|
37
|
-
if (node.type !== "CallExpression")
|
|
38
|
-
return undefined;
|
|
39
|
-
const [first] = node.arguments;
|
|
40
|
-
return first?.type === "ArrayExpression" ? first : undefined;
|
|
41
|
-
};
|
|
42
|
-
const takesQueryKeyOptions = (node, method) => {
|
|
43
|
-
const callee = node.type === "CallExpression" ? node.callee : undefined;
|
|
44
|
-
const isHook = callee?.type === "Identifier" && QUERY_KEY_HOOKS.has(callee.name);
|
|
45
|
-
return isHook || QUERY_KEY_METHODS.has(method);
|
|
46
|
-
};
|
|
47
|
-
const isQueryKeyProperty = (property) => {
|
|
48
|
-
if (property.type !== "Property" || property.computed)
|
|
49
|
-
return false;
|
|
50
|
-
const { key } = property;
|
|
51
|
-
if (key.type === "Identifier")
|
|
52
|
-
return key.name === "queryKey";
|
|
53
|
-
return key.type === "Literal" && key.value === "queryKey";
|
|
54
|
-
};
|
|
55
|
-
/** `useQueries({ queries: [...] })` carries its keys one level down, inside the array. */
|
|
56
|
-
const optionObjects = (options) => {
|
|
57
|
-
if (options.type !== "ObjectExpression")
|
|
58
|
-
return [];
|
|
59
|
-
const queries = options.properties.find(property => property.type === "Property" && propertyKeyName(property) === "queries");
|
|
60
|
-
if (queries?.type !== "Property" || queries.value.type !== "ArrayExpression")
|
|
61
|
-
return [options];
|
|
62
|
-
return [options, ...queries.value.elements.filter(element => element !== null)];
|
|
63
|
-
};
|
|
64
|
-
const inlineQueryKeys = (node) => {
|
|
65
|
-
if (node.type !== "CallExpression")
|
|
66
|
-
return [];
|
|
67
|
-
const [first] = node.arguments;
|
|
68
|
-
if (first === undefined)
|
|
69
|
-
return [];
|
|
70
|
-
const inline = [];
|
|
71
|
-
for (const options of optionObjects(first)) {
|
|
72
|
-
if (options.type !== "ObjectExpression")
|
|
73
|
-
continue;
|
|
74
|
-
for (const property of options.properties) {
|
|
75
|
-
if (property.type !== "Property" || !isQueryKeyProperty(property))
|
|
76
|
-
continue;
|
|
77
|
-
const { value } = property;
|
|
78
|
-
if (value.type === "ArrayExpression")
|
|
79
|
-
inline.push(value);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return inline;
|
|
83
|
-
};
|
|
84
|
-
export const noInlineKeys = problem("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.", {
|
|
85
|
-
createOnce(context) {
|
|
86
|
-
return {
|
|
87
|
-
CallExpression(node) {
|
|
88
|
-
const method = methodName(node);
|
|
89
|
-
if (method === "getQueryData" || method === "setQueryData") {
|
|
90
|
-
const key = firstArgumentArray(node);
|
|
91
|
-
if (key === undefined)
|
|
92
|
-
return;
|
|
93
|
-
context.report({
|
|
94
|
-
node: key,
|
|
95
|
-
message: method === "getQueryData" ? MESSAGES.inlineGetQueryData : MESSAGES.inlineSetQueryData,
|
|
96
|
-
});
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (!takesQueryKeyOptions(node, method))
|
|
100
|
-
return;
|
|
101
|
-
for (const key of inlineQueryKeys(node)) {
|
|
102
|
-
context.report({ node: key, message: MESSAGES.inlineQueryKey });
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
},
|
|
107
|
-
});
|
|
1
|
+
import{problem as a}from"../../../lib/ast.js";import{propertyKeyName as o}from"./shared.js";const i={inlineQueryKey:"Define this key in the feature's `*.keys.ts` factory and reference it: `queryKey: someKeys.scope(...)`, so invalidation elsewhere matches the same key.",inlineGetQueryData:"Read through the feature's `*.keys.ts` factory: `getQueryData(someKeys.scope())`.",inlineSetQueryData:"Write through the feature's `*.keys.ts` factory: `setQueryData(someKeys.scope(), updater)`."},y=new Set(["useQuery","useSuspenseQuery","useInfiniteQuery","useSuspenseInfiniteQuery","useQueries"]),p=new Set(["invalidateQueries","removeQueries","refetchQueries","cancelQueries","resetQueries","fetchQuery","prefetchQuery","ensureQueryData","getQueriesData","setQueriesData"]),f=(r)=>{if(r.type!=="CallExpression")return"";const{callee:e}=r;if(e.type!=="MemberExpression"||e.computed)return"";const{property:t}=e;return t.type==="Identifier"?t.name:""},c=(r)=>{if(r.type!=="CallExpression")return;const[e]=r.arguments;return e?.type==="ArrayExpression"?e:void 0},l=(r,e)=>{const t=r.type==="CallExpression"?r.callee:void 0;return t?.type==="Identifier"&&y.has(t.name)||p.has(e)},Q=(r)=>{if(r.type!=="Property"||r.computed)return!1;const{key:e}=r;if(e.type==="Identifier")return e.name==="queryKey";return e.type==="Literal"&&e.value==="queryKey"},m=(r)=>{if(r.type!=="ObjectExpression")return[];const e=r.properties.find((t)=>t.type==="Property"&&o(t)==="queries");if(e?.type!=="Property"||e.value.type!=="ArrayExpression")return[r];return[r,...e.value.elements.filter((t)=>t!==null)]},h=(r)=>{if(r.type!=="CallExpression")return[];const[e]=r.arguments;if(e===void 0)return[];const t=[];for(const s of m(e)){if(s.type!=="ObjectExpression")continue;for(const n of s.properties){if(n.type!=="Property"||!Q(n))continue;const{value:u}=n;if(u.type==="ArrayExpression")t.push(u)}}return t};export const noInlineKeys=a("Disallow a query key written as an array literal at the call site instead of coming from a keys factory.",{createOnce(r){return{CallExpression(e){const t=f(e);if(t==="getQueryData"||t==="setQueryData"){const s=c(e);if(s===void 0)return;r.report({node:s,message:t==="getQueryData"?i.inlineGetQueryData:i.inlineSetQueryData});return}if(!l(e,t))return;for(const s of h(e))r.report({node:s,message:i.inlineQueryKey})}}}});
|
|
@@ -1,41 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
const DESTRUCTURE_QUERY_HOOK = "Destructure this hook at the call site: `const { data } = useFooQuery()` rather than binding the whole result to one name.";
|
|
3
|
-
const API_HOOK_MODULE = /^@\/api\/.*\.(?:queries|mutations)$/;
|
|
4
|
-
export const requireDestructuredHooks = problem("Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.", {
|
|
5
|
-
createOnce(context) {
|
|
6
|
-
const apiHooks = new Set();
|
|
7
|
-
const candidates = [];
|
|
8
|
-
return {
|
|
9
|
-
before() {
|
|
10
|
-
apiHooks.clear();
|
|
11
|
-
candidates.length = 0;
|
|
12
|
-
},
|
|
13
|
-
ImportDeclaration(node) {
|
|
14
|
-
if (!API_HOOK_MODULE.test(node.source.value))
|
|
15
|
-
return;
|
|
16
|
-
for (const specifier of node.specifiers) {
|
|
17
|
-
if (specifier.type === "ImportSpecifier")
|
|
18
|
-
apiHooks.add(specifier.local.name);
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
VariableDeclarator(node) {
|
|
22
|
-
const { id, init } = node;
|
|
23
|
-
if (id.type !== "Identifier")
|
|
24
|
-
return;
|
|
25
|
-
if (init?.type !== "CallExpression")
|
|
26
|
-
return;
|
|
27
|
-
const { callee } = init;
|
|
28
|
-
if (callee.type !== "Identifier" || !/^use[A-Z]/.test(callee.name))
|
|
29
|
-
return;
|
|
30
|
-
candidates.push({ node: id, hook: callee.name });
|
|
31
|
-
},
|
|
32
|
-
"Program:exit"() {
|
|
33
|
-
for (const candidate of candidates) {
|
|
34
|
-
if (!apiHooks.has(candidate.hook))
|
|
35
|
-
continue;
|
|
36
|
-
context.report({ node: candidate.node, message: DESTRUCTURE_QUERY_HOOK });
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
});
|
|
1
|
+
import{problem as s}from"../../../lib/ast.js";const c="Destructure this hook at the call site: `const { data } = useFooQuery()` rather than binding the whole result to one name.",u=/^@\/api\/.*\.(?:queries|mutations)$/;export const requireDestructuredHooks=s("Require destructuring the result of a hook imported from an `@/api/*.queries` or `@/api/*.mutations` module.",{createOnce(a){const o=new Set,r=[];return{before(){o.clear();r.length=0},ImportDeclaration(e){if(!u.test(e.source.value))return;for(const t of e.specifiers)if(t.type==="ImportSpecifier")o.add(t.local.name)},VariableDeclarator(e){const{id:t,init:n}=e;if(t.type!=="Identifier")return;if(n?.type!=="CallExpression")return;const{callee:i}=n;if(i.type!=="Identifier"||!/^use[A-Z]/.test(i.name))return;r.push({node:t,hook:i.name})},"Program:exit"(){for(const e of r){if(!o.has(e.hook))continue;a.report({node:e.node,message:c})}}}}});
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export const propertyKeyName
|
|
2
|
-
if (node.type !== "Property" || node.computed)
|
|
3
|
-
return "";
|
|
4
|
-
const { key } = node;
|
|
5
|
-
if (key.type === "Identifier")
|
|
6
|
-
return key.name;
|
|
7
|
-
if (key.type === "Literal")
|
|
8
|
-
return String(key.value);
|
|
9
|
-
return "";
|
|
10
|
-
};
|
|
1
|
+
export const propertyKeyName=(r)=>{if(r.type!=="Property"||r.computed)return"";const{key:e}=r;if(e.type==="Identifier")return e.name;if(e.type==="Literal")return String(e.value);return""};
|
|
@@ -1,12 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { requireSelector } from "./require-selector.js";
|
|
3
|
-
export default defineModule({
|
|
4
|
-
meta: { name: "@ashstack/zustand" },
|
|
5
|
-
url: import.meta.url,
|
|
6
|
-
packages: ["zustand"],
|
|
7
|
-
option: "zustand",
|
|
8
|
-
docsWhen: "auto-enabled by `react()` when `zustand` is a dependency",
|
|
9
|
-
rules: {
|
|
10
|
-
"require-selector": requireSelector,
|
|
11
|
-
},
|
|
12
|
-
});
|
|
1
|
+
import{defineModule as e}from"../../../lib/module.js";import{requireSelector as t}from"./require-selector.js";export default e({meta:{name:"@ashstack/zustand"},url:import.meta.url,packages:["zustand"],option:"zustand",docsWhen:"auto-enabled by `react()` when `zustand` is a dependency",rules:{"require-selector":t}});
|