@mui/internal-code-infra 0.0.4-canary.31 → 0.0.4-canary.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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.31",
3
+ "version": "0.0.4-canary.33",
4
4
  "author": "MUI Team",
5
5
  "description": "Infra scripts and configs to be used across MUI repos.",
6
6
  "license": "MIT",
@@ -120,8 +120,8 @@
120
120
  "unified": "^11.0.5",
121
121
  "yargs": "^18.0.0",
122
122
  "@mui/internal-babel-plugin-display-name": "1.0.4-canary.18",
123
- "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.27",
124
- "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.36"
123
+ "@mui/internal-babel-plugin-resolve-imports": "2.0.7-canary.36",
124
+ "@mui/internal-babel-plugin-minify-errors": "2.0.8-canary.27"
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": "b5ecb08ed447855ed858e123d924a08949b6ad6d",
171
+ "gitSha": "9f107efaadc47ab0bd7c7474b69c9099e5c46dd7",
172
172
  "scripts": {
173
173
  "build": "tsgo -p tsconfig.build.json",
174
174
  "typescript": "tsgo -noEmit",
@@ -1,5 +1,6 @@
1
1
  // TODO: change back to 'eslint/config' once https://github.com/eslint/rewrite/issues/425 is fixed
2
2
  import { defineConfig } from '@eslint/config-helpers';
3
+ import { EXTENSION_DTS } from '../extensions.mjs';
3
4
 
4
5
  const restrictedMethods = ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'];
5
6
 
@@ -415,6 +416,7 @@ export function createCoreConfig(options = {}) {
415
416
  'mui/material-ui-no-styled-box': 'error',
416
417
  }
417
418
  : {}),
419
+ 'mui/no-guarded-throw': 'error',
418
420
  'mui/straight-quotes': 'off',
419
421
  'mui/consistent-production-guard': 'error',
420
422
  'mui/add-undef-to-optional': 'off',
@@ -535,5 +537,12 @@ export function createCoreConfig(options = {}) {
535
537
  '@typescript-eslint/return-await': 'off',
536
538
  },
537
539
  },
540
+ {
541
+ name: 'mui-base/dts',
542
+ files: [`**/*${EXTENSION_DTS}`],
543
+ rules: {
544
+ '@typescript-eslint/consistent-type-imports': 'off',
545
+ },
546
+ },
538
547
  ]);
539
548
  }
@@ -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 { isProcessEnvNodeEnv, isLiteralEq, isLiteralNeq } from './nodeEnvUtils.mjs';
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 a binary expression is comparing process.env.NODE_ENV appropriately
62
- * @param {import('estree').BinaryExpression} binaryExpression - The binary expression to check
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(binaryExpression, operator, value) {
68
- const { left, right } = binaryExpression;
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
- binaryExpression.operator === operator &&
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
- binaryExpression.operator === '===' &&
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
- /** @type {import('eslint').Rule.Node | null} */
99
- let current = node.parent;
100
- /** @type {import('eslint').Rule.Node} */
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
- // If we're in the consequent, we need !==
115
- // If we're in the alternate (else), we need ===
116
- const operator = isInConsequent ? '!==' : '===';
117
-
118
- // Check for the specific pattern with the right operator
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
- currentChild = current;
129
- current = current.parent;
130
- }
131
-
132
- return false;
116
+ return false;
117
+ });
133
118
  }
134
119
 
135
120
  return {