From d0a6b375d2df1eac12192affb3a849ea28ee0798 Mon Sep 17 00:00:00 2001 From: Dimitar Nestorov Date: Fri, 3 Feb 2023 16:34:09 +0200 Subject: [PATCH 1/3] chore: create an ESLint rule to detect `forwardRef` imports --- .eslintrc | 4 +++ eslint-local-rules/index.js | 1 + .../no-import-react-forwardref.js | 27 +++++++++++++++++++ package.json | 5 ++-- yarn.lock | 5 ++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 eslint-local-rules/index.js create mode 100644 eslint-local-rules/no-import-react-forwardref.js diff --git a/.eslintrc b/.eslintrc index 62a2ca85bc..6647cc601a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,11 @@ "extends": "@callstack", + "plugins": ["eslint-plugin-local-rules"], + "rules": { + "local-rules/no-import-react-forwardref": "error", + "one-var": "off", "no-multi-assign": "off", "no-nested-ternary": "off", diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js new file mode 100644 index 0000000000..4f331ad830 --- /dev/null +++ b/eslint-local-rules/index.js @@ -0,0 +1 @@ +Object.assign(exports, require('./no-import-react-forwardref')); diff --git a/eslint-local-rules/no-import-react-forwardref.js b/eslint-local-rules/no-import-react-forwardref.js new file mode 100644 index 0000000000..dff3cb07c9 --- /dev/null +++ b/eslint-local-rules/no-import-react-forwardref.js @@ -0,0 +1,27 @@ +exports['no-import-react-forwardref'] = { + meta: { + docs: { + description: 'Disallow importing of React.forwardRef', + category: 'Possible Errors', + recommended: false, + }, + schema: [], + }, + create(context) { + return { + ImportDeclaration(node) { + if (node.source.value !== 'react') return; + + for (const specifier of node.specifiers) { + if (specifier.type !== 'ImportSpecifier') continue; + if (specifier.imported.name !== 'forwardRef') continue; + + context.report({ + loc: specifier.loc, + message: 'Import forwardRef from src/utils/forwardRef instead', + }); + } + }, + }; + }, +}; diff --git a/package.json b/package.json index 240e73f8e8..70f9d5a47c 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "dedent": "^0.7.0", "eslint": "8.31.0", "eslint-plugin-flowtype": "^8.0.3", + "eslint-plugin-local-rules": "^1.3.2", "expo-constants": "^9.3.5", "flow-bin": "0.92.0", "glob": "^7.1.3", @@ -99,8 +100,8 @@ "peerDependencies": { "react": "*", "react-native": "*", - "react-native-vector-icons": "*", - "react-native-safe-area-context": "*" + "react-native-safe-area-context": "*", + "react-native-vector-icons": "*" }, "husky": { "hooks": { diff --git a/yarn.lock b/yarn.lock index fd14dd5928..60453d7695 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5199,6 +5199,11 @@ eslint-plugin-jest@^27.0.1: dependencies: "@typescript-eslint/utils" "^5.10.0" +eslint-plugin-local-rules@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-local-rules/-/eslint-plugin-local-rules-1.3.2.tgz#b9c9522915faeb9e430309fb909fc1dbcd7aedb3" + integrity sha512-X4ziX+cjlCYnZa+GB1ly3mmj44v2PeIld3tQVAxelY6AMrhHSjz6zsgsT6nt0+X5b7eZnvL/O7Q3pSSK2kF/+Q== + eslint-plugin-prettier@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" From 4a2c1990491ca49bce4c279dcf9a8d3a2c6addfb Mon Sep 17 00:00:00 2001 From: Dimitar Nestorov Date: Fri, 3 Feb 2023 16:34:11 +0200 Subject: [PATCH 2/3] chore: create an ESLint rule to detect `React.forwardRef` calls --- .eslintrc | 1 + eslint-local-rules/index.js | 1 + .../no-react-forwardref-usage.js | 28 +++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 eslint-local-rules/no-react-forwardref-usage.js diff --git a/.eslintrc b/.eslintrc index 6647cc601a..08f0d6e69e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,6 +9,7 @@ "rules": { "local-rules/no-import-react-forwardref": "error", + "local-rules/no-react-forwardref-usage": "error", "one-var": "off", "no-multi-assign": "off", diff --git a/eslint-local-rules/index.js b/eslint-local-rules/index.js index 4f331ad830..04cafac316 100644 --- a/eslint-local-rules/index.js +++ b/eslint-local-rules/index.js @@ -1 +1,2 @@ Object.assign(exports, require('./no-import-react-forwardref')); +Object.assign(exports, require('./no-react-forwardref-usage')); diff --git a/eslint-local-rules/no-react-forwardref-usage.js b/eslint-local-rules/no-react-forwardref-usage.js new file mode 100644 index 0000000000..e2d16a1645 --- /dev/null +++ b/eslint-local-rules/no-react-forwardref-usage.js @@ -0,0 +1,28 @@ +exports['no-react-forwardref-usage'] = { + meta: { + docs: { + description: 'Disallow usage of React.forwardRef', + category: 'Possible Errors', + recommended: false, + }, + schema: [], + }, + create(context) { + return { + CallExpression(node) { + if (node.callee?.type !== 'MemberExpression') return; + + const { callee } = node; + if (callee.object.type !== 'Identifier') return; + if (callee.object.name !== 'React') return; + if (callee.property.type !== 'Identifier') return; + if (callee.property.name !== 'forwardRef') return; + + context.report({ + loc: callee.loc, + message: 'Use forwardRef from src/utils/forwardRef instead', + }); + }, + }; + }, +}; From 2872c9f753ae26ac4a18432b4b4f67b974234706 Mon Sep 17 00:00:00 2001 From: Dimitar Nestorov Date: Fri, 3 Feb 2023 16:34:12 +0200 Subject: [PATCH 3/3] fix: linter errors --- .eslintrc | 9 +++++++++ src/components/Surface.tsx | 5 +++-- src/components/ToggleButton/ToggleButton.tsx | 3 ++- src/components/Typography/Text.tsx | 3 ++- src/components/Typography/v2/Text.tsx | 3 ++- src/utils/forwardRef.tsx | 6 +++--- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.eslintrc b/.eslintrc index 08f0d6e69e..380df91c1e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -54,6 +54,15 @@ "react-native-a11y/has-valid-accessibility-descriptors": "off" }, + "overrides": [ + { + "files": ["*.test.js", "*.test.tsx"], + "rules": { + "local-rules/no-react-forwardref-usage": "off" + } + } + ], + "settings": { "import/extensions": [".js", ".ts", ".tsx"], "import/parsers": { diff --git a/src/components/Surface.tsx b/src/components/Surface.tsx index 1e131863ad..a5bd9ab0ec 100644 --- a/src/components/Surface.tsx +++ b/src/components/Surface.tsx @@ -12,6 +12,7 @@ import { useInternalTheme } from '../core/theming'; import overlay, { isAnimatedValue } from '../styles/overlay'; import shadow from '../styles/shadow'; import type { ThemeProp, MD3Elevation } from '../types'; +import { forwardRef } from '../utils/forwardRef'; export type Props = React.ComponentPropsWithRef & { /** @@ -39,7 +40,7 @@ export type Props = React.ComponentPropsWithRef & { ref?: React.RefObject; }; -const MD2Surface = React.forwardRef( +const MD2Surface = forwardRef( ({ style, theme: overrideTheme, ...rest }: Omit, ref) => { const { elevation = 4 } = (StyleSheet.flatten(style) || {}) as ViewStyle; const { dark: isDarkTheme, mode, colors } = useInternalTheme(overrideTheme); @@ -105,7 +106,7 @@ const MD2Surface = React.forwardRef( * }); * ``` */ -const Surface = React.forwardRef( +const Surface = forwardRef( ( { elevation = 1, diff --git a/src/components/ToggleButton/ToggleButton.tsx b/src/components/ToggleButton/ToggleButton.tsx index 5d862846a6..33f057fed2 100644 --- a/src/components/ToggleButton/ToggleButton.tsx +++ b/src/components/ToggleButton/ToggleButton.tsx @@ -12,6 +12,7 @@ import color from 'color'; import { useInternalTheme } from '../../core/theming'; import { black, white } from '../../styles/themes/v2/colors'; import type { ThemeProp } from '../../types'; +import { forwardRef } from '../../utils/forwardRef'; import type { IconSource } from '../Icon'; import IconButton from '../IconButton/IconButton'; import { ToggleButtonGroupContext } from './ToggleButtonGroup'; @@ -92,7 +93,7 @@ export type Props = { * * ``` */ -const ToggleButton = React.forwardRef( +const ToggleButton = forwardRef( ( { icon, diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx index 28bba3a162..01f9403b4b 100644 --- a/src/components/Typography/Text.tsx +++ b/src/components/Typography/Text.tsx @@ -9,6 +9,7 @@ import { import { useInternalTheme } from '../../core/theming'; import { Font, MD3TypescaleKey, ThemeProp } from '../../types'; +import { forwardRef } from '../../utils/forwardRef'; export type Props = React.ComponentProps & { /** @@ -149,4 +150,4 @@ const styles = StyleSheet.create({ }, }); -export default React.forwardRef(Text); +export default forwardRef(Text); diff --git a/src/components/Typography/v2/Text.tsx b/src/components/Typography/v2/Text.tsx index eeae7d06f6..8114915189 100644 --- a/src/components/Typography/v2/Text.tsx +++ b/src/components/Typography/v2/Text.tsx @@ -9,6 +9,7 @@ import { import type { MD2Theme } from 'src/types'; import { useInternalTheme } from '../../../core/theming'; +import { forwardRef } from '../../../utils/forwardRef'; type Props = React.ComponentProps & { style?: StyleProp; @@ -58,4 +59,4 @@ const styles = StyleSheet.create({ }, }); -export default React.forwardRef(Text); +export default forwardRef(Text); diff --git a/src/utils/forwardRef.tsx b/src/utils/forwardRef.tsx index 8392a0c624..ea04071dd6 100644 --- a/src/utils/forwardRef.tsx +++ b/src/utils/forwardRef.tsx @@ -1,5 +1,5 @@ -import { - forwardRef as reactForwardRef, +import * as React from 'react'; +import type { ForwardRefRenderFunction, PropsWithoutRef, RefAttributes, @@ -20,4 +20,4 @@ export type ForwarRefComponent = ForwardRefExoticComponent< */ export const forwardRef: ( render: ForwardRefRenderFunction -) => ForwarRefComponent = reactForwardRef; +) => ForwarRefComponent = React.forwardRef;