@blumintinc/eslint-plugin-blumint 1.20.8 → 1.20.10

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.
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceQueryKeyTs = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ /**
7
+ * The canonical queryKeys module. The rule takes no options and already hard-codes
8
+ * this path as what makes a key valid, so the fixer writes the very same path
9
+ * instead of guessing one when a file has no queryKeys import to copy from.
10
+ */
11
+ const DEFAULT_QUERY_KEYS_SOURCE = '@/util/routing/queryKeys';
6
12
  /**
7
13
  * Rule to enforce the use of centralized router state key constants imported from
8
14
  * `src/util/routing/queryKeys.ts` instead of arbitrary string literals when calling
@@ -29,15 +35,26 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
29
35
  const queryKeyImports = new Map();
30
36
  const localUseRouterStateNames = new Set(['useRouterState']);
31
37
  const validQueryKeySources = new Set([
32
- '@/util/routing/queryKeys',
38
+ DEFAULT_QUERY_KEYS_SOURCE,
33
39
  'src/util/routing/queryKeys',
34
40
  ]);
35
41
  const allowedQueryKeyFactories = new Set(['makeQueryKey', 'getQueryKey']);
42
+ const sourceCode = context.getSourceCode();
36
43
  /**
37
- * Ensure a suggested constant is imported from queryKeys.ts and return combined fixes
44
+ * Reports are buffered until the whole file has been walked so the fixer can
45
+ * put every substituted constant into a single import instead of racing
46
+ * per-violation insertions that overlap and get dropped.
38
47
  */
