Skip to content

Commit d364a1b

Browse files
authored
Merge pull request #23 from Gelio/allow-detecting-hooks-from-all-namespaces
Allow detecting hooks from all namespaces
2 parents 7c0cccf + 091bc5f commit d364a1b

File tree

9 files changed

+169
-13
lines changed

9 files changed

+169
-13
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## v2.2.0 (2019-07-14)
4+
5+
- Add support for optionally detecting hook calls from sources other than the `React` namespace
6+
(e.g. `MyHooks.useHook`).
7+
8+
Usage is described in the README.
9+
310
## v2.1.1 (2019-06-09)
411

512
- Update dependencies due to security vulnerabilities

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The rule is based on an [ESLint plugin for react hooks](https://github.com/faceb
2222
- loops (`while`, `for`, `do ... while`)
2323
- functions that themselves are not custom hooks or components
2424
- detects using React hooks in spite of an early return
25+
- support for detecting hooks from namespaces other than `React` (e.g. `MyHooks.useHook`) (**optional**)
2526

2627
## Installation
2728

@@ -48,6 +49,37 @@ Then, enable the rule by modifying `tslint.json`:
4849

4950
To use report rule violations as warnings intead of errors, set it to `"warning"`.
5051

52+
## Options
53+
54+
While the rule works fine out-of-the-box, it can be customized. To specify options, use the
55+
following syntax when modifying `tslint.json`:
56+
57+
```js
58+
{
59+
"extends": [
60+
// your other plugins...
61+
"tslint-react-hooks"
62+
],
63+
"rules": {
64+
// your other rules...
65+
"react-hooks-nesting": ["error", {
66+
// options go here
67+
}]
68+
}
69+
}
70+
```
71+
72+
### Available options
73+
74+
- `"detect-hooks-from-non-react-namespace"` - when set to `true`, violations will be also reported
75+
hooks accessed from sources other than the `React` namespace (e.g. `MyHooks.useHook` will be
76+
treated as a hook).
77+
78+
By default, only direct calls (e.g. `useHook`) or calls from `React` namespace (e.g.
79+
`React.useState`) are treated as hooks.
80+
81+
Have an idea for an option? [Create a new issue](https://github.com/Gelio/tslint-react-hooks/issues/new).
82+
5183
## Workarounds
5284

5385
For some arrow functions/function expressions, the rule has no way to determine whether those are a
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

src/reactHooksNestingRule.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
import { SourceFile } from 'typescript';
2-
import { Rules, IRuleMetadata } from 'tslint';
2+
import { Rules, IRuleMetadata, Utils } from 'tslint';
33

44
import { ReactHooksNestingWalker } from './react-hooks-nesting-walker/react-hooks-nesting-walker';
5+
import { detectHooksFromNonReactNamespaceOptionName } from './react-hooks-nesting-walker/options';
56

67
export class Rule extends Rules.AbstractRule {
78
public static metadata: IRuleMetadata = {
89
ruleName: 'react-hooks-nesting',
910
description: 'Enforces Rules of Hooks',
1011
descriptionDetails: 'See https://reactjs.org/docs/hooks-rules.html',
1112

12-
optionsDescription: 'There are no available options.',
13-
options: null,
14-
optionExamples: [true],
13+
optionsDescription: Utils.dedent`
14+
An optional object with the property ${detectHooksFromNonReactNamespaceOptionName}.
15+
When set to true, violations will be reported for hooks from namespaces other
16+
than the React namespace (e.g. \`MyHooks.useHook\` will be treated as a hook).
17+
`,
18+
options: {
19+
type: 'object',
20+
properties: {
21+
[detectHooksFromNonReactNamespaceOptionName]: {
22+
type: 'boolean',
23+
},
24+
},
25+
},
26+
optionExamples: [
27+
true,
28+
[true, { [detectHooksFromNonReactNamespaceOptionName]: true }],
29+
],
1530

1631
hasFix: false,
1732
type: 'functionality',
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as MyHooks from './my-hooks';
2+
import MyHooks2 from './my-hooks-2';
3+
import { MyHooks3 } from './my-hooks-3';
4+
5+
function MyComponent() {
6+
if (true) {
7+
MyHooks.useHook();
8+
~~~~~~~~~~~~~~~~~ [A hook cannot appear inside an if statement]
9+
10+
MyHooks2.useHook();
11+
~~~~~~~~~~~~~~~~~~ [A hook cannot appear inside an if statement]
12+
13+
MyHooks3.useHook();
14+
~~~~~~~~~~~~~~~~~~ [A hook cannot appear inside an if statement]
15+
}
16+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react';
2+
3+
function MyComponent() {
4+
if (true) {
5+
React.useState();
6+
~~~~~~~~~~~~~~~~ [A hook cannot appear inside an if statement]
7+
}
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"rulesDirectory": "../../dist",
3+
"rules": {
4+
"react-hooks-nesting": [
5+
true,
6+
{
7+
"detect-hooks-from-non-react-namespace": true
8+
}
9+
]
10+
}
11+
}

0 commit comments

Comments
 (0)