@ashstack/lint 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/LICENSE +21 -0
- package/README.md +60 -0
- package/RULES.md +2881 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +222 -0
- package/dist/core/rules/core/components-tsx-only.d.ts +2 -0
- package/dist/core/rules/core/components-tsx-only.js +37 -0
- package/dist/core/rules/core/hoist-intl.d.ts +2 -0
- package/dist/core/rules/core/hoist-intl.js +32 -0
- package/dist/core/rules/core/index.d.ts +2 -0
- package/dist/core/rules/core/index.js +18 -0
- package/dist/core/rules/core/no-comments.d.ts +2 -0
- package/dist/core/rules/core/no-comments.js +194 -0
- package/dist/core/rules/core/no-naming-convention.d.ts +2 -0
- package/dist/core/rules/core/no-naming-convention.js +143 -0
- package/dist/core/rules/core/use-design-system.d.ts +2 -0
- package/dist/core/rules/core/use-design-system.js +147 -0
- package/dist/core/rules/zod/index.d.ts +2 -0
- package/dist/core/rules/zod/index.js +12 -0
- package/dist/core/rules/zod/prefer-enum.d.ts +2 -0
- package/dist/core/rules/zod/prefer-enum.js +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/lib/ast.d.ts +44 -0
- package/dist/lib/ast.js +162 -0
- package/dist/lib/detect.d.ts +9 -0
- package/dist/lib/detect.js +42 -0
- package/dist/lib/merge.d.ts +9 -0
- package/dist/lib/merge.js +18 -0
- package/dist/lib/module.d.ts +19 -0
- package/dist/lib/module.js +49 -0
- package/dist/lib/types.d.ts +114 -0
- package/dist/lib/types.js +1 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.js +126 -0
- package/dist/react/rules/i18n/index.d.ts +2 -0
- package/dist/react/rules/i18n/index.js +16 -0
- package/dist/react/rules/i18n/no-bare-attrs.d.ts +2 -0
- package/dist/react/rules/i18n/no-bare-attrs.js +38 -0
- package/dist/react/rules/i18n/no-bare-text.d.ts +2 -0
- package/dist/react/rules/i18n/no-bare-text.js +26 -0
- package/dist/react/rules/i18n/no-bare-toast.d.ts +2 -0
- package/dist/react/rules/i18n/no-bare-toast.js +38 -0
- package/dist/react/rules/query/index.d.ts +2 -0
- package/dist/react/rules/query/index.js +20 -0
- package/dist/react/rules/query/next-page-param-undefined.d.ts +2 -0
- package/dist/react/rules/query/next-page-param-undefined.js +26 -0
- package/dist/react/rules/query/no-deprecated-filters.d.ts +2 -0
- package/dist/react/rules/query/no-deprecated-filters.js +63 -0
- package/dist/react/rules/query/no-fetch-in-query-fn.d.ts +2 -0
- package/dist/react/rules/query/no-fetch-in-query-fn.js +22 -0
- package/dist/react/rules/query/no-inline-keys.d.ts +2 -0
- package/dist/react/rules/query/no-inline-keys.js +107 -0
- package/dist/react/rules/query/require-destructured-hooks.d.ts +2 -0
- package/dist/react/rules/query/require-destructured-hooks.js +41 -0
- package/dist/react/rules/query/shared.d.ts +2 -0
- package/dist/react/rules/query/shared.js +10 -0
- package/dist/react/rules/zustand/index.d.ts +2 -0
- package/dist/react/rules/zustand/index.js +12 -0
- package/dist/react/rules/zustand/require-selector.d.ts +2 -0
- package/dist/react/rules/zustand/require-selector.js +62 -0
- package/dist/react-native/index.d.ts +11 -0
- package/dist/react-native/index.js +113 -0
- package/dist/react-native/rules/keyboard/index.d.ts +2 -0
- package/dist/react-native/rules/keyboard/index.js +18 -0
- package/dist/react-native/rules/legend-list/index.d.ts +2 -0
- package/dist/react-native/rules/legend-list/index.js +51 -0
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-flex-in-content-container.js +77 -0
- package/dist/react-native/rules/legend-list/no-index-key-extractor.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-index-key-extractor.js +26 -0
- package/dist/react-native/rules/legend-list/no-inline-data.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-inline-data.js +25 -0
- package/dist/react-native/rules/legend-list/no-inline-extra-data.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-inline-extra-data.js +21 -0
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-inline-render-item-props.js +35 -0
- package/dist/react-native/rules/legend-list/no-mixed-children.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-mixed-children.js +24 -0
- package/dist/react-native/rules/legend-list/no-remount-key.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-remount-key.js +18 -0
- package/dist/react-native/rules/legend-list/no-scrollview-map.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-scrollview-map.js +27 -0
- package/dist/react-native/rules/legend-list/no-unsupported-props.d.ts +2 -0
- package/dist/react-native/rules/legend-list/no-unsupported-props.js +29 -0
- package/dist/react-native/rules/legend-list/required-props.d.ts +2 -0
- package/dist/react-native/rules/legend-list/required-props.js +23 -0
- package/dist/react-native/rules/legend-list/shared.d.ts +19 -0
- package/dist/react-native/rules/legend-list/shared.js +25 -0
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.d.ts +2 -0
- package/dist/react-native/rules/legend-list/typed-items-need-item-type.js +49 -0
- package/dist/react-native/rules/legend-state/index.d.ts +2 -0
- package/dist/react-native/rules/legend-state/index.js +35 -0
- package/dist/react-native/rules/legend-state/naming.d.ts +2 -0
- package/dist/react-native/rules/legend-state/naming.js +26 -0
- package/dist/react-native/rules/legend-state/no-assignment.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-assignment.js +31 -0
- package/dist/react-native/rules/legend-state/no-nested-observable.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-nested-observable.js +23 -0
- package/dist/react-native/rules/legend-state/no-object-selector.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-object-selector.js +47 -0
- package/dist/react-native/rules/legend-state/no-observable-in-component.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-observable-in-component.js +46 -0
- package/dist/react-native/rules/legend-state/no-peek-in-selector.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-peek-in-selector.js +67 -0
- package/dist/react-native/rules/legend-state/no-react-mirror.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-react-mirror.js +33 -0
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.d.ts +2 -0
- package/dist/react-native/rules/legend-state/no-untracked-get-in-jsx.js +52 -0
- package/dist/react-native/rules/legend-state/shared.d.ts +9 -0
- package/dist/react-native/rules/legend-state/shared.js +23 -0
- package/dist/react-native/rules/react-native/hoist-stateless-function.d.ts +2 -0
- package/dist/react-native/rules/react-native/hoist-stateless-function.js +59 -0
- package/dist/react-native/rules/react-native/index.d.ts +2 -0
- package/dist/react-native/rules/react-native/index.js +50 -0
- package/dist/react-native/rules/react-native/no-conditional-style-array.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-conditional-style-array.js +23 -0
- package/dist/react-native/rules/react-native/no-dynamic-import.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-dynamic-import.js +18 -0
- package/dist/react-native/rules/react-native/no-keyboard-will-events.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-keyboard-will-events.js +18 -0
- package/dist/react-native/rules/react-native/no-leaked-render.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-leaked-render.js +43 -0
- package/dist/react-native/rules/react-native/no-manual-memo.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-manual-memo.js +87 -0
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-redundant-view-nesting.js +36 -0
- package/dist/react-native/rules/react-native/no-rn-image-network-source.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-rn-image-network-source.js +58 -0
- package/dist/react-native/rules/react-native/no-rn-namespace-import.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-rn-namespace-import.js +38 -0
- package/dist/react-native/rules/react-native/no-scroll-position-state.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-scroll-position-state.js +26 -0
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.d.ts +2 -0
- package/dist/react-native/rules/react-native/no-unlabeled-icon-pressable.js +62 -0
- package/dist/react-native/rules/reanimated/animated-reaction-safety.d.ts +2 -0
- package/dist/react-native/rules/reanimated/animated-reaction-safety.js +110 -0
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.d.ts +2 -0
- package/dist/react-native/rules/reanimated/animated-style-needs-animated-component.js +72 -0
- package/dist/react-native/rules/reanimated/animated-updater-purity.d.ts +2 -0
- package/dist/react-native/rules/reanimated/animated-updater-purity.js +38 -0
- package/dist/react-native/rules/reanimated/gpu-properties-only.d.ts +2 -0
- package/dist/react-native/rules/reanimated/gpu-properties-only.js +80 -0
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.d.ts +2 -0
- package/dist/react-native/rules/reanimated/hoist-layout-animation-builder.js +43 -0
- package/dist/react-native/rules/reanimated/index.d.ts +2 -0
- package/dist/react-native/rules/reanimated/index.js +51 -0
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.d.ts +2 -0
- package/dist/react-native/rules/reanimated/interpolate-needs-clamp.js +41 -0
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.d.ts +2 -0
- package/dist/react-native/rules/reanimated/no-react-state-from-continuous-worklet.js +86 -0
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.d.ts +2 -0
- package/dist/react-native/rules/reanimated/no-shared-value-dot-value.js +112 -0
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.d.ts +2 -0
- package/dist/react-native/rules/reanimated/prefer-lazy-shared-value-initializer.js +24 -0
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.d.ts +2 -0
- package/dist/react-native/rules/reanimated/schedule-on-rn-scope.js +21 -0
- package/dist/react-native/rules/reanimated/shared-value-usage.d.ts +2 -0
- package/dist/react-native/rules/reanimated/shared-value-usage.js +105 -0
- package/dist/react-native/rules/reanimated/shared.d.ts +5 -0
- package/dist/react-native/rules/reanimated/shared.js +13 -0
- package/dist/react-native/rules/skia/canvas-opaque.d.ts +2 -0
- package/dist/react-native/rules/skia/canvas-opaque.js +49 -0
- package/dist/react-native/rules/skia/index.d.ts +2 -0
- package/dist/react-native/rules/skia/index.js +21 -0
- package/dist/react-native/rules/turbo-image/index.d.ts +2 -0
- package/dist/react-native/rules/turbo-image/index.js +14 -0
- package/dist/react-native/rules/turbo-image/require-cache-policy.d.ts +2 -0
- package/dist/react-native/rules/turbo-image/require-cache-policy.js +21 -0
- package/dist/react-native/rules/turbo-image/require-resize.d.ts +2 -0
- package/dist/react-native/rules/turbo-image/require-resize.js +22 -0
- package/dist/react-native/rules/unistyles/animated-theme.d.ts +2 -0
- package/dist/react-native/rules/unistyles/animated-theme.js +37 -0
- package/dist/react-native/rules/unistyles/content-container.d.ts +2 -0
- package/dist/react-native/rules/unistyles/content-container.js +73 -0
- package/dist/react-native/rules/unistyles/in-sheet.d.ts +2 -0
- package/dist/react-native/rules/unistyles/in-sheet.js +116 -0
- package/dist/react-native/rules/unistyles/index.d.ts +2 -0
- package/dist/react-native/rules/unistyles/index.js +58 -0
- package/dist/react-native/rules/unistyles/insets.d.ts +2 -0
- package/dist/react-native/rules/unistyles/insets.js +92 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-color.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-color.js +30 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-hardcoded-spacing.js +78 -0
- package/dist/react-native/rules/unistyles/no-margin.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-margin.js +19 -0
- package/dist/react-native/rules/unistyles/no-style-spread.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-style-spread.js +41 -0
- package/dist/react-native/rules/unistyles/no-unused-styles.d.ts +2 -0
- package/dist/react-native/rules/unistyles/no-unused-styles.js +99 -0
- package/dist/react-native/rules/unistyles/rtl-style-call.d.ts +2 -0
- package/dist/react-native/rules/unistyles/rtl-style-call.js +42 -0
- package/dist/react-native/rules/unistyles/shared.d.ts +16 -0
- package/dist/react-native/rules/unistyles/shared.js +70 -0
- package/dist/react-native/rules/unistyles/theme-screen-component.d.ts +2 -0
- package/dist/react-native/rules/unistyles/theme-screen-component.js +22 -0
- package/dist/react-native/rules/unistyles/theme-style-attr.d.ts +2 -0
- package/dist/react-native/rules/unistyles/theme-style-attr.js +18 -0
- package/dist/react-native/stylesheet.d.ts +11 -0
- package/dist/react-native/stylesheet.js +44 -0
- package/package.json +70 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BanGroup, ModuleManifest, RestrictedImports } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper: types the manifest and keeps the shape honest. The returned
|
|
4
|
+
* object is handed to oxlint as the plugin (it ignores the manifest extras).
|
|
5
|
+
*/
|
|
6
|
+
export declare const defineModule: (manifest: ModuleManifest) => ModuleManifest;
|
|
7
|
+
declare const shortName: (module: ModuleManifest) => string;
|
|
8
|
+
export interface Composed {
|
|
9
|
+
jsPlugins: string[];
|
|
10
|
+
rules: Record<string, "error">;
|
|
11
|
+
restricted: Required<RestrictedImports>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Turn manifests + options into the config fragments an entry spreads in:
|
|
15
|
+
* which plugin files to load, which rules to enable, and the merged import
|
|
16
|
+
* bans from the enabled modules plus the enabled ban-only groups.
|
|
17
|
+
*/
|
|
18
|
+
export declare const composeModules: (modules: ModuleManifest[], options: Record<string, boolean | undefined>, banGroups?: BanGroup[]) => Composed;
|
|
19
|
+
export { shortName };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { detect } from "./detect.js";
|
|
3
|
+
/**
|
|
4
|
+
* Identity helper: types the manifest and keeps the shape honest. The returned
|
|
5
|
+
* object is handed to oxlint as the plugin (it ignores the manifest extras).
|
|
6
|
+
*/
|
|
7
|
+
export const defineModule = (manifest) => manifest;
|
|
8
|
+
const shortName = (module) => module.meta.name.slice("@ashstack/".length);
|
|
9
|
+
const isModuleEnabled = (module, options) => detect(module.option === undefined ? undefined : options[module.option], module.packages);
|
|
10
|
+
const enabledRuleIds = (module) => {
|
|
11
|
+
const ids = [];
|
|
12
|
+
for (const [name, rule] of Object.entries(module.rules)) {
|
|
13
|
+
if (rule.meta.defaultOff)
|
|
14
|
+
continue;
|
|
15
|
+
if (rule.meta.packages && !detect(undefined, rule.meta.packages))
|
|
16
|
+
continue;
|
|
17
|
+
ids.push(`${module.meta.name}/${name}`);
|
|
18
|
+
}
|
|
19
|
+
return ids;
|
|
20
|
+
};
|
|
21
|
+
const addRestrictedImports = (into, from) => {
|
|
22
|
+
into.paths.push(...(from?.paths ?? []));
|
|
23
|
+
into.patterns.push(...(from?.patterns ?? []));
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Turn manifests + options into the config fragments an entry spreads in:
|
|
27
|
+
* which plugin files to load, which rules to enable, and the merged import
|
|
28
|
+
* bans from the enabled modules plus the enabled ban-only groups.
|
|
29
|
+
*/
|
|
30
|
+
export const composeModules = (modules, options, banGroups = []) => {
|
|
31
|
+
const composed = { jsPlugins: [], rules: {}, restricted: { paths: [], patterns: [] } };
|
|
32
|
+
for (const module of modules) {
|
|
33
|
+
if (!isModuleEnabled(module, options))
|
|
34
|
+
continue;
|
|
35
|
+
const ids = enabledRuleIds(module);
|
|
36
|
+
if (ids.length > 0)
|
|
37
|
+
composed.jsPlugins.push(fileURLToPath(module.url));
|
|
38
|
+
for (const id of ids)
|
|
39
|
+
composed.rules[id] = "error";
|
|
40
|
+
addRestrictedImports(composed.restricted, module.restrictedImports);
|
|
41
|
+
}
|
|
42
|
+
for (const group of banGroups) {
|
|
43
|
+
if (!detect(undefined, group.packages))
|
|
44
|
+
continue;
|
|
45
|
+
addRestrictedImports(composed.restricted, group.restrictedImports);
|
|
46
|
+
}
|
|
47
|
+
return composed;
|
|
48
|
+
};
|
|
49
|
+
export { shortName };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { OxlintConfig as OxlintNativeConfig } from "oxlint";
|
|
2
|
+
import type { RuleTester } from "oxlint/plugins-dev";
|
|
3
|
+
type OxRule = Parameters<RuleTester["run"]>[1];
|
|
4
|
+
type CreateRule = Extract<OxRule, {
|
|
5
|
+
create: unknown;
|
|
6
|
+
}>;
|
|
7
|
+
type CreateOnceRule = Extract<OxRule, {
|
|
8
|
+
createOnce: unknown;
|
|
9
|
+
}>;
|
|
10
|
+
type OxVisitor = ReturnType<CreateRule["create"]>;
|
|
11
|
+
/** A node in the AST oxlint hands JS plugins: a discriminated union on ESTree `type` names. */
|
|
12
|
+
export type AstNode = Parameters<NonNullable<OxVisitor[string]>>[0];
|
|
13
|
+
/** What a rule receives: `options`, `sourceCode`, `report`, `filename`. */
|
|
14
|
+
export type RuleContext = Parameters<CreateRule["create"]>[0];
|
|
15
|
+
export type Visitor = OxVisitor;
|
|
16
|
+
/**
|
|
17
|
+
* oxlint's rule metadata plus the two fields this package reads: `defaultOff`
|
|
18
|
+
* for rules a consumer opts into, `packages` for a rule gated on a dependency
|
|
19
|
+
* of its own. Rule docs are required here — RULES.md is generated from them.
|
|
20
|
+
*/
|
|
21
|
+
export type RuleMeta = NonNullable<CreateRule["meta"]> & {
|
|
22
|
+
docs: {
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
defaultOff?: boolean;
|
|
26
|
+
packages?: string[];
|
|
27
|
+
};
|
|
28
|
+
export type Rule = OxRule & {
|
|
29
|
+
meta: RuleMeta;
|
|
30
|
+
};
|
|
31
|
+
/** A rule without its docs: what `problem()` takes before it adds the description. */
|
|
32
|
+
export type RuleBody = (Pick<CreateRule, "create"> | {
|
|
33
|
+
createOnce: CreateOnceRule["createOnce"];
|
|
34
|
+
}) & {
|
|
35
|
+
meta?: Partial<RuleMeta>;
|
|
36
|
+
};
|
|
37
|
+
export interface RestrictedPath {
|
|
38
|
+
name: string;
|
|
39
|
+
importNames?: string[];
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
export interface RestrictedPattern {
|
|
43
|
+
group: string[];
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
export interface RestrictedImports {
|
|
47
|
+
paths?: RestrictedPath[];
|
|
48
|
+
patterns?: RestrictedPattern[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A module is the unit of rule organization: one rule namespace, one plugin
|
|
52
|
+
* file, one optional toggle. The default export of a module's index file IS
|
|
53
|
+
* the oxlint plugin (meta + rules); the extra fields make it the single
|
|
54
|
+
* source of truth for detection, bans, and docs (oxlint ignores them).
|
|
55
|
+
*/
|
|
56
|
+
export interface ModuleManifest {
|
|
57
|
+
meta: {
|
|
58
|
+
name: `@ashstack/${string}`;
|
|
59
|
+
};
|
|
60
|
+
rules: Record<string, Rule>;
|
|
61
|
+
/** import.meta.url of the module file — how entries hand oxlint an absolute plugin path */
|
|
62
|
+
url: string;
|
|
63
|
+
/** enable when one of these is a dependency; absent = always on */
|
|
64
|
+
packages?: string[];
|
|
65
|
+
/** key in the entry options that forces the module on/off; absent = auto-only */
|
|
66
|
+
option?: string;
|
|
67
|
+
/** activation line for RULES.md, e.g. "auto-enabled when `zod` is a dependency" */
|
|
68
|
+
docsWhen: string;
|
|
69
|
+
/** import bans that ship with (and only with) this module */
|
|
70
|
+
restrictedImports?: RestrictedImports;
|
|
71
|
+
}
|
|
72
|
+
/** A ban-only group: import bans gated on a library, with no rules of its own. */
|
|
73
|
+
export interface BanGroup {
|
|
74
|
+
packages: string[];
|
|
75
|
+
restrictedImports: RestrictedImports;
|
|
76
|
+
}
|
|
77
|
+
export type CoreOptions = {
|
|
78
|
+
/** `@ashstack/zod/` rules — detected from `zod` */
|
|
79
|
+
zod?: boolean;
|
|
80
|
+
};
|
|
81
|
+
export type ReactOptions = CoreOptions & {
|
|
82
|
+
/** `@ashstack/query/` rules — detected from `@tanstack/react-query` */
|
|
83
|
+
query?: boolean;
|
|
84
|
+
/** `@ashstack/zustand/` rules — detected from `zustand` */
|
|
85
|
+
zustand?: boolean;
|
|
86
|
+
/** `@ashstack/i18n/` rules — detected from i18next/lingui/react-intl/use-intl/next-intl/expo-localization */
|
|
87
|
+
i18n?: boolean;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Every toggle is a rule module — one rule namespace each. Import bans and
|
|
91
|
+
* package-gated single rules are auto-detect only; disable individual rules
|
|
92
|
+
* by name in your `rules` block.
|
|
93
|
+
*/
|
|
94
|
+
export type ReactNativeOptions = ReactOptions & {
|
|
95
|
+
/** `@ashstack/unistyles/` rules + StyleSheet/Dimensions/SafeArea import bans — `react-native-unistyles` */
|
|
96
|
+
unistyles?: boolean;
|
|
97
|
+
/** `@ashstack/legend-list/` rules + FlatList/FlashList bans — `@legendapp/list` */
|
|
98
|
+
legendList?: boolean;
|
|
99
|
+
/** `@ashstack/legend-state/` rules + use$/useSelector ban — `@legendapp/state` */
|
|
100
|
+
legendState?: boolean;
|
|
101
|
+
/** `@ashstack/reanimated/` rules + Animated/runOnJS bans — `react-native-reanimated` */
|
|
102
|
+
reanimated?: boolean;
|
|
103
|
+
/** `@ashstack/turbo-image/` rules — `react-native-turbo-image` */
|
|
104
|
+
turboImage?: boolean;
|
|
105
|
+
/** `@ashstack/skia/` rules — `@shopify/react-native-skia` */
|
|
106
|
+
skia?: boolean;
|
|
107
|
+
/** `@ashstack/keyboard` import bans — `react-native-keyboard-controller` */
|
|
108
|
+
keyboard?: boolean;
|
|
109
|
+
};
|
|
110
|
+
/** The config object an entry returns: oxlint's own shape, so `defineConfig({ extends: [core()] })` typechecks. */
|
|
111
|
+
export type OxlintConfig = OxlintNativeConfig;
|
|
112
|
+
/** The `rules` block of a config: oxlint's own map, so a built-in rule's options are checked. */
|
|
113
|
+
export type RuleMap = NonNullable<OxlintConfig["rules"]>;
|
|
114
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OxlintConfig, ReactOptions } from "../lib/types.js";
|
|
2
|
+
export declare const reactModules: import("../lib/types.js").ModuleManifest[];
|
|
3
|
+
/**
|
|
4
|
+
* React (web) entry — everything in core plus react, jsx-a11y, React Compiler
|
|
5
|
+
* diagnostics, you-might-not-need-an-effect, and the auto-detected library
|
|
6
|
+
* modules (query, zustand, i18n).
|
|
7
|
+
*/
|
|
8
|
+
declare const react: (options?: ReactOptions) => OxlintConfig;
|
|
9
|
+
export default react;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
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;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineModule } from "../../../lib/module.js";
|
|
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
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { attributeName, optionsOf } from "../../../lib/ast.js";
|
|
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
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { problem, tagIdentifier } from "../../../lib/ast.js";
|
|
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
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { problem } from "../../../lib/ast.js";
|
|
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
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineModule } from "../../../lib/module.js";
|
|
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
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FUNCTION_TYPES, crossesFunctionBefore, findInSubtree, gate, problem } from "../../../lib/ast.js";
|
|
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
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const DEPRECATED_FILTERS = "Pass a filter object here: `.invalidateQueries({ queryKey: someKeys.scope(...) })`. TanStack Query v5 removed the positional `(queryKey)` form.";
|
|
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
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { gate, problem, subtreeHas } from "../../../lib/ast.js";
|
|
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
|
+
});
|