@blumintinc/eslint-plugin-blumint 1.16.2 → 1.17.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 +21 -0
- package/lib/index.js +64 -1
- package/lib/rules/enforce-boolean-naming-prefixes.js +30 -14
- package/lib/rules/enforce-cloud-function-id-length.d.ts +5 -0
- package/lib/rules/enforce-cloud-function-id-length.js +104 -0
- package/lib/rules/enforce-is-prefix-validators.d.ts +13 -0
- package/lib/rules/enforce-is-prefix-validators.js +304 -0
- package/lib/rules/enforce-m3-sentence-case.d.ts +12 -0
- package/lib/rules/enforce-m3-sentence-case.js +430 -0
- package/lib/rules/enforce-snapshot-state-narrowing.d.ts +10 -0
- package/lib/rules/enforce-snapshot-state-narrowing.js +281 -0
- package/lib/rules/enforce-types-directory-placement.d.ts +9 -0
- package/lib/rules/enforce-types-directory-placement.js +276 -0
- package/lib/rules/no-direct-function-state.d.ts +8 -0
- package/lib/rules/no-direct-function-state.js +285 -0
- package/lib/rules/no-fill-template-mutation.d.ts +1 -0
- package/lib/rules/no-fill-template-mutation.js +324 -0
- package/lib/rules/no-hungarian.js +18 -26
- package/lib/rules/no-portal-inside-tooltip.d.ts +10 -0
- package/lib/rules/no-portal-inside-tooltip.js +219 -0
- package/lib/rules/no-redundant-boolean-callback-props.d.ts +11 -0
- package/lib/rules/no-redundant-boolean-callback-props.js +355 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.d.ts +8 -0
- package/lib/rules/no-satisfies-in-frontend-bundle.js +126 -0
- package/lib/rules/no-single-dismiss-dialog-button.d.ts +7 -0
- package/lib/rules/no-single-dismiss-dialog-button.js +127 -0
- package/lib/rules/no-stablehash-react-nodes.d.ts +1 -0
- package/lib/rules/no-stablehash-react-nodes.js +325 -0
- package/lib/rules/parallelize-loop-awaits.d.ts +8 -0
- package/lib/rules/parallelize-loop-awaits.js +582 -0
- package/lib/rules/prefer-flat-transform-each-keys.d.ts +1 -0
- package/lib/rules/prefer-flat-transform-each-keys.js +228 -0
- package/lib/rules/prefer-getter-over-parameterless-method.d.ts +1 -0
- package/lib/rules/prefer-getter-over-parameterless-method.js +44 -0
- package/lib/rules/prefer-spread-over-reassembly.d.ts +5 -0
- package/lib/rules/prefer-spread-over-reassembly.js +401 -0
- package/lib/rules/prefer-sx-prop-over-system-props.d.ts +9 -0
- package/lib/rules/prefer-sx-prop-over-system-props.js +401 -0
- package/lib/rules/prefer-use-base62-id.d.ts +8 -0
- package/lib/rules/prefer-use-base62-id.js +483 -0
- package/lib/rules/prefer-use-theme.d.ts +1 -0
- package/lib/rules/prefer-use-theme.js +206 -0
- package/lib/rules/prefer-utility-function-own-file.d.ts +9 -0
- package/lib/rules/prefer-utility-function-own-file.js +505 -0
- package/lib/rules/react-memoize-literals.js +106 -1
- package/lib/rules/require-props-composition.d.ts +10 -0
- package/lib/rules/require-props-composition.js +433 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.d.ts +9 -0
- package/lib/rules/require-server-timestamp-for-firestore-dates.js +313 -0
- package/package.json +1 -1
- package/release-manifest.json +212 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noSatisfiesInFrontendBundle = void 0;
|
|
4
|
+
const minimatch_1 = require("minimatch");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_EXCLUDE_PATHS = [
|
|
7
|
+
'functions/src/firestore/**',
|
|
8
|
+
'functions/src/callable/**',
|
|
9
|
+
'functions/src/pubsub/**',
|
|
10
|
+
'functions/src/webhooks/**',
|
|
11
|
+
'functions/src/queues/**',
|
|
12
|
+
'functions/src/realtime/**',
|
|
13
|
+
];
|
|
14
|
+
const DEFAULT_INCLUDE_PATHS = ['src/**', 'functions/src/**'];
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes a filesystem path to forward slashes for consistent
|
|
17
|
+
* glob matching on all platforms.
|
|
18
|
+
*/
|
|
19
|
+
function normalizePath(filePath) {
|
|
20
|
+
return filePath.replace(/\\/g, '/');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns the portion of the normalized path starting from a recognized
|
|
24
|
+
* root segment (`src/` or `functions/`), so that absolute paths from
|
|
25
|
+
* the RuleTester (e.g. `/project/src/components/Foo.tsx`) are matched
|
|
26
|
+
* against the configured include/exclude globs as if they were relative
|
|
27
|
+
* to the project root.
|
|
28
|
+
*
|
|
29
|
+
* Check `functions/` before `src/` because `functions/src/` paths contain
|
|
30
|
+
* `/src/` as a substring; matching `/src/` first would incorrectly strip
|
|
31
|
+
* the `functions/` prefix and break exclude-glob matching.
|
|
32
|
+
*/
|
|
33
|
+
function toRelativeLike(normalizedFilename) {
|
|
34
|
+
const functionsIdx = normalizedFilename.indexOf('/functions/');
|
|
35
|
+
if (functionsIdx !== -1) {
|
|
36
|
+
return normalizedFilename.slice(functionsIdx + 1); // "functions/..."
|
|
37
|
+
}
|
|
38
|
+
const srcIdx = normalizedFilename.indexOf('/src/');
|
|
39
|
+
if (srcIdx !== -1) {
|
|
40
|
+
return normalizedFilename.slice(srcIdx + 1); // "src/..."
|
|
41
|
+
}
|
|
42
|
+
return normalizedFilename;
|
|
43
|
+
}
|
|
44
|
+
exports.noSatisfiesInFrontendBundle = (0, createRule_1.createRule)({
|
|
45
|
+
name: 'no-satisfies-in-frontend-bundle',
|
|
46
|
+
meta: {
|
|
47
|
+
type: 'problem',
|
|
48
|
+
docs: {
|
|
49
|
+
description: 'Disallow the `satisfies` operator in files that reach the frontend webpack bundle (Next.js 12 SWC cannot parse it)',
|
|
50
|
+
recommended: 'error',
|
|
51
|
+
},
|
|
52
|
+
fixable: undefined,
|
|
53
|
+
schema: [
|
|
54
|
+
{
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
excludePaths: {
|
|
58
|
+
type: 'array',
|
|
59
|
+
items: { type: 'string' },
|
|
60
|
+
default: DEFAULT_EXCLUDE_PATHS,
|
|
61
|
+
},
|
|
62
|
+
includePaths: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string' },
|
|
65
|
+
default: DEFAULT_INCLUDE_PATHS,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
messages: {
|
|
72
|
+
noSatisfiesOperator: 'SWC in Next.js 12 cannot parse `satisfies`; this file is bundled by webpack and will fail the production build. Use an explicit type annotation, or a constrained identity helper when literal keys must be preserved.',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
defaultOptions: [
|
|
76
|
+
{
|
|
77
|
+
excludePaths: DEFAULT_EXCLUDE_PATHS,
|
|
78
|
+
includePaths: DEFAULT_INCLUDE_PATHS,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
create(context, [options]) {
|
|
82
|
+
const excludePaths = options.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
|
|
83
|
+
const includePaths = options.includePaths ?? DEFAULT_INCLUDE_PATHS;
|
|
84
|
+
const filename = context.getFilename();
|
|
85
|
+
// Skip synthetic filenames used by RuleTester when no filename is provided.
|
|
86
|
+
if (filename === '<input>' || filename === '<text>') {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const normalizedFilename = normalizePath(filename);
|
|
90
|
+
// Exempt declaration files — they are never bundled.
|
|
91
|
+
if (normalizedFilename.endsWith('.d.ts')) {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
// Exempt test and spec files — they are never bundled.
|
|
95
|
+
if (/\.(test|spec)\.[jt]sx?$/.test(normalizedFilename)) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
// Derive a path relative to the project root for glob matching.
|
|
99
|
+
const relPath = toRelativeLike(normalizedFilename);
|
|
100
|
+
// Only enforce within the configured include paths.
|
|
101
|
+
const includeMatchers = includePaths.map((pattern) => new minimatch_1.Minimatch(pattern, { dot: true }));
|
|
102
|
+
const isIncluded = includeMatchers.some((mm) => mm.match(normalizedFilename) || mm.match(relPath));
|
|
103
|
+
if (!isIncluded) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
// Skip files that match any exclude path.
|
|
107
|
+
const excludeMatchers = excludePaths.map((pattern) => new minimatch_1.Minimatch(pattern, { dot: true }));
|
|
108
|
+
const isExcluded = excludeMatchers.some((mm) => mm.match(normalizedFilename) || mm.match(relPath));
|
|
109
|
+
if (isExcluded) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
// Every TSSatisfiesExpression is reported regardless of wrapping context.
|
|
114
|
+
// "x as const satisfies T" is a TSSatisfiesExpression (wrapping a
|
|
115
|
+
// TSAsExpression), so it is caught. Bare "as const" (TSAsExpression
|
|
116
|
+
// only) is NOT caught — correct.
|
|
117
|
+
TSSatisfiesExpression(node) {
|
|
118
|
+
context.report({
|
|
119
|
+
node,
|
|
120
|
+
messageId: 'noSatisfiesOperator',
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=no-satisfies-in-frontend-bundle.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
dismissLabels?: string[];
|
|
4
|
+
}
|
|
5
|
+
];
|
|
6
|
+
export declare const noSingleDismissDialogButton: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noSingleDismissDialogButton", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noSingleDismissDialogButton = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
const DEFAULT_DISMISS_LABELS = [
|
|
7
|
+
'Cancel',
|
|
8
|
+
'Close',
|
|
9
|
+
'Dismiss',
|
|
10
|
+
'Not now',
|
|
11
|
+
'Never mind',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Extracts the string value from a node that is either a string Literal or a
|
|
15
|
+
* TemplateLiteral with no expressions (i.e. a plain template string). Returns
|
|
16
|
+
* null for anything more dynamic.
|
|
17
|
+
*/
|
|
18
|
+
function getStaticStringValue(node) {
|
|
19
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
|
|
20
|
+
return node.value;
|
|
21
|
+
}
|
|
22
|
+
if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
23
|
+
node.expressions.length === 0 &&
|
|
24
|
+
node.quasis.length === 1) {
|
|
25
|
+
return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when the value (after trimming) matches one of the dismiss
|
|
31
|
+
* labels, case-insensitively.
|
|
32
|
+
*/
|
|
33
|
+
function isDismissLabel(value, dismissLabels) {
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
return dismissLabels.some((label) => label.toLowerCase() === normalized);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Searches the properties of an ObjectExpression for a property named
|
|
39
|
+
* `children` whose value is a static string matching a dismiss label.
|
|
40
|
+
* Returns the button ObjectExpression node if found, null otherwise.
|
|
41
|
+
*/
|
|
42
|
+
function findDismissChildrenProp(element, dismissLabels) {
|
|
43
|
+
for (const prop of element.properties) {
|
|
44
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
45
|
+
continue;
|
|
46
|
+
if (prop.computed)
|
|
47
|
+
continue;
|
|
48
|
+
const key = prop.key;
|
|
49
|
+
const isChildrenKey = (key.type === utils_1.AST_NODE_TYPES.Identifier && key.name === 'children') ||
|
|
50
|
+
(key.type === utils_1.AST_NODE_TYPES.Literal && key.value === 'children');
|
|
51
|
+
if (!isChildrenKey)
|
|
52
|
+
continue;
|
|
53
|
+
const staticVal = getStaticStringValue(prop.value);
|
|
54
|
+
if (staticVal !== null && isDismissLabel(staticVal, dismissLabels)) {
|
|
55
|
+
return prop;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
exports.noSingleDismissDialogButton = (0, createRule_1.createRule)({
|
|
61
|
+
name: 'no-single-dismiss-dialog-button',
|
|
62
|
+
meta: {
|
|
63
|
+
type: 'suggestion',
|
|
64
|
+
docs: {
|
|
65
|
+
description: 'Disallow a single dialog button whose label is a dismiss action (Cancel, Close, Dismiss, Not now, Never mind). Use navigation.onClose for dismissal; the buttons array should only contain affirmative actions.',
|
|
66
|
+
recommended: 'error',
|
|
67
|
+
},
|
|
68
|
+
fixable: undefined,
|
|
69
|
+
schema: [
|
|
70
|
+
{
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
dismissLabels: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
items: { type: 'string' },
|
|
76
|
+
default: DEFAULT_DISMISS_LABELS,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
messages: {
|
|
83
|
+
noSingleDismissDialogButton: 'A single dialog button with label "{{label}}" is a dismiss action. Use navigation.onClose to handle dismissal via the X close button instead; the buttons array should only contain affirmative actions.',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
defaultOptions: [{ dismissLabels: DEFAULT_DISMISS_LABELS }],
|
|
87
|
+
create(context) {
|
|
88
|
+
const options = context.options[0] ?? {};
|
|
89
|
+
const dismissLabels = options.dismissLabels ?? DEFAULT_DISMISS_LABELS;
|
|
90
|
+
return {
|
|
91
|
+
Property(node) {
|
|
92
|
+
// Only care about properties named `buttons`
|
|
93
|
+
if (node.computed)
|
|
94
|
+
return;
|
|
95
|
+
const key = node.key;
|
|
96
|
+
const isButtonsKey = (key.type === utils_1.AST_NODE_TYPES.Identifier && key.name === 'buttons') ||
|
|
97
|
+
(key.type === utils_1.AST_NODE_TYPES.Literal && key.value === 'buttons');
|
|
98
|
+
if (!isButtonsKey)
|
|
99
|
+
return;
|
|
100
|
+
// The value must be an array literal
|
|
101
|
+
if (node.value.type !== utils_1.AST_NODE_TYPES.ArrayExpression)
|
|
102
|
+
return;
|
|
103
|
+
const arrayExpr = node.value;
|
|
104
|
+
// Only flag exactly one element
|
|
105
|
+
if (arrayExpr.elements.length !== 1)
|
|
106
|
+
return;
|
|
107
|
+
const element = arrayExpr.elements[0];
|
|
108
|
+
if (!element || element.type !== utils_1.AST_NODE_TYPES.ObjectExpression)
|
|
109
|
+
return;
|
|
110
|
+
const dismissProp = findDismissChildrenProp(element, dismissLabels);
|
|
111
|
+
if (!dismissProp)
|
|
112
|
+
return;
|
|
113
|
+
// Retrieve the label text for the error message
|
|
114
|
+
const labelNode = dismissProp.value;
|
|
115
|
+
const labelText = getStaticStringValue(labelNode) ?? '';
|
|
116
|
+
context.report({
|
|
117
|
+
node: element,
|
|
118
|
+
messageId: 'noSingleDismissDialogButton',
|
|
119
|
+
data: {
|
|
120
|
+
label: labelText.trim(),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=no-single-dismiss-dialog-button.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noStablehashReactNodes: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"noStableHashReactNode", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noStablehashReactNodes = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* Module path suffix that identifies the stableHash family of utilities.
|
|
8
|
+
* Matches both absolute and relative import paths that end in util/hash/stableHash.
|
|
9
|
+
*/
|
|
10
|
+
const HASH_MODULE_SUFFIX = 'util/hash/stableHash';
|
|
11
|
+
/**
|
|
12
|
+
* Named exports from the hash module that we care about. sortedHash is included
|
|
13
|
+
* because it wraps stableHash and has the same stringification danger.
|
|
14
|
+
*/
|
|
15
|
+
const HASH_EXPORT_NAMES = new Set(['stableHash', 'sortedHash']);
|
|
16
|
+
/**
|
|
17
|
+
* Type names that unambiguously denote React render trees or BluMint node
|
|
18
|
+
* wrappers. When a variable/parameter carries one of these annotations we know
|
|
19
|
+
* it is a ReactNode and must not be hashed.
|
|
20
|
+
*/
|
|
21
|
+
const REACT_NODE_TYPE_NAMES = new Set([
|
|
22
|
+
'ReactNode',
|
|
23
|
+
'ReactElement',
|
|
24
|
+
'JSX',
|
|
25
|
+
'KeyedNode',
|
|
26
|
+
'OrNode',
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Object-literal property names whose presence signals the object contains a
|
|
30
|
+
* React render tree (either a children prop or a BluMint "Node" field).
|
|
31
|
+
*/
|
|
32
|
+
const NODE_PROP_NAMES = new Set(['children', 'Node']);
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Return true when the import source path ends with the known hash module
|
|
38
|
+
* suffix, handling both absolute paths (functions/src/util/hash/stableHash)
|
|
39
|
+
* and relative variants (../../../util/hash/stableHash).
|
|
40
|
+
*/
|
|
41
|
+
function isHashModulePath(source) {
|
|
42
|
+
return source.endsWith(HASH_MODULE_SUFFIX);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the TSTypeAnnotation / TSTypeReference down to a base type name so
|
|
46
|
+
* we can match against REACT_NODE_TYPE_NAMES without depending on the type
|
|
47
|
+
* checker.
|
|
48
|
+
*
|
|
49
|
+
* Handles:
|
|
50
|
+
* ReactNode → 'ReactNode'
|
|
51
|
+
* React.ReactNode → 'ReactNode' (qualified reference)
|
|
52
|
+
* JSX.Element → 'JSX'
|
|
53
|
+
* OrNode<T> → 'OrNode'
|
|
54
|
+
* KeyedNode[] / readonly KeyedNode[] → 'KeyedNode' (array element)
|
|
55
|
+
*/
|
|
56
|
+
function extractBaseTypeName(typeNode) {
|
|
57
|
+
switch (typeNode.type) {
|
|
58
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
59
|
+
const name = typeNode.typeName;
|
|
60
|
+
if (name.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
61
|
+
return name.name;
|
|
62
|
+
}
|
|
63
|
+
// Qualified name: React.ReactNode → take the right-hand part
|
|
64
|
+
if (name.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
|
|
65
|
+
name.right.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
66
|
+
// For JSX.Element we want "JSX" (the left) so we can match the set
|
|
67
|
+
if (name.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
68
|
+
name.left.name === 'JSX') {
|
|
69
|
+
return 'JSX';
|
|
70
|
+
}
|
|
71
|
+
return name.right.name;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
case utils_1.AST_NODE_TYPES.TSArrayType:
|
|
76
|
+
return extractBaseTypeName(typeNode.elementType);
|
|
77
|
+
case utils_1.AST_NODE_TYPES.TSTypeOperator:
|
|
78
|
+
// readonly T[]
|
|
79
|
+
if (typeNode.typeAnnotation) {
|
|
80
|
+
return extractBaseTypeName(typeNode.typeAnnotation);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
default:
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Return true when a TS type annotation (already unwrapped to the inner type
|
|
89
|
+
* node) indicates a ReactNode / ReactElement / KeyedNode / OrNode / JSX.Element.
|
|
90
|
+
*/
|
|
91
|
+
function isReactNodeTypeAnnotation(typeNode) {
|
|
92
|
+
const base = extractBaseTypeName(typeNode);
|
|
93
|
+
if (base === null)
|
|
94
|
+
return false;
|
|
95
|
+
return REACT_NODE_TYPE_NAMES.has(base);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Walk the program body's import declarations to build a mapping from local
|
|
99
|
+
* binding name → { isNamespace: boolean }.
|
|
100
|
+
*
|
|
101
|
+
* isNamespace = true → `import * as Hash from '...'` — we flag Hash.stableHash(...)
|
|
102
|
+
* isNamespace = false → `import { stableHash } from '...'` (possibly aliased)
|
|
103
|
+
*/
|
|
104
|
+
function collectHashBindings(program) {
|
|
105
|
+
const bindings = new Map();
|
|
106
|
+
for (const node of program.body) {
|
|
107
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ImportDeclaration)
|
|
108
|
+
continue;
|
|
109
|
+
if (!isHashModulePath(String(node.source.value)))
|
|
110
|
+
continue;
|
|
111
|
+
for (const spec of node.specifiers) {
|
|
112
|
+
if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
|
|
113
|
+
// import * as Hash from '...'
|
|
114
|
+
bindings.set(spec.local.name, { isNamespace: true });
|
|
115
|
+
}
|
|
116
|
+
else if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
|
|
117
|
+
// import { stableHash } or import { stableHash as hash }
|
|
118
|
+
const importedName = spec.imported.type === utils_1.AST_NODE_TYPES.Identifier
|
|
119
|
+
? spec.imported.name
|
|
120
|
+
: spec.imported.value;
|
|
121
|
+
if (HASH_EXPORT_NAMES.has(importedName)) {
|
|
122
|
+
bindings.set(spec.local.name, { isNamespace: false });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return bindings;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Return true when the argument to a stableHash call is a JSX element or
|
|
131
|
+
* JSX fragment.
|
|
132
|
+
*/
|
|
133
|
+
function isJsxNode(node) {
|
|
134
|
+
return (node.type === utils_1.AST_NODE_TYPES.JSXElement ||
|
|
135
|
+
node.type === utils_1.AST_NODE_TYPES.JSXFragment);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Return true when the argument is an object literal that contains a property
|
|
139
|
+
* named `children` or `Node` (shorthand or keyed), signalling it wraps a
|
|
140
|
+
* render-tree value.
|
|
141
|
+
*/
|
|
142
|
+
function objectLiteralContainsNodeProp(node) {
|
|
143
|
+
for (const prop of node.properties) {
|
|
144
|
+
if (prop.type !== utils_1.AST_NODE_TYPES.Property)
|
|
145
|
+
continue;
|
|
146
|
+
const key = prop.key;
|
|
147
|
+
let propName = null;
|
|
148
|
+
if (key.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
149
|
+
propName = key.name;
|
|
150
|
+
}
|
|
151
|
+
else if (key.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
152
|
+
typeof key.value === 'string') {
|
|
153
|
+
propName = key.value;
|
|
154
|
+
}
|
|
155
|
+
if (propName !== null && NODE_PROP_NAMES.has(propName)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Attempt to resolve the type annotation of an identifier from the local
|
|
163
|
+
* scope without the type checker.
|
|
164
|
+
*
|
|
165
|
+
* Strategy (in order):
|
|
166
|
+
* 1. The identifier is a parameter of the immediately enclosing function with
|
|
167
|
+
* a TSTypeAnnotation — read it directly.
|
|
168
|
+
* 2. The identifier is declared as a variable with a type annotation
|
|
169
|
+
* (`const x: ReactNode = ...`).
|
|
170
|
+
*
|
|
171
|
+
* Returns the TSTypeAnnotation typeAnnotation node or null if not found.
|
|
172
|
+
*/
|
|
173
|
+
function resolveAnnotationForIdentifier(identNode) {
|
|
174
|
+
// Walk parents looking for a parameter list or variable declarator that
|
|
175
|
+
// matches the identifier's name.
|
|
176
|
+
let current = identNode.parent;
|
|
177
|
+
while (current) {
|
|
178
|
+
// Function or arrow-function parameter
|
|
179
|
+
if (current.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
180
|
+
current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
181
|
+
current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
182
|
+
const fn = current;
|
|
183
|
+
for (const param of fn.params) {
|
|
184
|
+
if (param.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
185
|
+
param.name === identNode.name &&
|
|
186
|
+
param.typeAnnotation?.typeAnnotation) {
|
|
187
|
+
return param.typeAnnotation.typeAnnotation;
|
|
188
|
+
}
|
|
189
|
+
// Handle default param: (node: KeyedNode = ...)
|
|
190
|
+
if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
191
|
+
param.left.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
192
|
+
param.left.name === identNode.name &&
|
|
193
|
+
param.left.typeAnnotation?.typeAnnotation) {
|
|
194
|
+
const annotation = param.left.typeAnnotation;
|
|
195
|
+
if (annotation) {
|
|
196
|
+
return annotation.typeAnnotation;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Variable declaration in the same scope
|
|
202
|
+
if (current.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
203
|
+
for (const decl of current.declarations) {
|
|
204
|
+
if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
205
|
+
decl.id.name === identNode.name &&
|
|
206
|
+
decl.id.typeAnnotation?.typeAnnotation) {
|
|
207
|
+
return decl.id.typeAnnotation.typeAnnotation;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
current = current.parent;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Return true when the argument expression is, or contains, a ReactNode based
|
|
217
|
+
* on purely syntactic (AST-level) evidence:
|
|
218
|
+
*
|
|
219
|
+
* 1. Literal JSX element/fragment.
|
|
220
|
+
* 2. Object literal with a `children` or `Node` property.
|
|
221
|
+
* 3. Identifier whose nearest resolvable TS type annotation names a ReactNode.
|
|
222
|
+
*/
|
|
223
|
+
function argIsReactNode(arg) {
|
|
224
|
+
// Strip as/type-assertion wrappers
|
|
225
|
+
let expr = arg;
|
|
226
|
+
while (expr.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
227
|
+
expr.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
|
|
228
|
+
expr.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
|
|
229
|
+
expr = expr.expression;
|
|
230
|
+
}
|
|
231
|
+
// Case 1: direct JSX
|
|
232
|
+
if (isJsxNode(expr)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
// Case 2: object literal with node-shaped prop
|
|
236
|
+
if (expr.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
|
|
237
|
+
objectLiteralContainsNodeProp(expr)) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
// Case 3: identifier with resolvable ReactNode annotation
|
|
241
|
+
if (expr.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
242
|
+
const typeNode = resolveAnnotationForIdentifier(expr);
|
|
243
|
+
if (typeNode && isReactNodeTypeAnnotation(typeNode)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Rule
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
exports.noStablehashReactNodes = (0, createRule_1.createRule)({
|
|
253
|
+
name: 'no-stablehash-react-nodes',
|
|
254
|
+
meta: {
|
|
255
|
+
type: 'problem',
|
|
256
|
+
docs: {
|
|
257
|
+
description: 'Prevent passing ReactNodes or render-tree values to stableHash(), which deep-stringifies its argument and can freeze the browser.',
|
|
258
|
+
recommended: 'error',
|
|
259
|
+
},
|
|
260
|
+
fixable: undefined,
|
|
261
|
+
schema: [],
|
|
262
|
+
messages: {
|
|
263
|
+
noStableHashReactNode: 'Do not stableHash ReactNodes / KeyedNodes — stableHash() stringifies the full render tree and can freeze the browser. Hash stable keys/ids instead (e.g. stableHash(nodes.map((n) => n.key))).',
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
defaultOptions: [],
|
|
267
|
+
create(context) {
|
|
268
|
+
// Collect hash-function bindings lazily on first CallExpression visit.
|
|
269
|
+
let bindings = null;
|
|
270
|
+
function getBindings() {
|
|
271
|
+
if (bindings === null) {
|
|
272
|
+
const sourceCode = context
|
|
273
|
+
.sourceCode ?? context.getSourceCode();
|
|
274
|
+
bindings = collectHashBindings(sourceCode.ast);
|
|
275
|
+
}
|
|
276
|
+
return bindings;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Determine whether a CallExpression's callee is a tracked stableHash /
|
|
280
|
+
* sortedHash binding and return the first argument if so.
|
|
281
|
+
*/
|
|
282
|
+
function getTrackedCallArg(node) {
|
|
283
|
+
if (node.arguments.length === 0)
|
|
284
|
+
return null;
|
|
285
|
+
const map = getBindings();
|
|
286
|
+
if (map.size === 0)
|
|
287
|
+
return null;
|
|
288
|
+
const callee = node.callee;
|
|
289
|
+
// Direct call: stableHash(arg) or hash(arg) (aliased)
|
|
290
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
291
|
+
const info = map.get(callee.name);
|
|
292
|
+
if (info && !info.isNamespace) {
|
|
293
|
+
return node.arguments[0];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Member expression: Hash.stableHash(arg)
|
|
297
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
298
|
+
!callee.computed &&
|
|
299
|
+
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
300
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
301
|
+
const objectName = callee.object.name;
|
|
302
|
+
const methodName = callee.property.name;
|
|
303
|
+
const info = map.get(objectName);
|
|
304
|
+
if (info && info.isNamespace && HASH_EXPORT_NAMES.has(methodName)) {
|
|
305
|
+
return node.arguments[0];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
CallExpression(node) {
|
|
312
|
+
const arg = getTrackedCallArg(node);
|
|
313
|
+
if (arg === null)
|
|
314
|
+
return;
|
|
315
|
+
if (argIsReactNode(arg)) {
|
|
316
|
+
context.report({
|
|
317
|
+
node,
|
|
318
|
+
messageId: 'noStableHashReactNode',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
//# sourceMappingURL=no-stablehash-react-nodes.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Options = [
|
|
2
|
+
{
|
|
3
|
+
coordinatorPatterns?: string[];
|
|
4
|
+
rateLimitedPatterns?: string[];
|
|
5
|
+
}
|
|
6
|
+
];
|
|
7
|
+
export declare const parallelizeLoopAwaits: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"parallelizeLoopAwaits", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
8
|
+
export {};
|