@blumintinc/eslint-plugin-blumint 1.19.13 → 1.19.15
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/no-hungarian.js +57 -2
- package/lib/rules/require-props-composition.js +19 -1
- package/package.json +1 -1
- package/release-manifest.json +28 -0
package/lib/index.js
CHANGED
|
@@ -256,10 +256,48 @@ const BUILT_IN_METHODS = new Set([
|
|
|
256
256
|
'catch',
|
|
257
257
|
'finally',
|
|
258
258
|
]);
|
|
259
|
+
// The camelCase/PascalCase splitter's core pattern. Kept separate from
|
|
260
|
+
// splitCamelSegments so precomputed constants (below) can fragment known type
|
|
261
|
+
// words without recursing through the merge step.
|
|
262
|
+
const CAMEL_SEGMENT_REGEX = /[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g;
|
|
263
|
+
function splitCamelSegmentsRaw(name) {
|
|
264
|
+
return name.match(CAMEL_SEGMENT_REGEX) ?? [];
|
|
265
|
+
}
|
|
266
|
+
// COMMON_TYPES words containing an internal capital — only `BigInt` today — are
|
|
267
|
+
// fragmented by the splitter into parts (["Big","Int"]) where a fragment ("Int")
|
|
268
|
+
// collides with an abbreviation marker ("int"), so ANY identifier containing
|
|
269
|
+
// BigInt would spuriously match that marker regardless of position (#1317).
|
|
270
|
+
// Precompute those fragment sequences so splitCamelSegments can re-merge them
|
|
271
|
+
// into a single atomic segment, ensuring a built-in type word is never mistaken
|
|
272
|
+
// for a Hungarian abbreviation tag.
|
|
273
|
+
const MULTI_CAPITAL_TYPE_WORD_PARTS = COMMON_TYPES.map(splitCamelSegmentsRaw).filter((parts) => parts.length > 1);
|
|
274
|
+
// Re-merge any consecutive segments that reconstitute a multi-capital built-in
|
|
275
|
+
// type word (Big + Int -> BigInt). Case-insensitive so lower/upper camelCase
|
|
276
|
+
// variants collapse identically.
|
|
277
|
+
function mergeMultiCapitalTypeWords(segments) {
|
|
278
|
+
if (MULTI_CAPITAL_TYPE_WORD_PARTS.length === 0) {
|
|
279
|
+
return segments;
|
|
280
|
+
}
|
|
281
|
+
const merged = [];
|
|
282
|
+
let index = 0;
|
|
283
|
+
while (index < segments.length) {
|
|
284
|
+
const match = MULTI_CAPITAL_TYPE_WORD_PARTS.find((parts) => parts.every((part, offset) => segments[index + offset]?.toLowerCase() === part.toLowerCase()));
|
|
285
|
+
if (match) {
|
|
286
|
+
merged.push(segments.slice(index, index + match.length).join(''));
|
|
287
|
+
index += match.length;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
merged.push(segments[index]);
|
|
291
|
+
index += 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return merged;
|
|
295
|
+
}
|
|
259
296
|
// Split a PascalCase/camelCase identifier into its word segments
|
|
260
297
|
// (e.g. "StringToNumber" -> ["String","To","Number"], "FuncKeys" -> ["Func","Keys"]).
|
|
298
|
+
// Multi-capital built-in type words (BigInt) are kept as one atomic segment.
|
|
261
299
|
function splitCamelSegments(name) {
|
|
262
|
-
return
|
|
300
|
+
return mergeMultiCapitalTypeWords(splitCamelSegmentsRaw(name));
|
|
263
301
|
}
|
|
264
302
|
// A TYPE name (alias/interface/class) is exempt from a full-type-word marker when
|
|
265
303
|
// that marker is one clean PascalCase segment among OTHER descriptive segments —
|
|
@@ -280,6 +318,13 @@ function isSemanticTypeConcept(typeName) {
|
|
|
280
318
|
// concept (e.g. Extract+Number) rather than bare type tags glued together.
|
|
281
319
|
return segments.some((segment) => !FULL_TYPE_WORDS.has(segment.toLowerCase()));
|
|
282
320
|
}
|
|
321
|
+
// A PascalCase declaration name (leading capital, at least one lowercase letter):
|
|
322
|
+
// a component, class, or type identifier. Distinguished from SCREAMING_SNAKE_CASE
|
|
323
|
+
// constants (all caps) and lowercase-initial variables, which are handled on their
|
|
324
|
+
// own code paths.
|
|
325
|
+
function isPascalCaseName(name) {
|
|
326
|
+
return /^[A-Z]/.test(name) && name !== name.toUpperCase();
|
|
327
|
+
}
|
|
283
328
|
// Is `name` a domain compound of the form <entity>Number, where the word directly
|
|
284
329
|
// before the trailing "Number" is a known domain-entity noun (issueNumber,
|
|
285
330
|
// lineNumber, roundNumber, versionNumber)? Only the LAST head segment is
|
|
@@ -336,7 +381,17 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
336
381
|
// Type names whose type-word denotes a concept/relation (StringToNumber,
|
|
337
382
|
// CapitalizedString, FuncKeys, PromiseOrValue) are not Hungarian — the word
|
|
338
383
|
// is part of the type's meaning, like the allowed compound noun PhoneNumber.
|
|
339
|
-
|
|
384
|
+
//
|
|
385
|
+
// Extended to any PascalCase declaration (components, classes, functions):
|
|
386
|
+
// when a built-in type word qualifies a DIFFERENT head noun the value is a
|
|
387
|
+
// component / props type, never a number/bigint (NumberAmountEditor,
|
|
388
|
+
// BigIntAmountEditorProps) — the leading-position analog of the
|
|
389
|
+
// DOMAIN_NUMBER_HEAD_NOUNS suffix carve-out, mirroring Intl.NumberFormat /
|
|
390
|
+
// StringBuilder / NumberFormatter. Keyed on PascalCase because a
|
|
391
|
+
// lowercase-initial variable (numberCount, stringValue) DOES encode its own
|
|
392
|
+
// value's type and must keep firing (#1317).
|
|
393
|
+
if ((isTypeName || isPascalCaseName(variableName)) &&
|
|
394
|
+
isSemanticTypeConcept(variableName)) {
|
|
340
395
|
return false;
|
|
341
396
|
}
|
|
342
397
|
// Single-letter Hungarian prefixes (bIsActive, iCount): a lone b/i directly
|
|
@@ -118,6 +118,13 @@ function getTypeReferenceName(node) {
|
|
|
118
118
|
function typeNodeComposesWithProps(typeNode, propsTypeName) {
|
|
119
119
|
switch (typeNode.type) {
|
|
120
120
|
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
121
|
+
// A direct reference to the child's whole props type (bare `ChildProps`
|
|
122
|
+
// or generic-instantiated `ChildProps<T>`) is the maximal form of
|
|
123
|
+
// composition: the entire surface is inherited verbatim, strictly
|
|
124
|
+
// stronger than Pick/Omit, so no duplication/drift is possible.
|
|
125
|
+
if (getTypeReferenceName(typeNode) === propsTypeName) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
121
128
|
if (typeReferenceContainsPickOrOmit(typeNode, propsTypeName)) {
|
|
122
129
|
return true;
|
|
123
130
|
}
|
|
@@ -313,6 +320,16 @@ function findComponentFunction(program, name, seen = new Set()) {
|
|
|
313
320
|
}
|
|
314
321
|
return null;
|
|
315
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* A rendered child that resolves in-file to a component function taking no
|
|
325
|
+
* parameters has no props surface to compose with, so it is not a composition
|
|
326
|
+
* dependency (same category as a decorative icon). Only in-file resolution is
|
|
327
|
+
* used; imported children are left to the normal composition check.
|
|
328
|
+
*/
|
|
329
|
+
function isZeroPropComponent(program, name) {
|
|
330
|
+
const fn = findComponentFunction(program, name);
|
|
331
|
+
return fn !== null && fn.params.length === 0;
|
|
332
|
+
}
|
|
316
333
|
/**
|
|
317
334
|
* Resolve the type node that defines a rendered dependency's props: its
|
|
318
335
|
* `{Dep}Props` alias if one exists, otherwise the dependency component's
|
|
@@ -446,7 +463,8 @@ exports.requirePropsComposition = (0, createRule_1.createRule)({
|
|
|
446
463
|
// Filter to non-excluded custom components
|
|
447
464
|
const depComponents = Array.from(allJsxNames).filter((name) => !excludeComponents.has(name) &&
|
|
448
465
|
!isDecorativeIcon(name) &&
|
|
449
|
-
name !== componentName
|
|
466
|
+
name !== componentName &&
|
|
467
|
+
!isZeroPropComponent(prog, name));
|
|
450
468
|
if (depComponents.length < minDependencyCount) {
|
|
451
469
|
return;
|
|
452
470
|
}
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.15",
|
|
4
|
+
"date": "2026-07-17T23:29:51.136Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "no-hungarian",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1317
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt PascalCase domain-qualifier names using built-in type words (closes #1317)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.14",
|
|
18
|
+
"date": "2026-07-17T21:35:22.880Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "require-props-composition",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1316
|
|
25
|
+
],
|
|
26
|
+
"summary": "recognize direct whole-props refs and skip zero-prop children (closes #1316)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.13",
|
|
4
32
|
"date": "2026-07-17T21:25:11.513Z",
|