Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/code-infra/src/eslint/material-ui/index.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import consistentProductionGuard from './rules/consistent-production-guard.mjs';
import disallowActiveElementAsKeyEventTarget from './rules/disallow-active-element-as-key-event-target.mjs';
import disallowReactApiInServerComponents from './rules/disallow-react-api-in-server-components.mjs';
import docgenIgnoreBeforeComment from './rules/docgen-ignore-before-comment.mjs';
import muiNameMatchesComponentName from './rules/mui-name-matches-component-name.mjs';
import noEmptyBox from './rules/no-empty-box.mjs';
import noRestrictedResolvedImports from './rules/no-restricted-resolved-imports.mjs';
import noStyledBox from './rules/no-styled-box.mjs';
import requireDevWrapper from './rules/require-dev-wrapper.mjs';
import rulesOfUseThemeVariants from './rules/rules-of-use-theme-variants.mjs';
import straightQuotes from './rules/straight-quotes.mjs';

Expand All @@ -14,6 +16,7 @@ export default /** @type {import('eslint').ESLint.Plugin} */ ({
version: '0.1.0',
},
rules: {
'consistent-production-guard': consistentProductionGuard,
'disallow-active-element-as-key-event-target': disallowActiveElementAsKeyEventTarget,
'docgen-ignore-before-comment': docgenIgnoreBeforeComment,
'mui-name-matches-component-name': muiNameMatchesComponentName,
Expand All @@ -23,5 +26,6 @@ export default /** @type {import('eslint').ESLint.Plugin} */ ({
'straight-quotes': straightQuotes,
'disallow-react-api-in-server-components': disallowReactApiInServerComponents,
'no-restricted-resolved-imports': noRestrictedResolvedImports,
'require-dev-wrapper': requireDevWrapper,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* ESLint rule that enforces consistent patterns for production guard checks.
*
* @example
* // Valid - comparing with 'production'
* if (process.env.NODE_ENV !== 'production') {}
*
* @example
* // Valid - comparing with 'production'
* if (process.env.NODE_ENV === 'production') {}
*
* @example
* // Invalid - comparing with 'development'
* if (process.env.NODE_ENV === 'development') {}
*
* @example
* // Invalid - comparing with 'test'
* if (process.env.NODE_ENV !== 'test') {}
*
* @example
* // Invalid - non-static construct
* const env = 'production';
* if (process.env.NODE_ENV !== env) {}
*
* @example
* // Usage in ESLint config
* {
* rules: {
* 'material-ui/consistent-production-guard': 'error'
* }
* }
*
* @type {import('eslint').Rule.RuleModule}
*/
const rule = {
meta: {
type: 'problem',
docs: {
description:
'Enforce consistent patterns for production guard checks using process.env.NODE_ENV',
},
messages: {
invalidComparison:
"Only compare process.env.NODE_ENV with 'production'. Use `process.env.NODE_ENV !== 'production'` or `process.env.NODE_ENV === 'production'` instead of comparing with '{{ comparedValue }}'.",
nonStaticComparison:
"Production guard must use a statically analyzable pattern. Use `process.env.NODE_ENV === 'production'` or `process.env.NODE_ENV !== 'production'` with a string literal.",
invalidUsage:
"process.env.NODE_ENV must be used in a binary comparison with === or !==. Use `process.env.NODE_ENV !== 'production'` or `process.env.NODE_ENV === 'production'`.",
},
schema: [],
},
create(context) {
/**
* Checks if a node is process.env.NODE_ENV
* @param {import('estree').Node} node
* @returns {boolean}
*/
function isProcessEnvNodeEnv(node) {
return (
node.type === 'MemberExpression' &&
node.object.type === 'MemberExpression' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'process' &&
node.object.property.type === 'Identifier' &&
node.object.property.name === 'env' &&
node.property.type === 'Identifier' &&
node.property.name === 'NODE_ENV'
);
}

return {
BinaryExpression(node) {
// Check if this is a comparison with === or !==
if (node.operator === '===' || node.operator === '!==') {
// Check if left side is process.env.NODE_ENV
if (isProcessEnvNodeEnv(node.left)) {
// Right side must be a literal
if (node.right.type !== 'Literal') {
context.report({
node,
messageId: 'nonStaticComparison',
});
return;
}

// Right side must be the string 'production'
if (node.right.value !== 'production') {
context.report({
node,
messageId: 'invalidComparison',
data: {
comparedValue: String(node.right.value),
},
});
}
}
// Check if right side is process.env.NODE_ENV (reversed comparison)
else if (isProcessEnvNodeEnv(node.right)) {
// Left side must be a literal
if (node.left.type !== 'Literal') {
context.report({
node,
messageId: 'nonStaticComparison',
});
return;
}

// Left side must be the string 'production'
if (node.left.value !== 'production') {
context.report({
node,
messageId: 'invalidComparison',
data: {
comparedValue: String(node.left.value),
},
});
}
}
}
},
// Catch any other usage of process.env.NODE_ENV (not in a valid binary expression)
MemberExpression(node) {
if (isProcessEnvNodeEnv(node)) {
// Check if it's part of a valid binary expression
const parent = node.parent;
if (
parent &&
parent.type === 'BinaryExpression' &&
(parent.operator === '===' || parent.operator === '!==')
) {
// This is handled by BinaryExpression visitor
return;
}

// Invalid usage
context.report({
node,
messageId: 'invalidUsage',
});
}
},
};
},
};

export default rule;
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import eslint from 'eslint';
import parser from '@typescript-eslint/parser';
import rule from './consistent-production-guard.mjs';

const ruleTester = new eslint.RuleTester({
languageOptions: {
parser,
},
});

ruleTester.run('consistent-production-guard', rule, {
valid: [
// Should pass: Valid !== comparison with 'production'
{
code: `
if (process.env.NODE_ENV !== 'production') {
console.log('dev');
}
`,
},
// Should pass: Valid === comparison with 'production'
{
code: `
if (process.env.NODE_ENV === 'production') {
console.log('prod');
}
`,
},
// Should pass: Reversed comparison (literal on left)
{
code: `
if ('production' !== process.env.NODE_ENV) {
console.log('dev');
}
`,
},
// Should pass: Reversed comparison with ===
{
code: `
if ('production' === process.env.NODE_ENV) {
console.log('prod');
}
`,
},
// Should pass: Code without process.env.NODE_ENV
{
code: `
const foo = 'bar';
if (foo === 'baz') {
console.log('test');
}
`,
},
],
invalid: [
// Should fail: Comparing with 'development'
{
code: `
if (process.env.NODE_ENV === 'development') {
console.log('dev');
}
`,
errors: [
{
messageId: 'invalidComparison',
data: { comparedValue: 'development' },
},
],
},
// Should fail: Comparing with 'test'
{
code: `
if (process.env.NODE_ENV !== 'test') {
console.log('not test');
}
`,
errors: [
{
messageId: 'invalidComparison',
data: { comparedValue: 'test' },
},
],
},
// Should fail: Reversed comparison with 'development'
{
code: `
if ('development' === process.env.NODE_ENV) {
console.log('dev');
}
`,
errors: [
{
messageId: 'invalidComparison',
data: { comparedValue: 'development' },
},
],
},
// Should fail: Non-static comparison (variable)
{
code: `
const env = 'production';
if (process.env.NODE_ENV !== env) {
console.log('check');
}
`,
errors: [
{
messageId: 'nonStaticComparison',
},
],
},
// Should fail: Non-static comparison (reversed)
{
code: `
const env = 'production';
if (env === process.env.NODE_ENV) {
console.log('check');
}
`,
errors: [
{
messageId: 'nonStaticComparison',
},
],
},
// Should fail: Invalid usage (function call)
{
code: `
foo(process.env.NODE_ENV);
`,
errors: [
{
messageId: 'invalidUsage',
},
],
},
// Should fail: Invalid usage (assignment)
{
code: `
const env = process.env.NODE_ENV;
`,
errors: [
{
messageId: 'invalidUsage',
},
],
},
// Should fail: Invalid usage (template literal)
{
code: `
const message = \`Environment: \${process.env.NODE_ENV}\`;
`,
errors: [
{
messageId: 'invalidUsage',
},
],
},
],
});
Loading
Loading