eslint-plugin-redux-sparse-kit 0.0.0-stage → 0.1.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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksandr Khizhnyi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,189 @@
1
- # Temporary Holding Version
1
+ # eslint-plugin-redux-sparse-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/eslint-plugin-redux-sparse-kit.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-redux-sparse-kit)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md)
5
+
6
+ ESLint rules for [redux-sparse-kit](https://github.com/veksa/redux-sparse-kit):
7
+ mistakes with segments and scopes that types cannot see, caught in the editor
8
+ instead of at runtime.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install --save-dev eslint-plugin-redux-sparse-kit
14
+ # or
15
+ yarn add --dev eslint-plugin-redux-sparse-kit
16
+ ```
17
+
18
+ Requires ESLint 8.57 or later. `no-static-state-key` needs type information,
19
+ which comes from `@typescript-eslint/parser` 8 and TypeScript 5.4 to 6.0.
20
+
21
+ ## Usage
22
+
23
+ `eslint.config.js`:
24
+
25
+ ```js
26
+ import { reduxSparseKitPlugin } from 'eslint-plugin-redux-sparse-kit';
27
+
28
+ export default [
29
+ // ...
30
+ ...reduxSparseKitPlugin.configs.recommended,
31
+ ];
32
+ ```
33
+
34
+ With type information, use `recommended-type-checked`, which adds the rules
35
+ that need it:
36
+
37
+ ```js
38
+ import tseslint from 'typescript-eslint';
39
+ import { reduxSparseKitPlugin } from 'eslint-plugin-redux-sparse-kit';
40
+
41
+ export default [
42
+ {
43
+ languageOptions: {
44
+ parser: tseslint.parser,
45
+ parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
46
+ },
47
+ },
48
+ ...reduxSparseKitPlugin.configs['recommended-type-checked'],
49
+ ];
50
+ ```
51
+
52
+ | Config | Rules |
53
+ | -------------------------- | ----------------------------------------------------------------------------------- |
54
+ | `recommended` | `no-define-segment-in-render`, `no-duplicate-segments`, `release-acquired-segments` |
55
+ | `recommended-type-checked` | the above and `no-static-state-key` |
56
+
57
+ The configs are flat config arrays. Under eslintrc, add the plugin and list the
58
+ rules:
59
+
60
+ ```json
61
+ {
62
+ "plugins": ["redux-sparse-kit"],
63
+ "rules": {
64
+ "redux-sparse-kit/no-define-segment-in-render": "error",
65
+ "redux-sparse-kit/no-duplicate-segments": "error",
66
+ "redux-sparse-kit/release-acquired-segments": "error"
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Settings
72
+
73
+ The rules find redux-sparse-kit's API by its imports from `redux-sparse-kit`
74
+ and `redux-sparse-kit/react`, renamed or not, through namespace imports, and
75
+ through what `createSparseReact()` returns. When an application re-exports the
76
+ API from a module of its own, name that module:
77
+
78
+ ```js
79
+ {
80
+ settings: {
81
+ 'redux-sparse-kit': { importSources: ['@app/state'] },
82
+ },
83
+ }
84
+ ```
85
+
86
+ ## Rules
87
+
88
+ ### `no-define-segment-in-render`
89
+
90
+ A segment is identified by the object `defineSegment` returns. Defined inside a
91
+ component or a hook, it is a new segment on every render: scopes acquire it
92
+ again, and the store refuses a second segment under a key that is still
93
+ present.
94
+
95
+ ```tsx
96
+ // ✗
97
+ function OrdersPage() {
98
+ const orders = defineSegment({ key: 'orders', reducer });
99
+ return <SparseScope segments={[orders]} />;
100
+ }
101
+
102
+ // ✓
103
+ const orders = defineSegment({ key: 'orders', reducer });
104
+
105
+ function OrdersPage() {
106
+ return <SparseScope segments={[orders]} />;
107
+ }
108
+ ```
109
+
110
+ Functions that are neither components nor hooks are left alone, so a factory
111
+ that defines segments at module scope is fine:
112
+
113
+ ```ts
114
+ const createListSegment = (key: string) => defineSegment({ key, reducer: listReducer });
115
+
116
+ export const orders = createListSegment('orders');
117
+ ```
118
+
119
+ Components and hooks are told by their names: a function, or the variable or
120
+ wrapper call it is assigned to, named in PascalCase or `useSomething`, and
121
+ class `render` methods.
122
+
123
+ ### `no-duplicate-segments`
124
+
125
+ Reports a segment listed twice in `deps`, in a `SparseScope`'s `segments`, in
126
+ `useSparseSegments([...])` and in `store.sparse.acquire(...)`, and two
127
+ segments defined with the same literal key in one file.
128
+
129
+ ```tsx
130
+ // ✗
131
+ defineSegment({ key: 'page', deps: [orders, prices, orders] });
132
+ <SparseScope segments={[orders, orders]} />;
133
+
134
+ export const a = defineSegment({ key: 'orders', reducer });
135
+ export const b = defineSegment({ key: 'orders', reducer: otherReducer });
136
+ ```
137
+
138
+ Segments are compared by their references, such as `orders` or
139
+ `segments.orders`; keys built at runtime are not compared.
140
+
141
+ ### `release-acquired-segments`
142
+
143
+ `store.sparse.acquire(...)` returns the function that releases what it
144
+ acquired. Discarding it keeps the segments in the store for good.
145
+
146
+ ```ts
147
+ // ✗
148
+ store.sparse.acquire(orders);
149
+
150
+ useEffect(() => {
151
+ store.sparse.acquire(orders);
152
+ }, []);
153
+
154
+ // ✓
155
+ const release = store.sparse.acquire(orders);
156
+
157
+ useEffect(() => store.sparse.acquire(orders), []);
158
+ ```
159
+
160
+ Calls are recognised as `<something>.sparse.acquire(...)` and
161
+ `sparse.acquire(...)`.
162
+
163
+ ### `no-static-state-key`
164
+
165
+ _Needs type information._ A segment with a reducer cannot use a key of the
166
+ static state: the store refuses to acquire it. The rule reads the segment's key
167
+ and reducer from its type, and the static state from `StaticState` as the
168
+ application registers it:
169
+
170
+ ```ts
171
+ declare module 'redux-sparse-kit' {
172
+ interface StaticState {
173
+ router: RouterState;
174
+ }
175
+ }
176
+
177
+ // ✗
178
+ export const routing = defineSegment({ key: 'router', reducer: routingReducer });
179
+
180
+ // ✓ a segment without a reducer owns no slice
181
+ export const routing = defineSegment({ key: 'router', epics: [routingEpic] });
182
+ ```
183
+
184
+ Keys typed as a union of literals are checked one by one; keys typed as
185
+ `string` are not known and not checked.
186
+
187
+ ## License
188
+
189
+ [MIT](LICENSE.md)
package/lib/index.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ declare const meta: {
3
+ name: string;
4
+ };
5
+ declare const rules: {
6
+ 'no-define-segment-in-render': TSESLint.RuleModule<"define-segment-in-render", [], unknown, TSESLint.RuleListener>;
7
+ 'no-duplicate-segments': TSESLint.RuleModule<"duplicate-segment" | "duplicate-key", [], unknown, TSESLint.RuleListener>;
8
+ 'no-static-state-key': TSESLint.RuleModule<"static-state-key", [], unknown, TSESLint.RuleListener>;
9
+ 'release-acquired-segments': TSESLint.RuleModule<"release-discarded", [], unknown, TSESLint.RuleListener>;
10
+ };
11
+ export declare const reduxSparseKitPlugin: {
12
+ meta: {
13
+ name: string;
14
+ };
15
+ rules: {
16
+ 'no-define-segment-in-render': TSESLint.RuleModule<"define-segment-in-render", [], unknown, TSESLint.RuleListener>;
17
+ 'no-duplicate-segments': TSESLint.RuleModule<"duplicate-segment" | "duplicate-key", [], unknown, TSESLint.RuleListener>;
18
+ 'no-static-state-key': TSESLint.RuleModule<"static-state-key", [], unknown, TSESLint.RuleListener>;
19
+ 'release-acquired-segments': TSESLint.RuleModule<"release-discarded", [], unknown, TSESLint.RuleListener>;
20
+ };
21
+ configs: {
22
+ recommended: TSESLint.FlatConfig.ConfigArray;
23
+ 'recommended-type-checked': TSESLint.FlatConfig.ConfigArray;
24
+ };
25
+ };
26
+ /**
27
+ * The eslintrc engine loads a plugin by name and looks the rules up on the
28
+ * module itself, so `plugins: ['redux-sparse-kit']` needs them exported here.
29
+ * Its configs are flat config arrays, so under eslintrc list the rules
30
+ * directly instead of extending them.
31
+ */
32
+ export { meta, rules };
package/lib/index.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rules = exports.meta = exports.reduxSparseKitPlugin = void 0;
4
+ const noDefineSegmentInRender_1 = require("./rules/noDefineSegmentInRender");
5
+ const noDuplicateSegments_1 = require("./rules/noDuplicateSegments");
6
+ const noStaticStateKey_1 = require("./rules/noStaticStateKey");
7
+ const releaseAcquiredSegments_1 = require("./rules/releaseAcquiredSegments");
8
+ const meta = {
9
+ name: 'redux-sparse-kit',
10
+ };
11
+ exports.meta = meta;
12
+ const rules = {
13
+ 'no-define-segment-in-render': noDefineSegmentInRender_1.noDefineSegmentInRenderRule,
14
+ 'no-duplicate-segments': noDuplicateSegments_1.noDuplicateSegmentsRule,
15
+ 'no-static-state-key': noStaticStateKey_1.noStaticStateKeyRule,
16
+ 'release-acquired-segments': releaseAcquiredSegments_1.releaseAcquiredSegmentsRule,
17
+ };
18
+ exports.rules = rules;
19
+ const plugin = { meta, rules };
20
+ /** The rules that need no type information. */
21
+ const recommended = [
22
+ {
23
+ name: 'redux-sparse-kit/recommended',
24
+ plugins: {
25
+ 'redux-sparse-kit': plugin,
26
+ },
27
+ rules: {
28
+ 'redux-sparse-kit/no-define-segment-in-render': 'error',
29
+ 'redux-sparse-kit/no-duplicate-segments': 'error',
30
+ 'redux-sparse-kit/release-acquired-segments': 'error',
31
+ },
32
+ },
33
+ ];
34
+ /** Every rule, for projects linted with type information. */
35
+ const recommendedTypeChecked = [
36
+ ...recommended,
37
+ {
38
+ name: 'redux-sparse-kit/recommended-type-checked',
39
+ rules: {
40
+ 'redux-sparse-kit/no-static-state-key': 'error',
41
+ },
42
+ },
43
+ ];
44
+ exports.reduxSparseKitPlugin = {
45
+ meta,
46
+ rules,
47
+ configs: {
48
+ recommended,
49
+ 'recommended-type-checked': recommendedTypeChecked,
50
+ },
51
+ };
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AACA,6EAA8E;AAC9E,qEAAsE;AACtE,+DAAgE;AAChE,6EAA8E;AAE9E,MAAM,IAAI,GAAG;IACX,IAAI,EAAE,kBAAkB;CACzB,CAAC;AAoDO,oBAAI;AAlDb,MAAM,KAAK,GAAG;IACZ,6BAA6B,EAAE,qDAA2B;IAC1D,uBAAuB,EAAE,6CAAuB;IAChD,qBAAqB,EAAE,uCAAoB;IAC3C,2BAA2B,EAAE,qDAA2B;CACzD,CAAC;AA6Ca,sBAAK;AA3CpB,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAE/B,+CAA+C;AAC/C,MAAM,WAAW,GAAoC;IACnD;QACE,IAAI,EAAE,8BAA8B;QACpC,OAAO,EAAE;YACP,kBAAkB,EAAE,MAAM;SAC3B;QACD,KAAK,EAAE;YACL,8CAA8C,EAAE,OAAO;YACvD,wCAAwC,EAAE,OAAO;YACjD,4CAA4C,EAAE,OAAO;SACtD;KACF;CACF,CAAC;AAEF,6DAA6D;AAC7D,MAAM,sBAAsB,GAAoC;IAC9D,GAAG,WAAW;IACd;QACE,IAAI,EAAE,2CAA2C;QACjD,KAAK,EAAE;YACL,sCAAsC,EAAE,OAAO;SAChD;KACF;CACF,CAAC;AAEW,QAAA,oBAAoB,GAAG;IAClC,IAAI;IACJ,KAAK;IACL,OAAO,EAAE;QACP,WAAW;QACX,0BAA0B,EAAE,sBAAsB;KACnD;CACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'define-segment-in-render';
3
+ type Rule = TSESLint.RuleModule<MessageIds, []>;
4
+ export declare const noDefineSegmentInRenderRule: Rule;
5
+ export {};
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noDefineSegmentInRenderRule = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const sparseApi_1 = require("../utils/sparseApi");
6
+ const componentName = /^[A-Z]/;
7
+ const hookName = /^use[A-Z0-9]/;
8
+ const isFunction = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
9
+ node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
10
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression;
11
+ /**
12
+ * The name a function is known by: its own, the variable or property it is
13
+ * assigned to, or the variable a wrapper such as `memo(...)` is assigned to.
14
+ */
15
+ const getFunctionName = (node) => {
16
+ if (node.id) {
17
+ return node.id.name;
18
+ }
19
+ let parent = node.parent;
20
+ while (parent.type === utils_1.AST_NODE_TYPES.CallExpression) {
21
+ parent = parent.parent;
22
+ }
23
+ if (parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
24
+ parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
25
+ return parent.id.name;
26
+ }
27
+ if ((parent.type === utils_1.AST_NODE_TYPES.Property || parent.type === utils_1.AST_NODE_TYPES.MethodDefinition) &&
28
+ parent.key.type === utils_1.AST_NODE_TYPES.Identifier) {
29
+ return parent.key.name;
30
+ }
31
+ return undefined;
32
+ };
33
+ /** A component or hook: its body runs on every render. */
34
+ const rendersOnEveryCall = (node) => {
35
+ if (isFunction(node)) {
36
+ const name = getFunctionName(node);
37
+ return name !== undefined && (componentName.test(name) || hookName.test(name));
38
+ }
39
+ return (node.type === utils_1.AST_NODE_TYPES.MethodDefinition &&
40
+ node.key.type === utils_1.AST_NODE_TYPES.Identifier &&
41
+ node.key.name === 'render');
42
+ };
43
+ const meta = {
44
+ docs: {
45
+ description: 'Disallow defining segments in components and hooks.',
46
+ },
47
+ messages: {
48
+ 'define-segment-in-render': 'Define segments at module scope: a segment defined in a component or hook is a new segment on every render.',
49
+ },
50
+ schema: [],
51
+ type: 'problem',
52
+ };
53
+ const create = (context) => {
54
+ const api = (0, sparseApi_1.getSparseApi)(context);
55
+ return {
56
+ CallExpression(node) {
57
+ if (api.resolve(node.callee) !== 'defineSegment') {
58
+ return;
59
+ }
60
+ if (context.sourceCode.getAncestors(node).some(rendersOnEveryCall)) {
61
+ context.report({ messageId: 'define-segment-in-render', node });
62
+ }
63
+ },
64
+ };
65
+ };
66
+ exports.noDefineSegmentInRenderRule = {
67
+ defaultOptions: [],
68
+ meta,
69
+ create,
70
+ };
71
+ //# sourceMappingURL=noDefineSegmentInRender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noDefineSegmentInRender.js","sourceRoot":"","sources":["../../src/rules/noDefineSegmentInRender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'duplicate-segment' | 'duplicate-key';
3
+ type Rule = TSESLint.RuleModule<MessageIds, []>;
4
+ export declare const noDuplicateSegmentsRule: Rule;
5
+ export {};
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noDuplicateSegmentsRule = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const isAcquireCall_1 = require("../utils/isAcquireCall");
6
+ const sparseApi_1 = require("../utils/sparseApi");
7
+ /** A segment reference that can be compared by its text: `a` or `a.b.c`. */
8
+ const isReference = (node) => node.type === utils_1.AST_NODE_TYPES.Identifier ||
9
+ (node.type === utils_1.AST_NODE_TYPES.MemberExpression && !node.computed && isReference(node.object));
10
+ /** The value of a string literal or of a template literal without expressions. */
11
+ const getStaticString = (node) => {
12
+ if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
13
+ return node.value;
14
+ }
15
+ if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral && node.expressions.length === 0) {
16
+ // Only a tagged template can leave an escape uncooked.
17
+ return node.quasis[0].value.cooked;
18
+ }
19
+ return undefined;
20
+ };
21
+ const getProperty = (node, name) => {
22
+ if ((node === null || node === void 0 ? void 0 : node.type) !== utils_1.AST_NODE_TYPES.ObjectExpression) {
23
+ return undefined;
24
+ }
25
+ for (const property of node.properties) {
26
+ if (property.type === utils_1.AST_NODE_TYPES.Property &&
27
+ !property.computed &&
28
+ ((property.key.type === utils_1.AST_NODE_TYPES.Identifier && property.key.name === name) ||
29
+ getStaticString(property.key) === name)) {
30
+ return property.value;
31
+ }
32
+ }
33
+ return undefined;
34
+ };
35
+ const meta = {
36
+ docs: {
37
+ description: 'Disallow listing a segment twice, and defining two segments with the same key in a file.',
38
+ },
39
+ messages: {
40
+ 'duplicate-segment': '`{{name}}` is already listed.',
41
+ 'duplicate-key': 'A segment with the key "{{key}}" is already defined in this file.',
42
+ },
43
+ schema: [],
44
+ type: 'problem',
45
+ };
46
+ const create = (context) => {
47
+ const api = (0, sparseApi_1.getSparseApi)(context);
48
+ const definedKeys = new Set();
49
+ const checkList = (items) => {
50
+ const seen = new Set();
51
+ for (const item of items) {
52
+ if (item && isReference(item)) {
53
+ const name = context.sourceCode.getText(item);
54
+ if (seen.has(name)) {
55
+ context.report({ messageId: 'duplicate-segment', node: item, data: { name } });
56
+ }
57
+ seen.add(name);
58
+ }
59
+ }
60
+ };
61
+ const checkArray = (node) => {
62
+ if ((node === null || node === void 0 ? void 0 : node.type) === utils_1.AST_NODE_TYPES.ArrayExpression) {
63
+ checkList(node.elements);
64
+ }
65
+ };
66
+ return {
67
+ CallExpression(node) {
68
+ if ((0, isAcquireCall_1.isAcquireCall)(node)) {
69
+ checkList(node.arguments);
70
+ return;
71
+ }
72
+ const name = api.resolve(node.callee);
73
+ if (name === 'useSparseSegments') {
74
+ checkArray(node.arguments[0]);
75
+ }
76
+ else if (name === 'defineSegment') {
77
+ const [options] = node.arguments;
78
+ checkArray(getProperty(options, 'deps'));
79
+ const keyNode = getProperty(options, 'key');
80
+ const key = keyNode && getStaticString(keyNode);
81
+ if (key !== undefined) {
82
+ if (definedKeys.has(key)) {
83
+ context.report({ messageId: 'duplicate-key', node: keyNode, data: { key } });
84
+ }
85
+ definedKeys.add(key);
86
+ }
87
+ }
88
+ },
89
+ JSXOpeningElement(node) {
90
+ var _a;
91
+ if (api.resolve(node.name) !== 'SparseScope') {
92
+ return;
93
+ }
94
+ for (const attribute of node.attributes) {
95
+ if (attribute.type === utils_1.AST_NODE_TYPES.JSXAttribute &&
96
+ attribute.name.name === 'segments' &&
97
+ ((_a = attribute.value) === null || _a === void 0 ? void 0 : _a.type) === utils_1.AST_NODE_TYPES.JSXExpressionContainer) {
98
+ checkArray(attribute.value.expression);
99
+ }
100
+ }
101
+ },
102
+ };
103
+ };
104
+ exports.noDuplicateSegmentsRule = {
105
+ defaultOptions: [],
106
+ meta,
107
+ create,
108
+ };
109
+ //# sourceMappingURL=noDuplicateSegments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noDuplicateSegments.js","sourceRoot":"","sources":["../../src/rules/noDuplicateSegments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'static-state-key';
3
+ type Rule = TSESLint.RuleModule<MessageIds, []>;
4
+ export declare const noStaticStateKeyRule: Rule;
5
+ export {};
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.noStaticStateKeyRule = void 0;
7
+ const utils_1 = require("@typescript-eslint/utils");
8
+ const typescript_1 = __importDefault(require("typescript"));
9
+ const sparseApi_1 = require("../utils/sparseApi");
10
+ /**
11
+ * The `StaticState` interface declared next to the type of the segment, with
12
+ * the augmentations the program adds to it. Read from the segment rather than
13
+ * from the function called, so that a function wrapping `defineSegment` is
14
+ * checked too.
15
+ */
16
+ const getStaticState = (segment, checker) => {
17
+ // A type with a key and a reducer is declared somewhere.
18
+ const staticState = segment
19
+ .getSymbol()
20
+ .declarations[0].getSourceFile()
21
+ .statements.find((statement) => typescript_1.default.isInterfaceDeclaration(statement) && statement.name.text === 'StaticState');
22
+ return staticState && checker.getTypeAtLocation(staticState.name);
23
+ };
24
+ /** The string literal types a key type allows. */
25
+ const getKeys = (type) => (type.isUnion() ? type.types : [type])
26
+ .filter((member) => member.isStringLiteral())
27
+ .map((member) => member.value);
28
+ const meta = {
29
+ docs: {
30
+ description: 'Disallow segments with a reducer under a key of the static state.',
31
+ },
32
+ messages: {
33
+ 'static-state-key': 'Segment key "{{key}}" is a key of the static state: acquiring a segment with a reducer under it throws.',
34
+ },
35
+ schema: [],
36
+ type: 'problem',
37
+ };
38
+ const create = (context) => {
39
+ const api = (0, sparseApi_1.getSparseApi)(context);
40
+ const { esTreeNodeToTSNodeMap, program } = utils_1.ESLintUtils.getParserServices(context);
41
+ const checker = program.getTypeChecker();
42
+ return {
43
+ CallExpression(node) {
44
+ if (api.resolve(node.callee) !== 'defineSegment') {
45
+ return;
46
+ }
47
+ const call = esTreeNodeToTSNodeMap.get(node);
48
+ const segment = checker.getTypeAtLocation(call);
49
+ const key = segment.getProperty('key');
50
+ const reducer = segment.getProperty('reducer');
51
+ if (!key || !reducer) {
52
+ return;
53
+ }
54
+ const reducerType = checker.getTypeOfSymbolAtLocation(reducer, call);
55
+ if (reducerType.flags & typescript_1.default.TypeFlags.Undefined) {
56
+ // A segment without a reducer owns no slice.
57
+ return;
58
+ }
59
+ const staticState = getStaticState(segment, checker);
60
+ if (!staticState) {
61
+ return;
62
+ }
63
+ for (const name of getKeys(checker.getTypeOfSymbolAtLocation(key, call))) {
64
+ if (staticState.getProperty(name)) {
65
+ context.report({ messageId: 'static-state-key', node, data: { key: name } });
66
+ }
67
+ }
68
+ },
69
+ };
70
+ };
71
+ exports.noStaticStateKeyRule = {
72
+ defaultOptions: [],
73
+ meta,
74
+ create,
75
+ };
76
+ //# sourceMappingURL=noStaticStateKey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noStaticStateKey.js","sourceRoot":"","sources":["../../src/rules/noStaticStateKey.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'release-discarded';
3
+ type Rule = TSESLint.RuleModule<MessageIds, []>;
4
+ export declare const releaseAcquiredSegmentsRule: Rule;
5
+ export {};
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.releaseAcquiredSegmentsRule = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const isAcquireCall_1 = require("../utils/isAcquireCall");
6
+ /** Whether the value of `node` is thrown away. */
7
+ const isDiscarded = (node) => {
8
+ // Never the program itself: a call or an expression around it.
9
+ const parent = node.parent;
10
+ switch (parent.type) {
11
+ case utils_1.AST_NODE_TYPES.ChainExpression:
12
+ return isDiscarded(parent);
13
+ case utils_1.AST_NODE_TYPES.ExpressionStatement:
14
+ return true;
15
+ case utils_1.AST_NODE_TYPES.UnaryExpression:
16
+ return parent.operator === 'void';
17
+ case utils_1.AST_NODE_TYPES.SequenceExpression:
18
+ return parent.expressions[parent.expressions.length - 1] !== node || isDiscarded(parent);
19
+ default:
20
+ return false;
21
+ }
22
+ };
23
+ const meta = {
24
+ docs: {
25
+ description: 'Require keeping the release function that acquire returns.',
26
+ },
27
+ messages: {
28
+ 'release-discarded': 'The release function acquire returns is discarded, so the segments are held for good. Keep it and call it once they are no longer needed.',
29
+ },
30
+ schema: [],
31
+ type: 'problem',
32
+ };
33
+ const create = (context) => ({
34
+ CallExpression(node) {
35
+ if ((0, isAcquireCall_1.isAcquireCall)(node) && isDiscarded(node)) {
36
+ context.report({ messageId: 'release-discarded', node });
37
+ }
38
+ },
39
+ });
40
+ exports.releaseAcquiredSegmentsRule = {
41
+ defaultOptions: [],
42
+ meta,
43
+ create,
44
+ };
45
+ //# sourceMappingURL=releaseAcquiredSegments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"releaseAcquiredSegments.js","sourceRoot":"","sources":["../../src/rules/releaseAcquiredSegments.ts"],"names":[],"mappings":";;;AAAA,oDAA8E;AAC9E,0DAAuD;AAMvD,kDAAkD;AAClD,MAAM,WAAW,GAAG,CAAC,IAAmB,EAAW,EAAE;IACnD,+DAA+D;IAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAO,CAAC;IAE5B,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,sBAAc,CAAC,eAAe;YACjC,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;QAC7B,KAAK,sBAAc,CAAC,mBAAmB;YACrC,OAAO,IAAI,CAAC;QACd,KAAK,sBAAc,CAAC,eAAe;YACjC,OAAO,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC;QACpC,KAAK,sBAAc,CAAC,kBAAkB;YACpC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC;QAC3F;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,IAAI,GAAiB;IACzB,IAAI,EAAE;QACJ,WAAW,EAAE,4DAA4D;KAC1E;IACD,QAAQ,EAAE;QACR,mBAAmB,EACjB,2IAA2I;KAC9I;IACD,MAAM,EAAE,EAAE;IACV,IAAI,EAAE,SAAS;CAChB,CAAC;AAEF,MAAM,MAAM,GAAmB,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC3C,cAAc,CAAC,IAAI;QACjB,IAAI,IAAA,6BAAa,EAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;CACF,CAAC,CAAC;AAEU,QAAA,2BAA2B,GAAS;IAC/C,cAAc,EAAE,EAAE;IAClB,IAAI;IACJ,MAAM;CACP,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { TSESTree } from '@typescript-eslint/utils';
2
+ /**
3
+ * A call to the sparse controller's `acquire`: `store.sparse.acquire(...)`,
4
+ * or `sparse.acquire(...)` on a controller taken out of the store.
5
+ */
6
+ export declare const isAcquireCall: (node: TSESTree.CallExpression) => boolean;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAcquireCall = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const isNamed = (node, name) => node.type === utils_1.AST_NODE_TYPES.Identifier && node.name === name;
6
+ /**
7
+ * A call to the sparse controller's `acquire`: `store.sparse.acquire(...)`,
8
+ * or `sparse.acquire(...)` on a controller taken out of the store.
9
+ */
10
+ const isAcquireCall = (node) => {
11
+ const { callee } = node;
12
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
13
+ callee.computed ||
14
+ !isNamed(callee.property, 'acquire')) {
15
+ return false;
16
+ }
17
+ const { object } = callee;
18
+ return (isNamed(object, 'sparse') ||
19
+ (object.type === utils_1.AST_NODE_TYPES.MemberExpression &&
20
+ !object.computed &&
21
+ isNamed(object.property, 'sparse')));
22
+ };
23
+ exports.isAcquireCall = isAcquireCall;
24
+ //# sourceMappingURL=isAcquireCall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isAcquireCall.js","sourceRoot":"","sources":["../../src/utils/isAcquireCall.ts"],"names":[],"mappings":";;;AAAA,oDAAoE;AAEpE,MAAM,OAAO,GAAG,CAAC,IAAmB,EAAE,IAAY,EAAW,EAAE,CAC7D,IAAI,CAAC,IAAI,KAAK,sBAAc,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC;AAEhE;;;GAGG;AACI,MAAM,aAAa,GAAG,CAAC,IAA6B,EAAW,EAAE;IACtE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAExB,IACE,MAAM,CAAC,IAAI,KAAK,sBAAc,CAAC,gBAAgB;QAC/C,MAAM,CAAC,QAAQ;QACf,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,EACpC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAE1B,OAAO,CACL,OAAO,CAAC,MAAM,EAAE,QAAQ,CAAC;QACzB,CAAC,MAAM,CAAC,IAAI,KAAK,sBAAc,CAAC,gBAAgB;YAC9C,CAAC,MAAM,CAAC,QAAQ;YAChB,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CACtC,CAAC;AACJ,CAAC,CAAC;AAnBW,QAAA,aAAa,iBAmBxB"}
@@ -0,0 +1,23 @@
1
+ import { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
+ /** The redux-sparse-kit exports the rules recognise. */
3
+ export type SparseExport = 'defineSegment' | 'createSparseReact' | 'SparseScope' | 'useSparseSegments';
4
+ /** The modules redux-sparse-kit's API is imported from by default. */
5
+ export declare const defaultImportSources: readonly string[];
6
+ /** Shared settings: `settings['redux-sparse-kit']` in an ESLint config. */
7
+ export interface SparseSettings {
8
+ /**
9
+ * Further modules that re-export redux-sparse-kit's API, such as an
10
+ * application's own barrel module.
11
+ */
12
+ importSources?: string[];
13
+ }
14
+ export interface SparseApi {
15
+ /** Which export `node` refers to, if any. */
16
+ resolve(node: TSESTree.Node): SparseExport | undefined;
17
+ }
18
+ /**
19
+ * Finds the names a file binds to redux-sparse-kit's API: named imports, with
20
+ * or without an alias, namespace imports, and the members of the bindings
21
+ * `createSparseReact()` returns.
22
+ */
23
+ export declare function getSparseApi(context: Readonly<TSESLint.RuleContext<string, unknown[]>>): SparseApi;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultImportSources = void 0;
4
+ exports.getSparseApi = getSparseApi;
5
+ const utils_1 = require("@typescript-eslint/utils");
6
+ /** The modules redux-sparse-kit's API is imported from by default. */
7
+ exports.defaultImportSources = [
8
+ 'redux-sparse-kit',
9
+ 'redux-sparse-kit/react',
10
+ ];
11
+ const sparseExports = new Set([
12
+ 'defineSegment',
13
+ 'createSparseReact',
14
+ 'SparseScope',
15
+ 'useSparseSegments',
16
+ ]);
17
+ const isSparseExport = (name) => sparseExports.has(name);
18
+ /** The members of `createSparseReact`'s result. */
19
+ const reactMembers = new Set(['SparseScope', 'useSparseSegments']);
20
+ /**
21
+ * Finds the names a file binds to redux-sparse-kit's API: named imports, with
22
+ * or without an alias, namespace imports, and the members of the bindings
23
+ * `createSparseReact()` returns.
24
+ */
25
+ function getSparseApi(context) {
26
+ var _a;
27
+ const settings = context.settings['redux-sparse-kit'];
28
+ const sources = new Set([...exports.defaultImportSources, ...((_a = settings === null || settings === void 0 ? void 0 : settings.importSources) !== null && _a !== void 0 ? _a : [])]);
29
+ // Local names bound to an export.
30
+ const named = new Map();
31
+ // Local names bound to the whole module, or to a `createSparseReact()` result.
32
+ const namespaces = new Set();
33
+ const isReactFactoryCall = (node) => (node === null || node === void 0 ? void 0 : node.type) === utils_1.AST_NODE_TYPES.CallExpression && resolve(node.callee) === 'createSparseReact';
34
+ const resolve = (node) => {
35
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
36
+ return named.get(node.name);
37
+ }
38
+ if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
39
+ !node.computed &&
40
+ node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
41
+ isSparseExport(node.property.name)) {
42
+ const { object } = node;
43
+ if (object.type === utils_1.AST_NODE_TYPES.Identifier && namespaces.has(object.name)) {
44
+ return node.property.name;
45
+ }
46
+ if (isReactFactoryCall(object) && reactMembers.has(node.property.name)) {
47
+ return node.property.name;
48
+ }
49
+ }
50
+ if (node.type === utils_1.AST_NODE_TYPES.JSXMemberExpression &&
51
+ node.object.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
52
+ namespaces.has(node.object.name) &&
53
+ node.property.name === 'SparseScope') {
54
+ return 'SparseScope';
55
+ }
56
+ if (node.type === utils_1.AST_NODE_TYPES.JSXIdentifier) {
57
+ return named.get(node.name);
58
+ }
59
+ return undefined;
60
+ };
61
+ for (const statement of context.sourceCode.ast.body) {
62
+ if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
63
+ !sources.has(statement.source.value)) {
64
+ continue;
65
+ }
66
+ for (const specifier of statement.specifiers) {
67
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
68
+ namespaces.add(specifier.local.name);
69
+ }
70
+ else if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
71
+ const imported = specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier
72
+ ? specifier.imported.name
73
+ : specifier.imported.value;
74
+ if (isSparseExport(imported)) {
75
+ named.set(specifier.local.name, imported);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ // Bindings made from `createSparseReact()`: `const custom = ...` and
81
+ // `const { SparseScope, useSparseSegments: useSegments } = ...`.
82
+ const visit = (node) => {
83
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && isReactFactoryCall(node.init)) {
84
+ if (node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
85
+ namespaces.add(node.id.name);
86
+ }
87
+ else if (node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
88
+ for (const property of node.id.properties) {
89
+ if (property.type === utils_1.AST_NODE_TYPES.Property &&
90
+ property.key.type === utils_1.AST_NODE_TYPES.Identifier &&
91
+ reactMembers.has(property.key.name) &&
92
+ property.value.type === utils_1.AST_NODE_TYPES.Identifier) {
93
+ named.set(property.value.name, property.key.name);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ // The parser knows the keys of every node it produces; they lead to
99
+ // nodes, arrays of nodes, or nothing.
100
+ for (const key of context.sourceCode.visitorKeys[node.type]) {
101
+ const child = node[key];
102
+ for (const item of Array.isArray(child) ? child : [child]) {
103
+ if (item) {
104
+ visit(item);
105
+ }
106
+ }
107
+ }
108
+ };
109
+ // Only worth a walk when the factory is in reach.
110
+ if ([...named.values()].includes('createSparseReact') || namespaces.size > 0) {
111
+ visit(context.sourceCode.ast);
112
+ }
113
+ return { resolve };
114
+ }
115
+ //# sourceMappingURL=sparseApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sparseApi.js","sourceRoot":"","sources":["../../src/utils/sparseApi.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "eslint-plugin-redux-sparse-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "ESLint rules for redux-sparse-kit segments and scopes",
5
+ "keywords": [
6
+ "dynamic-reducers",
7
+ "eslint",
8
+ "eslintplugin",
9
+ "redux",
10
+ "redux-sparse-kit"
11
+ ],
12
+ "homepage": "https://github.com/veksa/redux-sparse-kit-eslint-plugin#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/veksa/redux-sparse-kit-eslint-plugin/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Aleksandr Khizhnyi",
19
+ "email": "mendler88@gmail.com"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/veksa/redux-sparse-kit-eslint-plugin.git"
24
+ },
25
+ "files": [
26
+ "lib/"
27
+ ],
28
+ "sideEffects": false,
29
+ "main": "./lib/index.js",
30
+ "types": "./lib/index.d.ts",
31
+ "exports": {
32
+ "./package.json": "./package.json",
33
+ ".": {
34
+ "types": "./lib/index.d.ts",
35
+ "default": "./lib/index.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "compile": "tsc --noEmit",
40
+ "lint": "oxlint src/",
41
+ "fmt": "oxfmt",
42
+ "fmt:check": "oxfmt --check",
43
+ "test": "jest",
44
+ "test:coverage": "jest --coverage",
45
+ "build": "shx rm -rf lib && tsc --outDir lib -m node16 --moduleResolution node16 --declaration",
46
+ "prepack": "yarn build"
47
+ },
48
+ "dependencies": {
49
+ "@typescript-eslint/utils": "^8.66.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/jest": "^30.0.0",
53
+ "@types/node": "^24.0.3",
54
+ "@typescript-eslint/parser": "^8.66.0",
55
+ "@typescript-eslint/rule-tester": "^8.66.0",
56
+ "eslint": "^10.8.1",
57
+ "jest": "^30.4.2",
58
+ "oxfmt": "^0.63.0",
59
+ "oxlint": "^1.78.0",
60
+ "redux": "^5.0.1",
61
+ "redux-sparse-kit": "0.1.0",
62
+ "shx": "^0.4.0",
63
+ "ts-jest": "^29.4.12",
64
+ "typescript": "6.0.3"
65
+ },
66
+ "peerDependencies": {
67
+ "@typescript-eslint/parser": "^8.44.1",
68
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
69
+ "typescript": ">=5.4 <6.1"
70
+ },
71
+ "packageManager": "yarn@4.18.0"
72
+ }