@eslint-react/kit 2.1.2-next.2 → 4.0.0-beta.1

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.
Files changed (4) hide show
  1. package/README.md +381 -0
  2. package/dist/index.d.ts +121 -154
  3. package/dist/index.js +131 -99
  4. package/package.json +31 -10
package/README.md ADDED
@@ -0,0 +1,381 @@
1
+ # @eslint-react/kit
2
+
3
+ ESLint React's toolkit for building custom React lint rules right inside your `eslint.config.ts`.
4
+
5
+ ## Index
6
+
7
+ - [Installation](#installation)
8
+ - [Quick Start](#quick-start)
9
+ - [API Reference](#api-reference)
10
+ - [`defineConfig` (default export)](#defineconfig-default-export)
11
+ - [`merge`](#merge)
12
+ - [`Kit` — the toolkit object](#kit--the-toolkit-object)
13
+ - [`kit.collect`](#kitcollect) — Semantic collectors
14
+ - [`kit.is`](#kitis) — Predicates
15
+ - [`kit.hint`](#kithint) — Detection hint bit-flags
16
+ - [`kit.flag`](#kitflag) — Component characteristic flags
17
+ - [Examples](#examples)
18
+ - [Simple: Ban `forwardRef`](#simple-ban-forwardref)
19
+ - [Component: Destructure component props](#component-destructure-component-props)
20
+ - [Hooks: Warn on custom hooks that don't call other hooks](#hooks-warn-on-custom-hooks-that-dont-call-other-hooks)
21
+ - [Multiple Collectors: No component/hook factories](#multiple-collectors-no-componenthook-factories)
22
+ - [More Examples](#more-examples)
23
+
24
+ ## Installation
25
+
26
+ ```sh
27
+ npm install --save-dev @eslint-react/kit
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```ts
33
+ import eslintReact from "@eslint-react/eslint-plugin";
34
+ import eslintReactKit, { merge } from "@eslint-react/kit";
35
+ import eslintJs from "@eslint/js";
36
+ import { defineConfig } from "eslint/config";
37
+ import tseslint from "typescript-eslint";
38
+
39
+ export default defineConfig(
40
+ {
41
+ files: ["**/*.{ts,tsx}"],
42
+ extends: [
43
+ eslintJs.configs.recommended,
44
+ tseslint.configs.recommended,
45
+ eslintReact.configs["recommended-typescript"],
46
+ eslintReactKit(
47
+ {
48
+ name: "function-component-definition",
49
+ make: (context, { collect }) => {
50
+ const { query, visitor } = collect.components(context);
51
+
52
+ return merge(
53
+ visitor,
54
+ {
55
+ "Program:exit"(program) {
56
+ for (const { node } of query.all(program)) {
57
+ if (node.type === "FunctionDeclaration") continue;
58
+ context.report({
59
+ node,
60
+ message: "Function components must be defined with function declarations.",
61
+ });
62
+ }
63
+ },
64
+ },
65
+ );
66
+ },
67
+ },
68
+ ),
69
+ ],
70
+ rules: {
71
+ "@eslint-react/kit/function-component-definition": "error",
72
+ },
73
+ },
74
+ );
75
+ ```
76
+
77
+ ## API Reference
78
+
79
+ ### `defineConfig` (default export)
80
+
81
+ ```ts
82
+ import defineReactConfig from "@eslint-react/kit";
83
+
84
+ defineReactConfig(...rules: RuleDefinition[]): Linter.Config
85
+ ```
86
+
87
+ Creates an ESLint flat-config object from one or more custom rule definitions. Rules are registered under the `@eslint-react/kit` plugin namespace and enabled at `"error"` severity by default.
88
+
89
+ **`RuleDefinition`:**
90
+
91
+ | Field | Type | Description |
92
+ | ------ | -------------------------------- | -------------------------------------------------------------------------------- |
93
+ | `name` | `string` | Unique rule name. Used as `@eslint-react/kit/<name>` in config. |
94
+ | `make` | `(context, kit) => RuleListener` | Rule factory. Receives the ESLint rule context and the structured `Kit` toolkit. |
95
+
96
+ ### `merge`
97
+
98
+ ```ts
99
+ import { merge } from "@eslint-react/kit";
100
+
101
+ merge(...listeners: RuleListener[]): RuleListener
102
+ ```
103
+
104
+ Merges multiple `RuleListener` (visitor) objects into a single listener. When two or more listeners define the same visitor key, the handlers are chained and execute in order.
105
+
106
+ This is essential for combining a collector's `visitor` with your own inspection logic.
107
+
108
+ ### Kit — the toolkit object
109
+
110
+ The second argument to `make` is a structured `Kit` object:
111
+
112
+ ```
113
+ kit
114
+ ├── collect -> Semantic collectors (components, hooks)
115
+ ├── is -> All predicates (component, hook, React API, import source)
116
+ ├── hint -> Detection hint bit-flags
117
+ ├── flag -> Component characteristic bit-flags
118
+ ```
119
+
120
+ ---
121
+
122
+ #### `kit.collect`
123
+
124
+ Collector factories create a `{ query, visitor }` pair. The `visitor` must be merged into your rule listener via `merge()`. After traversal completes, `query.all(program)` yields all detected semantic nodes.
125
+
126
+ | Method | Returns | Description |
127
+ | ------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
128
+ | `components(context, options?)` | `CollectorWithContext<FunctionComponentSemanticNode>` | Detects function components. Options: `{ hint?: bigint, collectDisplayName?: boolean }` |
129
+ | `hooks(context)` | `CollectorWithContext<HookSemanticNode>` | Detects custom hook definitions. |
130
+
131
+ **`CollectorWithContext`** extends `Collector` with contextual queries:
132
+
133
+ | Query | Description |
134
+ | -------------------- | ----------------------------------------- |
135
+ | `query.all(program)` | All collected semantic nodes in the file. |
136
+
137
+ ---
138
+
139
+ #### `kit.is`
140
+
141
+ All predicates live under `kit.is` — organized into four sub-sections.
142
+
143
+ ##### Component
144
+
145
+ | Predicate | Signature | Description |
146
+ | --------------------------- | ------------------------- | --------------------------------------------------------------------------- |
147
+ | `componentDefinition` | `(node, hint) -> boolean` | Whether a function node is a component definition. (context pre-bound) |
148
+ | `componentName` | `(name) -> boolean` | Strict PascalCase component name check. |
149
+ | `componentNameLoose` | `(name) -> boolean` | Loose component name check. |
150
+ | `componentWrapperCall` | `(node) -> boolean` | Whether a node is a `memo(…)` or `forwardRef(…)` call. (context pre-bound) |
151
+ | `componentWrapperCallLoose` | `(node) -> boolean` | Like above, but also matches `useCallback(…)`. (context pre-bound) |
152
+ | `componentWrapperCallback` | `(node) -> boolean` | Whether a function is the callback passed to a wrapper. (context pre-bound) |
153
+
154
+ ##### Hook
155
+
156
+ General hook predicates:
157
+
158
+ | Predicate | Signature | Description |
159
+ | -------------------------- | ------------------------------------- | ------------------------------------------------------------ |
160
+ | `hook` | `(node) -> boolean` | Whether a function node is a hook (by name). |
161
+ | `hookCall` | `(node) -> boolean` | Whether a node is a hook call. |
162
+ | `hookName` | `(name) -> boolean` | Whether a string matches the `use[A-Z]` convention. |
163
+ | `useEffectLikeCall` | `(node, additionalHooks?) -> boolean` | Whether a node is a `useEffect`/`useLayoutEffect`-like call. |
164
+ | `useStateLikeCall` | `(node, additionalHooks?) -> boolean` | Whether a node is a `useState`-like call. |
165
+ | `useEffectSetupCallback` | `(node) -> boolean` | Whether a node is a useEffect setup function. |
166
+ | `useEffectCleanupCallback` | `(node) -> boolean` | Whether a node is a useEffect cleanup function. |
167
+
168
+ ##### React API
169
+
170
+ Factory functions (context pre-bound):
171
+
172
+ | Predicate | Signature | Description |
173
+ | -------------- | -------------------------------- | ---------------------------------------------------------------------------- |
174
+ | `reactAPI` | `(apiName) -> (node) -> boolean` | Factory: creates a predicate for a React API identifier. (context pre-bound) |
175
+ | `reactAPICall` | `(apiName) -> (node) -> boolean` | Factory: creates a predicate for a React API call. (context pre-bound) |
176
+
177
+ Pre-built identifier predicates (context pre-bound):
178
+
179
+ `captureOwnerStack`, `childrenCount`, `childrenForEach`, `childrenMap`, `childrenOnly`, `childrenToArray`, `cloneElement`, `createContext`, `createElement`, `forwardRef`, `memo`, `lazy`
180
+
181
+ Pre-built call predicates (context pre-bound):
182
+
183
+ `captureOwnerStackCall`, `childrenCountCall`, `childrenForEachCall`, `childrenMapCall`, `childrenOnlyCall`, `childrenToArrayCall`, `cloneElementCall`, `createContextCall`, `createElementCall`, `forwardRefCall`, `memoCall`, `lazyCall`
184
+
185
+ All React API predicates and factories have `context` pre-bound — no need to pass the rule context manually:
186
+
187
+ ```ts
188
+ // Direct check
189
+ is.memo(node);
190
+ is.memoCall(node);
191
+
192
+ // Useful in filter/find
193
+ nodes.filter(is.memoCall);
194
+
195
+ // Factory for any API name
196
+ const isCreateRefCall = is.reactAPICall("createRef");
197
+ isCreateRefCall(node);
198
+ ```
199
+
200
+ ##### Import source
201
+
202
+ | Predicate | Signature | Description |
203
+ | ---------------------------- | ----------------------------------------- | ---------------------------------------------------- |
204
+ | `initializedFromReact` | `(name, scope, importSource?) -> boolean` | Whether a variable comes from a React import. |
205
+ | `initializedFromReactNative` | `(name, scope, importSource?) -> boolean` | Whether a variable comes from a React Native import. |
206
+
207
+ ---
208
+
209
+ #### `kit.hint`
210
+
211
+ Bit-flags that control what the component collector considers a "component". Combine with bitwise OR (`|`) and remove with bitwise AND-NOT (`& ~`).
212
+
213
+ ```ts
214
+ // The default hint used when none is specified
215
+ hint.component.Default;
216
+
217
+ // All available flags
218
+ hint.component.DoNotIncludeFunctionDefinedAsObjectMethod;
219
+ hint.component.DoNotIncludeFunctionDefinedAsClassMethod;
220
+ hint.component.DoNotIncludeFunctionDefinedAsArrayMapCallback;
221
+ hint.component.DoNotIncludeFunctionDefinedAsArbitraryCallExpressionCallback;
222
+ // … and more (inherits all JsxDetectionHint flags)
223
+ ```
224
+
225
+ **Customization example:**
226
+
227
+ ```ts
228
+ const { query, visitor } = collect.components(context, {
229
+ // Also treat object methods as components (remove the exclusion flag)
230
+ hint: hint.component.Default & ~hint.component.DoNotIncludeFunctionDefinedAsObjectMethod,
231
+ });
232
+ ```
233
+
234
+ ---
235
+
236
+ #### `kit.flag`
237
+
238
+ Bit-flags indicating component characteristics. Check with bitwise AND (`&`).
239
+
240
+ ```ts
241
+ flag.component.None; // 0n — no flags
242
+ flag.component.Memo; // wrapped in React.memo
243
+ flag.component.ForwardRef; // wrapped in React.forwardRef
244
+ ```
245
+
246
+ **Usage:**
247
+
248
+ ```ts
249
+ for (const component of query.all(program)) {
250
+ if (component.flag & flag.component.Memo) {
251
+ // This component is memoized
252
+ }
253
+ }
254
+ ```
255
+
256
+ ## Examples
257
+
258
+ ### Simple: Ban `forwardRef`
259
+
260
+ This is a simplified kit reimplementation of the built-in [`react-x/no-forwardRef`](https://eslint-react.xyz/docs/rules/no-forwardRef) rule.
261
+
262
+ ```ts
263
+ defineReactConfig({
264
+ name: "no-forward-ref",
265
+ make: (context, { is }) => ({
266
+ CallExpression(node) {
267
+ if (is.forwardRefCall(node)) {
268
+ context.report({ node, message: "forwardRef is deprecated in React 19. Pass ref as a prop instead." });
269
+ }
270
+ },
271
+ }),
272
+ });
273
+ ```
274
+
275
+ ### Component: Destructure component props
276
+
277
+ This is a simplified kit reimplementation of the built-in [`react-x/prefer-destructuring-assignment`](https://eslint-react.xyz/docs/rules/prefer-destructuring-assignment) rule.
278
+
279
+ ```ts
280
+ defineReactConfig({
281
+ name: "destructure-component-props",
282
+ make: (context, { collect }) => {
283
+ const { query, visitor } = collect.components(context);
284
+
285
+ return merge(visitor, {
286
+ "Program:exit"(program) {
287
+ for (const { node } of query.all(program)) {
288
+ const [props] = node.params;
289
+ if (props == null) continue;
290
+ if (props.type !== AST.Identifier) continue;
291
+ const propName = props.name;
292
+ const propVariable = context.sourceCode.getScope(node).variables.find((v) => v.name === propName);
293
+ const propReferences = propVariable?.references ?? [];
294
+ for (const ref of propReferences) {
295
+ const { parent } = ref.identifier;
296
+ if (parent.type !== AST.MemberExpression) continue;
297
+ context.report({
298
+ message: "Use destructuring assignment for component props.",
299
+ node: parent,
300
+ });
301
+ }
302
+ }
303
+ },
304
+ });
305
+ },
306
+ });
307
+ ```
308
+
309
+ ### Hooks: Warn on custom hooks that don't call other hooks
310
+
311
+ This is a simplified kit reimplementation of the built-in [`react-x/no-unnecessary-use-prefix`](https://eslint-react.xyz/docs/rules/no-unnecessary-use-prefix) rule.
312
+
313
+ ```ts
314
+ defineReactConfig({
315
+ name: "no-unnecessary-use-prefix",
316
+ make: (context, { collect }) => {
317
+ const { query, visitor } = collect.hooks(context);
318
+
319
+ return merge(visitor, {
320
+ "Program:exit"(program) {
321
+ for (const { node, hookCalls } of query.all(program)) {
322
+ if (hookCalls.length === 0) {
323
+ context.report({
324
+ node,
325
+ message: "A custom hook should use at least one hook, otherwise it's just a regular function.",
326
+ });
327
+ }
328
+ }
329
+ },
330
+ });
331
+ },
332
+ });
333
+ ```
334
+
335
+ ### Multiple Collectors: No component/hook factories
336
+
337
+ Disallow defining components or hooks inside other functions (factory pattern).
338
+ This is a simplified kit reimplementation of the built-in [`react-x/component-hook-factories`](https://eslint-react.xyz/docs/rules/component-hook-factories) rule.
339
+
340
+ ```ts
341
+ defineReactConfig({
342
+ name: "component-hook-factories",
343
+ make: (context, { collect }) => {
344
+ const fc = collect.components(context);
345
+ const hk = collect.hooks(context);
346
+ return merge(
347
+ fc.visitor,
348
+ hk.visitor,
349
+ {
350
+ "Program:exit"(program) {
351
+ const comps = fc.query.all(program);
352
+ const hooks = hk.query.all(program);
353
+ for (const { name, node, kind } of [...comps, ...hooks]) {
354
+ if (name == null) continue;
355
+ if (findParent(node, isFunction) == null) continue;
356
+ context.report({
357
+ node,
358
+ message: `Don't define ${kind} "${name}" inside a function. Move it to the module level.`,
359
+ });
360
+ }
361
+ },
362
+ },
363
+ );
364
+ },
365
+ });
366
+
367
+ function findParent({ parent }: TSESTree.Node, test: (n: TSESTree.Node) => boolean): TSESTree.Node | null {
368
+ if (parent == null) return null;
369
+ if (test(parent)) return parent;
370
+ if (parent.type === "Program") return null;
371
+ return findParent(parent, test);
372
+ }
373
+
374
+ function isFunction({ type }: TSESTree.Node) {
375
+ return type === "FunctionDeclaration" || type === "FunctionExpression" || type === "ArrowFunctionExpression";
376
+ }
377
+ ```
378
+
379
+ ## More Examples
380
+
381
+ Please check the [Rule Recipes](https://eslint-react.xyz/docs/configuration/configure-custom-rules#rule-recipes) in the documentation site.
package/dist/index.d.ts CHANGED
@@ -1,161 +1,128 @@
1
- import { unit } from "@eslint-react/eff";
2
- import * as tseslint from "@typescript-eslint/utils/ts-eslint";
3
- import { ReportDescriptor } from "@typescript-eslint/utils/ts-eslint";
1
+ import * as core from "@eslint-react/core";
2
+ import { TSESTreeFunction } from "@eslint-react/ast";
3
+ import { TSESTree } from "@typescript-eslint/utils";
4
+ import { RuleContext, RuleFix, RuleFixer, RuleListener } from "@typescript-eslint/utils/ts-eslint";
5
+ import { Linter } from "eslint";
4
6
 
5
- //#region src/constants.d.ts
6
- /**
7
- * Regular expressions for matching a HTML tag name
8
- */
9
- declare const RE_HTML_TAG: RegExp;
10
- /**
11
- * Regular expression for matching a TypeScript file extension.
12
- */
13
- declare const RE_TS_EXT: RegExp;
14
- /**
15
- * Regular expression for matching a JavaScript file extension.
16
- */
17
- declare const RE_JS_EXT: RegExp;
18
- /**
19
- * Regular expression for matching a PascalCase string.
20
- */
21
- declare const RE_PASCAL_CASE: RegExp;
22
- /**
23
- * Regular expression for matching a camelCase string.
24
- */
25
- declare const RE_CAMEL_CASE: RegExp;
26
- /**
27
- * Regular expression for matching a kebab-case string.
28
- */
29
- declare const RE_KEBAB_CASE: RegExp;
30
- /**
31
- * Regular expression for matching a snake_case string.
32
- */
33
- declare const RE_SNAKE_CASE: RegExp;
34
- /**
35
- * Regular expression for matching a CONSTANT_CASE string.
36
- */
37
- declare const RE_CONSTANT_CASE: RegExp;
38
- declare const RE_JAVASCRIPT_PROTOCOL: RegExp;
39
- /**
40
- * Regular expression for matching a valid JavaScript identifier.
41
- */
42
- declare const RE_JS_IDENTIFIER: RegExp;
43
- /**
44
- * Regular expression for matching a RegExp string.
45
- */
46
- declare const RE_REGEXP_STR: RegExp;
47
- /**
48
- * Regular expression for matching a `@jsx` annotation comment.
49
- */
50
- declare const RE_ANNOTATION_JSX: RegExp;
51
- /**
52
- * Regular expression for matching a `@jsxFrag` annotation comment.
53
- */
54
- declare const RE_ANNOTATION_JSX_FRAG: RegExp;
55
- /**
56
- * Regular expression for matching a `@jsxRuntime` annotation comment.
57
- */
58
- declare const RE_ANNOTATION_JSX_RUNTIME: RegExp;
59
- /**
60
- * Regular expression for matching a `@jsxImportSource` annotation comment.
61
- */
62
- declare const RE_ANNOTATION_JSX_IMPORT_SOURCE: RegExp;
63
- /**
64
- * Regular expression for matching a React component name.
65
- */
66
- declare const RE_COMPONENT_NAME: RegExp;
67
- /**
68
- * Regular expression for matching a React component name (loose).
69
- */
70
- declare const RE_COMPONENT_NAME_LOOSE: RegExp;
71
- /**
72
- * Regular expression for matching a React Hook name.
73
- */
74
- declare const RE_HOOK_NAME: RegExp;
75
- //#endregion
76
- //#region src/types/index.d.ts
77
- /**
78
- * Rule severity.
79
- * @since 0.0.1
80
- */
81
- type SeverityName = "off" | "warn" | "error";
82
- /**
83
- * The numeric severity level for a rule.
84
- *
85
- * - `0` means off.
86
- * - `1` means warn.
87
- * - `2` means error.
88
- */
89
- type SeverityLevel = 0 | 1 | 2;
90
- /**
91
- * The severity of a rule in a configuration.
92
- */
93
- type Severity = SeverityName | SeverityLevel;
94
- /**
95
- * Rule declaration.
96
- * @internal
97
- * @since 0.0.1
98
- */
99
- type RuleConfig<RuleOptions extends unknown[] = unknown[]> = Severity | [Severity, ...Partial<RuleOptions>];
100
- /**
101
- * Rule context.
102
- * @since 0.0.1
103
- */
104
- type RuleContext<MessageIds extends string = string, Options extends readonly unknown[] = readonly unknown[]> = tseslint.RuleContext<MessageIds, Options>;
105
- /**
106
- * Rule feature.
107
- * @since 1.20.0
108
- */
109
- type RuleFeature = "CFG" | "DBG" | "FIX" | "MOD" | "TSC" | "EXP";
110
- type RulePolicy = number;
111
- type RuleSuggest<MessageIds extends string = string> = {
112
- messageId: MessageIds;
113
- data?: Record<string, unknown>;
114
- fix: tseslint.ReportFixFunction;
115
- };
116
- /**
117
- * A collection of settings.
118
- */
119
- interface SettingsConfig {
120
- [key: string]: unknown;
7
+ //#region src/index.d.ts
8
+ interface Collector<T> {
9
+ query: {
10
+ all(program: TSESTree.Program): T[];
11
+ };
12
+ visitor: RuleListener;
121
13
  }
122
- interface CompatibleRule {
123
- meta: Record<string, any>;
124
- create: (...args: any[]) => any;
14
+ interface CollectorWithContext<T, E> extends Collector<T> {
15
+ query: {
16
+ all(program: TSESTree.Program): T[];
17
+ };
125
18
  }
126
- interface CompatiblePlugin {
127
- meta: {
128
- name: string;
129
- version: string;
19
+ declare function components(ctx: RuleContext<string, unknown[]>, options?: {
20
+ collectDisplayName?: boolean;
21
+ hint?: bigint;
22
+ }): CollectorWithContext<core.FunctionComponentSemanticNode, core.FunctionComponentSemanticNode>;
23
+ declare function hooks(ctx: RuleContext<string, unknown[]>): CollectorWithContext<core.HookSemanticNode, {
24
+ key: string;
25
+ node: TSESTree.Node;
26
+ }>;
27
+ interface RuleToolkit {
28
+ is: {
29
+ componentDefinition: (node: TSESTreeFunction, hint: bigint) => boolean;
30
+ componentName: typeof core.isComponentName;
31
+ componentNameLoose: typeof core.isComponentNameLoose;
32
+ componentWrapperCall: (node: TSESTree.Node) => boolean;
33
+ componentWrapperCallLoose: (node: TSESTree.Node) => boolean;
34
+ componentWrapperCallback: (node: TSESTree.Node) => boolean;
35
+ hook: typeof core.isHook;
36
+ hookCall: typeof core.isHookCall;
37
+ hookName: typeof core.isHookName;
38
+ useEffectLikeCall: typeof core.isUseEffectLikeCall;
39
+ useStateLikeCall: typeof core.isUseStateLikeCall;
40
+ useEffectSetupCallback: typeof core.isUseEffectSetupCallback;
41
+ useEffectCleanupCallback: typeof core.isUseEffectCleanupCallback;
42
+ useCall: typeof core.isUseCall;
43
+ useActionStateCall: typeof core.isUseActionStateCall;
44
+ useCallbackCall: typeof core.isUseCallbackCall;
45
+ useContextCall: typeof core.isUseContextCall;
46
+ useDebugValueCall: typeof core.isUseDebugValueCall;
47
+ useDeferredValueCall: typeof core.isUseDeferredValueCall;
48
+ useEffectCall: typeof core.isUseEffectCall;
49
+ useFormStatusCall: typeof core.isUseFormStatusCall;
50
+ useIdCall: typeof core.isUseIdCall;
51
+ useImperativeHandleCall: typeof core.isUseImperativeHandleCall;
52
+ useInsertionEffectCall: typeof core.isUseInsertionEffectCall;
53
+ useLayoutEffectCall: typeof core.isUseLayoutEffectCall;
54
+ useMemoCall: typeof core.isUseMemoCall;
55
+ useOptimisticCall: typeof core.isUseOptimisticCall;
56
+ useReducerCall: typeof core.isUseReducerCall;
57
+ useRefCall: typeof core.isUseRefCall;
58
+ useStateCall: typeof core.isUseStateCall;
59
+ useSyncExternalStoreCall: typeof core.isUseSyncExternalStoreCall;
60
+ useTransitionCall: typeof core.isUseTransitionCall;
61
+ reactAPI: (api: string) => (node: null | TSESTree.Node) => boolean;
62
+ reactAPICall: (api: string) => (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
63
+ captureOwnerStack: (node: null | TSESTree.Node) => boolean;
64
+ childrenCount: (node: null | TSESTree.Node) => boolean;
65
+ childrenForEach: (node: null | TSESTree.Node) => boolean;
66
+ childrenMap: (node: null | TSESTree.Node) => boolean;
67
+ childrenOnly: (node: null | TSESTree.Node) => boolean;
68
+ childrenToArray: (node: null | TSESTree.Node) => boolean;
69
+ cloneElement: (node: null | TSESTree.Node) => boolean;
70
+ createContext: (node: null | TSESTree.Node) => boolean;
71
+ createElement: (node: null | TSESTree.Node) => boolean;
72
+ createRef: (node: null | TSESTree.Node) => boolean;
73
+ forwardRef: (node: null | TSESTree.Node) => boolean;
74
+ memo: (node: null | TSESTree.Node) => boolean;
75
+ lazy: (node: null | TSESTree.Node) => boolean;
76
+ captureOwnerStackCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
77
+ childrenCountCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
78
+ childrenForEachCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
79
+ childrenMapCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
80
+ childrenOnlyCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
81
+ childrenToArrayCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
82
+ cloneElementCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
83
+ createContextCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
84
+ createElementCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
85
+ createRefCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
86
+ forwardRefCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
87
+ memoCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
88
+ lazyCall: (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
89
+ initializedFromReact: typeof core.isInitializedFromReact;
90
+ initializedFromReactNative: typeof core.isInitializedFromReactNative;
91
+ };
92
+ hint: {
93
+ component: typeof core.ComponentDetectionHint & {
94
+ Default: bigint;
95
+ };
96
+ };
97
+ flag: {
98
+ component: typeof core.ComponentFlag;
99
+ };
100
+ collect: {
101
+ components: typeof components;
102
+ hooks: typeof hooks;
130
103
  };
131
- rules: Record<string, CompatibleRule>;
132
104
  }
133
- interface CompatibleConfig {
134
- name?: string;
135
- rules?: Record<string, RuleConfig>;
136
- settings?: SettingsConfig;
105
+ interface RuleDefinition {
106
+ name: string;
107
+ make(ctx: RuleContext<string, unknown[]>, kit: RuleToolkit): RuleListener;
108
+ }
109
+ declare function defineConfig(...rules: RuleDefinition[]): Linter.Config;
110
+ declare function merge(...listeners: RuleListener[]): RuleListener;
111
+ declare module "@typescript-eslint/utils/ts-eslint" {
112
+ interface RuleContext<MessageIds extends string, Options extends readonly unknown[]> {
113
+ report(descriptor: {
114
+ readonly data?: Readonly<Record<string, unknown>>;
115
+ readonly fix?: ((fixer: RuleFixer) => IterableIterator<RuleFix> | readonly RuleFix[] | RuleFix | null) | null;
116
+ readonly loc?: Readonly<TSESTree.SourceLocation> | Readonly<TSESTree.Position>;
117
+ readonly message: string;
118
+ readonly node: TSESTree.Node;
119
+ readonly suggest?: readonly {
120
+ readonly data?: Readonly<Record<string, unknown>>;
121
+ readonly desc: string;
122
+ readonly fix: (fixer: RuleFixer) => IterableIterator<RuleFix> | readonly RuleFix[] | RuleFix | null;
123
+ }[] | null;
124
+ }): void;
125
+ }
137
126
  }
138
127
  //#endregion
139
- //#region src/utils/regexp.d.ts
140
- /**
141
- * Convert a string to the `RegExp`.
142
- * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`.
143
- * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`.
144
- * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts
145
- * @param string The string to convert.
146
- * @returns Returns the `RegExp`.
147
- */
148
- declare function toRegExp(string: string): {
149
- test(s: string): boolean;
150
- };
151
- /**
152
- * Checks whether given string is regexp string
153
- * @param string The string to check
154
- * @returns boolean
155
- */
156
- declare function isRegExp(string: string): boolean;
157
- //#endregion
158
- //#region src/utils/reporting.d.ts
159
- declare function report(context: RuleContext): (descriptor: unit | null | ReportDescriptor<string>) => void;
160
- //#endregion
161
- export { CompatibleConfig, CompatiblePlugin, CompatibleRule, RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_CAMEL_CASE, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE, RE_CONSTANT_CASE, RE_HOOK_NAME, RE_HTML_TAG, RE_JAVASCRIPT_PROTOCOL, RE_JS_EXT, RE_JS_IDENTIFIER, RE_KEBAB_CASE, RE_PASCAL_CASE, RE_REGEXP_STR, RE_SNAKE_CASE, RE_TS_EXT, RuleConfig, RuleContext, RuleFeature, RulePolicy, RuleSuggest, SettingsConfig, Severity, SeverityLevel, SeverityName, isRegExp, report, toRegExp };
128
+ export { Collector, CollectorWithContext, type RuleContext, RuleToolkit, defineConfig as default, defineConfig, merge };
package/dist/index.js CHANGED
@@ -1,108 +1,140 @@
1
- import "@eslint-react/eff";
1
+ import * as core from "@eslint-react/core";
2
2
 
3
- //#region src/constants.ts
4
- /**
5
- * Regular expressions for matching a HTML tag name
6
- */
7
- const RE_HTML_TAG = /^[a-z][^-]*$/u;
8
- /**
9
- * Regular expression for matching a TypeScript file extension.
10
- */
11
- const RE_TS_EXT = /^[cm]?tsx?$/u;
12
- /**
13
- * Regular expression for matching a JavaScript file extension.
14
- */
15
- const RE_JS_EXT = /^[cm]?jsx?$/u;
16
- /**
17
- * Regular expression for matching a PascalCase string.
18
- */
19
- const RE_PASCAL_CASE = /^[A-Z][\dA-Za-z]*$/u;
20
- /**
21
- * Regular expression for matching a camelCase string.
22
- */
23
- const RE_CAMEL_CASE = /^[a-z][\dA-Za-z]*$/u;
24
- /**
25
- * Regular expression for matching a kebab-case string.
26
- */
27
- const RE_KEBAB_CASE = /^[a-z][\d\-a-z]*$/u;
28
- /**
29
- * Regular expression for matching a snake_case string.
30
- */
31
- const RE_SNAKE_CASE = /^[a-z][\d_a-z]*$/u;
32
- /**
33
- * Regular expression for matching a CONSTANT_CASE string.
34
- */
35
- const RE_CONSTANT_CASE = /^[A-Z][\d_A-Z]*$/u;
36
- const RE_JAVASCRIPT_PROTOCOL = /^[\u0000-\u001F ]*j[\t\n\r]*a[\t\n\r]*v[\t\n\r]*a[\t\n\r]*s[\t\n\r]*c[\t\n\r]*r[\t\n\r]*i[\t\n\r]*p[\t\n\r]*t[\t\n\r]*:/iu;
37
- /**
38
- * Regular expression for matching a valid JavaScript identifier.
39
- */
40
- const RE_JS_IDENTIFIER = /^[_$a-z][\w$]*$/i;
41
- /**
42
- * Regular expression for matching a RegExp string.
43
- */
44
- const RE_REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u;
45
- /**
46
- * Regular expression for matching a `@jsx` annotation comment.
47
- */
48
- const RE_ANNOTATION_JSX = /@jsx\s+(\S+)/u;
49
- /**
50
- * Regular expression for matching a `@jsxFrag` annotation comment.
51
- */
52
- const RE_ANNOTATION_JSX_FRAG = /@jsxFrag\s+(\S+)/u;
53
- /**
54
- * Regular expression for matching a `@jsxRuntime` annotation comment.
55
- */
56
- const RE_ANNOTATION_JSX_RUNTIME = /@jsxRuntime\s+(\S+)/u;
57
- /**
58
- * Regular expression for matching a `@jsxImportSource` annotation comment.
59
- */
60
- const RE_ANNOTATION_JSX_IMPORT_SOURCE = /@jsxImportSource\s+(\S+)/u;
61
- /**
62
- * Regular expression for matching a React component name.
63
- */
64
- const RE_COMPONENT_NAME = /^[A-Z]/u;
65
- /**
66
- * Regular expression for matching a React component name (loose).
67
- */
68
- const RE_COMPONENT_NAME_LOOSE = /^_?[A-Z]/u;
69
- /**
70
- * Regular expression for matching a React Hook name.
71
- */
72
- const RE_HOOK_NAME = /^use/u;
3
+ //#region package.json
4
+ var name = "@eslint-react/kit";
5
+ var version = "4.0.0-beta.1";
73
6
 
74
7
  //#endregion
75
- //#region src/utils/regexp.ts
76
- /**
77
- * Convert a string to the `RegExp`.
78
- * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`.
79
- * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`.
80
- * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts
81
- * @param string The string to convert.
82
- * @returns Returns the `RegExp`.
83
- */
84
- function toRegExp(string) {
85
- const [, pattern, flags = "u"] = RE_REGEXP_STR.exec(string) ?? [];
86
- if (pattern != null) return new RegExp(pattern, flags);
87
- return { test: (s) => s === string };
8
+ //#region src/index.ts
9
+ function components(ctx, options) {
10
+ const { api, visitor } = core.getComponentCollector(ctx, options);
11
+ return {
12
+ query: { all: (program) => api.getAllComponents(program) },
13
+ visitor
14
+ };
88
15
  }
89
- /**
90
- * Checks whether given string is regexp string
91
- * @param string The string to check
92
- * @returns boolean
93
- */
94
- function isRegExp(string) {
95
- return RE_REGEXP_STR.test(string);
16
+ function hooks(ctx) {
17
+ const { api, visitor } = core.getHookCollector(ctx);
18
+ return {
19
+ query: { all: (program) => api.getAllHooks(program) },
20
+ visitor
21
+ };
96
22
  }
97
-
98
- //#endregion
99
- //#region src/utils/reporting.ts
100
- function report(context) {
101
- return (descriptor) => {
102
- if (descriptor == null) return;
103
- return context.report(descriptor);
23
+ function createKit(ctx) {
24
+ return {
25
+ is: {
26
+ componentDefinition: (node, hint) => core.isComponentDefinition(ctx, node, hint),
27
+ componentName: core.isComponentName,
28
+ componentNameLoose: core.isComponentNameLoose,
29
+ componentWrapperCall: (node) => core.isComponentWrapperCall(ctx, node),
30
+ componentWrapperCallLoose: (node) => core.isComponentWrapperCallLoose(ctx, node),
31
+ componentWrapperCallback: (node) => core.isComponentWrapperCallback(ctx, node),
32
+ hook: core.isHook,
33
+ hookCall: core.isHookCall,
34
+ hookName: core.isHookName,
35
+ useEffectLikeCall: core.isUseEffectLikeCall,
36
+ useStateLikeCall: core.isUseStateLikeCall,
37
+ useEffectSetupCallback: core.isUseEffectSetupCallback,
38
+ useEffectCleanupCallback: core.isUseEffectCleanupCallback,
39
+ useCall: core.isUseCall,
40
+ useActionStateCall: core.isUseActionStateCall,
41
+ useCallbackCall: core.isUseCallbackCall,
42
+ useContextCall: core.isUseContextCall,
43
+ useDebugValueCall: core.isUseDebugValueCall,
44
+ useDeferredValueCall: core.isUseDeferredValueCall,
45
+ useEffectCall: core.isUseEffectCall,
46
+ useFormStatusCall: core.isUseFormStatusCall,
47
+ useIdCall: core.isUseIdCall,
48
+ useImperativeHandleCall: core.isUseImperativeHandleCall,
49
+ useInsertionEffectCall: core.isUseInsertionEffectCall,
50
+ useLayoutEffectCall: core.isUseLayoutEffectCall,
51
+ useMemoCall: core.isUseMemoCall,
52
+ useOptimisticCall: core.isUseOptimisticCall,
53
+ useReducerCall: core.isUseReducerCall,
54
+ useRefCall: core.isUseRefCall,
55
+ useStateCall: core.isUseStateCall,
56
+ useSyncExternalStoreCall: core.isUseSyncExternalStoreCall,
57
+ useTransitionCall: core.isUseTransitionCall,
58
+ reactAPI: (api) => core.isReactAPI(api)(ctx),
59
+ reactAPICall: (api) => core.isReactAPICall(api)(ctx),
60
+ captureOwnerStack: core.isCaptureOwnerStack(ctx),
61
+ childrenCount: core.isChildrenCount(ctx),
62
+ childrenForEach: core.isChildrenForEach(ctx),
63
+ childrenMap: core.isChildrenMap(ctx),
64
+ childrenOnly: core.isChildrenOnly(ctx),
65
+ childrenToArray: core.isChildrenToArray(ctx),
66
+ cloneElement: core.isCloneElement(ctx),
67
+ createContext: core.isCreateContext(ctx),
68
+ createElement: core.isCreateElement(ctx),
69
+ createRef: core.isCreateRef(ctx),
70
+ forwardRef: core.isForwardRef(ctx),
71
+ memo: core.isMemo(ctx),
72
+ lazy: core.isLazy(ctx),
73
+ captureOwnerStackCall: core.isCaptureOwnerStackCall(ctx),
74
+ childrenCountCall: core.isChildrenCountCall(ctx),
75
+ childrenForEachCall: core.isChildrenForEachCall(ctx),
76
+ childrenMapCall: core.isChildrenMapCall(ctx),
77
+ childrenOnlyCall: core.isChildrenOnlyCall(ctx),
78
+ childrenToArrayCall: core.isChildrenToArrayCall(ctx),
79
+ cloneElementCall: core.isCloneElementCall(ctx),
80
+ createContextCall: core.isCreateContextCall(ctx),
81
+ createElementCall: core.isCreateElementCall(ctx),
82
+ createRefCall: core.isCreateRefCall(ctx),
83
+ forwardRefCall: core.isForwardRefCall(ctx),
84
+ memoCall: core.isMemoCall(ctx),
85
+ lazyCall: core.isLazyCall(ctx),
86
+ initializedFromReact: core.isInitializedFromReact,
87
+ initializedFromReactNative: core.isInitializedFromReactNative
88
+ },
89
+ hint: { component: {
90
+ ...core.ComponentDetectionHint,
91
+ Default: core.DEFAULT_COMPONENT_DETECTION_HINT
92
+ } },
93
+ flag: { component: core.ComponentFlag },
94
+ collect: {
95
+ components,
96
+ hooks
97
+ }
104
98
  };
105
99
  }
100
+ function defineConfig(...rules) {
101
+ return {
102
+ files: ["**/*.ts", "**/*.tsx"],
103
+ plugins: { [name]: {
104
+ meta: {
105
+ name,
106
+ version
107
+ },
108
+ rules: rules.reduce((acc, { name, make }) => {
109
+ Reflect.set(acc, name, {
110
+ meta: {
111
+ fixable: "code",
112
+ hasSuggestions: true
113
+ },
114
+ create(ctx) {
115
+ return make(ctx, createKit(ctx));
116
+ }
117
+ });
118
+ return acc;
119
+ }, {})
120
+ } },
121
+ rules: rules.reduce((acc, { name: name$1 }) => {
122
+ acc[`${name}/${name$1}`] = "error";
123
+ return acc;
124
+ }, {})
125
+ };
126
+ }
127
+ function merge(...listeners) {
128
+ const [base = {}, ...rest] = listeners;
129
+ for (const r of rest) for (const key in r) {
130
+ const existing = base[key];
131
+ base[key] = existing ? (...args) => {
132
+ existing(...args);
133
+ r[key]?.(...args);
134
+ } : r[key];
135
+ }
136
+ return base;
137
+ }
106
138
 
107
139
  //#endregion
108
- export { RE_ANNOTATION_JSX, RE_ANNOTATION_JSX_FRAG, RE_ANNOTATION_JSX_IMPORT_SOURCE, RE_ANNOTATION_JSX_RUNTIME, RE_CAMEL_CASE, RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE, RE_CONSTANT_CASE, RE_HOOK_NAME, RE_HTML_TAG, RE_JAVASCRIPT_PROTOCOL, RE_JS_EXT, RE_JS_IDENTIFIER, RE_KEBAB_CASE, RE_PASCAL_CASE, RE_REGEXP_STR, RE_SNAKE_CASE, RE_TS_EXT, isRegExp, report, toRegExp };
140
+ export { defineConfig as default, defineConfig, merge };
package/package.json CHANGED
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "name": "@eslint-react/kit",
3
- "version": "2.1.2-next.2",
4
- "description": "ESLint React's plugin kit for building plugins and rules.",
3
+ "version": "4.0.0-beta.1",
4
+ "description": "ESLint React's utility module for building custom rules.",
5
+ "keywords": [
6
+ "react",
7
+ "eslint",
8
+ "eslint-react",
9
+ "@eslint-react/kit"
10
+ ],
5
11
  "homepage": "https://github.com/Rel1cx/eslint-react",
6
12
  "bugs": {
7
13
  "url": "https://github.com/Rel1cx/eslint-react/issues"
@@ -12,7 +18,7 @@
12
18
  "directory": "packages/utilities/kit"
13
19
  },
14
20
  "license": "MIT",
15
- "author": "Rel1cx<rel1cx@proton.me>",
21
+ "author": "Rel1cx",
16
22
  "sideEffects": false,
17
23
  "type": "module",
18
24
  "exports": {
@@ -22,26 +28,41 @@
22
28
  },
23
29
  "./package.json": "./package.json"
24
30
  },
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
25
34
  "files": [
26
35
  "dist",
27
36
  "./package.json"
28
37
  ],
29
38
  "dependencies": {
30
- "@typescript-eslint/utils": "^8.46.0",
31
- "@eslint-react/eff": "2.1.2-next.2"
39
+ "@typescript-eslint/utils": "^8.57.2",
40
+ "@eslint-react/ast": "4.0.0-beta.1",
41
+ "@eslint-react/core": "4.0.0-beta.1"
32
42
  },
33
43
  "devDependencies": {
34
- "@tsconfig/node22": "^22.0.2",
35
- "tsdown": "^0.15.6",
44
+ "eslint": "^10.1.0",
45
+ "tsdown": "^0.21.4",
46
+ "@local/eff": "3.0.0-beta.72",
36
47
  "@local/configs": "0.0.0"
37
48
  },
49
+ "peerDependencies": {
50
+ "eslint": "^10.0.0",
51
+ "typescript": "*"
52
+ },
38
53
  "engines": {
39
- "node": ">=20.19.0"
54
+ "node": ">=22.0.0"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "inlinedDependencies": {
60
+ "@local/eff": "workspace:*"
40
61
  },
41
62
  "scripts": {
42
- "build": "tsdown --dts-resolve",
63
+ "build": "tsdown",
43
64
  "build:docs": "typedoc",
44
65
  "lint:publish": "publint",
45
- "lint:ts": "tsc --noEmit"
66
+ "lint:ts": "tsl"
46
67
  }
47
68
  }