Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions packages/core/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
- [isFunctionOfRenderMethod](functions/isFunctionOfRenderMethod.md)
- [isFunctionOfUseEffectCleanup](functions/isFunctionOfUseEffectCleanup.md)
- [isFunctionOfUseEffectSetup](functions/isFunctionOfUseEffectSetup.md)
- [isInitializedFromReact](functions/isInitializedFromReact.md)
- [isJsxFragmentElement](functions/isJsxFragmentElement.md)
- [isJsxHostElement](functions/isJsxHostElement.md)
- [isJsxLike](functions/isJsxLike.md)
Expand Down
26 changes: 26 additions & 0 deletions packages/core/docs/functions/isInitializedFromReact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[@eslint-react/core](../README.md) / isInitializedFromReact

# Function: isInitializedFromReact()

```ts
function isInitializedFromReact(
name: string,
importSource: string,
initialScope: Scope): boolean;
```

Check if an identifier name is initialized from react

## Parameters

| Parameter | Type | Description |
| ------ | ------ | ------ |
| `name` | `string` | The top-level identifier's name |
| `importSource` | `string` | The import source to check against |
| `initialScope` | `Scope` | Initial scope to search for the identifier |

## Returns

`boolean`

Whether the identifier name is initialized from react
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./get-instance-id";
export * from "./is-from-react";
export * from "./is-from-source";
export * from "./is-instance-id-equal";
export * from "./is-react-api";
49 changes: 3 additions & 46 deletions packages/core/src/utils/is-from-react.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,18 @@
import * as AST from "@eslint-react/ast";
import { identity } from "@eslint-react/eff";
import { findVariable } from "@eslint-react/var";
import type { Scope } from "@typescript-eslint/scope-manager";
import type { TSESTree } from "@typescript-eslint/types";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
import { P, match } from "ts-pattern";

/**
* Get the arguments of a require expression
* @param node The node to match
* @returns The require expression arguments or undefined if the node is not a require expression
*/
function getRequireExpressionArguments(node: TSESTree.Node) {
return match<typeof node, TSESTree.CallExpressionArgument[] | null>(node)
// require("source")
.with({ type: T.CallExpression, arguments: P.select(), callee: { type: T.Identifier, name: "require" } }, identity)
// require("source").variable
.with({ type: T.MemberExpression, object: P.select() }, getRequireExpressionArguments)
.otherwise(() => null);
}
import { isInitializedFromSource } from "./is-from-source";

/**
* Check if an identifier name is initialized from react
* @param name The top-level identifier's name
* @param importSource The import source to check against
* @param initialScope Initial scope to search for the identifier
* @returns Whether the identifier name is initialized from react
* @internal
*/
export function isInitializedFromReact(
name: string,
importSource: string,
initialScope: Scope,
): boolean {
if (name.toLowerCase() === "react") return true;
const latestDef = findVariable(name, initialScope)?.defs.at(-1);
if (latestDef == null) return false;
const { node, parent } = latestDef;
if (node.type === T.VariableDeclarator && node.init != null) {
const { init } = node;
// check for: `variable = React.variable`
if (init.type === T.MemberExpression && init.object.type === T.Identifier) {
return isInitializedFromReact(init.object.name, importSource, initialScope);
}
// check for: `{ variable } = React`
if (init.type === T.Identifier) {
return isInitializedFromReact(init.name, importSource, initialScope);
}
// check for: `variable = require('react')` or `variable = require('react').variable`
const args = getRequireExpressionArguments(init);
const arg0 = args?.[0];
if (arg0 == null || !AST.isLiteral(arg0, "string")) {
return false;
}
// check for: `require('react')` or `require('react/...')`
return arg0.value === importSource || arg0.value.startsWith(`${importSource}/`);
}
// latest definition is an import declaration: import { variable } from 'react'
return parent?.type === T.ImportDeclaration && parent.source.value === importSource;
) {
return name.toLowerCase() === "react" || isInitializedFromSource(name, importSource, initialScope);
}
60 changes: 60 additions & 0 deletions packages/core/src/utils/is-from-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as AST from "@eslint-react/ast";
import { identity } from "@eslint-react/eff";
import { findVariable } from "@eslint-react/var";
import type { Scope } from "@typescript-eslint/scope-manager";
import type { TSESTree } from "@typescript-eslint/types";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
import { P, match } from "ts-pattern";

/**
* Get the arguments of a require expression
* @param node The node to match
* @returns The require expression arguments or undefined if the node is not a require expression
*/
function getRequireExpressionArguments(node: TSESTree.Node) {
return match<typeof node, TSESTree.CallExpressionArgument[] | null>(node)
// require("source")
.with({ type: T.CallExpression, arguments: P.select(), callee: { type: T.Identifier, name: "require" } }, identity)
// require("source").variable
.with({ type: T.MemberExpression, object: P.select() }, getRequireExpressionArguments)
.otherwise(() => null);
}

/**
* Check if an identifier name is initialized from source
* @param name The top-level identifier's name
* @param source The import source to check against
* @param initialScope Initial scope to search for the identifier
* @returns Whether the identifier name is initialized from source
* @internal
*/
export function isInitializedFromSource(
name: string,
source: string,
initialScope: Scope,
) {
const latestDef = findVariable(name, initialScope)?.defs.at(-1);
if (latestDef == null) return false;
const { node, parent } = latestDef;
if (node.type === T.VariableDeclarator && node.init != null) {
const { init } = node;
// check for: `variable = React.variable`
if (init.type === T.MemberExpression && init.object.type === T.Identifier) {
return isInitializedFromSource(init.object.name, source, initialScope);
}
// check for: `{ variable } = React`
if (init.type === T.Identifier) {
return isInitializedFromSource(init.name, source, initialScope);
}
// check for: `variable = require('react')` or `variable = require('react').variable`
const args = getRequireExpressionArguments(init);
const arg0 = args?.[0];
if (arg0 == null || !AST.isLiteral(arg0, "string")) {
return false;
}
// check for: `require('react')` or `require('react/...')`
return arg0.value === source || arg0.value.startsWith(`${source}/`);
}
// latest definition is an import declaration: import { variable } from 'react'
return parent?.type === T.ImportDeclaration && parent.source.value === source;
}
Original file line number Diff line number Diff line change
Expand Up @@ -1182,5 +1182,15 @@ ruleTester.run(RULE_NAME, rule, {
return null;
};
`,
tsx`
import { BackHandler } from "react-native";

useEffect(() => {
const { remove } = BackHandler.addEventListener("hardwareBackPress", onBackPress);
return () => {
remove();
}
});
`,
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type ComponentPhaseKind,
ComponentPhaseRelevance,
getPhaseKindOfFunction,
isInitializedFromSource,
isInversePhase,
} from "@eslint-react/core";
import { unit } from "@eslint-react/eff";
Expand Down Expand Up @@ -245,6 +246,13 @@ export function create(context: RuleContext<MessageID, []>): RuleListener {
}
match(getCallKind(node))
.with("addEventListener", (callKind) => {
// https://github.com/Rel1cx/eslint-react/issues/1323
const isFromReactNative = node.callee.type === T.MemberExpression
&& node.callee.object.type === T.Identifier
&& isInitializedFromSource(node.callee.object.name, "react-native", context.sourceCode.getScope(node));
if (isFromReactNative) {
return;
}
const [type, listener, options] = node.arguments;
if (type == null || listener == null) {
return;
Expand Down