eslint-plugin-absolute 0.2.7 → 0.3.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.
@@ -1,88 +0,0 @@
1
- import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
-
3
- const DEFAULT_MAX_KEYS = 3;
4
-
5
- type Options = [number | { maxKeys?: number }];
6
- type MessageIds = "extractStyle";
7
-
8
- export const inlineStyleLimit: TSESLint.RuleModule<MessageIds, Options> = {
9
- create(context) {
10
- const [option] = context.options;
11
- // If a number is passed directly, use it as maxKeys; otherwise, extract maxKeys from the object (default to 3)
12
- const maxKeys =
13
- typeof option === "number"
14
- ? option
15
- : (option && option.maxKeys) || DEFAULT_MAX_KEYS;
16
-
17
- return {
18
- JSXAttribute(node: TSESTree.JSXAttribute) {
19
- // Check if the attribute name is 'style'
20
- if (
21
- node.name.type !== "JSXIdentifier" ||
22
- node.name.name !== "style"
23
- ) {
24
- return;
25
- }
26
-
27
- // Ensure the value is a JSX expression container with an object literal
28
- if (
29
- !node.value ||
30
- node.value.type !== "JSXExpressionContainer" ||
31
- !node.value.expression ||
32
- node.value.expression.type !== "ObjectExpression"
33
- ) {
34
- return;
35
- }
36
-
37
- const styleObject = node.value.expression;
38
-
39
- // Count only "Property" nodes (ignoring spread elements or others)
40
- const keyCount = styleObject.properties.filter(
41
- (prop): prop is TSESTree.Property =>
42
- prop.type === "Property"
43
- ).length;
44
-
45
- // Report only if the number of keys exceeds the allowed maximum
46
- if (keyCount > maxKeys) {
47
- context.report({
48
- data: { max: maxKeys },
49
- messageId: "extractStyle",
50
- node
51
- });
52
- }
53
- }
54
- };
55
- },
56
- defaultOptions: [DEFAULT_MAX_KEYS],
57
- meta: {
58
- docs: {
59
- description:
60
- "Disallow inline style objects with too many keys and encourage extracting them"
61
- },
62
- messages: {
63
- extractStyle:
64
- "Inline style objects should be extracted into a separate object or file when containing more than {{max}} keys."
65
- },
66
- schema: [
67
- {
68
- anyOf: [
69
- {
70
- type: "number"
71
- },
72
- {
73
- additionalProperties: false,
74
- properties: {
75
- maxKeys: {
76
- description:
77
- "Maximum number of keys allowed in an inline style object before it must be extracted.",
78
- type: "number"
79
- }
80
- },
81
- type: "object"
82
- }
83
- ]
84
- }
85
- ],
86
- type: "suggestion"
87
- }
88
- };
@@ -1,454 +0,0 @@
1
- import { TSESLint, TSESTree, AST_NODE_TYPES } from "@typescript-eslint/utils";
2
-
3
- type Options = [];
4
- type MessageIds = "stateAndSetterToChild" | "variableToChild";
5
-
6
- type ComponentFunction =
7
- | TSESTree.FunctionDeclaration
8
- | TSESTree.FunctionExpression
9
- | TSESTree.ArrowFunctionExpression;
10
-
11
- type Usage = {
12
- jsxUsageSet: Set<TSESTree.JSXElement>;
13
- hasOutsideUsage: boolean;
14
- };
15
-
16
- type CandidateVariable = {
17
- node: TSESTree.VariableDeclarator;
18
- varName: string;
19
- componentName: string;
20
- };
21
-
22
- export const localizeReactProps: TSESLint.RuleModule<MessageIds, Options> = {
23
- create(context) {
24
- // A list of candidate variables for reporting (for general variables only).
25
- const candidateVariables: CandidateVariable[] = [];
26
-
27
- const getSingleSetElement = <T>(set: Set<T>) => {
28
- for (const value of set) {
29
- return value;
30
- }
31
- return null;
32
- };
33
-
34
- const getRightmostJSXIdentifier = (
35
- name: TSESTree.JSXTagNameExpression
36
- ) => {
37
- let current: TSESTree.JSXTagNameExpression = name;
38
- while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
39
- current = current.property;
40
- }
41
- if (current.type === AST_NODE_TYPES.JSXIdentifier) {
42
- return current;
43
- }
44
- return null;
45
- };
46
-
47
- const getLeftmostJSXIdentifier = (
48
- name: TSESTree.JSXTagNameExpression
49
- ) => {
50
- let current: TSESTree.JSXTagNameExpression = name;
51
- while (current.type === AST_NODE_TYPES.JSXMemberExpression) {
52
- current = current.object;
53
- }
54
- if (current.type === AST_NODE_TYPES.JSXIdentifier) {
55
- return current;
56
- }
57
- return null;
58
- };
59
-
60
- // Helper: Extract the component name from a JSXElement.
61
- const getJSXElementName = (jsxElement: TSESTree.JSXElement | null) => {
62
- if (
63
- !jsxElement ||
64
- !jsxElement.openingElement ||
65
- !jsxElement.openingElement.name
66
- ) {
67
- return "";
68
- }
69
- const nameNode = jsxElement.openingElement.name;
70
- if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
71
- return nameNode.name;
72
- }
73
- const rightmost = getRightmostJSXIdentifier(nameNode);
74
- if (rightmost) {
75
- return rightmost.name;
76
- }
77
- return "";
78
- };
79
-
80
- // Helper: Check if the node is a call to useState.
81
- const isUseStateCall = (
82
- node: TSESTree.Node | null
83
- ): node is TSESTree.CallExpression =>
84
- node !== null &&
85
- node.type === AST_NODE_TYPES.CallExpression &&
86
- node.callee !== null &&
87
- ((node.callee.type === AST_NODE_TYPES.Identifier &&
88
- node.callee.name === "useState") ||
89
- (node.callee.type === AST_NODE_TYPES.MemberExpression &&
90
- node.callee.property !== null &&
91
- node.callee.property.type === AST_NODE_TYPES.Identifier &&
92
- node.callee.property.name === "useState"));
93
-
94
- // Helper: Check if a call expression is a hook call (other than useState).
95
- const isHookCall = (
96
- node: TSESTree.Node | null
97
- ): node is TSESTree.CallExpression =>
98
- node !== null &&
99
- node.type === AST_NODE_TYPES.CallExpression &&
100
- node.callee !== null &&
101
- node.callee.type === AST_NODE_TYPES.Identifier &&
102
- /^use[A-Z]/.test(node.callee.name) &&
103
- node.callee.name !== "useState";
104
-
105
- // Helper: Walk upward to find the closest JSXElement ancestor.
106
- const getJSXAncestor = (node: TSESTree.Node) => {
107
- let current: TSESTree.Node | null | undefined = node.parent;
108
- while (current) {
109
- if (current.type === AST_NODE_TYPES.JSXElement) {
110
- return current;
111
- }
112
- current = current.parent;
113
- }
114
- return null;
115
- };
116
-
117
- const getTagNameFromOpening = (
118
- openingElement: TSESTree.JSXOpeningElement
119
- ) => {
120
- const nameNode = openingElement.name;
121
- if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
122
- return nameNode.name;
123
- }
124
- const rightmost = getRightmostJSXIdentifier(nameNode);
125
- return rightmost ? rightmost.name : null;
126
- };
127
-
128
- const isProviderOrContext = (tagName: string) =>
129
- tagName.endsWith("Provider") || tagName.endsWith("Context");
130
-
131
- const isValueAttributeOnProvider = (node: TSESTree.Node) =>
132
- node.type === AST_NODE_TYPES.JSXAttribute &&
133
- node.name &&
134
- node.name.type === AST_NODE_TYPES.JSXIdentifier &&
135
- node.name.name === "value" &&
136
- node.parent &&
137
- node.parent.type === AST_NODE_TYPES.JSXOpeningElement &&
138
- (() => {
139
- const tagName = getTagNameFromOpening(node.parent);
140
- return tagName !== null && isProviderOrContext(tagName);
141
- })();
142
-
143
- // Helper: Check whether the given node is inside a JSXAttribute "value"
144
- // that belongs to a context-like component (i.e. tag name ends with Provider or Context).
145
- const isContextProviderValueProp = (node: TSESTree.Node) => {
146
- let current: TSESTree.Node | null | undefined = node.parent;
147
- while (current) {
148
- if (isValueAttributeOnProvider(current)) {
149
- return true;
150
- }
151
- current = current.parent;
152
- }
153
- return false;
154
- };
155
-
156
- // Helper: Determine if a JSXElement is a custom component (tag name begins with an uppercase letter).
157
- const isCustomJSXElement = (jsxElement: TSESTree.JSXElement | null) => {
158
- if (
159
- !jsxElement ||
160
- !jsxElement.openingElement ||
161
- !jsxElement.openingElement.name
162
- ) {
163
- return false;
164
- }
165
- const nameNode = jsxElement.openingElement.name;
166
- if (nameNode.type === AST_NODE_TYPES.JSXIdentifier) {
167
- return /^[A-Z]/.test(nameNode.name);
168
- }
169
- const leftmost = getLeftmostJSXIdentifier(nameNode);
170
- return leftmost !== null && /^[A-Z]/.test(leftmost.name);
171
- };
172
-
173
- // Helper: Find the nearest enclosing function (assumed to be the component).
174
- const getComponentFunction = (node: TSESTree.Node | null) => {
175
- let current: TSESTree.Node | null | undefined = node;
176
- while (current) {
177
- if (
178
- current.type === AST_NODE_TYPES.FunctionDeclaration ||
179
- current.type === AST_NODE_TYPES.FunctionExpression ||
180
- current.type === AST_NODE_TYPES.ArrowFunctionExpression
181
- ) {
182
- return current;
183
- }
184
- current = current.parent;
185
- }
186
- return null;
187
- };
188
-
189
- const findVariableForIdentifier = (identifier: TSESTree.Identifier) => {
190
- let scope: TSESLint.Scope.Scope | null =
191
- context.sourceCode.getScope(identifier);
192
- while (scope) {
193
- const found = scope.variables.find((variable) =>
194
- variable.defs.some((def) => def.name === identifier)
195
- );
196
- if (found) {
197
- return found;
198
- }
199
- scope = scope.upper ?? null;
200
- }
201
- return null;
202
- };
203
-
204
- // Analyze variable usage using ESLint scopes (no manual AST crawling).
205
- // Only count a usage if it occurs inside a custom JSX element (and is not inside a context provider's "value" prop).
206
- const classifyReference = (
207
- reference: TSESLint.Scope.Reference,
208
- declarationId: TSESTree.Identifier,
209
- jsxUsageSet: Set<TSESTree.JSXElement>
210
- ) => {
211
- const { identifier } = reference;
212
-
213
- if (
214
- identifier === declarationId ||
215
- isContextProviderValueProp(identifier)
216
- ) {
217
- return false;
218
- }
219
-
220
- const jsxAncestor = getJSXAncestor(identifier);
221
- if (jsxAncestor && isCustomJSXElement(jsxAncestor)) {
222
- jsxUsageSet.add(jsxAncestor);
223
- return false;
224
- }
225
- return true;
226
- };
227
-
228
- const analyzeVariableUsage = (
229
- declarationId: TSESTree.Identifier
230
- ): Usage => {
231
- const variable = findVariableForIdentifier(declarationId);
232
- if (!variable) {
233
- return {
234
- hasOutsideUsage: false,
235
- jsxUsageSet: new Set<TSESTree.JSXElement>()
236
- };
237
- }
238
-
239
- const jsxUsageSet = new Set<TSESTree.JSXElement>();
240
- const hasOutsideUsage = variable.references.some((ref) =>
241
- classifyReference(ref, declarationId, jsxUsageSet)
242
- );
243
-
244
- return {
245
- hasOutsideUsage,
246
- jsxUsageSet
247
- };
248
- };
249
-
250
- // Manage hook-derived variables.
251
- const componentHookVars = new WeakMap<ComponentFunction, Set<string>>();
252
- const getHookSet = (componentFunction: ComponentFunction) => {
253
- let hookSet = componentHookVars.get(componentFunction);
254
- if (!hookSet) {
255
- hookSet = new Set<string>();
256
- componentHookVars.set(componentFunction, hookSet);
257
- }
258
- return hookSet;
259
- };
260
-
261
- const isRangeContained = (
262
- refRange: [number, number],
263
- nodeRange: [number, number]
264
- ) => refRange[0] >= nodeRange[0] && refRange[1] <= nodeRange[1];
265
-
266
- const variableHasReferenceInRange = (
267
- variable: TSESLint.Scope.Variable,
268
- nodeRange: [number, number]
269
- ) =>
270
- variable.references.some(
271
- (reference) =>
272
- reference.identifier.range !== undefined &&
273
- isRangeContained(reference.identifier.range, nodeRange)
274
- );
275
-
276
- const hasHookDependency = (
277
- node: TSESTree.Node,
278
- hookSet: Set<string>
279
- ) => {
280
- if (!node.range) {
281
- return false;
282
- }
283
- const nodeRange = node.range;
284
-
285
- let scope: TSESLint.Scope.Scope | null =
286
- context.sourceCode.getScope(node);
287
-
288
- while (scope) {
289
- const hookVars = scope.variables.filter((variable) =>
290
- hookSet.has(variable.name)
291
- );
292
- if (
293
- hookVars.some((variable) =>
294
- variableHasReferenceInRange(variable, nodeRange)
295
- )
296
- ) {
297
- return true;
298
- }
299
- scope = scope.upper ?? null;
300
- }
301
-
302
- return false;
303
- };
304
-
305
- const processUseStateDeclarator = (
306
- node: TSESTree.VariableDeclarator
307
- ) => {
308
- if (
309
- !node.init ||
310
- !isUseStateCall(node.init) ||
311
- node.id.type !== AST_NODE_TYPES.ArrayPattern ||
312
- node.id.elements.length < 2
313
- ) {
314
- return false;
315
- }
316
-
317
- const [stateElem, setterElem] = node.id.elements;
318
- if (
319
- !stateElem ||
320
- stateElem.type !== AST_NODE_TYPES.Identifier ||
321
- !setterElem ||
322
- setterElem.type !== AST_NODE_TYPES.Identifier
323
- ) {
324
- return false;
325
- }
326
-
327
- const stateVarName = stateElem.name;
328
- const setterVarName = setterElem.name;
329
-
330
- const stateUsage = analyzeVariableUsage(stateElem);
331
- const setterUsage = analyzeVariableUsage(setterElem);
332
-
333
- const stateExclusivelySingleJSX =
334
- !stateUsage.hasOutsideUsage &&
335
- stateUsage.jsxUsageSet.size === 1;
336
- const setterExclusivelySingleJSX =
337
- !setterUsage.hasOutsideUsage &&
338
- setterUsage.jsxUsageSet.size === 1;
339
-
340
- if (!stateExclusivelySingleJSX || !setterExclusivelySingleJSX) {
341
- return true;
342
- }
343
-
344
- const stateTarget = getSingleSetElement(stateUsage.jsxUsageSet);
345
- const setterTarget = getSingleSetElement(setterUsage.jsxUsageSet);
346
- if (stateTarget && stateTarget === setterTarget) {
347
- context.report({
348
- data: { setterVarName, stateVarName },
349
- messageId: "stateAndSetterToChild",
350
- node: node
351
- });
352
- }
353
- return true;
354
- };
355
-
356
- const processGeneralVariable = (
357
- node: TSESTree.VariableDeclarator,
358
- componentFunction: ComponentFunction
359
- ) => {
360
- if (!node.id || node.id.type !== AST_NODE_TYPES.Identifier) {
361
- return;
362
- }
363
-
364
- const varName = node.id.name;
365
- // Exempt variables that depend on hooks.
366
- if (node.init) {
367
- const hookSet = getHookSet(componentFunction);
368
- if (hasHookDependency(node.init, hookSet)) {
369
- return;
370
- }
371
- }
372
- const usage = analyzeVariableUsage(node.id);
373
- if (!usage.hasOutsideUsage && usage.jsxUsageSet.size === 1) {
374
- const target = getSingleSetElement(usage.jsxUsageSet);
375
- const componentName = getJSXElementName(target);
376
- candidateVariables.push({
377
- componentName,
378
- node,
379
- varName
380
- });
381
- }
382
- };
383
-
384
- return {
385
- // At the end of the traversal, group candidate variables by the target component name.
386
- "Program:exit"() {
387
- const groups = new Map<string, CandidateVariable[]>();
388
- candidateVariables.forEach((candidate) => {
389
- const key = candidate.componentName;
390
- const existing = groups.get(key);
391
- if (existing) {
392
- existing.push(candidate);
393
- } else {
394
- groups.set(key, [candidate]);
395
- }
396
- });
397
- // Only report candidates for a given component type if there is exactly one candidate.
398
- groups.forEach((candidates) => {
399
- if (candidates.length !== 1) {
400
- return;
401
- }
402
- const [candidate] = candidates;
403
- if (!candidate) {
404
- return;
405
- }
406
- context.report({
407
- data: { varName: candidate.varName },
408
- messageId: "variableToChild",
409
- node: candidate.node
410
- });
411
- });
412
- },
413
- VariableDeclarator(node: TSESTree.VariableDeclarator) {
414
- const componentFunction = getComponentFunction(node);
415
- if (!componentFunction || !componentFunction.body) return;
416
-
417
- // Record hook-derived variables (for hooks other than useState).
418
- if (
419
- node.init &&
420
- node.id &&
421
- node.id.type === AST_NODE_TYPES.Identifier &&
422
- node.init.type === AST_NODE_TYPES.CallExpression &&
423
- isHookCall(node.init)
424
- ) {
425
- const hookSet = getHookSet(componentFunction);
426
- hookSet.add(node.id.name);
427
- }
428
-
429
- // Case 1: useState destructuring (state & setter).
430
- const wasUseState = processUseStateDeclarator(node);
431
-
432
- // Case 2: General variable.
433
- if (!wasUseState) {
434
- processGeneralVariable(node, componentFunction);
435
- }
436
- }
437
- };
438
- },
439
- defaultOptions: [],
440
- meta: {
441
- docs: {
442
- description:
443
- "Disallow variables that are only passed to a single custom child component. For useState, only report if both the state and its setter are exclusively passed to a single custom child. For general variables, only report if a given child receives exactly one such candidate – if two or more are passed to the same component type, they're assumed to be settings that belong on the parent."
444
- },
445
- messages: {
446
- stateAndSetterToChild:
447
- "State variable '{{stateVarName}}' and its setter '{{setterVarName}}' are only passed to a single custom child component. Consider moving the state into that component.",
448
- variableToChild:
449
- "Variable '{{varName}}' is only passed to a single custom child component. Consider moving it to that component."
450
- },
451
- schema: [],
452
- type: "suggestion"
453
- }
454
- };
@@ -1,153 +0,0 @@
1
- import { TSESLint, TSESTree } from "@typescript-eslint/utils";
2
-
3
- type Options = [number?];
4
- type MessageIds = "tooDeep";
5
-
6
- export const maxDepthExtended: TSESLint.RuleModule<MessageIds, Options> = {
7
- create(context) {
8
- const [option] = context.options;
9
- const maxDepth = typeof option === "number" ? option : 1;
10
- const functionStack: number[] = [];
11
-
12
- // Helper to get ancestors of a node by walking its parent chain.
13
- const getAncestors = (node: TSESTree.Node) => {
14
- const ancestors: TSESTree.Node[] = [];
15
- let current: TSESTree.Node | null | undefined = node.parent;
16
- while (current) {
17
- ancestors.push(current);
18
- current = current.parent;
19
- }
20
- return ancestors;
21
- };
22
-
23
- // Check if a block only contains a single early exit: return or throw.
24
- const isEarlyExitBlock = (node: TSESTree.BlockStatement) => {
25
- if (node.body.length !== 1) {
26
- return false;
27
- }
28
- const [first] = node.body;
29
- if (!first) {
30
- return false;
31
- }
32
- return (
33
- first.type === "ReturnStatement" ||
34
- first.type === "ThrowStatement"
35
- );
36
- };
37
-
38
- const isFunctionBody = (node: TSESTree.BlockStatement) => {
39
- const ancestors = getAncestors(node);
40
- const [parent] = ancestors;
41
- return (
42
- parent &&
43
- (parent.type === "FunctionDeclaration" ||
44
- parent.type === "FunctionExpression" ||
45
- parent.type === "ArrowFunctionExpression") &&
46
- node === parent.body
47
- );
48
- };
49
-
50
- const incrementCurrentDepth = () => {
51
- if (functionStack.length === 0) {
52
- return null;
53
- }
54
- const index = functionStack.length - 1;
55
- const currentDepth = functionStack[index];
56
- if (typeof currentDepth !== "number") {
57
- return null;
58
- }
59
- const nextDepth = currentDepth + 1;
60
- functionStack[index] = nextDepth;
61
- return nextDepth;
62
- };
63
-
64
- const decrementCurrentDepth = () => {
65
- if (functionStack.length === 0) {
66
- return;
67
- }
68
- const index = functionStack.length - 1;
69
- const currentDepth = functionStack[index];
70
- if (typeof currentDepth !== "number") {
71
- return;
72
- }
73
- functionStack[index] = currentDepth - 1;
74
- };
75
-
76
- // Report if the current depth exceeds the allowed maximum.
77
- const checkDepth = (node: TSESTree.BlockStatement, depth: number) => {
78
- if (depth > maxDepth) {
79
- context.report({
80
- data: { depth, maxDepth },
81
- messageId: "tooDeep",
82
- node
83
- });
84
- }
85
- };
86
-
87
- return {
88
- ArrowFunctionExpression() {
89
- functionStack.push(0);
90
- },
91
- "ArrowFunctionExpression:exit"() {
92
- functionStack.pop();
93
- },
94
- BlockStatement(node: TSESTree.BlockStatement) {
95
- // Do not count if this block is the body of a function.
96
- if (isFunctionBody(node)) {
97
- return;
98
- }
99
-
100
- // Skip blocks that only have an early exit.
101
- if (isEarlyExitBlock(node)) {
102
- return;
103
- }
104
-
105
- const depth = incrementCurrentDepth();
106
- if (depth !== null) {
107
- checkDepth(node, depth);
108
- }
109
- },
110
- "BlockStatement:exit"(node: TSESTree.BlockStatement) {
111
- if (isFunctionBody(node)) {
112
- return;
113
- }
114
-
115
- if (isEarlyExitBlock(node)) {
116
- return;
117
- }
118
-
119
- decrementCurrentDepth();
120
- },
121
- FunctionDeclaration() {
122
- functionStack.push(0);
123
- },
124
- "FunctionDeclaration:exit"() {
125
- functionStack.pop();
126
- },
127
- FunctionExpression() {
128
- functionStack.push(0);
129
- },
130
- "FunctionExpression:exit"() {
131
- functionStack.pop();
132
- }
133
- };
134
- },
135
- defaultOptions: [1],
136
- meta: {
137
- docs: {
138
- description:
139
- "disallow too many nested blocks except when the block only contains an early exit (return or throw)"
140
- },
141
- messages: {
142
- tooDeep:
143
- "Blocks are nested too deeply ({{depth}}). Maximum allowed is {{maxDepth}} or an early exit."
144
- },
145
- schema: [
146
- {
147
- // Accepts a single number as the maximum allowed depth.
148
- type: "number"
149
- }
150
- ],
151
- type: "suggestion"
152
- }
153
- };