@blumintinc/eslint-plugin-blumint 1.17.3 → 1.18.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.
- package/README.md +1 -0
- package/lib/index.js +4 -1
- package/lib/rules/consistent-callback-naming.js +40 -0
- package/lib/rules/enforce-positive-naming.js +6 -0
- package/lib/rules/enforce-types-directory-placement.d.ts +1 -0
- package/lib/rules/enforce-types-directory-placement.js +79 -1
- package/lib/rules/global-const-style.js +22 -0
- package/lib/rules/no-hungarian.js +25 -11
- package/lib/rules/prefer-use-theme.d.ts +3 -1
- package/lib/rules/prefer-use-theme.js +37 -4
- package/package.json +1 -1
- package/release-manifest.json +69 -0
package/README.md
CHANGED
|
@@ -176,6 +176,7 @@ full closed loop is documented in agora's `.claude/skills/eslint-autonomy/SKILL.
|
|
|
176
176
|
| [no-firestore-object-arrays](docs/rules/no-firestore-object-arrays.md) | Disallow arrays of object types in Firestore models. Prefer Record maps keyed by id with an index field, or subcollections/arrays of IDs. | ✅ | | | | | |
|
|
177
177
|
| [no-handler-suffix](docs/rules/no-handler-suffix.md) | Disallow the generic "handler" suffix in callback names so names explain the action they perform | ✅ | | | | | |
|
|
178
178
|
| [no-hungarian](docs/rules/no-hungarian.md) | Disallow Hungarian notation in locally declared variables, types, and classes | ✅ | | | | | |
|
|
179
|
+
| [no-inline-component-prop](docs/rules/no-inline-component-prop.md) | Prevent inline function components defined in render from being passed to component-type props like CatalogWrapper to avoid remounts and UI flashes. | ✅ | | | | | |
|
|
179
180
|
| [no-jsx-in-hooks](docs/rules/no-jsx-in-hooks.md) | Prevent hooks from returning JSX | ✅ | | | | | |
|
|
180
181
|
| [no-jsx-whitespace-literal](docs/rules/no-jsx-whitespace-literal.md) | Disallow the use of {" "} elements in JSX code | ✅ | | | | | |
|
|
181
182
|
| [no-margin-properties](docs/rules/no-margin-properties.md) | Prevent margin properties (margin, marginLeft, marginRight, marginTop, marginBottom, mx, my, etc.) in MUI styling because margins fight container-controlled spacing, double gutters, and misaligned breakpoints; keep spacing centralized with padding, gap, or spacing props instead. | ✅ | | | | | |
|
package/lib/index.js
CHANGED
|
@@ -38,6 +38,7 @@ const no_hungarian_1 = require("./rules/no-hungarian");
|
|
|
38
38
|
const no_misused_switch_case_1 = require("./rules/no-misused-switch-case");
|
|
39
39
|
const no_unpinned_dependencies_1 = require("./rules/no-unpinned-dependencies");
|
|
40
40
|
const no_unused_props_1 = require("./rules/no-unused-props");
|
|
41
|
+
const no_inline_component_prop_1 = require("./rules/no-inline-component-prop");
|
|
41
42
|
const no_useless_fragment_1 = require("./rules/no-useless-fragment");
|
|
42
43
|
const prefer_fragment_shorthand_1 = require("./rules/prefer-fragment-shorthand");
|
|
43
44
|
const prefer_getter_over_parameterless_method_1 = require("./rules/prefer-getter-over-parameterless-method");
|
|
@@ -217,7 +218,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
|
|
|
217
218
|
module.exports = {
|
|
218
219
|
meta: {
|
|
219
220
|
name: '@blumintinc/eslint-plugin-blumint',
|
|
220
|
-
version: '1.
|
|
221
|
+
version: '1.18.1',
|
|
221
222
|
},
|
|
222
223
|
parseOptions: {
|
|
223
224
|
ecmaVersion: 2020,
|
|
@@ -271,6 +272,7 @@ module.exports = {
|
|
|
271
272
|
'@blumintinc/blumint/no-misused-switch-case': 'error',
|
|
272
273
|
'@blumintinc/blumint/no-unpinned-dependencies': 'error',
|
|
273
274
|
'@blumintinc/blumint/no-unused-props': 'error',
|
|
275
|
+
'@blumintinc/blumint/no-inline-component-prop': 'error',
|
|
274
276
|
'@blumintinc/blumint/no-uuidv4-base62-as-key': 'error',
|
|
275
277
|
'@blumintinc/blumint/no-useless-fragment': 'error',
|
|
276
278
|
'@blumintinc/blumint/no-useless-usememo-primitives': 'error',
|
|
@@ -537,6 +539,7 @@ module.exports = {
|
|
|
537
539
|
'no-misused-switch-case': no_misused_switch_case_1.noMisusedSwitchCase,
|
|
538
540
|
'no-unpinned-dependencies': no_unpinned_dependencies_1.noUnpinnedDependencies,
|
|
539
541
|
'no-unused-props': no_unused_props_1.noUnusedProps,
|
|
542
|
+
'no-inline-component-prop': no_inline_component_prop_1.noInlineComponentProp,
|
|
540
543
|
'no-useless-fragment': no_useless_fragment_1.noUselessFragment,
|
|
541
544
|
'no-uuidv4-base62-as-key': no_uuidv4_base62_as_key_1.noUuidv4Base62AsKey,
|
|
542
545
|
'enforce-dynamic-file-naming': enforce_dynamic_file_naming_1.default,
|
|
@@ -142,6 +142,45 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
142
142
|
return false;
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
// An event-handler callback's return value is discarded: `void`, `undefined`,
|
|
146
|
+
// `never`, or a `Promise` thereof. Anything else is a value the caller
|
|
147
|
+
// consumes, which marks the prop as an accessor rather than a handler.
|
|
148
|
+
function returnsVoidLike(type) {
|
|
149
|
+
if (type.isUnion()) {
|
|
150
|
+
return type.types.every((member) => returnsVoidLike(member));
|
|
151
|
+
}
|
|
152
|
+
if (type.flags &
|
|
153
|
+
(ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// `Promise<void>` (async handlers) unwraps to its resolved type.
|
|
157
|
+
if (type.getSymbol()?.getName() === 'Promise') {
|
|
158
|
+
const typeArgs = checker.getTypeArguments(type);
|
|
159
|
+
if (typeArgs.length === 1) {
|
|
160
|
+
return returnsVoidLike(typeArgs[0]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
// A function-typed prop is an accessor (getRowId, valueGetter, filterOptions,
|
|
166
|
+
// a per-item props deriver, ...) rather than an event handler when its call
|
|
167
|
+
// signature returns a value the component consumes instead of a discarded
|
|
168
|
+
// `void`. The `on` prefix signals event handlers, so accessors are exempt.
|
|
169
|
+
// The prop's declared (contextual) type is authoritative because the passed
|
|
170
|
+
// value's inferred return type can be wider than the prop contract.
|
|
171
|
+
function isValueAccessor(node) {
|
|
172
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
173
|
+
const contextualType = checker.getContextualType(tsNode);
|
|
174
|
+
const type = contextualType ?? checker.getTypeAtLocation(tsNode);
|
|
175
|
+
const members = type.isUnion() ? type.types : [type];
|
|
176
|
+
const signatures = members.flatMap((member) => member.getCallSignatures());
|
|
177
|
+
if (signatures.length === 0) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
// Exempt only when every call signature returns a consumed value; a mix
|
|
181
|
+
// that includes a void-returning signature keeps the handler semantics.
|
|
182
|
+
return signatures.every((signature) => !returnsVoidLike(checker.getReturnTypeOfSignature(signature)));
|
|
183
|
+
}
|
|
145
184
|
return {
|
|
146
185
|
// Check JSX attributes for callback props
|
|
147
186
|
JSXAttribute(node) {
|
|
@@ -190,6 +229,7 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
190
229
|
!propName.startsWith('on') &&
|
|
191
230
|
!propName.startsWith('render') &&
|
|
192
231
|
!acceptsNonFunctionValue(node.value.expression) &&
|
|
232
|
+
!isValueAccessor(node.value.expression) &&
|
|
193
233
|
!isRenderFunction(node.value.expression) &&
|
|
194
234
|
!isReactComponentType(node.value.expression)) {
|
|
195
235
|
const eventName = propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
@@ -851,6 +851,12 @@ const DIS_EXCEPTIONS = [
|
|
|
851
851
|
'disputed',
|
|
852
852
|
'disputes',
|
|
853
853
|
'disputing',
|
|
854
|
+
// "disabled" is a single English adjective and the native HTML/React/MUI prop
|
|
855
|
+
// name; its "dis" is a bound morpheme, not a separable negation of "abled".
|
|
856
|
+
'disable',
|
|
857
|
+
'disabled',
|
|
858
|
+
'disables',
|
|
859
|
+
'disabling',
|
|
854
860
|
];
|
|
855
861
|
// Words that contain negative prefixes but should be treated as valid
|
|
856
862
|
const EXCEPTION_WORDS_SET = new Set([...IN_EXCEPTIONS, ...NO_EXCEPTIONS, ...UN_EXCEPTIONS, ...DIS_EXCEPTIONS].map((word) => word.toLowerCase()));
|
|
@@ -3,6 +3,7 @@ type Options = [
|
|
|
3
3
|
typesDirectory?: string;
|
|
4
4
|
excludePatterns?: string[];
|
|
5
5
|
includePaths?: string[];
|
|
6
|
+
frontendCoupledImportPatterns?: string[];
|
|
6
7
|
}
|
|
7
8
|
];
|
|
8
9
|
export declare const enforceTypesDirectoryPlacement: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"typeOnlyFileOutsideTypesDir", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -9,6 +9,18 @@ const utils_1 = require("@typescript-eslint/utils");
|
|
|
9
9
|
const minimatch_1 = require("minimatch");
|
|
10
10
|
const createRule_1 = require("../utils/createRule");
|
|
11
11
|
const DEFAULT_TYPES_DIRECTORY = 'functions/src/types';
|
|
12
|
+
// A type-only file that imports one of these frontend-only module families is
|
|
13
|
+
// frontend-scoped: it cannot be relocated under `functions/src/types/**`, since
|
|
14
|
+
// that directory is compiled by the backend tsconfig and the backend must not
|
|
15
|
+
// import from `src/` (frontend). Such files have no valid backend target and
|
|
16
|
+
// legitimately live under `src/`, so the rule exempts them. Matched against
|
|
17
|
+
// absolute specifiers directly and against relative specifiers after resolving
|
|
18
|
+
// them relative to the importing file (so backend files importing relatively —
|
|
19
|
+
// which resolve to `functions/src/**` — stay flagged).
|
|
20
|
+
const DEFAULT_FRONTEND_COUPLED_IMPORT_PATTERNS = [
|
|
21
|
+
'src/components/**',
|
|
22
|
+
'src/hooks/**',
|
|
23
|
+
];
|
|
12
24
|
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
13
25
|
'**/*.d.ts',
|
|
14
26
|
'**/*.test.ts',
|
|
@@ -62,6 +74,57 @@ function suggestTargetPath(filePath, typesDirectory) {
|
|
|
62
74
|
}
|
|
63
75
|
return null;
|
|
64
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Extracts the repo-relative portion of a path starting at `functions/src/`
|
|
79
|
+
* (backend) or `src/` (frontend), so absolute filenames and resolved relative
|
|
80
|
+
* specifiers match the same globs the rest of the rule uses. Backend is checked
|
|
81
|
+
* first because a backend path also contains `src/`.
|
|
82
|
+
*/
|
|
83
|
+
function toRepoRelative(filePath) {
|
|
84
|
+
const normalized = normalizePath(filePath);
|
|
85
|
+
const match = normalized.match(/(?:^|\/)(functions\/src\/.*|src\/.*)$/);
|
|
86
|
+
return match ? match[1] : normalized;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolves an import/export module specifier to a repo-relative path for glob
|
|
90
|
+
* matching. Relative specifiers are resolved against the importing file's
|
|
91
|
+
* directory (so `./DialogHeader` from `src/components/dialog/x.ts` becomes
|
|
92
|
+
* `src/components/dialog/DialogHeader`); bare/absolute specifiers are used as-is.
|
|
93
|
+
*/
|
|
94
|
+
function resolveSpecifier(specifier, fromFile) {
|
|
95
|
+
if (specifier.startsWith('.')) {
|
|
96
|
+
const dir = path_1.default.posix.dirname(normalizePath(fromFile));
|
|
97
|
+
return toRepoRelative(path_1.default.posix.join(dir, specifier));
|
|
98
|
+
}
|
|
99
|
+
return toRepoRelative(specifier);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns the module specifier of a top-level import/export statement that
|
|
103
|
+
* carries a `from '...'` source, or null for statements that don't.
|
|
104
|
+
*/
|
|
105
|
+
function importSourceOf(node) {
|
|
106
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration ||
|
|
107
|
+
node.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
108
|
+
node.type === utils_1.AST_NODE_TYPES.ExportAllDeclaration) {
|
|
109
|
+
const source = node.source;
|
|
110
|
+
return source && typeof source.value === 'string' ? source.value : null;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns true when the file imports at least one frontend-coupled module,
|
|
116
|
+
* meaning it cannot be relocated under the backend types directory.
|
|
117
|
+
*/
|
|
118
|
+
function isFrontendCoupled(body, fromFile, matchers) {
|
|
119
|
+
return body.some((stmt) => {
|
|
120
|
+
const specifier = importSourceOf(stmt);
|
|
121
|
+
if (specifier === null) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const resolved = resolveSpecifier(specifier, fromFile);
|
|
125
|
+
return matchers.some((mm) => mm.match(resolved));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
65
128
|
/**
|
|
66
129
|
* Returns true when the given top-level statement is "type-only":
|
|
67
130
|
* - ImportDeclaration (imports alone are not runtime code)
|
|
@@ -194,6 +257,11 @@ exports.enforceTypesDirectoryPlacement = (0, createRule_1.createRule)({
|
|
|
194
257
|
items: { type: 'string' },
|
|
195
258
|
default: DEFAULT_INCLUDE_PATHS,
|
|
196
259
|
},
|
|
260
|
+
frontendCoupledImportPatterns: {
|
|
261
|
+
type: 'array',
|
|
262
|
+
items: { type: 'string' },
|
|
263
|
+
default: DEFAULT_FRONTEND_COUPLED_IMPORT_PATTERNS,
|
|
264
|
+
},
|
|
197
265
|
},
|
|
198
266
|
additionalProperties: false,
|
|
199
267
|
},
|
|
@@ -208,12 +276,15 @@ exports.enforceTypesDirectoryPlacement = (0, createRule_1.createRule)({
|
|
|
208
276
|
typesDirectory: DEFAULT_TYPES_DIRECTORY,
|
|
209
277
|
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
|
|
210
278
|
includePaths: DEFAULT_INCLUDE_PATHS,
|
|
279
|
+
frontendCoupledImportPatterns: DEFAULT_FRONTEND_COUPLED_IMPORT_PATTERNS,
|
|
211
280
|
},
|
|
212
281
|
],
|
|
213
282
|
create(context, [options]) {
|
|
214
283
|
const typesDirectory = options.typesDirectory ?? DEFAULT_TYPES_DIRECTORY;
|
|
215
284
|
const excludePatterns = options.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
|
|
216
285
|
const includePaths = options.includePaths ?? DEFAULT_INCLUDE_PATHS;
|
|
286
|
+
const frontendCoupledImportPatterns = options.frontendCoupledImportPatterns ??
|
|
287
|
+
DEFAULT_FRONTEND_COUPLED_IMPORT_PATTERNS;
|
|
217
288
|
const filename = context.getFilename();
|
|
218
289
|
// Skip synthetic filenames used by RuleTester when no filename is provided.
|
|
219
290
|
if (filename === '<input>' || filename === '<text>') {
|
|
@@ -251,9 +322,16 @@ exports.enforceTypesDirectoryPlacement = (0, createRule_1.createRule)({
|
|
|
251
322
|
return {};
|
|
252
323
|
}
|
|
253
324
|
}
|
|
325
|
+
const frontendCoupledMatchers = frontendCoupledImportPatterns.map((pattern) => new minimatch_1.Minimatch(pattern, { dot: true }));
|
|
254
326
|
return {
|
|
255
327
|
Program(programNode) {
|
|
256
|
-
|
|
328
|
+
const body = programNode.body;
|
|
329
|
+
if (!isTypeOnlyFile(body)) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// A frontend-coupled type file has no valid target under the backend
|
|
333
|
+
// types directory, so it must not be flagged (issue #1263).
|
|
334
|
+
if (isFrontendCoupled(body, normalizedFilename, frontendCoupledMatchers)) {
|
|
257
335
|
return;
|
|
258
336
|
}
|
|
259
337
|
const suggested = suggestTargetPath(normalizedFilename, typesDirectory);
|
|
@@ -3,6 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const utils_1 = require("@typescript-eslint/utils");
|
|
4
4
|
const createRule_1 = require("../utils/createRule");
|
|
5
5
|
const isUpperSnakeCase = (str) => /^[A-Z][A-Z0-9_]*$/.test(str);
|
|
6
|
+
// Next.js recognizes these export names by their literal identifier, so
|
|
7
|
+
// renaming them to UPPER_SNAKE_CASE silently breaks the framework contract
|
|
8
|
+
// (e.g. `export const config` controls the API-route body parser / runtime).
|
|
9
|
+
// Only the export name matters to Next.js, so the exemption is gated on the
|
|
10
|
+
// declaration being exported — a local, unexported `config` is safe to rename.
|
|
11
|
+
const NEXTJS_RESERVED_EXPORTS = new Set([
|
|
12
|
+
'config',
|
|
13
|
+
'getServerSideProps',
|
|
14
|
+
'getStaticProps',
|
|
15
|
+
'getStaticPaths',
|
|
16
|
+
'getInitialProps',
|
|
17
|
+
'middleware',
|
|
18
|
+
]);
|
|
6
19
|
exports.default = (0, createRule_1.createRule)({
|
|
7
20
|
name: 'global-const-style',
|
|
8
21
|
meta: {
|
|
@@ -185,6 +198,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
185
198
|
});
|
|
186
199
|
}
|
|
187
200
|
}
|
|
201
|
+
// Skip the rename for exported Next.js reserved export names. Their
|
|
202
|
+
// identifier is an external framework contract that cannot be
|
|
203
|
+
// statically verified as safe to rename, so autofixing the rename
|
|
204
|
+
// silently regresses behavior (Issue #1257). The `as const` check
|
|
205
|
+
// above still applies since it never touches the export name.
|
|
206
|
+
const isExported = node.parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
207
|
+
if (isExported && NEXTJS_RESERVED_EXPORTS.has(name)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
188
210
|
// Check for UPPER_SNAKE_CASE
|
|
189
211
|
if (!isUpperSnakeCase(name)) {
|
|
190
212
|
const newName = name
|
|
@@ -339,7 +339,31 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
339
339
|
if (normalizedVarName === normalizedMarker) {
|
|
340
340
|
return false;
|
|
341
341
|
}
|
|
342
|
-
//
|
|
342
|
+
// Abbreviation markers (str, num, int, bool, arr, obj) are short enough
|
|
343
|
+
// that the raw-character boundary checks below fire on them as
|
|
344
|
+
// substrings inside real English words (e.g. "int" inside "Mint", "str"
|
|
345
|
+
// inside "stream"). The ONLY correct test for an abbreviation marker is
|
|
346
|
+
// therefore an exact match against a full camelCase segment: "Mint" →
|
|
347
|
+
// ["Mint"] never matches "int", while genuine Hungarian like intValue →
|
|
348
|
+
// ["int","Value"] still does.
|
|
349
|
+
//
|
|
350
|
+
// This guard MUST run before the prefix/suffix boundary checks. A
|
|
351
|
+
// capitalized terminal segment makes its own initial capital double as
|
|
352
|
+
// the raw suffix-boundary character: "appendHoldHint" ends with the
|
|
353
|
+
// marker "int" AND its preceding char is the capital "H" of "Hint", so
|
|
354
|
+
// the suffix check below would short-circuit to `true` and short-circuit
|
|
355
|
+
// this guard entirely (issue #1258). Because it always returns, an
|
|
356
|
+
// abbreviation marker never reaches the raw-character heuristics, so
|
|
357
|
+
// words like Hint/Blueprint/Waypoint/Checkpoint/Paint are spared.
|
|
358
|
+
if (ABBREVIATION_MARKER_SET.has(normalizedMarker)) {
|
|
359
|
+
const segments = splitCamelSegments(variableName);
|
|
360
|
+
return segments.some((s) => s.toLowerCase() === normalizedMarker);
|
|
361
|
+
}
|
|
362
|
+
// The prefix/suffix boundary checks below apply only to FULL type-word
|
|
363
|
+
// markers (String, Number, Boolean, Array, Object, ...); abbreviation
|
|
364
|
+
// markers have already returned above.
|
|
365
|
+
// Check if it's a prefix with proper boundary (e.g., stringValue,
|
|
366
|
+
// numberCount)
|
|
343
367
|
if (normalizedVarName.startsWith(normalizedMarker) &&
|
|
344
368
|
normalizedVarName.length > normalizedMarker.length &&
|
|
345
369
|
/[A-Z0-9]/.test(variableName[normalizedMarker.length])) {
|
|
@@ -352,16 +376,6 @@ exports.noHungarian = (0, createRule_1.createRule)({
|
|
|
352
376
|
/[A-Z]/.test(variableName[variableName.length - normalizedMarker.length]))) {
|
|
353
377
|
return true;
|
|
354
378
|
}
|
|
355
|
-
// Abbreviation markers (str, num, int, bool, arr, obj, fn, func) are
|
|
356
|
-
// short enough that a raw-character boundary check can fire on them as
|
|
357
|
-
// substrings inside real English words (e.g. "int" inside "Mint", "str"
|
|
358
|
-
// inside "stream"). Require an exact match against a full camelCase
|
|
359
|
-
// token so that "Mint" → ["Mint"] never matches "int", while genuine
|
|
360
|
-
// Hungarian like intValue → ["int","Value"] still does.
|
|
361
|
-
if (ABBREVIATION_MARKER_SET.has(normalizedMarker)) {
|
|
362
|
-
const segments = splitCamelSegments(variableName);
|
|
363
|
-
return segments.some((s) => s.toLowerCase() === normalizedMarker);
|
|
364
|
-
}
|
|
365
379
|
// Full type-word markers (non-abbreviations: String, Number, Function,
|
|
366
380
|
// Array, Object, Boolean, …) are Hungarian only when they occupy the
|
|
367
381
|
// first or last camelCase segment — a genuine prefix or suffix that
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
type MessageIds = 'preferUseTheme' | 'preferUseThemeNoEquivalent';
|
|
2
|
+
export declare const preferUseTheme: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
3
|
+
export {};
|
|
@@ -50,7 +50,9 @@ const BANNED_CONSTANTS = new Set([
|
|
|
50
50
|
/**
|
|
51
51
|
* Map from constant name to its theme-object equivalent path, used in the
|
|
52
52
|
* error message so that developers know exactly where to find the value on the
|
|
53
|
-
* theme object.
|
|
53
|
+
* theme object. Only constants that map to a value the theme actually carries
|
|
54
|
+
* appear here; constants without a faithful location are listed in
|
|
55
|
+
* CONSTANTS_WITHOUT_THEME_EQUIVALENT and get the no-equivalent message instead.
|
|
54
56
|
*/
|
|
55
57
|
const THEME_EQUIVALENTS = {
|
|
56
58
|
PALETTE: 'theme.palette',
|
|
@@ -70,9 +72,20 @@ const THEME_EQUIVALENTS = {
|
|
|
70
72
|
ZINDEX: 'theme.zIndex',
|
|
71
73
|
ASPECT_RATIO: 'theme.aspectRatio',
|
|
72
74
|
BREAKPOINTS: 'theme.breakpoints',
|
|
73
|
-
BORDER_RADIUS: 'theme.shape.borderRadius',
|
|
74
|
-
CONTAINER_WIDTH: 'theme.mixins',
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Banned constants that have NO faithful single location on the theme object:
|
|
78
|
+
* - BORDER_RADIUS is the full M3 shape scale; `theme.shape.borderRadius` is
|
|
79
|
+
* MUI's default `4`, not this scale — following that hint ships a visual
|
|
80
|
+
* regression, so the message points at real alternatives instead.
|
|
81
|
+
* - CONTAINER_WIDTH is not carried anywhere on the theme today.
|
|
82
|
+
* These still bypass the theme system, so they remain flagged — but with the
|
|
83
|
+
* no-equivalent message rather than a mapping that misdirects developers.
|
|
84
|
+
*/
|
|
85
|
+
const CONSTANTS_WITHOUT_THEME_EQUIVALENT = new Set([
|
|
86
|
+
'BORDER_RADIUS',
|
|
87
|
+
'CONTAINER_WIDTH',
|
|
88
|
+
]);
|
|
76
89
|
/**
|
|
77
90
|
* Target file path fragments that the rule enforces inside. Files outside
|
|
78
91
|
* these directories are not flagged — this avoids false positives in utility
|
|
@@ -130,13 +143,20 @@ exports.preferUseTheme = (0, createRule_1.createRule)({
|
|
|
130
143
|
schema: [],
|
|
131
144
|
messages: {
|
|
132
145
|
preferUseTheme: "Import '{{importName}}' from '{{sourceModule}}' bypasses the MUI theme system. Use useTheme() and access {{themeEquivalent}} instead.",
|
|
146
|
+
preferUseThemeNoEquivalent: "Import '{{importName}}' from '{{sourceModule}}' bypasses the MUI theme system. This constant has no direct equivalent on the theme object: reuse a theme token that already carries the value (e.g. theme.panels[n].borderRadius) or add {{importName}} to the theme in src/styles/theme.ts and read it via useTheme().",
|
|
133
147
|
},
|
|
134
148
|
},
|
|
135
149
|
defaultOptions: [],
|
|
136
150
|
create(context) {
|
|
137
|
-
const
|
|
151
|
+
const rawFilename = context.getFilename?.() ??
|
|
138
152
|
context.filename ??
|
|
139
153
|
'';
|
|
154
|
+
// Normalize Windows backslash separators to forward slashes so the
|
|
155
|
+
// forward-slash path fragments below (`src/components/`, `src/styles/`,
|
|
156
|
+
// `__tests__/`) match on every platform. Without this, `getFilename()`
|
|
157
|
+
// returns `C:\repo\src\components\Foo.tsx` on Windows, `isInTargetPath`
|
|
158
|
+
// never matches, and the rule silently no-ops for every file (issue #1259).
|
|
159
|
+
const filename = rawFilename.replace(/\\/g, '/');
|
|
140
160
|
// Files that build the theme are always exempt.
|
|
141
161
|
if (isInStylesDir(filename)) {
|
|
142
162
|
return {};
|
|
@@ -186,6 +206,19 @@ exports.preferUseTheme = (0, createRule_1.createRule)({
|
|
|
186
206
|
if (!BANNED_CONSTANTS.has(importedName)) {
|
|
187
207
|
continue;
|
|
188
208
|
}
|
|
209
|
+
// Constants without a faithful theme location get guidance toward
|
|
210
|
+
// real alternatives rather than a mapping that misdirects (issue #1260).
|
|
211
|
+
if (CONSTANTS_WITHOUT_THEME_EQUIVALENT.has(importedName)) {
|
|
212
|
+
context.report({
|
|
213
|
+
node: specifier,
|
|
214
|
+
messageId: 'preferUseThemeNoEquivalent',
|
|
215
|
+
data: {
|
|
216
|
+
importName: importedName,
|
|
217
|
+
sourceModule: source,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
189
222
|
const themeEquivalent = THEME_EQUIVALENTS[importedName] ??
|
|
190
223
|
'the theme object via useTheme()';
|
|
191
224
|
context.report({
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.18.1",
|
|
4
|
+
"date": "2026-07-06T03:35:53.804Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "consistent-callback-naming",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1262
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt value-returning accessor props (closes #1262)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "enforce-positive-naming",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1261
|
|
19
|
+
],
|
|
20
|
+
"summary": "treat the \"disabled\" word family as valid (closes #1261)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "enforce-types-directory-placement",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1263
|
|
27
|
+
],
|
|
28
|
+
"summary": "exempt frontend-coupled type files (closes #1263)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "global-const-style",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
1257
|
|
35
|
+
],
|
|
36
|
+
"summary": "exempt Next.js reserved exports from autofix rename (closes #1257)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "no-hungarian",
|
|
40
|
+
"changeType": "fix",
|
|
41
|
+
"issues": [
|
|
42
|
+
1258
|
|
43
|
+
],
|
|
44
|
+
"summary": "don't flag words ending in an abbreviation marker (closes #1258)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "prefer-use-theme",
|
|
48
|
+
"changeType": "fix",
|
|
49
|
+
"issues": [
|
|
50
|
+
1259,
|
|
51
|
+
1260
|
|
52
|
+
],
|
|
53
|
+
"summary": "stop pointing BORDER_RADIUS/CONTAINER_WIDTH at absent theme paths (closes #1260); normalize Windows path separators before path checks (closes #1259)"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"version": "1.18.0",
|
|
59
|
+
"date": "2026-07-02T16:19:34.385Z",
|
|
60
|
+
"rules": [
|
|
61
|
+
{
|
|
62
|
+
"name": "no-inline-component-prop",
|
|
63
|
+
"changeType": "feat",
|
|
64
|
+
"issues": [
|
|
65
|
+
833
|
|
66
|
+
],
|
|
67
|
+
"summary": "register the orphaned rule in the plugin (#1233)"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
2
71
|
{
|
|
3
72
|
"version": "1.17.3",
|
|
4
73
|
"date": "2026-07-02T16:07:25.816Z",
|