Merge 984ccbac71 into sapling-pr-archive-jbrown215

This commit is contained in:
Jordan Brown
2025-09-15 12:39:54 -04:00
committed by GitHub
2 changed files with 136 additions and 3 deletions

View File

@@ -581,6 +581,60 @@ const allTests = {
};
`,
},
{
code: normalizeIndent`
// Valid: useEffectEvent can be called in custom effect hooks configured via rule options
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useCustomEffect(() => {
onClick();
});
}
`,
options: [{additionalHooks: 'useCustomEffect'}],
},
{
code: normalizeIndent`
// Valid: useEffectEvent can be called in custom effect hooks configured via ESLint settings
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useMyEffect(() => {
onClick();
});
useServerEffect(() => {
onClick();
});
}
`,
settings: {
'react-hooks': {
additionalHooks: '(useMyEffect|useServerEffect)',
},
},
},
{
code: normalizeIndent`
// Valid: Rule options take precedence over settings
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useOptionEffect(() => {
onClick(); // This should work since rule options take precedence
});
}
`,
options: [{additionalHooks: 'useOptionEffect'}],
settings: {
'react-hooks': {
additionalHooks: 'useSettingsEffect',
},
},
},
],
invalid: [
{
@@ -1353,6 +1407,39 @@ const allTests = {
`,
errors: [tryCatchUseError('use')],
},
{
code: normalizeIndent`
// Invalid: useEffectEvent should not be callable in regular custom hooks without additional configuration
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useCustomHook(() => {
onClick();
});
}
`,
errors: [useEffectEventError('onClick', true)],
},
{
code: normalizeIndent`
// Invalid: useEffectEvent should not be callable in hooks not matching the settings regex
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useWrongHook(() => {
onClick();
});
}
`,
settings: {
'react-hooks': {
additionalHooks: 'useMyEffect',
},
},
errors: [useEffectEventError('onClick', true)],
},
],
};

View File

@@ -147,8 +147,23 @@ function getNodeWithoutReactNamespace(
return node;
}
function isEffectIdentifier(node: Node): boolean {
return node.type === 'Identifier' && (node.name === 'useEffect' || node.name === 'useLayoutEffect' || node.name === 'useInsertionEffect');
function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean {
const isBuiltInEffect =
node.type === 'Identifier' &&
(node.name === 'useEffect' ||
node.name === 'useLayoutEffect' ||
node.name === 'useInsertionEffect');
if (isBuiltInEffect) {
return true;
}
// Check if this matches additional hooks configured by the user
if (additionalHooks && node.type === 'Identifier') {
return additionalHooks.test(node.name);
}
return false;
}
function isUseEffectEventIdentifier(node: Node): boolean {
if (__EXPERIMENTAL__) {
@@ -169,8 +184,39 @@ const rule = {
recommended: true,
url: 'https://react.dev/reference/rules/rules-of-hooks',
},
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
additionalHooks: {
type: 'string',
},
},
},
],
},
create(context: Rule.RuleContext) {
const rawOptions = context.options && context.options[0];
const settings = context.settings || {};
// Parse the `additionalHooks` regex from either rule options or ESLint settings
const additionalHooksFromOption =
rawOptions && rawOptions.additionalHooks
? rawOptions.additionalHooks
: undefined;
const additionalHooksFromSettings =
settings['react-hooks'] && settings['react-hooks'].additionalHooks
? settings['react-hooks'].additionalHooks
: undefined;
// Prefer rule option over settings, but allow either
const additionalHooksRegex =
additionalHooksFromOption || additionalHooksFromSettings;
const additionalHooks = additionalHooksRegex
? new RegExp(additionalHooksRegex)
: undefined;
let lastEffect: CallExpression | null = null;
const codePathReactHooksMapStack: Array<
Map<Rule.CodePathSegment, Array<Node>>
@@ -726,7 +772,7 @@ const rule = {
// Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`
const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);
if (
(isEffectIdentifier(nodeWithoutNamespace) ||
(isEffectIdentifier(nodeWithoutNamespace, additionalHooks) ||
isUseEffectEventIdentifier(nodeWithoutNamespace)) &&
node.arguments.length > 0
) {