Skip to content

Commit 03ed815

Browse files
committed
Add support for detecting hooks from all namespaces
Hooks can now be detected even when not used from the `React` namespace.
1 parent 61a37b5 commit 03ed815

File tree

3 files changed

+76
-9
lines changed

3 files changed

+76
-9
lines changed
Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
1-
import { CallExpression } from 'typescript';
1+
import {
2+
CallExpression,
3+
isIdentifier,
4+
isPropertyAccessExpression,
5+
Expression,
6+
} from 'typescript';
27

38
import { isHookIdentifier } from './is-hook-identifier';
4-
import { isReactApiExpression } from './is-react-api-expression';
9+
import {
10+
RuleOptions,
11+
detectHooksFromNonReactNamespaceOptionName,
12+
} from './options';
513

614
/**
715
* Tests if a `CallExpression` calls a React Hook
816
* @see https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#L26
917
*/
10-
export function isHookCall({ expression }: CallExpression) {
11-
return isHookAccessExpression(expression);
18+
export function isHookCall(
19+
{ expression }: CallExpression,
20+
ruleOptions: RuleOptions,
21+
) {
22+
if (isIdentifier(expression) && isHookIdentifier(expression)) {
23+
return true;
24+
} else if (
25+
isPropertyAccessExpression(expression) &&
26+
isHookIdentifier(expression.name)
27+
) {
28+
if (ruleOptions[detectHooksFromNonReactNamespaceOptionName]) {
29+
return true;
30+
}
31+
32+
/**
33+
* The expression from which the property is accessed.
34+
*
35+
* @example for `React.useState`, this would be the `React` identifier
36+
*/
37+
const sourceExpression = expression.expression;
38+
39+
return isReactIdentifier(sourceExpression);
40+
}
41+
42+
return false;
1243
}
1344

14-
/**
15-
* Tests for `useHook` or `React.useHook` calls
16-
*/
17-
const isHookAccessExpression = isReactApiExpression(isHookIdentifier);
45+
const isReactIdentifier = (expression: Expression) =>
46+
isIdentifier(expression) && expression.text === 'React';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export const detectHooksFromNonReactNamespaceOptionName =
2+
'detect-hooks-from-non-react-namespace';
3+
4+
export interface RuleOptions {
5+
/**
6+
* When set to `true`, violations will be reported for hooks from namespaces other than `React
7+
* (e.g. `MyHooks.useHook` will be treated as a hook).
8+
*/
9+
[detectHooksFromNonReactNamespaceOptionName]?: boolean;
10+
}
11+
12+
const defaultRuleOptions: RuleOptions = {};
13+
14+
export function parseRuleOptions(rawOptionsArray: unknown): RuleOptions {
15+
if (!Array.isArray(rawOptionsArray)) {
16+
return defaultRuleOptions;
17+
}
18+
19+
const rawOptions: Record<string, unknown> | undefined = rawOptionsArray[0];
20+
if (!rawOptions) {
21+
return defaultRuleOptions;
22+
}
23+
24+
let parsedOptions: RuleOptions = { ...defaultRuleOptions };
25+
26+
const detectHooksFromNonReactNamespaceOption =
27+
rawOptions[detectHooksFromNonReactNamespaceOptionName];
28+
if (typeof detectHooksFromNonReactNamespaceOption === 'boolean') {
29+
parsedOptions[
30+
detectHooksFromNonReactNamespaceOptionName
31+
] = detectHooksFromNonReactNamespaceOption;
32+
}
33+
34+
return parsedOptions;
35+
}

src/react-hooks-nesting-walker/react-hooks-nesting-walker.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ import { isReactComponentDecorator } from './is-react-component-decorator';
2626
import { findAncestorFunction } from './find-ancestor-function';
2727
import { FunctionNode, isFunctionNode } from './function-node';
2828
import { findClosestAncestorNode } from './find-closest-ancestor-node';
29+
import { parseRuleOptions } from './options';
2930

3031
export class ReactHooksNestingWalker extends RuleWalker {
3132
private functionsWithReturnStatements = new Set<FunctionNode>();
3233

34+
private readonly ruleOptions = parseRuleOptions(this.getOptions());
35+
3336
public visitCallExpression(node: CallExpression) {
34-
if (isHookCall(node)) {
37+
if (isHookCall(node, this.ruleOptions)) {
3538
this.visitHookAncestor(node, node.parent);
3639
}
3740

0 commit comments

Comments
 (0)