@mui/internal-code-infra 0.0.4-canary.30 → 0.0.4-canary.32
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/build/eslint/mui/rules/no-guarded-throw.d.mts +31 -0
- package/build/eslint/mui/rules/nodeEnvUtils.d.mts +18 -0
- package/package.json +4 -4
- package/src/brokenLinksChecker/index.test.ts +8 -2
- package/src/eslint/mui/config.mjs +1 -0
- package/src/eslint/mui/index.mjs +2 -0
- package/src/eslint/mui/rules/no-guarded-throw.mjs +115 -0
- package/src/eslint/mui/rules/no-guarded-throw.test.mjs +206 -0
- package/src/eslint/mui/rules/nodeEnvUtils.mjs +52 -0
- package/src/eslint/mui/rules/require-dev-wrapper.mjs +25 -40
- package/src/estree-typescript.d.ts +1 -1
- package/src/untyped-plugins.d.ts +11 -11
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule that disallows throw statements guarded by process.env.NODE_ENV checks.
|
|
3
|
+
*
|
|
4
|
+
* NODE_ENV guards cause throw statements to only execute in certain environments,
|
|
5
|
+
* leading to inconsistent control flow between development and production. Whether
|
|
6
|
+
* the guard excludes production (tree-shaking the throw away) or targets production
|
|
7
|
+
* specifically, the result is environment-dependent behavior that should be avoided.
|
|
8
|
+
*
|
|
9
|
+
* The rule stops at function boundaries, so throws inside functions defined within
|
|
10
|
+
* a NODE_ENV guard are not flagged, as the function may be called from other contexts.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Invalid - throw only in development, removed in production
|
|
14
|
+
* if (process.env.NODE_ENV !== 'production') {
|
|
15
|
+
* throw new Error('Missing required prop');
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Invalid - throw only in production
|
|
20
|
+
* if (process.env.NODE_ENV === 'production') {
|
|
21
|
+
* throw new Error('Production-only error');
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Valid - unconditional throw
|
|
26
|
+
* throw new Error('Something went wrong');
|
|
27
|
+
*
|
|
28
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
29
|
+
*/
|
|
30
|
+
declare const rule: import('eslint').Rule.RuleModule;
|
|
31
|
+
export default rule;
|
|
@@ -21,3 +21,21 @@ export declare function isLiteralEq(node: import('estree').Node, value: string):
|
|
|
21
21
|
* @returns {boolean}
|
|
22
22
|
*/
|
|
23
23
|
export declare function isLiteralNeq(node: import('estree').Node, value: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a BinaryExpression compares process.env.NODE_ENV with === or !==
|
|
26
|
+
* @param {import('estree').Node} node
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export declare function isNodeEnvBinaryComparison(node: import('estree').Node): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Walks up the parent chain and checks if the node is inside an IfStatement
|
|
32
|
+
* whose test is a NODE_ENV binary comparison.
|
|
33
|
+
* If a callback is provided, it is called with the IfStatement and the direct
|
|
34
|
+
* child that leads to the node. The function returns true only when the callback
|
|
35
|
+
* returns true. Without a callback the function returns true when the node is
|
|
36
|
+
* inside any branch (consequent or alternate) of such an IfStatement.
|
|
37
|
+
* @param {import('eslint').Rule.Node} node
|
|
38
|
+
* @param {(ifStatement: import('estree').IfStatement & import('eslint').Rule.NodeParentExtension, child: import('eslint').Rule.Node) => boolean} [callback]
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
export declare function isInsideNodeEnvCheck(node: import('eslint').Rule.Node, callback?: (ifStatement: import('estree').IfStatement & import('eslint').Rule.NodeParentExtension, child: import('eslint').Rule.Node) => boolean): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-code-infra",
|
|
3
|
-
"version": "0.0.4-canary.
|
|
3
|
+
"version": "0.0.4-canary.32",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "Infra scripts and configs to be used across MUI repos.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -119,9 +119,9 @@
|
|
|
119
119
|
"typescript-eslint": "^8.57.1",
|
|
120
120
|
"unified": "^11.0.5",
|
|
121
121
|
"yargs": "^18.0.0",
|
|
122
|
-
"@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.35",
|
|
123
122
|
"@mui/internal-babel-plugin-display-name": "1.0.4-canary.18",
|
|
124
|
-
"@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.
|
|
123
|
+
"@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.27",
|
|
124
|
+
"@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.36"
|
|
125
125
|
},
|
|
126
126
|
"peerDependencies": {
|
|
127
127
|
"@next/eslint-plugin-next": "*",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"publishConfig": {
|
|
169
169
|
"access": "public"
|
|
170
170
|
},
|
|
171
|
-
"gitSha": "
|
|
171
|
+
"gitSha": "c53e4890f90deaf9e50f0a2989a004cf0d7d7975",
|
|
172
172
|
"scripts": {
|
|
173
173
|
"build": "tsgo -p tsconfig.build.json",
|
|
174
174
|
"typescript": "tsgo -noEmit",
|
|
@@ -2,8 +2,14 @@ import path from 'node:path';
|
|
|
2
2
|
import getPort from 'get-port';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
crawl,
|
|
7
|
+
type BrokenLinkIssue,
|
|
8
|
+
type HtmlValidateIssue,
|
|
9
|
+
type Issue,
|
|
10
|
+
type Link,
|
|
11
|
+
// eslint-disable-next-line import/extensions
|
|
12
|
+
} from './index.mjs';
|
|
7
13
|
|
|
8
14
|
type ExpectedBrokenLinkIssue = Omit<Partial<BrokenLinkIssue>, 'link'> & { link?: Partial<Link> };
|
|
9
15
|
|
|
@@ -415,6 +415,7 @@ export function createCoreConfig(options = {}) {
|
|
|
415
415
|
'mui/material-ui-no-styled-box': 'error',
|
|
416
416
|
}
|
|
417
417
|
: {}),
|
|
418
|
+
'mui/no-guarded-throw': 'error',
|
|
418
419
|
'mui/straight-quotes': 'off',
|
|
419
420
|
'mui/consistent-production-guard': 'error',
|
|
420
421
|
'mui/add-undef-to-optional': 'off',
|
package/src/eslint/mui/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import disallowReactApiInServerComponents from './rules/disallow-react-api-in-se
|
|
|
4
4
|
import docgenIgnoreBeforeComment from './rules/docgen-ignore-before-comment.mjs';
|
|
5
5
|
import muiNameMatchesComponentName from './rules/mui-name-matches-component-name.mjs';
|
|
6
6
|
import noEmptyBox from './rules/no-empty-box.mjs';
|
|
7
|
+
import noGuardedThrow from './rules/no-guarded-throw.mjs';
|
|
7
8
|
import noRestrictedResolvedImports from './rules/no-restricted-resolved-imports.mjs';
|
|
8
9
|
import noStyledBox from './rules/no-styled-box.mjs';
|
|
9
10
|
import requireDevWrapper from './rules/require-dev-wrapper.mjs';
|
|
@@ -22,6 +23,7 @@ const muiPlugin = {
|
|
|
22
23
|
'consistent-production-guard': consistentProductionGuard,
|
|
23
24
|
'disallow-active-element-as-key-event-target': disallowActiveElementAsKeyEventTarget,
|
|
24
25
|
'docgen-ignore-before-comment': docgenIgnoreBeforeComment,
|
|
26
|
+
'no-guarded-throw': noGuardedThrow,
|
|
25
27
|
'material-ui-name-matches-component-name': muiNameMatchesComponentName,
|
|
26
28
|
'material-ui-rules-of-use-theme-variants': rulesOfUseThemeVariants,
|
|
27
29
|
'material-ui-no-empty-box': noEmptyBox,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isProcessEnvNodeEnv } from './nodeEnvUtils.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursively checks if process.env.NODE_ENV appears anywhere in the node tree
|
|
5
|
+
* @param {import('estree').Node | null | undefined} node
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
function containsProcessEnvNodeEnv(node) {
|
|
9
|
+
if (!node || typeof node !== 'object') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (isProcessEnvNodeEnv(node)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Traverse all child nodes, skipping parent references to avoid circular traversal
|
|
18
|
+
for (const key of Object.keys(node)) {
|
|
19
|
+
if (key === 'parent') {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const child = /** @type {unknown} */ (/** @type {any} */ (node)[key]);
|
|
23
|
+
if (Array.isArray(child)) {
|
|
24
|
+
if (child.some(containsProcessEnvNodeEnv)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
} else if (
|
|
28
|
+
child &&
|
|
29
|
+
typeof child === 'object' &&
|
|
30
|
+
/** @type {import('estree').Node} */ (child).type
|
|
31
|
+
) {
|
|
32
|
+
if (containsProcessEnvNodeEnv(/** @type {import('estree').Node} */ (child))) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ESLint rule that disallows throw statements guarded by process.env.NODE_ENV checks.
|
|
42
|
+
*
|
|
43
|
+
* NODE_ENV guards cause throw statements to only execute in certain environments,
|
|
44
|
+
* leading to inconsistent control flow between development and production. Whether
|
|
45
|
+
* the guard excludes production (tree-shaking the throw away) or targets production
|
|
46
|
+
* specifically, the result is environment-dependent behavior that should be avoided.
|
|
47
|
+
*
|
|
48
|
+
* The rule stops at function boundaries, so throws inside functions defined within
|
|
49
|
+
* a NODE_ENV guard are not flagged, as the function may be called from other contexts.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // Invalid - throw only in development, removed in production
|
|
53
|
+
* if (process.env.NODE_ENV !== 'production') {
|
|
54
|
+
* throw new Error('Missing required prop');
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Invalid - throw only in production
|
|
59
|
+
* if (process.env.NODE_ENV === 'production') {
|
|
60
|
+
* throw new Error('Production-only error');
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // Valid - unconditional throw
|
|
65
|
+
* throw new Error('Something went wrong');
|
|
66
|
+
*
|
|
67
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
68
|
+
*/
|
|
69
|
+
const rule = {
|
|
70
|
+
meta: {
|
|
71
|
+
type: 'problem',
|
|
72
|
+
docs: {
|
|
73
|
+
description:
|
|
74
|
+
'Disallow throw statements guarded by process.env.NODE_ENV checks, as they cause environment-dependent control flow',
|
|
75
|
+
},
|
|
76
|
+
messages: {
|
|
77
|
+
guardedThrow:
|
|
78
|
+
'Do not guard `throw` statements with `process.env.NODE_ENV` checks. Guarded throws execute only in certain environments, causing inconsistent control flow between development and production.',
|
|
79
|
+
},
|
|
80
|
+
schema: [],
|
|
81
|
+
},
|
|
82
|
+
create(context) {
|
|
83
|
+
return {
|
|
84
|
+
ThrowStatement(node) {
|
|
85
|
+
/** @type {import('eslint').Rule.Node | null} */
|
|
86
|
+
let current = node.parent;
|
|
87
|
+
/** @type {import('eslint').Rule.Node} */
|
|
88
|
+
let currentChild = node;
|
|
89
|
+
|
|
90
|
+
while (current) {
|
|
91
|
+
if (
|
|
92
|
+
current.type === 'FunctionDeclaration' ||
|
|
93
|
+
current.type === 'FunctionExpression' ||
|
|
94
|
+
current.type === 'ArrowFunctionExpression'
|
|
95
|
+
) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (current.type === 'IfStatement') {
|
|
99
|
+
const isInConsequent = current.consequent === currentChild;
|
|
100
|
+
const isInAlternate = current.alternate === currentChild;
|
|
101
|
+
|
|
102
|
+
if ((isInConsequent || isInAlternate) && containsProcessEnvNodeEnv(current.test)) {
|
|
103
|
+
context.report({ node, messageId: 'guardedThrow' });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
currentChild = current;
|
|
108
|
+
current = current.parent;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default rule;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import eslint from 'eslint';
|
|
2
|
+
import parser from '@typescript-eslint/parser';
|
|
3
|
+
import rule from './no-guarded-throw.mjs';
|
|
4
|
+
|
|
5
|
+
const ruleTester = new eslint.RuleTester({
|
|
6
|
+
languageOptions: {
|
|
7
|
+
parser,
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
ruleTester.run('no-guarded-throw', rule, {
|
|
12
|
+
valid: [
|
|
13
|
+
// Should pass: Unconditional throw
|
|
14
|
+
{
|
|
15
|
+
code: `
|
|
16
|
+
throw new Error('Something went wrong');
|
|
17
|
+
`,
|
|
18
|
+
},
|
|
19
|
+
// Should pass: Throw inside a non-NODE_ENV conditional
|
|
20
|
+
{
|
|
21
|
+
code: `
|
|
22
|
+
if (value == null) {
|
|
23
|
+
throw new TypeError('value is required');
|
|
24
|
+
}
|
|
25
|
+
`,
|
|
26
|
+
},
|
|
27
|
+
// Should pass: Throw inside a catch block (no NODE_ENV guard)
|
|
28
|
+
{
|
|
29
|
+
code: `
|
|
30
|
+
try {
|
|
31
|
+
doSomething();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error('Failed');
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
},
|
|
37
|
+
// Should pass: Throw inside arrow function inside NODE_ENV guard
|
|
38
|
+
{
|
|
39
|
+
code: `
|
|
40
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
41
|
+
const fn = () => {
|
|
42
|
+
throw new Error('inside arrow function');
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
// Should pass: Throw inside function expression inside NODE_ENV guard
|
|
48
|
+
{
|
|
49
|
+
code: `
|
|
50
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
51
|
+
const fn = function () {
|
|
52
|
+
throw new Error('inside function expression');
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
`,
|
|
56
|
+
},
|
|
57
|
+
// Should pass: Throw inside function declaration inside NODE_ENV guard
|
|
58
|
+
{
|
|
59
|
+
code: `
|
|
60
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
61
|
+
function validate() {
|
|
62
|
+
throw new Error('inside function declaration');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
invalid: [
|
|
69
|
+
// Should fail: Throw inside !== 'production' guard
|
|
70
|
+
{
|
|
71
|
+
code: `
|
|
72
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
73
|
+
throw new Error('Dev-only error');
|
|
74
|
+
}
|
|
75
|
+
`,
|
|
76
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
77
|
+
},
|
|
78
|
+
// Should fail: Throw inside === 'production' guard
|
|
79
|
+
{
|
|
80
|
+
code: `
|
|
81
|
+
if (process.env.NODE_ENV === 'production') {
|
|
82
|
+
throw new Error('Prod-only error');
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
86
|
+
},
|
|
87
|
+
// Should fail: Throw in else block of === 'production' check
|
|
88
|
+
{
|
|
89
|
+
code: `
|
|
90
|
+
if (process.env.NODE_ENV === 'production') {
|
|
91
|
+
// production path
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error('Non-production error');
|
|
94
|
+
}
|
|
95
|
+
`,
|
|
96
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
97
|
+
},
|
|
98
|
+
// Should fail: Throw nested inside NODE_ENV guard
|
|
99
|
+
{
|
|
100
|
+
code: `
|
|
101
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
102
|
+
if (value == null) {
|
|
103
|
+
throw new TypeError('value is required');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`,
|
|
107
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
108
|
+
},
|
|
109
|
+
// Should fail: Reversed comparison (literal on left)
|
|
110
|
+
{
|
|
111
|
+
code: `
|
|
112
|
+
if ('production' !== process.env.NODE_ENV) {
|
|
113
|
+
throw new Error('Dev-only error');
|
|
114
|
+
}
|
|
115
|
+
`,
|
|
116
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
117
|
+
},
|
|
118
|
+
// Should fail: Throw in loop inside NODE_ENV guard
|
|
119
|
+
{
|
|
120
|
+
code: `
|
|
121
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
122
|
+
for (const item of items) {
|
|
123
|
+
throw new Error('Invalid item');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`,
|
|
127
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
128
|
+
},
|
|
129
|
+
// Should fail: NODE_ENV combined with other conditions using &&
|
|
130
|
+
{
|
|
131
|
+
code: `
|
|
132
|
+
if (process.env.NODE_ENV !== 'production' && value == null) {
|
|
133
|
+
throw new TypeError('value is required');
|
|
134
|
+
}
|
|
135
|
+
`,
|
|
136
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
137
|
+
},
|
|
138
|
+
// Should fail: NODE_ENV combined with other conditions using ||
|
|
139
|
+
{
|
|
140
|
+
code: `
|
|
141
|
+
if (condition || process.env.NODE_ENV === 'test') {
|
|
142
|
+
throw new Error('Test or condition error');
|
|
143
|
+
}
|
|
144
|
+
`,
|
|
145
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
146
|
+
},
|
|
147
|
+
// Should fail: Unary not on process.env.NODE_ENV
|
|
148
|
+
{
|
|
149
|
+
code: `
|
|
150
|
+
if (!process.env.NODE_ENV) {
|
|
151
|
+
throw new Error('NODE_ENV not set');
|
|
152
|
+
}
|
|
153
|
+
`,
|
|
154
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
155
|
+
},
|
|
156
|
+
// Should fail: NODE_ENV passed to a function
|
|
157
|
+
{
|
|
158
|
+
code: `
|
|
159
|
+
if (fn(process.env.NODE_ENV)) {
|
|
160
|
+
throw new Error('Function check failed');
|
|
161
|
+
}
|
|
162
|
+
`,
|
|
163
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
164
|
+
},
|
|
165
|
+
// Should fail: Throw inside try/catch inside NODE_ENV guard
|
|
166
|
+
{
|
|
167
|
+
code: `
|
|
168
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
169
|
+
try {
|
|
170
|
+
doSomething();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new Error('caught inside guard');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
`,
|
|
176
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
177
|
+
},
|
|
178
|
+
// Should fail: Throw deeply nested in control flow inside NODE_ENV guard
|
|
179
|
+
{
|
|
180
|
+
code: `
|
|
181
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
182
|
+
if (value == null) {
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
if (!item.valid) {
|
|
185
|
+
throw new Error('invalid item');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`,
|
|
191
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
192
|
+
},
|
|
193
|
+
// Should fail: Throw inside switch/case inside NODE_ENV guard
|
|
194
|
+
{
|
|
195
|
+
code: `
|
|
196
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
197
|
+
switch (type) {
|
|
198
|
+
case 'a':
|
|
199
|
+
throw new Error('invalid type a');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`,
|
|
203
|
+
errors: [{ messageId: 'guardedThrow' }],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
@@ -39,3 +39,55 @@ export function isLiteralEq(node, value) {
|
|
|
39
39
|
export function isLiteralNeq(node, value) {
|
|
40
40
|
return node.type === 'Literal' && node.value !== value;
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if a BinaryExpression compares process.env.NODE_ENV with === or !==
|
|
45
|
+
* @param {import('estree').Node} node
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function isNodeEnvBinaryComparison(node) {
|
|
49
|
+
return (
|
|
50
|
+
node.type === 'BinaryExpression' &&
|
|
51
|
+
(node.operator === '===' || node.operator === '!==') &&
|
|
52
|
+
(isProcessEnvNodeEnv(node.left) || isProcessEnvNodeEnv(node.right))
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Walks up the parent chain and checks if the node is inside an IfStatement
|
|
58
|
+
* whose test is a NODE_ENV binary comparison.
|
|
59
|
+
* If a callback is provided, it is called with the IfStatement and the direct
|
|
60
|
+
* child that leads to the node. The function returns true only when the callback
|
|
61
|
+
* returns true. Without a callback the function returns true when the node is
|
|
62
|
+
* inside any branch (consequent or alternate) of such an IfStatement.
|
|
63
|
+
* @param {import('eslint').Rule.Node} node
|
|
64
|
+
* @param {(ifStatement: import('estree').IfStatement & import('eslint').Rule.NodeParentExtension, child: import('eslint').Rule.Node) => boolean} [callback]
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
export function isInsideNodeEnvCheck(node, callback) {
|
|
68
|
+
/** @type {import('eslint').Rule.Node | null} */
|
|
69
|
+
let current = node.parent;
|
|
70
|
+
/** @type {import('eslint').Rule.Node} */
|
|
71
|
+
let currentChild = node;
|
|
72
|
+
|
|
73
|
+
while (current) {
|
|
74
|
+
if (current.type === 'IfStatement' && isNodeEnvBinaryComparison(current.test)) {
|
|
75
|
+
if (callback) {
|
|
76
|
+
if (callback(current, currentChild)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
const isInConsequent = current.consequent === currentChild;
|
|
81
|
+
const isInAlternate = current.alternate === currentChild;
|
|
82
|
+
if (isInConsequent || isInAlternate) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
currentChild = current;
|
|
89
|
+
current = current.parent;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isProcessEnvNodeEnv,
|
|
3
|
+
isLiteralEq,
|
|
4
|
+
isLiteralNeq,
|
|
5
|
+
isInsideNodeEnvCheck,
|
|
6
|
+
} from './nodeEnvUtils.mjs';
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* ESLint rule that enforces certain function calls to be wrapped with
|
|
@@ -58,18 +63,21 @@ const rule = {
|
|
|
58
63
|
const functionNames = options.functionNames || ['warnOnce', 'warn', 'checkSlot'];
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
|
-
* Checks if
|
|
62
|
-
* @param {import('estree').
|
|
66
|
+
* Checks if an expression is comparing process.env.NODE_ENV appropriately
|
|
67
|
+
* @param {import('estree').Expression} expression - The expression to check
|
|
63
68
|
* @param {string} operator - The expected comparison operator (===, !==, etc.)
|
|
64
69
|
* @param {string} value - The value to compare with
|
|
65
70
|
* @returns {boolean}
|
|
66
71
|
*/
|
|
67
|
-
function isNodeEnvComparison(
|
|
68
|
-
|
|
72
|
+
function isNodeEnvComparison(expression, operator, value) {
|
|
73
|
+
if (expression.type !== 'BinaryExpression') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const { left, right } = expression;
|
|
69
77
|
|
|
70
78
|
// Check for exact match with the specified value
|
|
71
79
|
if (
|
|
72
|
-
|
|
80
|
+
expression.operator === operator &&
|
|
73
81
|
((isProcessEnvNodeEnv(left) && isLiteralEq(right, value)) ||
|
|
74
82
|
(isProcessEnvNodeEnv(right) && isLiteralEq(left, value)))
|
|
75
83
|
) {
|
|
@@ -79,7 +87,7 @@ const rule = {
|
|
|
79
87
|
// For !== operator also allow === with any literal value that's NOT 'production'
|
|
80
88
|
if (
|
|
81
89
|
operator === '!==' &&
|
|
82
|
-
|
|
90
|
+
expression.operator === '===' &&
|
|
83
91
|
((isProcessEnvNodeEnv(left) && isLiteralNeq(right, value)) ||
|
|
84
92
|
(isProcessEnvNodeEnv(right) && isLiteralNeq(left, value)))
|
|
85
93
|
) {
|
|
@@ -95,41 +103,18 @@ const rule = {
|
|
|
95
103
|
* @returns {boolean}
|
|
96
104
|
*/
|
|
97
105
|
function isWrappedInProductionCheck(node) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let currentChild = node;
|
|
102
|
-
|
|
103
|
-
while (current) {
|
|
104
|
-
// Check if we're inside an if statement
|
|
105
|
-
if (current.type === 'IfStatement') {
|
|
106
|
-
// Determine which branch we're in
|
|
107
|
-
const isInConsequent = current.consequent === currentChild;
|
|
108
|
-
const isInAlternate = current.alternate === currentChild;
|
|
109
|
-
|
|
110
|
-
// Skip if not in a branch
|
|
111
|
-
if (isInConsequent || isInAlternate) {
|
|
112
|
-
const test = current.test;
|
|
106
|
+
return isInsideNodeEnvCheck(node, (ifStatement, child) => {
|
|
107
|
+
const isInConsequent = ifStatement.consequent === child;
|
|
108
|
+
const isInAlternate = ifStatement.alternate === child;
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
test.type === 'BinaryExpression' &&
|
|
121
|
-
isNodeEnvComparison(test, operator, 'production')
|
|
122
|
-
) {
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
110
|
+
if (isInConsequent || isInAlternate) {
|
|
111
|
+
// If we're in the consequent, we need !==
|
|
112
|
+
// If we're in the alternate (else), we need ===
|
|
113
|
+
const operator = isInConsequent ? '!==' : '===';
|
|
114
|
+
return isNodeEnvComparison(ifStatement.test, operator, 'production');
|
|
126
115
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
current = current.parent;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return false;
|
|
116
|
+
return false;
|
|
117
|
+
});
|
|
133
118
|
}
|
|
134
119
|
|
|
135
120
|
return {
|
package/src/untyped-plugins.d.ts
CHANGED
|
@@ -13,77 +13,77 @@ declare module 'eslint-config-airbnb' {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
declare module 'eslint-config-airbnb-base/rules/best-practices' {
|
|
16
|
-
import { Linter } from 'eslint';
|
|
16
|
+
import { type Linter } from 'eslint';
|
|
17
17
|
|
|
18
18
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
19
19
|
export default config;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
declare module 'eslint-config-airbnb-base/rules/errors' {
|
|
23
|
-
import { Linter } from 'eslint';
|
|
23
|
+
import { type Linter } from 'eslint';
|
|
24
24
|
|
|
25
25
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
26
26
|
export default config;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
declare module 'eslint-config-airbnb-base/rules/es6' {
|
|
30
|
-
import { Linter } from 'eslint';
|
|
30
|
+
import { type Linter } from 'eslint';
|
|
31
31
|
|
|
32
32
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
33
33
|
export default config;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
declare module 'eslint-config-airbnb-base/rules/imports' {
|
|
37
|
-
import { Linter } from 'eslint';
|
|
37
|
+
import { type Linter } from 'eslint';
|
|
38
38
|
|
|
39
39
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
40
40
|
export default config;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
declare module 'eslint-config-airbnb-base/rules/node' {
|
|
44
|
-
import { Linter } from 'eslint';
|
|
44
|
+
import { type Linter } from 'eslint';
|
|
45
45
|
|
|
46
46
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
47
47
|
export default config;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
declare module 'eslint-config-airbnb-base/rules/strict' {
|
|
51
|
-
import { Linter } from 'eslint';
|
|
51
|
+
import { type Linter } from 'eslint';
|
|
52
52
|
|
|
53
53
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
54
54
|
export default config;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
declare module 'eslint-config-airbnb-base/rules/style' {
|
|
58
|
-
import { Linter } from 'eslint';
|
|
58
|
+
import { type Linter } from 'eslint';
|
|
59
59
|
|
|
60
60
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
61
61
|
export default config;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
declare module 'eslint-config-airbnb-base/rules/variables' {
|
|
65
|
-
import { Linter } from 'eslint';
|
|
65
|
+
import { type Linter } from 'eslint';
|
|
66
66
|
|
|
67
67
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
68
68
|
export default config;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
declare module 'eslint-config-airbnb/rules/react' {
|
|
72
|
-
import { Linter } from 'eslint';
|
|
72
|
+
import { type Linter } from 'eslint';
|
|
73
73
|
|
|
74
74
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
75
75
|
export default config;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
declare module 'eslint-config-airbnb/rules/react-a11y' {
|
|
79
|
-
import { Linter } from 'eslint';
|
|
79
|
+
import { type Linter } from 'eslint';
|
|
80
80
|
|
|
81
81
|
declare const config: Omit<Linter.LegacyConfig, 'extends' | 'plugins'>;
|
|
82
82
|
export default config;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
declare module '@next/eslint-plugin-next' {
|
|
86
|
-
import { Linter } from 'eslint';
|
|
86
|
+
import { type Linter } from 'eslint';
|
|
87
87
|
|
|
88
88
|
interface NextEslintPluginConfig extends Linter.LegacyConfig {
|
|
89
89
|
flatConfig: {
|