@blumintinc/eslint-plugin-blumint 1.18.16 → 1.19.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.
- package/README.md +4 -0
- package/lib/index.js +13 -1
- package/lib/rules/enforce-single-exported-unit-per-file.d.ts +3 -0
- package/lib/rules/enforce-single-exported-unit-per-file.js +571 -0
- package/lib/rules/no-harness-coupled-disables.d.ts +1 -0
- package/lib/rules/no-harness-coupled-disables.js +134 -0
- package/lib/rules/prefer-clone-deep.js +8 -3
- package/lib/rules/prefer-map-over-conditional-dispatch.d.ts +4 -0
- package/lib/rules/prefer-map-over-conditional-dispatch.js +907 -0
- package/lib/rules/prefer-union-from-const-array.d.ts +3 -0
- package/lib/rules/prefer-union-from-const-array.js +137 -0
- package/package.json +1 -1
- package/release-manifest.json +46 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferUnionFromConstArray = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* Mechanically derive an UPPER_SNAKE constant base from a PascalCase/camelCase
|
|
8
|
+
* type name by inserting `_` at every lowercase/digit-to-uppercase boundary and
|
|
9
|
+
* uppercasing. `TournamentActionId` -> `TOURNAMENT_ACTION_ID`. Acronym runs stay
|
|
10
|
+
* glued together (`HTTPStatus` -> `HTTPSTATUS`), which is an acceptable simple
|
|
11
|
+
* behavior per the spec since no reliable acronym segmentation exists.
|
|
12
|
+
*/
|
|
13
|
+
function toUpperSnake(name) {
|
|
14
|
+
return name
|
|
15
|
+
.split(/(?<=[a-z0-9])(?=[A-Z])/)
|
|
16
|
+
.join('_')
|
|
17
|
+
.toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A union member qualifies only when it is a string-literal type. Numbers,
|
|
21
|
+
* `null`, `undefined`, `string`, template-literal types, and type references
|
|
22
|
+
* all fail this guard and disqualify the entire union (string-literal-only).
|
|
23
|
+
*/
|
|
24
|
+
function isStringLiteralType(member) {
|
|
25
|
+
if (member.type !== utils_1.AST_NODE_TYPES.TSLiteralType) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const { literal } = member;
|
|
29
|
+
return (literal.type === utils_1.AST_NODE_TYPES.Literal && typeof literal.value === 'string');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walk the scope chain upward collecting declared variable names. Used to skip
|
|
33
|
+
* the autofix (report-only) when the derived `{TYPE}_VALUES` name is already
|
|
34
|
+
* bound, so the fix never shadows or duplicates an existing identifier.
|
|
35
|
+
*/
|
|
36
|
+
function isNameInScope(scope, name) {
|
|
37
|
+
let current = scope;
|
|
38
|
+
while (current) {
|
|
39
|
+
if (current.variables.some((variable) => variable.name === name)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
current = current.upper;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
47
|
+
name: 'prefer-union-from-const-array',
|
|
48
|
+
meta: {
|
|
49
|
+
type: 'suggestion',
|
|
50
|
+
docs: {
|
|
51
|
+
description: 'Derive string-literal union types from an `as const` array instead of declaring the union inline, so the runtime value set and the type share a single source of truth',
|
|
52
|
+
recommended: 'error',
|
|
53
|
+
},
|
|
54
|
+
fixable: 'code',
|
|
55
|
+
schema: [],
|
|
56
|
+
messages: {
|
|
57
|
+
preferDerivedUnion: 'Derive this union from an `as const` array: `const {{constName}} = [...] as const; type {{typeName}} = (typeof {{constName}})[number];`. This tethers the type to importable runtime values and prevents silent drift. Consider also naming each literal as its own exported constant so call sites never repeat magic strings.',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
defaultOptions: [],
|
|
61
|
+
create(context) {
|
|
62
|
+
const sourceCode = context.getSourceCode();
|
|
63
|
+
return {
|
|
64
|
+
TSTypeAliasDeclaration(node) {
|
|
65
|
+
const { typeAnnotation } = node;
|
|
66
|
+
if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const members = typeAnnotation.types;
|
|
70
|
+
// A single-member "union" is not a TSUnionType, but guard anyway; only
|
|
71
|
+
// multi-member unions are in scope.
|
|
72
|
+
if (members.length < 2) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Every member must be a string literal; any other member kind
|
|
76
|
+
// (number, null, undefined, string, template literal, type reference)
|
|
77
|
+
// disqualifies the whole union.
|
|
78
|
+
if (!members.every(isStringLiteralType)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const typeName = node.id.name;
|
|
82
|
+
const constName = `${toUpperSnake(typeName)}_VALUES`;
|
|
83
|
+
// The type alias is exported when wrapped in `export type X = ...`
|
|
84
|
+
// (a local export declaration, not a re-export with a `source`).
|
|
85
|
+
const isExported = node.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
86
|
+
!node.parent.source;
|
|
87
|
+
const fixNode = isExported
|
|
88
|
+
? node.parent
|
|
89
|
+
: node;
|
|
90
|
+
// Skip the fix (report-only) when the derived const name already exists
|
|
91
|
+
// in scope, to avoid shadowing/duplicating an existing declaration.
|
|
92
|
+
const hasCollision = isNameInScope(context.getScope(), constName);
|
|
93
|
+
if (hasCollision) {
|
|
94
|
+
context.report({
|
|
95
|
+
node,
|
|
96
|
+
messageId: 'preferDerivedUnion',
|
|
97
|
+
data: { constName, typeName },
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
context.report({
|
|
102
|
+
node,
|
|
103
|
+
messageId: 'preferDerivedUnion',
|
|
104
|
+
data: { constName, typeName },
|
|
105
|
+
fix(fixer) {
|
|
106
|
+
const literals = members
|
|
107
|
+
.map((member) => {
|
|
108
|
+
const literal = member.literal;
|
|
109
|
+
return literal.raw ?? sourceCode.getText(literal);
|
|
110
|
+
})
|
|
111
|
+
.join(', ');
|
|
112
|
+
const typeParams = node.typeParameters
|
|
113
|
+
? sourceCode.getText(node.typeParameters)
|
|
114
|
+
: '';
|
|
115
|
+
const constKeyword = isExported ? 'export const' : 'const';
|
|
116
|
+
const typeKeyword = isExported ? 'export type' : 'type';
|
|
117
|
+
// Preserve the declaration's own indentation for the second line;
|
|
118
|
+
// the first line reuses the original leading whitespace (untouched
|
|
119
|
+
// because the fix range starts at the `export`/`type` keyword, not
|
|
120
|
+
// at the start of the line — this also leaves leading comments in
|
|
121
|
+
// place).
|
|
122
|
+
const lineText = sourceCode.lines[fixNode.loc.start.line - 1] ?? '';
|
|
123
|
+
const indentMatch = lineText
|
|
124
|
+
.slice(0, fixNode.loc.start.column)
|
|
125
|
+
.match(/^\s*/);
|
|
126
|
+
const indent = indentMatch ? indentMatch[0] : '';
|
|
127
|
+
const constDecl = `${constKeyword} ${constName} = [${literals}] as const;`;
|
|
128
|
+
const typeDecl = `${typeKeyword} ${typeName}${typeParams} = (typeof ${constName})[number];`;
|
|
129
|
+
return fixer.replaceText(fixNode, `${constDecl}\n${indent}${typeDecl}`);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
exports.default = exports.preferUnionFromConstArray;
|
|
137
|
+
//# sourceMappingURL=prefer-union-from-const-array.js.map
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.0",
|
|
4
|
+
"date": "2026-07-13T23:57:00.324Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-single-exported-unit-per-file",
|
|
8
|
+
"changeType": "feat",
|
|
9
|
+
"issues": [
|
|
10
|
+
1295
|
|
11
|
+
],
|
|
12
|
+
"summary": "limit each file to one exported component/class (closes #1295)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-harness-coupled-disables",
|
|
16
|
+
"changeType": "feat",
|
|
17
|
+
"issues": [
|
|
18
|
+
1296
|
|
19
|
+
],
|
|
20
|
+
"summary": "flag eslint-disable justifications coupled to the dev harness (closes #1296)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "prefer-clone-deep",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1299
|
|
27
|
+
],
|
|
28
|
+
"summary": "stop flagging top-level spread beside a sibling nested object (closes #1299)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "prefer-map-over-conditional-dispatch",
|
|
32
|
+
"changeType": "feat",
|
|
33
|
+
"issues": [
|
|
34
|
+
1298
|
|
35
|
+
],
|
|
36
|
+
"summary": "flag literal-union dispatch that should be a Record lookup (closes #1298)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "prefer-union-from-const-array",
|
|
40
|
+
"changeType": "feat",
|
|
41
|
+
"issues": [
|
|
42
|
+
1297
|
|
43
|
+
],
|
|
44
|
+
"summary": "derive string-literal union types from an as-const array (closes #1297)"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
2
48
|
{
|
|
3
49
|
"version": "1.18.16",
|
|
4
50
|
"date": "2026-07-13T17:31:09.661Z",
|