diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ef40948..b62eae2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,5 +1,5 @@ pool: - vmImage: 'Ubuntu-16.04' + vmImage: 'ubuntu-18.04' steps: - task: NodeTool@0 diff --git a/src/react-hooks-nesting-walker/error-messages.ts b/src/react-hooks-nesting-walker/error-messages.ts index 6a40580..918e0ac 100644 --- a/src/react-hooks-nesting-walker/error-messages.ts +++ b/src/react-hooks-nesting-walker/error-messages.ts @@ -17,4 +17,5 @@ export const ERROR_MESSAGES = { 'A hook cannot be used inside of another function', invalidFunctionExpression: 'A hook cannot be used inside of another function', hookAfterEarlyReturn: 'A hook should not appear after a return statement', + anonymousFunctionIllegalCallback: `Hook is in an anonymous function that is passed to an illegal callback. Legal callbacks identifiers that can receive anonymous functions as arguments are memo and forwardRef`, }; diff --git a/src/react-hooks-nesting-walker/react-hooks-nesting-walker.ts b/src/react-hooks-nesting-walker/react-hooks-nesting-walker.ts index f4eefd2..4d852fe 100644 --- a/src/react-hooks-nesting-walker/react-hooks-nesting-walker.ts +++ b/src/react-hooks-nesting-walker/react-hooks-nesting-walker.ts @@ -190,6 +190,21 @@ export class ReactHooksNestingWalker extends RuleWalker { return; } + /** + * Detect if the unnamed expression is wrapped in a illegal function call + */ + if ( + isCallExpression(ancestor.parent) && + isIdentifier(ancestor.parent.expression) && + !isComponentOrHookIdentifier(ancestor.parent.expression) + ) { + this.addFailureAtNode( + hookNode, + ERROR_MESSAGES.anonymousFunctionIllegalCallback, + ); + return; + } + // Disallow using hooks inside other kinds of functions this.addFailureAtNode(hookNode, ERROR_MESSAGES.invalidFunctionExpression); return; diff --git a/test/tslint-rule/anonymous-function-error.ts.lint b/test/tslint-rule/anonymous-function-error.ts.lint new file mode 100644 index 0000000..b0c37e9 --- /dev/null +++ b/test/tslint-rule/anonymous-function-error.ts.lint @@ -0,0 +1,37 @@ +const IllegalComponent = wrapper(function() { + useEffect(() => {}); + ~~~~~~~~~~~~~~~~~~~ [Hook is in an anonymous function that is passed to an illegal callback. Legal callbacks identifiers that can receive anonymous functions as arguments are memo and forwardRef] +}) + +const LegalAnonymousComponent = function() { + useEffect(() => {}); +} + +const ForwardedComponent = React.forwardRef(function(props, ref) { + useEffect(() => { + console.log("I am legal") + }); +}) + +const MemoizedComponent = React.memo((props) => { + const [state, setState] = React.useState(props.initValue); + return {state} +}) + +const Functor = function() { + const cb = React.useCallback(() => { + const r = React.useRef() + ~~~~~~~~~~~~~~ [A hook cannot be used inside of another function] + }, []) + + const cb = useCallback(() => { + const r = React.useRef() + ~~~~~~~~~~~~~~ [A hook cannot be used inside of another function] + }, []) + + obj[(props) => { useRef() }] = 123; + ~~~~~~~~ [A hook cannot be used inside of another function] + + new Abc((props) => { useRef() }) + ~~~~~~~~ [A hook cannot be used inside of another function] +}