Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ const tests: CompilerTestCases = {
};
`,
},
{
name: '[Heuristic/Flow] Skips lowercase variable initialized with call expression wrapping a function',
filename: 'utils.js',
// Lowercase name means the heuristic skips this file entirely,
// even though the init is a call expression wrapping a function.
code: normalizeIndent`
const helper = someWrapper(function(obj) {
obj.key = 'value';
return obj;
});
`,
},
],
invalid: [
// ===========================================
Expand Down Expand Up @@ -134,6 +146,87 @@ const tests: CompilerTestCases = {
},
],
},
// ===========================================
// Tests for HOC-wrapped components (memo, forwardRef, etc.) with Flow parser
// The heuristic must unwrap one level of CallExpression to detect these.
// Regression tests for: const Comp = memo(function Comp() {...}) being
// silently skipped while an equivalent plain function declaration was compiled.
// ===========================================
{
name: '[Heuristic/Flow] Compiles memo-wrapped named function expression - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
const MyComponent = memo(function MyComponent({a}) {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic/Flow] Compiles memo-wrapped arrow function - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
const MyComponent = memo(({a}) => {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic/Flow] Compiles React.memo-wrapped function expression - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
const MyComponent = React.memo(function MyComponent({a}) {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic/Flow] Compiles forwardRef-wrapped function expression - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
const MyComponent = forwardRef(function MyComponent({a}, ref) {
a.key = 'value';
return <div ref={ref} />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic/Flow] Compiles exported memo-wrapped component - detects prop mutation',
filename: 'component.js',
code: normalizeIndent`
export const MyComponent = memo(function MyComponent({a}) {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ const tests: CompilerTestCases = {
};
`,
},
{
name: '[Heuristic] Skips lowercase variable initialized with call expression wrapping a function',
filename: 'utils.ts',
// Lowercase name means the heuristic skips this file entirely,
// even though the init is a call expression wrapping a function.
code: normalizeIndent`
const helper = someWrapper(function(obj) {
obj.key = 'value';
return obj;
});
`,
},
],
invalid: [
{
Expand Down Expand Up @@ -192,6 +204,87 @@ const tests: CompilerTestCases = {
},
],
},
// ===========================================
// Tests for HOC-wrapped components (memo, forwardRef, etc.)
// The heuristic must unwrap one level of CallExpression to detect these.
// Regression tests for: const Comp = memo(function Comp() {...}) being
// silently skipped while an equivalent plain function declaration was compiled.
// ===========================================
{
name: '[Heuristic] Compiles memo-wrapped named function expression - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
const MyComponent = memo(function MyComponent({a}) {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles memo-wrapped arrow function - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
const MyComponent = memo(({a}) => {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles React.memo-wrapped function expression - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
const MyComponent = React.memo(function MyComponent({a}) {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles forwardRef-wrapped function expression - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
const MyComponent = forwardRef(function MyComponent({a}, ref) {
a.key = 'value';
return <div ref={ref} />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
{
name: '[Heuristic] Compiles exported memo-wrapped component - detects prop mutation',
filename: 'component.tsx',
code: normalizeIndent`
export const MyComponent = memo(function MyComponent({a}) {
a.key = 'value';
return <div />;
});
`,
errors: [
{
message: /Modifying component props/,
},
],
},
],
};

Expand Down
37 changes: 28 additions & 9 deletions packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,40 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
}

// Handle: const MyComponent = () => {} or const useHook = function() {}
// Also handles: const MyComponent = memo(function MyComponent() {}) or
// const MyComponent = React.memo(() => {})
if (node.type === 'VariableDeclaration') {
for (const decl of (node as ESTree.VariableDeclaration).declarations) {
if (decl.id.type === 'Identifier') {
const name = decl.id.name;
if (
!COMPONENT_NAME_PATTERN.test(name) &&
!HOOK_NAME_PATTERN.test(name)
) {
continue;
}
const init = decl.init;
if (init == null) {
continue;
}
if (
init != null &&
(init.type === 'ArrowFunctionExpression' ||
init.type === 'FunctionExpression')
init.type === 'ArrowFunctionExpression' ||
init.type === 'FunctionExpression'
) {
const name = decl.id.name;
if (
COMPONENT_NAME_PATTERN.test(name) ||
HOOK_NAME_PATTERN.test(name)
) {
return true;
return true;
}
// Unwrap one level of call expression to catch patterns like:
// const Comp = memo(function Comp() {...})
// const Comp = React.memo(() => {...})
// const Comp = forwardRef(function Comp() {...})
if (init.type === 'CallExpression') {
for (const arg of (init as ESTree.CallExpression).arguments) {
if (
arg.type === 'ArrowFunctionExpression' ||
arg.type === 'FunctionExpression'
) {
return true;
}
}
}
}
Expand Down