@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.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 +28 -5
- package/lib/index.js +7 -1
- package/lib/rules/consistent-callback-naming.js +26 -0
- package/lib/rules/dynamic-https-errors.js +5 -26
- package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
- package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
- package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
- package/lib/rules/enforce-dynamic-imports.js +42 -21
- package/lib/rules/enforce-memoize-async.js +1 -4
- package/lib/rules/enforce-mui-rounded-icons.js +42 -1
- package/lib/rules/enforce-verb-noun-naming.js +3 -0
- package/lib/rules/global-const-style.js +9 -0
- package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
- package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
- package/lib/rules/memo-nested-react-components.js +143 -8
- package/lib/rules/no-array-length-in-deps.js +74 -3
- package/lib/rules/no-circular-references.js +145 -482
- package/lib/rules/no-compositing-layer-props.js +31 -0
- package/lib/rules/no-entire-object-hook-deps.js +132 -97
- package/lib/rules/no-explicit-return-type.js +6 -0
- package/lib/rules/no-hungarian.js +119 -24
- package/lib/rules/no-margin-properties.js +7 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
- package/lib/rules/no-unused-props.js +215 -37
- package/lib/rules/no-useless-fragment.js +10 -2
- package/lib/rules/parallelize-async-operations.js +1 -3
- package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
- package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
- package/lib/rules/react-memoize-literals.js +87 -1
- package/lib/rules/require-migration-script-metadata.d.ts +9 -0
- package/lib/rules/require-migration-script-metadata.js +206 -0
- package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
- package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
- package/lib/utils/ASTHelpers.d.ts +15 -0
- package/lib/utils/ASTHelpers.js +48 -0
- package/package.json +7 -6
- package/release-manifest.json +166 -0
|
@@ -60,8 +60,6 @@ function isNonPrimitiveWithoutTypes(expr) {
|
|
|
60
60
|
switch (expr.type) {
|
|
61
61
|
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
62
62
|
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
63
|
-
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
64
|
-
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
65
63
|
case utils_1.AST_NODE_TYPES.NewExpression:
|
|
66
64
|
case utils_1.AST_NODE_TYPES.ClassExpression:
|
|
67
65
|
return true;
|
|
@@ -74,6 +72,8 @@ function isNonPrimitiveWithoutTypes(expr) {
|
|
|
74
72
|
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
75
73
|
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
76
74
|
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
75
|
+
case utils_1.AST_NODE_TYPES.FunctionExpression:
|
|
76
|
+
case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
|
|
77
77
|
default:
|
|
78
78
|
return false;
|
|
79
79
|
}
|
|
@@ -92,9 +92,9 @@ function isNonPrimitiveWithTypes(context, expr) {
|
|
|
92
92
|
if (!tsNode)
|
|
93
93
|
return false;
|
|
94
94
|
const type = checker.getTypeAtLocation(tsNode);
|
|
95
|
-
//
|
|
95
|
+
// Explicitly ignore functions even though they are non-primitive
|
|
96
96
|
if (type.getCallSignatures().length > 0)
|
|
97
|
-
return
|
|
97
|
+
return false;
|
|
98
98
|
// Avoid guessing for unknown/any or non-function types
|
|
99
99
|
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))
|
|
100
100
|
return false;
|
|
@@ -243,7 +243,7 @@ function isImportedIdentifier(context, name) {
|
|
|
243
243
|
}
|
|
244
244
|
return false;
|
|
245
245
|
}
|
|
246
|
-
function
|
|
246
|
+
function identifierUsedAsObjectOrArray(callback, name) {
|
|
247
247
|
const stack = [callback.body];
|
|
248
248
|
while (stack.length) {
|
|
249
249
|
const node = stack.pop();
|
|
@@ -268,12 +268,6 @@ function identifierUsedAsObjectOrFunction(callback, name) {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
-
if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
272
|
-
const callee = node.callee;
|
|
273
|
-
if (callee.type === utils_1.AST_NODE_TYPES.Identifier && callee.name === name) {
|
|
274
|
-
return true; // function usage
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
271
|
if (node.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute) {
|
|
278
272
|
if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
279
273
|
node.argument.name === name) {
|
|
@@ -313,13 +307,13 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
|
|
|
313
307
|
meta: {
|
|
314
308
|
type: 'suggestion',
|
|
315
309
|
docs: {
|
|
316
|
-
description: 'Enforce using useDeepCompareMemo when dependency array contains non-primitive values (objects, arrays
|
|
310
|
+
description: 'Enforce using useDeepCompareMemo when dependency array contains non-primitive values (objects, arrays) that are not already memoized. This prevents unnecessary re-renders due to reference changes.',
|
|
317
311
|
recommended: 'error',
|
|
318
312
|
},
|
|
319
313
|
fixable: 'code',
|
|
320
314
|
schema: [],
|
|
321
315
|
messages: {
|
|
322
|
-
preferUseDeepCompareMemo: 'Dependency array for "{{hook}}" includes objects/arrays
|
|
316
|
+
preferUseDeepCompareMemo: 'Dependency array for "{{hook}}" includes objects/arrays that change identity each render, so React treats them as changed and reruns the memoized computation, triggering avoidable renders. Use useDeepCompareMemo (or memoize those dependencies first) so comparisons use deep equality and the memo stays stable.',
|
|
323
317
|
},
|
|
324
318
|
},
|
|
325
319
|
defaultOptions: [],
|
|
@@ -366,7 +360,7 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
|
|
|
366
360
|
if (isImportedIdentifier(context, expr.name)) {
|
|
367
361
|
isNonPrimitive = false;
|
|
368
362
|
}
|
|
369
|
-
else if (
|
|
363
|
+
else if (identifierUsedAsObjectOrArray(callback, expr.name) &&
|
|
370
364
|
!isIdentifierMemoizedAbove(expr.name, memoizedIds)) {
|
|
371
365
|
isNonPrimitive = true;
|
|
372
366
|
}
|
|
@@ -46,6 +46,22 @@ const SAFE_HOOK_ARGUMENTS = new Set([
|
|
|
46
46
|
'useDeepCompareEffect',
|
|
47
47
|
'useProgressionCallback',
|
|
48
48
|
]);
|
|
49
|
+
/**
|
|
50
|
+
* Hooks where any literal defined inside their arguments is safe because
|
|
51
|
+
* the hook either doesn't use the identity for comparison or provides its
|
|
52
|
+
* own stability.
|
|
53
|
+
*/
|
|
54
|
+
const HOOKS_ALLOWING_NESTED_LITERALS = new Set([
|
|
55
|
+
'useState',
|
|
56
|
+
'useReducer',
|
|
57
|
+
'useRef',
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* JSX attribute names whose values are style descriptors consumed by the
|
|
61
|
+
* library (not via referential equality), so inline object literals are safe.
|
|
62
|
+
* MUI's `sx` and the standard `style` prop both fall into this category.
|
|
63
|
+
*/
|
|
64
|
+
const STYLE_JSX_ATTRIBUTE_NAMES = new Set(['sx', 'style']);
|
|
49
65
|
const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
|
|
50
66
|
const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
|
|
51
67
|
const PARENTHESIZED_EXPRESSION_TYPE = utils_1.AST_NODE_TYPES.ParenthesizedExpression ??
|
|
@@ -409,6 +425,66 @@ function buildMemoSuggestions(node, descriptor, sourceCode) {
|
|
|
409
425
|
},
|
|
410
426
|
];
|
|
411
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Returns true when the node's value resolves to a JSX attribute that carries
|
|
430
|
+
* style data consumed by a library rather than compared by reference (MUI `sx`
|
|
431
|
+
* or the standard `style` prop).
|
|
432
|
+
*
|
|
433
|
+
* Walks up from the literal only through positions where it remains the
|
|
434
|
+
* attribute's resolved value: conditional branches (`sx={c ? {…} : {…}}`),
|
|
435
|
+
* logical fallbacks (`sx={c && {…}}`), array entries (MUI accepts `sx` arrays),
|
|
436
|
+
* and expression wrappers (`{…} as const`, parentheses). Any other parent — a
|
|
437
|
+
* function call, an object property, a spread — means the reference is observed
|
|
438
|
+
* or transformed before reaching the attribute, so the walk stops and the
|
|
439
|
+
* literal stays reported. This keeps the exemption tied to genuine style values
|
|
440
|
+
* without silencing unrelated literals that merely appear deeper in the tree.
|
|
441
|
+
*/
|
|
442
|
+
function isStyleJSXAttributeValue(node) {
|
|
443
|
+
let current = node;
|
|
444
|
+
let parent = current.parent;
|
|
445
|
+
while (parent) {
|
|
446
|
+
switch (parent.type) {
|
|
447
|
+
case utils_1.AST_NODE_TYPES.JSXExpressionContainer: {
|
|
448
|
+
if (parent.expression !== current) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
const attribute = parent.parent;
|
|
452
|
+
if (!attribute || attribute.type !== utils_1.AST_NODE_TYPES.JSXAttribute) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const { name } = attribute;
|
|
456
|
+
return (name.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
457
|
+
STYLE_JSX_ATTRIBUTE_NAMES.has(name.name));
|
|
458
|
+
}
|
|
459
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression: {
|
|
460
|
+
if (parent.consequent !== current && parent.alternate !== current) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression: {
|
|
466
|
+
if (parent.left !== current && parent.right !== current) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression: {
|
|
472
|
+
if (!parent.elements.some((element) => element === current)) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
default: {
|
|
478
|
+
if (!isExpressionWrapper(parent) || parent.expression !== current) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
current = parent;
|
|
484
|
+
parent = current.parent;
|
|
485
|
+
}
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
412
488
|
/**
|
|
413
489
|
* Formats a readable label for diagnostics based on the owning function.
|
|
414
490
|
* @param fn Owning component or hook function.
|
|
@@ -556,10 +632,20 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
|
|
|
556
632
|
if (isTerminalUsage(node)) {
|
|
557
633
|
return;
|
|
558
634
|
}
|
|
635
|
+
// Inline literals that resolve to a style JSX attribute (sx, style),
|
|
636
|
+
// whether directly or via conditional/logical/array wrappers, are
|
|
637
|
+
// consumed by the library without reference equality checks, so
|
|
638
|
+
// memoization adds no stability benefit.
|
|
639
|
+
if (isStyleJSXAttributeValue(node)) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
559
642
|
const hookCall = findEnclosingHookCall(node);
|
|
560
643
|
if (hookCall) {
|
|
561
|
-
if (hookCall.isDirectArgument
|
|
644
|
+
if (hookCall.isDirectArgument ||
|
|
645
|
+
HOOKS_ALLOWING_NESTED_LITERALS.has(hookCall.hookName)) {
|
|
562
646
|
// Top-level literal passed directly to a hook argument is allowed.
|
|
647
|
+
// Also allow nested literals for hooks that don't rely on argument
|
|
648
|
+
// reference stability for memoization (e.g. useState, useReducer).
|
|
563
649
|
return;
|
|
564
650
|
}
|
|
565
651
|
context.report({
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type MessageIds = 'missingMetadata' | 'metadataAfterStatement' | 'missingMigrationTag' | 'invalidMigrationTag' | 'missingPhaseTag' | 'invalidPhaseTag' | 'missingDependenciesTag' | 'invalidDependenciesTag' | 'missingDescriptionTag' | 'extensionInDependencies' | 'noneCasing' | 'legacyHeaderNotAllowed' | 'multipleMetadataBlocks';
|
|
2
|
+
type Options = [
|
|
3
|
+
{
|
|
4
|
+
targetGlobs?: string[];
|
|
5
|
+
allowLegacyHeader?: boolean;
|
|
6
|
+
}
|
|
7
|
+
];
|
|
8
|
+
export declare const requireMigrationScriptMetadata: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireMigrationScriptMetadata = void 0;
|
|
4
|
+
const minimatch_1 = require("minimatch");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
targetGlobs: ['**/functions/src/callable/scripts/**/*.f.ts'],
|
|
8
|
+
allowLegacyHeader: true,
|
|
9
|
+
};
|
|
10
|
+
exports.requireMigrationScriptMetadata = (0, createRule_1.createRule)({
|
|
11
|
+
name: 'require-migration-script-metadata',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Enforce JSDoc migration metadata in callable scripts',
|
|
16
|
+
recommended: 'error',
|
|
17
|
+
},
|
|
18
|
+
schema: [
|
|
19
|
+
{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
targetGlobs: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
items: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
allowLegacyHeader: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
messages: {
|
|
34
|
+
missingMetadata: 'Missing migration metadata JSDoc block → Release tooling cannot determine participation or ordering, which can skip or misorder migrations → Add a top-of-file JSDoc block with `@migration` true/false; when true include `@migrationPhase`, `@migrationDependencies` (or NONE), and `@migrationDescription`.',
|
|
35
|
+
metadataAfterStatement: 'Migration metadata appears after code/imports → The release scanner only reads the top-of-file block and will miss this metadata → Move the metadata JSDoc above all imports/statements (legacy header allowed only when configured).',
|
|
36
|
+
missingMigrationTag: 'Metadata block is missing `@migration` → Without an explicit true/false, automation cannot decide whether to run this script → Add `@migration` true or `@migration` false to the metadata block.',
|
|
37
|
+
invalidMigrationTag: 'Invalid `@migration` value → Only true/false is supported so automation can branch deterministically → Replace with `@migration` true or `@migration` false.',
|
|
38
|
+
missingPhaseTag: 'Missing `@migrationPhase` while `@migration` is true → Release ordering cannot place this script → Add `@migrationPhase` before or after.',
|
|
39
|
+
invalidPhaseTag: 'Invalid `@migrationPhase` value → Release ordering only understands before/after → Use `@migrationPhase` before or `@migrationPhase` after.',
|
|
40
|
+
missingDependenciesTag: 'Missing `@migrationDependencies` while `@migration` is true → Dependency ordering cannot be computed → Add `@migrationDependencies` NONE or a comma-separated list of script names.',
|
|
41
|
+
invalidDependenciesTag: 'Invalid `@migrationDependencies` list → Empty or whitespace entries break dependency resolution → Provide a comma-separated list of non-empty names or NONE.',
|
|
42
|
+
missingDescriptionTag: 'Missing `@migrationDescription` while `@migration` is true → Release reports lose context and reviewers cannot assess impact → Add a brief `@migrationDescription`.',
|
|
43
|
+
extensionInDependencies: 'Dependency "{{name}}" includes a file extension → Dependencies are script identifiers, and extensions cause mismatches → Remove the ".f.ts" extension from "{{name}}".',
|
|
44
|
+
noneCasing: 'Invalid `@migrationDependencies` casing → Automation expects exactly "NONE" in all-caps to signal no dependencies → Change to uppercase "NONE".',
|
|
45
|
+
legacyHeaderNotAllowed: 'Legacy header/comment appears before metadata → The metadata block must be the first file-level block to keep linting deterministic → Remove leading comments or enable allowLegacyHeader.',
|
|
46
|
+
multipleMetadataBlocks: 'Multiple metadata blocks found → Conflicting `@migration` tags make automation ambiguous → Keep exactly one JSDoc block with `@migration`.',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
50
|
+
create(context, [options]) {
|
|
51
|
+
const rawFilename = context.filename ?? context.getFilename();
|
|
52
|
+
const filename = rawFilename.replace(/\\/g, '/');
|
|
53
|
+
const { targetGlobs, allowLegacyHeader } = {
|
|
54
|
+
...DEFAULT_OPTIONS,
|
|
55
|
+
...options,
|
|
56
|
+
};
|
|
57
|
+
// Only run on files matching targetGlobs
|
|
58
|
+
const isTargetFile = targetGlobs?.some((glob) => (0, minimatch_1.minimatch)(filename, glob, { dot: true, matchBase: true }));
|
|
59
|
+
if (!isTargetFile) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
63
|
+
const comments = sourceCode.getAllComments();
|
|
64
|
+
const firstToken = sourceCode.getFirstToken(sourceCode.ast, {
|
|
65
|
+
includeComments: false,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
Program() {
|
|
69
|
+
const metadataCandidates = comments
|
|
70
|
+
.filter((comment) => comment.type === 'Block' && comment.value.startsWith('*'))
|
|
71
|
+
.map((comment) => ({ comment, tags: parseJSDocTags(comment.value) }))
|
|
72
|
+
.filter(({ tags }) => Object.keys(tags).some((tag) => tag.startsWith('migration')));
|
|
73
|
+
if (metadataCandidates.length > 1) {
|
|
74
|
+
for (const extra of metadataCandidates.slice(1)) {
|
|
75
|
+
context.report({
|
|
76
|
+
node: extra.comment,
|
|
77
|
+
messageId: 'multipleMetadataBlocks',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const migrationMetadata = metadataCandidates[0] ?? null;
|
|
82
|
+
if (!migrationMetadata) {
|
|
83
|
+
context.report({
|
|
84
|
+
loc: { line: 1, column: 0 },
|
|
85
|
+
messageId: 'missingMetadata',
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const { comment, tags } = migrationMetadata;
|
|
90
|
+
if (!allowLegacyHeader) {
|
|
91
|
+
const hasLeadingComment = comments.some((c) => c.type === 'Block' && c.range[0] < comment.range[0]);
|
|
92
|
+
if (hasLeadingComment) {
|
|
93
|
+
context.report({
|
|
94
|
+
node: comment,
|
|
95
|
+
messageId: 'legacyHeaderNotAllowed',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Check if metadata appears after the first statement/import
|
|
100
|
+
if (firstToken && comment.range[0] > firstToken.range[0]) {
|
|
101
|
+
context.report({
|
|
102
|
+
node: comment,
|
|
103
|
+
messageId: 'metadataAfterStatement',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!('migration' in tags)) {
|
|
107
|
+
context.report({
|
|
108
|
+
node: comment,
|
|
109
|
+
messageId: 'missingMigrationTag',
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const migration = tags.migration;
|
|
114
|
+
if (migration !== 'true' && migration !== 'false') {
|
|
115
|
+
context.report({
|
|
116
|
+
node: comment,
|
|
117
|
+
messageId: 'invalidMigrationTag',
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (migration === 'true') {
|
|
122
|
+
// Check phase
|
|
123
|
+
if (!tags.migrationPhase) {
|
|
124
|
+
context.report({
|
|
125
|
+
node: comment,
|
|
126
|
+
messageId: 'missingPhaseTag',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else if (tags.migrationPhase !== 'before' &&
|
|
130
|
+
tags.migrationPhase !== 'after') {
|
|
131
|
+
context.report({
|
|
132
|
+
node: comment,
|
|
133
|
+
messageId: 'invalidPhaseTag',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Check dependencies
|
|
137
|
+
if (!tags.migrationDependencies) {
|
|
138
|
+
context.report({
|
|
139
|
+
node: comment,
|
|
140
|
+
messageId: 'missingDependenciesTag',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const deps = tags.migrationDependencies
|
|
145
|
+
.split(',')
|
|
146
|
+
.map((d) => d.trim());
|
|
147
|
+
const hasNone = deps.some((d) => d.toUpperCase() === 'NONE');
|
|
148
|
+
if (deps.some((d) => d === '')) {
|
|
149
|
+
context.report({
|
|
150
|
+
node: comment,
|
|
151
|
+
messageId: 'invalidDependenciesTag',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (hasNone && deps.length > 1) {
|
|
155
|
+
context.report({
|
|
156
|
+
node: comment,
|
|
157
|
+
messageId: 'invalidDependenciesTag',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else if (hasNone && deps[0] !== 'NONE') {
|
|
161
|
+
context.report({
|
|
162
|
+
node: comment,
|
|
163
|
+
messageId: 'noneCasing',
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else if (hasNone && deps[0] === 'NONE') {
|
|
167
|
+
// Valid: exactly NONE
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
for (const dep of deps) {
|
|
171
|
+
if (dep.endsWith('.f.ts')) {
|
|
172
|
+
context.report({
|
|
173
|
+
node: comment,
|
|
174
|
+
messageId: 'extensionInDependencies',
|
|
175
|
+
data: { name: dep },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Check description
|
|
182
|
+
if (!tags.migrationDescription) {
|
|
183
|
+
context.report({
|
|
184
|
+
node: comment,
|
|
185
|
+
messageId: 'missingDescriptionTag',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
function parseJSDocTags(commentValue) {
|
|
194
|
+
const tags = {};
|
|
195
|
+
const lines = commentValue.split('\n');
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
const trimmedLine = line.trim().replace(/^\*+\s*/, '');
|
|
198
|
+
const match = trimmedLine.match(/^@(\w+)(?:\s+(.*))?$/);
|
|
199
|
+
if (match) {
|
|
200
|
+
const [, tagName, tagValue] = match;
|
|
201
|
+
tags[tagName] = (tagValue ?? '').trim();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return tags;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=require-migration-script-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const warnHttpsErrorMessageUserFriendly: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"warnHttpsErrorMessageUserFriendly", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.warnHttpsErrorMessageUserFriendly = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
7
|
+
exports.warnHttpsErrorMessageUserFriendly = (0, createRule_1.createRule)({
|
|
8
|
+
name: 'warn-https-error-message-user-friendly',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'suggestion',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Warn when messageUserFriendly is used in HttpsError or toHttpsError to ensure it is truly a user-caused error.',
|
|
13
|
+
recommended: 'warn',
|
|
14
|
+
},
|
|
15
|
+
fixable: undefined,
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
warnHttpsErrorMessageUserFriendly: "What's wrong: '{{propertyName}}' is set on an HttpsError/toHttpsError options object. " +
|
|
19
|
+
'Why it matters: this marks the error as user-caused and can suppress automated error monitoring and QA issue creation for real defects. ' +
|
|
20
|
+
"How to fix: remove '{{propertyName}}' unless the error is genuinely user-caused; if it is, keep it and add // eslint-disable-next-line to document the exception.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
create(context) {
|
|
25
|
+
/**
|
|
26
|
+
* Extracts problematic property definitions to provide precise reporting locations,
|
|
27
|
+
* even when properties are deeply nested or mixed via spread elements.
|
|
28
|
+
*/
|
|
29
|
+
const findMessageUserFriendlyProperties = (node, visited = new Set()) => {
|
|
30
|
+
const matches = [];
|
|
31
|
+
for (const prop of node.properties) {
|
|
32
|
+
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
33
|
+
if ((!prop.computed &&
|
|
34
|
+
((prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
35
|
+
prop.key.name === 'messageUserFriendly') ||
|
|
36
|
+
(prop.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
37
|
+
prop.key.value === 'messageUserFriendly'))) ||
|
|
38
|
+
(prop.computed &&
|
|
39
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
40
|
+
prop.key.value === 'messageUserFriendly')) {
|
|
41
|
+
matches.push(prop);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (prop.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
45
|
+
if (checkNode(prop.argument, visited)) {
|
|
46
|
+
matches.push(prop);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return matches;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Analyzes variable assignments to prevent rule bypass via intermediate identifiers,
|
|
54
|
+
* ensuring that 'messageUserFriendly' cannot be hidden behind a variable name.
|
|
55
|
+
*/
|
|
56
|
+
const traceVariable = (identifier, visited = new Set()) => {
|
|
57
|
+
if (visited.has(identifier.name))
|
|
58
|
+
return false;
|
|
59
|
+
visited.add(identifier.name);
|
|
60
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, identifier);
|
|
61
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, identifier.name);
|
|
62
|
+
if (!variable)
|
|
63
|
+
return false;
|
|
64
|
+
for (const def of variable.defs) {
|
|
65
|
+
if (def.type === 'Variable' && def.node.init) {
|
|
66
|
+
if (checkNode(def.node.init, visited)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (def.type === 'FunctionName' &&
|
|
71
|
+
def.node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
72
|
+
if (traceFunctionReturn(def.node, visited)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Inspects function bodies to detect cases where the options object is generated
|
|
81
|
+
* dynamically, closing a common bypass pattern where factories or helpers are used.
|
|
82
|
+
*/
|
|
83
|
+
const traceFunctionReturn = (node, visited = new Set()) => {
|
|
84
|
+
if (!node.body)
|
|
85
|
+
return false;
|
|
86
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
87
|
+
const walk = (current) => {
|
|
88
|
+
if (current !== node &&
|
|
89
|
+
(current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
90
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
91
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression)) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
95
|
+
current.argument) {
|
|
96
|
+
return checkNode(current.argument, visited);
|
|
97
|
+
}
|
|
98
|
+
const keys = sourceCode.visitorKeys[current.type] ?? [];
|
|
99
|
+
return keys.some((key) => {
|
|
100
|
+
const value = current[key];
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
return value.some((child) => child &&
|
|
103
|
+
typeof child === 'object' &&
|
|
104
|
+
'type' in child &&
|
|
105
|
+
walk(child));
|
|
106
|
+
}
|
|
107
|
+
if (value && typeof value === 'object' && 'type' in value) {
|
|
108
|
+
return walk(value);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
114
|
+
return walk(node.body);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Arrow function with expression body
|
|
118
|
+
return checkNode(node.body, visited);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Recursively evaluates diverse AST structures (logical, conditional, calls) to ensure
|
|
123
|
+
* that the presence of 'messageUserFriendly' is detected regardless of expression complexity.
|
|
124
|
+
*
|
|
125
|
+
* Note: This implementation intentionally covers only Identifier callees for simplicity,
|
|
126
|
+
* as MemberExpression callees (e.g., optionsFactory.build()) are not currently used in
|
|
127
|
+
* the codebase for HttpsError options. Handling for MemberExpression callees can be
|
|
128
|
+
* added as a future enhancement if such patterns emerge.
|
|
129
|
+
*/
|
|
130
|
+
const checkNode = (node, visited = new Set()) => {
|
|
131
|
+
const unwrappedNode = ASTHelpers_1.ASTHelpers.unwrapTSAssertions(node);
|
|
132
|
+
if (unwrappedNode.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
133
|
+
return (findMessageUserFriendlyProperties(unwrappedNode, visited).length > 0);
|
|
134
|
+
}
|
|
135
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
136
|
+
return traceVariable(unwrappedNode, visited);
|
|
137
|
+
}
|
|
138
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
139
|
+
return (checkNode(unwrappedNode.left, visited) ||
|
|
140
|
+
checkNode(unwrappedNode.right, visited));
|
|
141
|
+
}
|
|
142
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
143
|
+
return (checkNode(unwrappedNode.consequent, visited) ||
|
|
144
|
+
checkNode(unwrappedNode.alternate, visited));
|
|
145
|
+
}
|
|
146
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
147
|
+
unwrappedNode.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
148
|
+
if (unwrappedNode.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
149
|
+
const calleeName = unwrappedNode.callee.name;
|
|
150
|
+
if (visited.has(calleeName))
|
|
151
|
+
return false;
|
|
152
|
+
visited.add(calleeName);
|
|
153
|
+
const scope = ASTHelpers_1.ASTHelpers.getScope(context, unwrappedNode.callee);
|
|
154
|
+
const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, calleeName);
|
|
155
|
+
if (variable) {
|
|
156
|
+
for (const def of variable.defs) {
|
|
157
|
+
if (def.type === 'FunctionName' &&
|
|
158
|
+
def.node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
|
|
159
|
+
if (traceFunctionReturn(def.node, visited)) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (def.type === 'Variable' && def.node.init) {
|
|
164
|
+
if (def.node.init.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
165
|
+
def.node.init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
166
|
+
if (traceFunctionReturn(def.node.init, visited)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
const validateOptions = (node) => {
|
|
178
|
+
const unwrappedNode = ASTHelpers_1.ASTHelpers.unwrapTSAssertions(node);
|
|
179
|
+
if (unwrappedNode.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
180
|
+
findMessageUserFriendlyProperties(unwrappedNode).forEach((prop) => {
|
|
181
|
+
context.report({
|
|
182
|
+
node: prop.type === utils_1.AST_NODE_TYPES.Property ? prop.key : prop,
|
|
183
|
+
messageId: 'warnHttpsErrorMessageUserFriendly',
|
|
184
|
+
data: { propertyName: 'messageUserFriendly' },
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
189
|
+
if (traceVariable(unwrappedNode)) {
|
|
190
|
+
context.report({
|
|
191
|
+
node: unwrappedNode,
|
|
192
|
+
messageId: 'warnHttpsErrorMessageUserFriendly',
|
|
193
|
+
data: { propertyName: 'messageUserFriendly' },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.CallExpression ||
|
|
198
|
+
unwrappedNode.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
199
|
+
if (checkNode(unwrappedNode)) {
|
|
200
|
+
context.report({
|
|
201
|
+
node,
|
|
202
|
+
messageId: 'warnHttpsErrorMessageUserFriendly',
|
|
203
|
+
data: { propertyName: 'messageUserFriendly' },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
|
|
208
|
+
validateOptions(unwrappedNode.left);
|
|
209
|
+
validateOptions(unwrappedNode.right);
|
|
210
|
+
}
|
|
211
|
+
else if (unwrappedNode.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
|
|
212
|
+
validateOptions(unwrappedNode.consequent);
|
|
213
|
+
validateOptions(unwrappedNode.alternate);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
NewExpression(node) {
|
|
218
|
+
if (ASTHelpers_1.ASTHelpers.isHttpsErrorCall(node.callee)) {
|
|
219
|
+
if (node.arguments.length > 0) {
|
|
220
|
+
validateOptions(node.arguments[0]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
CallExpression(node) {
|
|
225
|
+
if (ASTHelpers_1.ASTHelpers.isHttpsErrorCall(node.callee)) {
|
|
226
|
+
if (node.arguments.length > 0) {
|
|
227
|
+
validateOptions(node.arguments[0]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (ASTHelpers_1.ASTHelpers.isToHttpsErrorCall(node.callee)) {
|
|
231
|
+
if (node.arguments.length > 1) {
|
|
232
|
+
validateOptions(node.arguments[1]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
//# sourceMappingURL=warn-https-error-message-user-friendly.js.map
|
|
@@ -46,6 +46,21 @@ export declare class ASTHelpers {
|
|
|
46
46
|
* Compatibility wrapper for getting declared variables across ESLint versions.
|
|
47
47
|
*/
|
|
48
48
|
static getDeclaredVariables(context: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>, node: TSESTree.Node): readonly TSESLint.Scope.Variable[];
|
|
49
|
+
/**
|
|
50
|
+
* Checks if a call expression or new expression is a call to HttpsError.
|
|
51
|
+
* Handles both 'HttpsError' and 'https.HttpsError'.
|
|
52
|
+
*/
|
|
53
|
+
static isHttpsErrorCall(callee: TSESTree.LeftHandSideExpression): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a call expression is a call to toHttpsError.
|
|
56
|
+
* Handles both 'toHttpsError' and 'https.toHttpsError'.
|
|
57
|
+
*/
|
|
58
|
+
static isToHttpsErrorCall(callee: TSESTree.LeftHandSideExpression): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Unwraps TypeScript-specific nodes (assertions, non-null, satisfies) and
|
|
61
|
+
* parenthesized expressions to get to the underlying expression.
|
|
62
|
+
*/
|
|
63
|
+
static unwrapTSAssertions(node: TSESTree.Node): TSESTree.Node;
|
|
49
64
|
/**
|
|
50
65
|
* Helper to get ancestors of a node in a way that is compatible with both ESLint v8 and v9.
|
|
51
66
|
* In ESLint v9, context.getAncestors() is deprecated and moved to context.sourceCode.getAncestors(node).
|