@blumintinc/eslint-plugin-blumint 1.20.160 → 1.20.161
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/lib/index.js +1 -1
- package/lib/rules/enforce-react-type-naming.js +94 -3
- package/package.json +1 -1
- package/release-manifest.json +14 -0
package/lib/index.js
CHANGED
|
@@ -191,6 +191,68 @@ const isGlobalConstStyleGoverned = (declarator) => {
|
|
|
191
191
|
}
|
|
192
192
|
return (!isDynamicValue(init) && !isBindingAlias(init) && !isJestMockCast(init));
|
|
193
193
|
};
|
|
194
|
+
// `as const` is spelled as a `TSTypeReference` named `const`, so it reaches
|
|
195
|
+
// `getTypeName` like any other type name and has to be excluded by name.
|
|
196
|
+
const isAsConstAssertion = (typeAnnotation) => typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
197
|
+
typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
198
|
+
typeAnnotation.typeName.name === 'const';
|
|
199
|
+
/**
|
|
200
|
+
* The React type an `as`/`<T>` assertion pins on an initializer, or `undefined`.
|
|
201
|
+
*
|
|
202
|
+
* An assertion declares the binding's type exactly as bindingly as an
|
|
203
|
+
* annotation does — `tsc --emitDeclarationOnly` emits the same
|
|
204
|
+
* `export declare const config: FC;` for `const config: FC = {} as FC` and for
|
|
205
|
+
* `const config = {} as FC` — so it is a detection carrier, not decoration.
|
|
206
|
+
* Reading only the annotation made the shipped config's own `--fix` erase this
|
|
207
|
+
* rule's report: `no-redundant-annotation-assertion` deletes the redundant
|
|
208
|
+
* ANNOTATION and keeps the assertion (its fix direction is the type-safe one,
|
|
209
|
+
* since dropping the assertion instead leaves `const config: FC = {}`, TS2322),
|
|
210
|
+
* so a reported violation became an unreportable one (Issue #2029).
|
|
211
|
+
*
|
|
212
|
+
* Only the OUTERMOST assertion applied to the initializer answers. A nested one
|
|
213
|
+
* describes a sub-expression rather than the binding: in `(e as FC)()` the
|
|
214
|
+
* binding holds FC's RETURN value, and in `thing as unknown as FC` the
|
|
215
|
+
* intermediate `unknown` is discarded by the outer `as FC`. Parentheses need no
|
|
216
|
+
* handling because TSESTree does not model them, so `({} as FC)` IS the
|
|
217
|
+
* assertion node; `!` does, and is read through — non-nullability cannot change
|
|
218
|
+
* which React type names the value.
|
|
219
|
+
*
|
|
220
|
+
* `satisfies` is excluded because it leaves the expression's type alone
|
|
221
|
+
* (`{} satisfies FC` is still `{}`), and `as const` names no React type.
|
|
222
|
+
*/
|
|
223
|
+
const assertedTypeOf = (init) => {
|
|
224
|
+
let target = init ?? undefined;
|
|
225
|
+
while (target?.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
226
|
+
target = target.expression;
|
|
227
|
+
}
|
|
228
|
+
if (target?.type !== utils_1.AST_NODE_TYPES.TSAsExpression &&
|
|
229
|
+
target?.type !== utils_1.AST_NODE_TYPES.TSTypeAssertion) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
return isAsConstAssertion(target.typeAnnotation)
|
|
233
|
+
? undefined
|
|
234
|
+
: target.typeAnnotation;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Reports whether this identifier is the NAME of a JSX element (`<Content />`),
|
|
238
|
+
* as opposed to a value reference inside one (`{Content}`).
|
|
239
|
+
*
|
|
240
|
+
* A JSX element name resolves to a binding only while it starts uppercase: the
|
|
241
|
+
* scope analyzer models `<content />` as an intrinsic host element instead. So
|
|
242
|
+
* lowercasing such a name does not carry the reference across — it unbinds it,
|
|
243
|
+
* leaving the declaration orphaned and an unknown HTML tag rendered in its
|
|
244
|
+
* place. `JSXMemberExpression` is excluded on purpose: a dot always means value
|
|
245
|
+
* access, so `<ns.Thing />` keeps resolving whatever the case of `ns`.
|
|
246
|
+
*/
|
|
247
|
+
const isJsxElementName = (node) => {
|
|
248
|
+
const { parent } = node;
|
|
249
|
+
if (!parent) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return ((parent.type === utils_1.AST_NODE_TYPES.JSXOpeningElement ||
|
|
253
|
+
parent.type === utils_1.AST_NODE_TYPES.JSXClosingElement) &&
|
|
254
|
+
parent.name === node);
|
|
255
|
+
};
|
|
194
256
|
exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
|
|
195
257
|
name: 'enforce-react-type-naming',
|
|
196
258
|
meta: {
|
|
@@ -296,6 +358,28 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
|
|
|
296
358
|
newName,
|
|
297
359
|
});
|
|
298
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Reports whether lowercasing this variable's name would unbind a
|
|
363
|
+
* `<Name />` that resolves to it.
|
|
364
|
+
*
|
|
365
|
+
* The rename is withheld there and the report stands alone, on the same
|
|
366
|
+
* terms as a colliding rename: an incomplete rename is worse than none. The
|
|
367
|
+
* rewritten element would render an intrinsic host tag while the declaration
|
|
368
|
+
* it used to name sits unreferenced, which is dead code the fix itself
|
|
369
|
+
* created — and a consumer building with `noUnusedLocals` gets a red CI out
|
|
370
|
+
* of running `--fix`.
|
|
371
|
+
*
|
|
372
|
+
* Asked of variables only. An unused PARAMETER is an ordinary
|
|
373
|
+
* signature-driven shape rather than dead code, so the parameter rename
|
|
374
|
+
* stays as #1357 pinned it.
|
|
375
|
+
*/
|
|
376
|
+
function renameUnbindsJsxElement(owner, declarationId) {
|
|
377
|
+
const variable = context
|
|
378
|
+
.getDeclaredVariables(owner)
|
|
379
|
+
.find((candidate) => candidate.defs.some((def) => def.name === declarationId));
|
|
380
|
+
return !!variable?.references.some((reference) => reference.identifier !== declarationId &&
|
|
381
|
+
isJsxElementName(reference.identifier));
|
|
382
|
+
}
|
|
299
383
|
/**
|
|
300
384
|
* Check variable declarations for React type naming conventions
|
|
301
385
|
*/
|
|
@@ -313,8 +397,11 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
|
|
|
313
397
|
if (isGlobalConstStyleGoverned(node))
|
|
314
398
|
return;
|
|
315
399
|
const variableName = id.name;
|
|
316
|
-
//
|
|
317
|
-
|
|
400
|
+
// The declared type, from either carrier: the annotation when it is
|
|
401
|
+
// written, otherwise the assertion that stands in for it (#2029). The
|
|
402
|
+
// annotation wins when both are present — it is what the binding's type
|
|
403
|
+
// resolves to — so `const x: unknown = {} as FC` reads `unknown`.
|
|
404
|
+
const typeAnnotation = id.typeAnnotation?.typeAnnotation ?? assertedTypeOf(node.init);
|
|
318
405
|
const typeName = getTypeName(typeAnnotation);
|
|
319
406
|
if (!typeName)
|
|
320
407
|
return;
|
|
@@ -328,7 +415,9 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
|
|
|
328
415
|
type: typeName,
|
|
329
416
|
suggestion,
|
|
330
417
|
},
|
|
331
|
-
fix: (fixer) =>
|
|
418
|
+
fix: (fixer) => renameUnbindsJsxElement(node, id)
|
|
419
|
+
? null
|
|
420
|
+
: buildRenameFixes(fixer, node, id, suggestion),
|
|
332
421
|
});
|
|
333
422
|
}
|
|
334
423
|
// Check if it's a ComponentType or FC (should be uppercase)
|
|
@@ -341,6 +430,8 @@ exports.enforceReactTypeNaming = (0, createRule_1.createRule)({
|
|
|
341
430
|
type: typeName,
|
|
342
431
|
suggestion,
|
|
343
432
|
},
|
|
433
|
+
// Uppercasing cannot unbind a `<Name />`: the target starts uppercase,
|
|
434
|
+
// which is exactly what a JSX element name needs to resolve at all.
|
|
344
435
|
fix: (fixer) => buildRenameFixes(fixer, node, id, suggestion),
|
|
345
436
|
});
|
|
346
437
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.161",
|
|
4
|
+
"date": "2026-08-17T13:22:13.676Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-react-type-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2029
|
|
11
|
+
],
|
|
12
|
+
"summary": "read the type from a type assertion (closes #2029)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.20.160",
|
|
4
18
|
"date": "2026-08-17T02:55:06.538Z",
|