39
- function buildImportFixes(fixer, keyNode, suggestedConstant) {
40
- return [fixer.replaceText(keyNode, suggestedConstant)];
48
+ const pendingReports = [];
49
+ /**
50
+ * `SourceCode#getScope` supersedes the deprecated `context.getScope`; the
51
+ * fallback keeps the rule working on ESLint versions that predate it.
52
+ */
53
+ function scopeOf(node) {
54
+ const scoped = sourceCode;
55
+ return typeof scoped.getScope === 'function'
56
+ ? scoped.getScope(node)
57
+ : context.getScope();
41
58
  }
42
59
  /**
43
60
  * Check if a source path refers to queryKeys.ts
@@ -46,6 +63,138 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
46
63
  return (validQueryKeySources.has(source) ||
47
64
  source.endsWith('/util/routing/queryKeys'));
48
65
  }
66
+ function importDeclarationsOf() {
67
+ return sourceCode.ast.body.filter((statement) => statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
68
+ typeof statement.source.value === 'string');
69
+ }
70
+ function isValueImportSpecifier(specifier) {
71
+ return (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
72
+ specifier.importKind !== 'type');
73
+ }
74
+ /**
75
+ * A file that already imports the export under another name reaches it
76
+ * through that name; re-importing it would leave two bindings for one
77
+ * constant.
78
+ */
79
+ function localNameOf(constant) {
80
+ for (const declaration of importDeclarationsOf()) {
81
+ if (declaration.importKind === 'type' ||
82
+ !isQueryKeysSource(String(declaration.source.value))) {
83
+ continue;
84
+ }
85
+ for (const specifier of declaration.specifiers) {
86
+ if (isValueImportSpecifier(specifier) &&
87
+ specifier.imported.name === constant) {
88
+ return specifier.local.name;
89
+ }
90
+ }
91
+ }
92
+ return constant;
93
+ }
94
+ /**
95
+ * Decide whether the suggested constant can be substituted, and whether the
96
+ * substitution has to carry an import with it.
97
+ */
98
+ function resolveBinding(scope, constant) {
99
+ const variable = utils_1.ASTUtils.findVariable(scope, constant);
100
+ if (!variable) {
101
+ return 'missing';
102
+ }
103
+ const [definition] = variable.defs;
104
+ if (!definition) {
105
+ return 'conflict';
106
+ }
107
+ const definitionNode = definition.node;
108
+ if (definitionNode.type !== utils_1.AST_NODE_TYPES.ImportSpecifier ||
109
+ definitionNode.importKind === 'type' ||
110
+ !isValidQueryKeyConstant(definitionNode.imported.name)) {
111
+ return 'conflict';
112
+ }
113
+ const declaration = definitionNode.parent;
114
+ if (!declaration ||
115
+ declaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
116
+ declaration.importKind === 'type' ||
117
+ typeof declaration.source.value !== 'string' ||
118
+ !isQueryKeysSource(declaration.source.value)) {
119
+ return 'conflict';
120
+ }
121
+ return 'bound';
122
+ }
123
+ /**
124
+ * Make the substituted constants resolve: extend the file's queryKeys import
125
+ * when there is one to extend, otherwise add a fresh import statement.
126
+ */
127
+ function buildImportFix(fixer, constants) {
128
+ const importDeclarations = importDeclarationsOf();
129
+ const queryKeysDeclarations = importDeclarations.filter((declaration) => isQueryKeysSource(String(declaration.source.value)));
130
+ const reusable = queryKeysDeclarations.find((declaration) => declaration.importKind !== 'type' &&
131
+ declaration.specifiers.some(isValueImportSpecifier));
132
+ if (reusable) {
133
+ const namedSpecifiers = reusable.specifiers.filter(isValueImportSpecifier);
134
+ const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
135
+ return fixer.insertTextAfter(lastSpecifier, constants.map((constant) => `, ${constant}`).join(''));
136
+ }
137
+ // A namespace or type-only queryKeys import cannot take named value
138
+ // specifiers, but its path is proof of how this file reaches the module.
139
+ const source = queryKeysDeclarations.length
140
+ ? String(queryKeysDeclarations[0].source.value)
141
+ : DEFAULT_QUERY_KEYS_SOURCE;
142
+ const importText = `import { ${constants.join(', ')} } from '${source}';\n`;
143
+ const [firstImport] = importDeclarations;
144
+ if (firstImport) {
145
+ return fixer.insertTextBefore(firstImport, importText);
146
+ }
147
+ // Keep the import visually separated from the code it precedes unless the
148
+ // file already opens with a blank line.
149
+ const separator = /^\r?\n/.test(sourceCode.text) ? '' : '\n';
150
+ return fixer.insertTextBeforeRange([0, 0], `${importText}${separator}`);
151
+ }
152
+ function flushReports() {
153
+ const resolutions = new Map();
154
+ const missingConstants = [];
155
+ for (const report of pendingReports) {
156
+ if (!report.substitution) {
157
+ continue;
158
+ }
159
+ const { constant, scope } = report.substitution;
160
+ const name = localNameOf(constant);
161
+ const state = resolveBinding(scope, name);
162
+ resolutions.set(report, { name, state });
163
+ if (state === 'missing' && !missingConstants.includes(name)) {
164
+ missingConstants.push(name);
165
+ }
166
+ }
167
+ // The first applied substitution carries the import for every other one:
168
+ // its fix range then starts at the top of the file and ends before the
169
+ // remaining literals, so no two fixes of this rule overlap in a pass.
170
+ const importCarrier = pendingReports.find((report) => {
171
+ const resolution = resolutions.get(report);
172
+ return resolution !== undefined && resolution.state !== 'conflict';
173
+ });
174
+ for (const report of pendingReports) {
175
+ context.report({
176
+ node: report.node,
177
+ messageId: report.messageId,
178
+ data: report.data,
179
+ fix(fixer) {
180
+ const { substitution } = report;
181
+ const resolution = resolutions.get(report);
182
+ if (!substitution ||
183
+ !resolution ||
184
+ resolution.state === 'conflict') {
185
+ return null;
186
+ }
187
+ const fixes = [
188
+ fixer.replaceText(substitution.keyNode, resolution.name),
189
+ ];
190
+ if (report === importCarrier && missingConstants.length > 0) {
191
+ fixes.unshift(buildImportFix(fixer, missingConstants));
192
+ }
193
+ return fixes;
194
+ },
195
+ });
196
+ }
197
+ }
49
198
  /**
50
199
  * Check if an identifier is a valid QUERY_KEY constant
51
200
  */
@@ -236,25 +385,27 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
236
385
  if (!isValidQueryKeyUsage(keyValue)) {
237
386
  // Check if it contains invalid string literals
238
387
  if (containsInvalidStringLiteral(keyValue)) {
239
- context.report({
388
+ // Only simple string literals can be auto-fixed
389
+ const suggestedConstant = keyValue.type === utils_1.AST_NODE_TYPES.Literal &&
390
+ typeof keyValue.value === 'string'
391
+ ? generateAutoFix(keyValue.value)
392
+ : null;
393
+ pendingReports.push({
240
394
  node: keyValue,
241
395
  messageId: 'enforceQueryKeyImport',
242
- fix(fixer) {
243
- // Only provide auto-fix for simple string literals
244
- if (keyValue.type === utils_1.AST_NODE_TYPES.Literal &&
245
- typeof keyValue.value === 'string') {
246
- const suggestedConstant = generateAutoFix(keyValue.value);
247
- if (suggestedConstant) {
248
- return buildImportFixes(fixer, keyValue, suggestedConstant);
249
- }
396
+ substitution: suggestedConstant &&
397
+ keyValue.type === utils_1.AST_NODE_TYPES.Literal
398
+ ? {
399
+ keyNode: keyValue,
400
+ constant: suggestedConstant,
401
+ scope: scopeOf(keyValue),
250
402
  }
251
- return null;
252
- },
403
+ : undefined,
253
404
  });
254
405
  }
255
406
  else if (keyValue.type === utils_1.AST_NODE_TYPES.Identifier) {
256
407
  // Report variables that aren't from the correct source
257
- context.report({
408
+ pendingReports.push({
258
409
  node: keyValue,
259
410
  messageId: 'enforceQueryKeyConstant',
260
411
  data: {
@@ -268,6 +419,9 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
268
419
  }
269
420
  }
270
421
  },
422
+ 'Program:exit'() {
423
+ flushReports();
424
+ },
271
425
  };
272
426
  },
273
427
  });
@@ -1,3 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'reactNodeShouldBeLowercase' | 'componentTypeShouldBeUppercase';
2
- export declare const enforceReactTypeNaming: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export declare const enforceReactTypeNaming: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
3
4
  export {};
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceReactTypeNaming = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const renameFixes_1 = require("../utils/renameFixes");
6
7
  // Types that should have lowercase variable names
7
8
  const LOWERCASE_TYPES = ['ReactNode', 'JSX.Element'];
8
9
  // Types that should have uppercase variable names
@@ -86,18 +87,45 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
86
87
  (node.parent?.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
87
88
  node.parent.local.name !== node.parent.imported?.name));
88
89
  }
90
+ /**
91
+ * Builds the rename for a reported identifier: the declaration plus every
92
+ * in-file reference, rewriting only name tokens so the type annotation that
93
+ * triggered the report survives (Issue #1357).
94
+ *
95
+ * The variable is resolved by declaration identity rather than by name,
96
+ * because a shape like `function Content(Content: ReactNode)` declares two
97
+ * distinct symbols under one name and a name-based lookup picks the wrong
98
+ * one. When the symbol cannot be resolved the fix is withheld entirely — a
99
+ * partial rename is worse than no rename.
100
+ */
101
+ function buildRenameFixes(fixer, owner, declarationId, newName) {
102
+ const variable = context
103
+ .getDeclaredVariables(owner)
104
+ .find((candidate) => candidate.defs.some((def) => def.name === declarationId)) ?? null;
105
+ if (!variable) {
106
+ return null;
107
+ }
108
+ return (0, renameFixes_1.buildVariableRenameFixes)({
109
+ fixer,
110
+ sourceCode: context.getSourceCode(),
111
+ variable,
112
+ declarationId,
113
+ newName,
114
+ });
115
+ }
89
116
  /**
90
117
  * Check variable declarations for React type naming conventions
91
118
  */
92
119
  function checkVariableDeclaration(node) {
93
120
  if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
94
121
  return;
122
+ const id = node.id;
95
123
  // Skip destructured variables
96
- if (isDestructured(node.id))
124
+ if (isDestructured(id))
97
125
  return;
98
- const variableName = node.id.name;
126
+ const variableName = id.name;
99
127
  // Get the type annotation
100
- const typeAnnotation = node.id.typeAnnotation?.typeAnnotation;
128
+ const typeAnnotation = id.typeAnnotation?.typeAnnotation;
101
129
  const typeName = getTypeName(typeAnnotation);
102
130
  if (!typeName)
103
131
  return;
@@ -105,44 +133,45 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
105
133
  if (LOWERCASE_TYPES.includes(typeName) && isUppercase(variableName)) {
106
134
  const suggestion = toLowercase(variableName);
107
135
  context.report({
108
- node: node.id,
136
+ node: id,
109
137
  messageId: 'reactNodeShouldBeLowercase',
110
138
  data: {
111
139
  type: typeName,
112
140
  suggestion,
113
141
  },
114
- fix: (fixer) => fixer.replaceText(node.id, suggestion),
142
+ fix: (fixer) => buildRenameFixes(fixer, node, id, suggestion),
115
143
  });
116
144
  }
117
145
  // Check if it's a ComponentType or FC (should be uppercase)
118
146
  if (UPPERCASE_TYPES.includes(typeName) && !isUppercase(variableName)) {
119
147
  const suggestion = toUppercase(variableName);
120
148
  context.report({
121
- node: node.id,
149
+ node: id,
122
150
  messageId: 'componentTypeShouldBeUppercase',
123
151
  data: {
124
152
  type: typeName,
125
153
  suggestion,
126
154
  },
127
- fix: (fixer) => fixer.replaceText(node.id, suggestion),
155
+ fix: (fixer) => buildRenameFixes(fixer, node, id, suggestion),
128
156
  });
129
157
  }
130
158
  }
131
159
  /**
132
160
  * Check function parameters for React type naming conventions
133
161
  */
134
- function checkParameter(node) {
162
+ function checkParameter(node, owner) {
135
163
  if (node.type !== utils_1.AST_NODE_TYPES.Identifier)
136
164
  return;
165
+ const id = node;
137
166
  // Skip destructured parameters
138
- if (isDestructured(node))
167
+ if (isDestructured(id))
139
168
  return;
140
169
  // Skip default imports
141
- if (isDefaultImport(node))
170
+ if (isDefaultImport(id))
142
171
  return;
143
- const paramName = node.name;
172
+ const paramName = id.name;
144
173
  // Get the type annotation
145
- const typeAnnotation = node.typeAnnotation?.typeAnnotation;
174
+ const typeAnnotation = id.typeAnnotation?.typeAnnotation;
146
175
  const typeName = getTypeName(typeAnnotation);
147
176
  if (!typeName)
148
177
  return;
@@ -150,26 +179,26 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
150
179
  if (LOWERCASE_TYPES.includes(typeName) && isUppercase(paramName)) {
151
180
  const suggestion = toLowercase(paramName);
152
181
  context.report({
153
- node,
182
+ node: id,
154
183
  messageId: 'reactNodeShouldBeLowercase',
155
184
  data: {
156
185
  type: typeName,
157
186
  suggestion,
158
187
  },
159
- fix: (fixer) => fixer.replaceText(node, suggestion),
188
+ fix: (fixer) => buildRenameFixes(fixer, owner, id, suggestion),
160
189
  });
161
190
  }
162
191
  // Check if it's a ComponentType or FC (should be uppercase)
163
192
  if (UPPERCASE_TYPES.includes(typeName) && !isUppercase(paramName)) {
164
193
  const suggestion = toUppercase(paramName);
165
194
  context.report({
166
- node,
195
+ node: id,
167
196
  messageId: 'componentTypeShouldBeUppercase',
168
197
  data: {
169
198
  type: typeName,
170
199
  suggestion,
171
200
  },
172
- fix: (fixer) => fixer.replaceText(node, suggestion),
201
+ fix: (fixer) => buildRenameFixes(fixer, owner, id, suggestion),
173
202
  });
174
203
  }
175
204
  }
@@ -182,7 +211,7 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
182
211
  node.parent.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
183
212
  node.parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) &&
184
213
  node.parent.params.includes(node)) {
185
- checkParameter(node);
214
+ checkParameter(node, node.parent);
186
215
  }
187
216
  },
188
217
  };
@@ -1,10 +1,12 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'noFalsyCheck' | 'noRawTypeof';
2
3
  type Options = [
3
4
  {
4
5
  snapshotHooks?: string[];
5
6
  guardFunctions?: string[];
6
7
  excludeFiles?: string[];
8
+ guardImportSource?: string;
7
9
  }?
8
10
  ];
9
- export declare const enforceSnapshotStateNarrowing: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
11
+ export declare const enforceSnapshotStateNarrowing: TSESLint.RuleModule<MessageIds, Options, TSESLint.RuleListener>;
10
12
  export {};