@blumintinc/eslint-plugin-blumint 1.18.16 → 1.19.1

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.
@@ -0,0 +1,3 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const preferUnionFromConstArray: TSESLint.RuleModule<"preferDerivedUnion", [], TSESLint.RuleListener>;
3
+ export default preferUnionFromConstArray;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.18.16",
3
+ "version": "1.19.1",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,65 @@
1
1
  [
2
+ {
3
+ "version": "1.19.1",
4
+ "date": "2026-07-14T01:28:36.352Z",
5
+ "rules": [
6
+ {
7
+ "name": "consistent-callback-naming",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1301,
11
+ 1302
12
+ ],
13
+ "summary": "skip files without TS project services instead of aborting the run (closes #1302); require uppercase after \"handle\" so past-participle identifiers aren't stripped (closes #1301)"
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "version": "1.19.0",
19
+ "date": "2026-07-13T23:57:00.324Z",
20
+ "rules": [
21
+ {
22
+ "name": "enforce-single-exported-unit-per-file",
23
+ "changeType": "feat",
24
+ "issues": [
25
+ 1295
26
+ ],
27
+ "summary": "limit each file to one exported component/class (closes #1295)"
28
+ },
29
+ {
30
+ "name": "no-harness-coupled-disables",
31
+ "changeType": "feat",
32
+ "issues": [
33
+ 1296
34
+ ],
35
+ "summary": "flag eslint-disable justifications coupled to the dev harness (closes #1296)"
36
+ },
37
+ {
38
+ "name": "prefer-clone-deep",
39
+ "changeType": "fix",
40
+ "issues": [
41
+ 1299
42
+ ],
43
+ "summary": "stop flagging top-level spread beside a sibling nested object (closes #1299)"
44
+ },
45
+ {
46
+ "name": "prefer-map-over-conditional-dispatch",
47
+ "changeType": "feat",
48
+ "issues": [
49
+ 1298
50
+ ],
51
+ "summary": "flag literal-union dispatch that should be a Record lookup (closes #1298)"
52
+ },
53
+ {
54
+ "name": "prefer-union-from-const-array",
55
+ "changeType": "feat",
56
+ "issues": [
57
+ 1297
58
+ ],
59
+ "summary": "derive string-literal union types from an as-const array (closes #1297)"
60
+ }
61
+ ]
62
+ },
2
63
  {
3
64
  "version": "1.18.16",
4
65
  "date": "2026-07-13T17:31:09.661Z",