├── .cursor └── rules │ ├── code-style.mdc │ ├── eslint.mdc │ ├── project-general.mdc │ └── testing.mdc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── pull_request_template.md └── workflows │ ├── pullRequest.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .npmrc ├── .tool-versions ├── README.md ├── __tests__ ├── eslint-v8 │ ├── ruleTester.ts │ └── rules.test.ts ├── eslint-v9 │ ├── ruleTester.ts │ └── rules.test.ts └── testcases │ ├── requireMemo.ts │ ├── requireUseCallback.ts │ ├── requireUseMemo.ts │ └── requireUseMemoChildren.ts ├── commitlint.config.js ├── docs └── rules │ ├── eslint-v9-migration-guide.md │ ├── require-memo.md │ ├── require-usememo-children.md │ └── require-usememo.md ├── examples ├── README.md ├── v8-traditional │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── App.js │ │ ├── Custom.js │ │ └── index.js │ └── yarn.lock └── v9-flat │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── src │ ├── App.js │ ├── Custom.js │ └── index.js │ └── yarn.lock ├── jest.config.js ├── knip.json ├── package.json ├── rollup.config.js ├── src ├── constants │ └── messages.ts ├── flat-config.ts ├── index.ts ├── require-memo │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── require-usememo-children.ts ├── require-usememo │ ├── constants.ts │ ├── index.ts │ ├── types.ts │ └── utils.ts ├── traditional-config.ts ├── types.ts └── utils │ ├── compatibility.ts │ ├── getVariableInScope.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.cursor/rules/code-style.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: src/**/*,*.tsx,*.ts,*.js 4 | alwaysApply: false 5 | --- 6 | 7 | # Code Style Guidelines 8 | 9 | ## TypeScript 10 | 11 | The project uses TypeScript. 12 | 13 | ## React Best Practices 14 | 15 | Follow React best practices for component design, state management, and performance optimization. 16 | 17 | ## Memoization 18 | 19 | Use `useMemo`, `useCallback`, and `React.memo` appropriately to optimize performance. 20 | 21 | ## Error Handling 22 | 23 | Implement robust error handling and validation. 24 | 25 | ## Performance Considerations 26 | 27 | - Avoid RegExp where simple string operations suffice 28 | - Consider the real-time nature of ESLint plugins in IDEs 29 | - Optimize for frequent calls and minimal overhead 30 | - Profile critical paths before making optimizations 31 | - Prefer simple, direct implementations over complex ones 32 | - Be especially careful with operations that run on every file parse -------------------------------------------------------------------------------- /.cursor/rules/eslint.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | 7 | # ESLint Guidelines 8 | 9 | ## What is ESLint? 10 | 11 | ESLint is a static code analysis tool for identifying problematic patterns found in ECMAScript/JavaScript code. 12 | 13 | ## Purpose 14 | 15 | To enforce code style, catch potential errors, and improve code quality. 16 | 17 | ## Configuration 18 | 19 | This project supports both ESLint v8 (traditional configuration using `.eslintrc.json`) and ESLint v9 (flat configuration using `eslint.config.js`). 20 | 21 | ## Plugin 22 | 23 | This project provides an ESLint plugin named `@arthurgeron/eslint-plugin-react-usememo`. 24 | 25 | ## Rules 26 | 27 | The plugin includes the following rules: 28 | 29 | - `require-usememo`: Requires complex values passed as props or used as hook dependencies to be wrapped in `useMemo` or `useCallback`. 30 | - `require-memo`: Requires all function components to be wrapped in `React.memo()`. 31 | - `require-usememo-children`: Requires complex values passed as children to be wrapped in `useMemo` or `useCallback`. 32 | 33 | ## Compatibility 34 | 35 | The plugin is designed to be compatible with both ESLint v8 and v9, using utility functions in `src/utils/compatibility.ts` to handle differences between the versions. 36 | 37 | 38 | ## Documentation 39 | 40 | For more details on individual rules and migration guides, refer to the documentation files in the `docs/rules` directory: 41 | 42 | - [require-memo](mdc:../docs/rules/require-memo.md) 43 | - [require-usememo](mdc:../docs/rules/require-usememo.md) 44 | - [require-usememo-children](mdc:../docs/rules/require-usememo-children.md) 45 | - [Migrating to V9.x](mdc:https:/eslint.org/docs/latest/use/migrate-to-9.0.0) 46 | - [ESLint v9 Migration Guide](mdc:../docs/rules/eslint-v9-migration-guide.md) 47 | - [Creating ESLint V9 Plugins](mdc:https:/eslint.org/docs/latest/extend/plugins) 48 | - [Sharing ESLint V9 Configurations](mdc:https:/eslint.org/docs/latest/extend/shareable-configs) 49 | - [Custom ESLint V9 Processors](mdc:https:/eslint.org/docs/latest/extend/custom-processors) 50 | - [Custom ESLint V9 Parsers](mdc:https:/eslint.org/docs/latest/extend/custom-parsers) 51 | - [Custom ESLint V9 Formatters](mdc:https:/eslint.org/docs/latest/extend/custom-formatters) 52 | - [Custom ESLint V9 Rules](mdc:https:/eslint.org/docs/latest/extend/custom-rules) 53 | - [Custom ESLint V9 Rule Tutorial](mdc:https:/eslint.org/docs/latest/extend/custom-rule-tutorial) 54 | 55 | ## ESLint Plugin Development 56 | 57 | - Remember rules run frequently in IDE environments 58 | - Optimize core operations that run on every node visit 59 | - Cache results where possible 60 | - Use simple type checks and string operations over complex regex 61 | - Consider the impact on IDE performance -------------------------------------------------------------------------------- /.cursor/rules/project-general.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: package.json,README.md,src/**/* 4 | alwaysApply: false 5 | --- 6 | # Project General Guidelines 7 | 8 | ## Project Objective 9 | 10 | The primary goal of this project is to provide an ESLint plugin that helps developers optimize React applications by enforcing the use of `useMemo`, `useCallback`, and `React.memo` to prevent unnecessary re-renders and improve performance. 11 | 12 | ## General Structure 13 | 14 | - `src/`: Contains the source code for the ESLint plugin, including the rule implementations and utility functions. 15 | - `docs/`: Contains documentation for the plugin and its rules. 16 | - `examples/`: Contains example React projects demonstrating the use of the plugin with different ESLint configurations. 17 | - `__tests__/`: Contains test cases for the plugin rules. 18 | - `src/utils/compatibility.ts`: Provides compatibility utilities for ESLint v8 and v9. 19 | 20 | ## Key Dependencies 21 | 22 | - `yarn`: Package manager 23 | - `eslint`: The core ESLint library. Both V8 and V9 (eslint-v9). 24 | - `react`: The React library (used in examples and tests). 25 | 26 | ## ESLint Version Support 27 | 28 | The project is designed to work with both ESLint v8 and v9. The `src/utils/compatibility.ts` file provides utilities to handle differences between the two versions. 29 | 30 | ## Configuration Files 31 | 32 | - ESLint v8: `.eslintrc.json` 33 | - ESLint v9: `eslint.config.js` 34 | 35 | ## Documentation 36 | 37 | For more details on project setup and overall guidelines, please refer to [README.md](mdc:../../README.md) and the documentation in the `docs/` directory. -------------------------------------------------------------------------------- /.cursor/rules/testing.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Testing Guidelines for AI Agents 3 | globs: 4 | - "__tests__/**/*" 5 | - "test-plugin.js" 6 | alwaysApply: false 7 | --- 8 | 9 | # Testing Guidelines 10 | 11 | ## Testing Approach 12 | 13 | The project uses Jest for testing. 14 | 15 | ## Test Case Structure 16 | 17 | Test cases are located in the `__tests__` directory and are separated into `validTestCases` and `invalidTestCases` to cover both passing and failing scenarios. 18 | 19 | ## Version Compatibility Testing 20 | 21 | The same test cases are executed against both ESLint v8 and ESLint v9 to ensure compatibility. This is achieved by extracting shared test cases into the `__tests__/testcases` directory and utilizing the appropriate ESLint runner in: 22 | - `__tests__/eslint-v8/rules.test.ts` for ESLint v8 23 | - `__tests__/eslint-v9/rules.test.ts` for ESLint v9 24 | 25 | ## Test Execution 26 | 27 | Tests are executed using the command `yarn test`. 28 | 29 | ## Test Case Utility 30 | 31 | The test cases are generated by a set of utility functions located in the `__tests__/testcases` directory: 32 | - `createRequireMemoTestCases` from `__tests__/testcases/requireMemo.ts` 33 | - `createRequireUseCallbackTestCases` from `__tests__/testcases/requireUseCallback.ts` 34 | - `createRequireUseMemoTestCases` from `__tests__/testcases/requireUseMemo.ts` 35 | - `createRequireUseMemoChildrenTestCases` from `__tests__/testcases/requireUseMemoChildren.ts` 36 | 37 | ## Example Test Execution 38 | 39 | ```bash 40 | yarn test 41 | ``` 42 | 43 | **4. `.cursor/rules/code-style.mdc`** 44 | 45 | ```markdown 46 | --- 47 | description: Code Style Guidelines for AI Agents 48 | globs: 49 | - "src/**/*" 50 | alwaysApply: false 51 | --- 52 | 53 | # Code Style Guidelines 54 | 55 | ## TypeScript 56 | 57 | The project uses TypeScript. 58 | 59 | ## React Best Practices 60 | 61 | Follow React best practices for component design, state management, and performance optimization. 62 | 63 | ## Memoization 64 | 65 | Use `useMemo`, `useCallback`, and `React.memo` appropriately to optimize performance. 66 | 67 | ## Error Handling 68 | 69 | Implement robust error handling and validation. 70 | ``` 71 | 72 | These files are now ready to be placed in the `.cursor/rules` directory of your project. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: not verified 6 | assignees: arthurgeron 7 | 8 | --- 9 | 10 | --- 11 | name: Bug report for @arthurgeron/eslint-plugin-react-usememo 12 | about: Report a problem or unexpected behavior 13 | --- 14 | 15 | ## Prerequisites 16 | 17 | Before you create a new issue, please ensure that this is not a duplicate issue and that you have gone through all the debugging steps. 18 | 19 | ## Minimal repro 20 | 21 | Please provide a minimal piece of code or reproducible example that demonstrates the issue. 22 | 23 | ```javascript 24 | // Paste your code here 25 | `````` 26 | > Some rules process import statements. Make sure to include them if they're relevant to this issue. 27 | 28 | ### Installed packages versions 29 | **ESLint:** 30 | **Eslint-plugin-react-usememo:** 31 | 32 | > You can get these details by using `npm ls --depth=0`. 33 | 34 | ### Rule and its options 35 | 36 | Which rule from the eslint-plugin-react-usememo is being used? What configuration options are set for this rule? 37 | 38 | ### Error log / Stack trace 39 | 40 | Please provide the full error log or stack trace, if any. 41 | 42 | ``` 43 | # copy and paste your error log here 44 | ``` 45 | 46 | 47 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | ## Description 5 | 6 | 7 | ## Checklist 8 | 9 | 10 | 11 | - [ ] **Tests**: I have created multiple test scenarios for my changes. 12 | - [ ] **Passing Tests**: All existing and new tests are pass successfully. 13 | - [ ] **Version Bump**: I have increased the package version number in package.json, following the [Semantic Versioning](https://semver.org/) (SEMVER) standard. 14 | - [ ] **Version Bump**: I have increased the package version number in src/flat-config.ts and src/traditional-config.ts, following the [Semantic Versioning](https://semver.org/) (SEMVER) standard. 15 | - [ ] **Documentation**: I have updated documentation to reflect the changes made, if necessary. 16 | - [ ] **Focused Changes**: My code changes are focused solely on the matter described above. 17 | 18 | ## Additional Information 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/pullRequest.yml: -------------------------------------------------------------------------------- 1 | name: Pull-request 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | install: 7 | name: Installing packages 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 1 14 | 15 | - name: Read .tool-versions 16 | id: tool-versions 17 | run: | 18 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 19 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 20 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 21 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 22 | 23 | - name: Use Node.js from .tool-versions 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ steps.tool-versions.outputs.node_version }} 27 | cache: 'yarn' 28 | cache-dependency-path: '**/yarn.lock' 29 | 30 | - name: Setup Yarn from .tool-versions 31 | run: | 32 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 33 | 34 | - name: Cache node modules 35 | uses: actions/cache@v3 36 | id: cache 37 | with: 38 | path: node_modules 39 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 40 | restore-keys: | 41 | ${{ runner.os }}- 42 | 43 | - name: Install Dependencies 44 | if: steps.cache.outputs.cache-hit != 'true' # It will run install dependencies just if hash of yarn.lock changed 45 | run: yarn install --frozen-lockfile --prefer-offline 46 | env: 47 | NODE_AUTH_TOKEN: ${{ secrets.GIT_PACKAGES_TOKEN }} 48 | 49 | test: 50 | name: Run Unit Tests 51 | runs-on: ubuntu-latest 52 | needs: install 53 | steps: 54 | - name: Checkout 55 | uses: actions/checkout@v3 56 | with: 57 | fetch-depth: 1 58 | 59 | - name: Read .tool-versions 60 | id: tool-versions 61 | run: | 62 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 63 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 64 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 65 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 66 | 67 | - name: Use Node.js from .tool-versions 68 | uses: actions/setup-node@v3 69 | with: 70 | node-version: ${{ steps.tool-versions.outputs.node_version }} 71 | cache: 'yarn' 72 | cache-dependency-path: '**/yarn.lock' 73 | 74 | - name: Setup Yarn from .tool-versions 75 | run: | 76 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 77 | 78 | - name: Restore node_modules 79 | uses: actions/cache@v3 80 | id: restore-build 81 | with: 82 | path: node_modules 83 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 84 | 85 | - name: Restore jest cache 86 | uses: actions/cache@v3 87 | with: 88 | path: .jest-cache 89 | key: ${{ runner.os }}-${{ hashFiles('**/.jest-cache/**') }} 90 | 91 | - name: Run tests 92 | run: yarn test 93 | 94 | build: 95 | name: Test build 96 | runs-on: ubuntu-latest 97 | needs: install 98 | steps: 99 | - name: Checkout 100 | uses: actions/checkout@v3 101 | with: 102 | fetch-depth: 1 103 | 104 | - name: Read .tool-versions 105 | id: tool-versions 106 | run: | 107 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 108 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 109 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 110 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 111 | 112 | - name: Use Node.js from .tool-versions 113 | uses: actions/setup-node@v3 114 | with: 115 | node-version: ${{ steps.tool-versions.outputs.node_version }} 116 | cache: 'yarn' 117 | cache-dependency-path: '**/yarn.lock' 118 | 119 | - name: Setup Yarn from .tool-versions 120 | run: | 121 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 122 | 123 | - name: Restore node_modules 124 | uses: actions/cache@v3 125 | id: restore-build 126 | with: 127 | path: node_modules 128 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 129 | 130 | - name: Run build 131 | run: yarn build 132 | 133 | deadcode: 134 | name: Find dead code 135 | runs-on: ubuntu-latest 136 | needs: install 137 | steps: 138 | - name: Checkout 139 | uses: actions/checkout@v3 140 | with: 141 | fetch-depth: 1 142 | 143 | - name: Read .tool-versions 144 | id: tool-versions 145 | run: | 146 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 147 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 148 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 149 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 150 | 151 | - name: Use Node.js from .tool-versions 152 | uses: actions/setup-node@v3 153 | with: 154 | node-version: ${{ steps.tool-versions.outputs.node_version }} 155 | cache: 'yarn' 156 | cache-dependency-path: '**/yarn.lock' 157 | 158 | - name: Setup Yarn from .tool-versions 159 | run: | 160 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 161 | 162 | - name: Restore node_modules 163 | uses: actions/cache@v3 164 | id: restore-build 165 | with: 166 | path: node_modules 167 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 168 | 169 | - name: Find dead code 170 | run: yarn deadCode 171 | 172 | version-check: 173 | name: Check package version 174 | runs-on: ubuntu-latest 175 | steps: 176 | - name: Checkout PR 177 | uses: actions/checkout@v3 178 | with: 179 | fetch-depth: 1 180 | 181 | - name: Checkout main 182 | uses: actions/checkout@v3 183 | with: 184 | ref: main 185 | path: main 186 | fetch-depth: 1 187 | 188 | - name: Compare versions 189 | id: version_check 190 | run: | 191 | PR_VERSION=$(node -p "require('./package.json').version") 192 | MAIN_VERSION=$(node -p "require('./main/package.json').version") 193 | if [ "$PR_VERSION" = "$MAIN_VERSION" ]; then 194 | echo "ERROR_MESSAGE=Error: Package version has not been updated" >> $GITHUB_OUTPUT 195 | exit 1 196 | elif ! npx semver -r ">$MAIN_VERSION" "$PR_VERSION" > /dev/null; then 197 | echo "ERROR_MESSAGE=Error: New version ($PR_VERSION) is not greater than current version ($MAIN_VERSION)" >> $GITHUB_OUTPUT 198 | exit 1 199 | else 200 | echo "Package version has been updated correctly" 201 | fi 202 | 203 | - name: Comment PR 204 | if: failure() 205 | uses: actions/github-script@v6 206 | with: 207 | github-token: ${{secrets.GITHUB_TOKEN}} 208 | script: | 209 | github.rest.issues.createComment({ 210 | issue_number: context.issue.number, 211 | owner: context.repo.owner, 212 | repo: context.repo.repo, 213 | body: '${{ steps.version_check.outputs.ERROR_MESSAGE }}' 214 | }) 215 | 216 | check-v9-example: 217 | name: Check v9-flat example for eslint errors 218 | runs-on: ubuntu-latest 219 | needs: install 220 | steps: 221 | - name: Checkout 222 | uses: actions/checkout@v3 223 | with: 224 | fetch-depth: 1 225 | 226 | - name: Use Node.js 23.10.0 227 | uses: actions/setup-node@v3 228 | with: 229 | node-version: 23.10.0 230 | cache: 'yarn' 231 | cache-dependency-path: '**/yarn.lock' 232 | 233 | - name: Setup Yarn 1.22.22 234 | run: | 235 | npm install -g yarn@1.22.22 236 | 237 | - name: Restore node_modules 238 | uses: actions/cache@v3 239 | id: restore-build 240 | with: 241 | path: node_modules 242 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 243 | 244 | - name: Build plugin 245 | run: | 246 | yarn 247 | yarn build 248 | 249 | - name: Check for eslint errors in v9-flat example 250 | run: | 251 | cd examples/v9-flat 252 | yarn 253 | if ! yarn lint 2>&1 | grep -q "@arthurgeron/react-usememo/require-usememo"; then 254 | echo "Error: Expected eslint error '@arthurgeron/react-usememo/require-usememo' not found in v9-flat example" 255 | exit 1 256 | fi 257 | 258 | check-v8-example: 259 | name: Check v8-traditional example for eslint errors 260 | runs-on: ubuntu-latest 261 | needs: install 262 | steps: 263 | - name: Checkout 264 | uses: actions/checkout@v3 265 | with: 266 | fetch-depth: 1 267 | 268 | - name: Use Node.js 23.10.0 269 | uses: actions/setup-node@v3 270 | with: 271 | node-version: 23.10.0 272 | cache: 'yarn' 273 | cache-dependency-path: '**/yarn.lock' 274 | 275 | - name: Setup Yarn 1.22.22 276 | run: | 277 | npm install -g yarn@1.22.22 278 | 279 | - name: Restore node_modules 280 | uses: actions/cache@v3 281 | id: restore-build 282 | with: 283 | path: node_modules 284 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 285 | 286 | - name: Build plugin 287 | run: | 288 | yarn 289 | yarn build 290 | 291 | - name: Check for eslint errors in v8-traditional example 292 | run: | 293 | cd examples/v8-traditional 294 | yarn 295 | if ! yarn lint 2>&1 | grep -q "@arthurgeron/react-usememo/require-usememo"; then 296 | echo "Error: Expected eslint error '@arthurgeron/react-usememo/require-usememo' not found in v8-traditional example" 297 | exit 1 298 | fi 299 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | install: 9 | name: Installing packages 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 1 16 | 17 | - name: Read .tool-versions 18 | id: tool-versions 19 | run: | 20 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 21 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 22 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 23 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 24 | 25 | - name: Use Node.js from .tool-versions 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ steps.tool-versions.outputs.node_version }} 29 | cache: 'yarn' 30 | cache-dependency-path: '**/yarn.lock' 31 | 32 | - name: Setup Yarn from .tool-versions 33 | run: | 34 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 35 | 36 | - name: Cache node modules 37 | uses: actions/cache@v3 38 | id: cache 39 | with: 40 | path: node_modules 41 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 42 | restore-keys: | 43 | ${{ runner.os }}- 44 | 45 | - name: Install Dependencies 46 | if: steps.cache.outputs.cache-hit != 'true' # It will run install dependencies just if hash of yarn.lock changed 47 | run: yarn install --frozen-lockfile --prefer-offline 48 | env: 49 | NODE_AUTH_TOKEN: ${{ secrets.GIT_PACKAGES_TOKEN }} 50 | 51 | test: 52 | name: Run Unit Tests 53 | runs-on: ubuntu-latest 54 | needs: install 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 1 60 | 61 | - name: Read .tool-versions 62 | id: tool-versions 63 | run: | 64 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 65 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 66 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 67 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 68 | 69 | - name: Use Node.js from .tool-versions 70 | uses: actions/setup-node@v3 71 | with: 72 | node-version: ${{ steps.tool-versions.outputs.node_version }} 73 | cache: 'yarn' 74 | cache-dependency-path: '**/yarn.lock' 75 | 76 | - name: Setup Yarn from .tool-versions 77 | run: | 78 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 79 | 80 | - name: Restore node_modules 81 | uses: actions/cache@v3 82 | id: restore-build 83 | with: 84 | path: node_modules 85 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 86 | 87 | - name: Restore jest cache 88 | uses: actions/cache@v3 89 | with: 90 | path: .jest-cache 91 | key: ${{ runner.os }}-${{ hashFiles('**/.jest-cache/**') }} 92 | 93 | - name: Run tests 94 | run: yarn test 95 | 96 | build: 97 | name: Test build 98 | runs-on: ubuntu-latest 99 | needs: install 100 | steps: 101 | - name: Checkout 102 | uses: actions/checkout@v3 103 | with: 104 | fetch-depth: 1 105 | 106 | - name: Read .tool-versions 107 | id: tool-versions 108 | run: | 109 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 110 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 111 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 112 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 113 | 114 | - name: Use Node.js from .tool-versions 115 | uses: actions/setup-node@v3 116 | with: 117 | node-version: ${{ steps.tool-versions.outputs.node_version }} 118 | cache: 'yarn' 119 | cache-dependency-path: '**/yarn.lock' 120 | 121 | - name: Setup Yarn from .tool-versions 122 | run: | 123 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 124 | 125 | - name: Restore node_modules 126 | uses: actions/cache@v3 127 | id: restore-build 128 | with: 129 | path: node_modules 130 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 131 | 132 | - name: Run build 133 | run: yarn build 134 | 135 | deadcode: 136 | name: Find dead code 137 | runs-on: ubuntu-latest 138 | needs: install 139 | steps: 140 | - name: Checkout 141 | uses: actions/checkout@v3 142 | with: 143 | fetch-depth: 1 144 | 145 | - name: Read .tool-versions 146 | id: tool-versions 147 | run: | 148 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 149 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 150 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 151 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 152 | 153 | - name: Use Node.js from .tool-versions 154 | uses: actions/setup-node@v3 155 | with: 156 | node-version: ${{ steps.tool-versions.outputs.node_version }} 157 | cache: 'yarn' 158 | cache-dependency-path: '**/yarn.lock' 159 | 160 | - name: Setup Yarn from .tool-versions 161 | run: | 162 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 163 | 164 | - name: Restore node_modules 165 | uses: actions/cache@v3 166 | id: restore-build 167 | with: 168 | path: node_modules 169 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 170 | 171 | - name: Find dead code 172 | run: yarn deadCode 173 | 174 | release: 175 | name: Release version 176 | runs-on: ubuntu-latest 177 | needs: [test, build, deadcode] 178 | steps: 179 | - name: Checkout 180 | uses: actions/checkout@v3 181 | with: 182 | fetch-depth: 1 183 | 184 | - name: Read .tool-versions 185 | id: tool-versions 186 | run: | 187 | NODE_VERSION=$(grep "nodejs" .tool-versions | awk '{print $2}') 188 | YARN_VERSION=$(grep "yarn" .tool-versions | awk '{print $2}') 189 | echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT 190 | echo "yarn_version=$YARN_VERSION" >> $GITHUB_OUTPUT 191 | 192 | - name: Use Node.js from .tool-versions 193 | uses: actions/setup-node@v3 194 | with: 195 | node-version: ${{ steps.tool-versions.outputs.node_version }} 196 | cache: 'yarn' 197 | cache-dependency-path: '**/yarn.lock' 198 | registry-url: 'https://registry.npmjs.org' 199 | scope: '@arthurgeron' 200 | 201 | - name: Setup Yarn from .tool-versions 202 | run: | 203 | npm install -g yarn@${{ steps.tool-versions.outputs.yarn_version }} 204 | 205 | - name: Restore node_modules 206 | uses: actions/cache@v3 207 | id: restore-build 208 | with: 209 | path: node_modules 210 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 211 | 212 | - name: Publish Release 213 | uses: LumaKernel/npm-release-pack-action@v1.0.1 214 | id: release 215 | env: 216 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 217 | with: 218 | github_token: ${{ secrets.GITHUB_TOKEN }} 219 | publish_command: npm publish --access public 220 | generate_release_notes: true 221 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | /dist/ 3 | .vscode 4 | yarn-error.log 5 | .jest-cache 6 | coverage -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit "${1}" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn deadCode # find unused exports 2 | yarn test 3 | yarn build 4 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arthurgeron/eslint-plugin-react-usememo/d036dad6a27c2eb038a25482bbbd9a4ad0732917/.lintstagedrc.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @arthurgeron:registry=https://registry.npmjs.org/ 2 | save-exact = true -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 23.10.0 2 | yarn 1.22.22 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESLint-Plugin-React-UseMemo 2 | 3 | This plugin enforces the wrapping of complex objects or functions (which might generate unnecessary renders or side-effects) in `useMemo` or `useCallback`. It also allows you to programmatically enforce the wrapping of functional components in `memo`, and that all props and dependencies are wrapped in `useMemo`/`useCallback`. 4 | 5 | ## Purpose 6 | The objective is to ensure that your application's component tree and/or expensive lifecycles (such as React Native's FlatLists, useEffect, useMemo, etc.) only re-calculate or render again when absolutely necessary. By controlling expensive expressions, you can achieve optimal scalability and performance for your application. 7 | 8 | _**Note:**_ Use of memoization everywhere is not advised, as everything comes with a cost. Overusing memoization might slow down your application instead of speeding it up. 9 | 10 | ## Guidelines for Memoization 11 | > For more details, please refer to React's [documentation](https://react.dev/reference/react/useMemo) on hooks, re-rendering and memoization. 12 | ### There are two primary rules for situations where dynamic objects should be memoed: 13 | 14 | 1. Variables or expressions that return non-primitive objects or functions passed as props to other components. 15 | 16 | ***Incorrect*** 17 | ```js 18 | function Component({incomingData}) { 19 | const complexData = { 20 | ...incomingData, 21 | checked: true 22 | }; // generated each render, breaks hooks shallow comparison 23 | 24 | return 25 | } 26 | ``` 27 | ***Correct*** 28 | ```js 29 | function Component({incomingData}) { 30 | const complexData = useMemo(() => ({ 31 | ...incomingData, 32 | checked: true 33 | }), [incomingData]); // generated only when incomingData changes 34 | 35 | return 36 | } 37 | ``` 38 | 39 | 2. Variables or expressions that return non-primitive objects returned from custom hooks. 40 | 41 | ***Incorrect*** 42 | ```js 43 | function useMyData({incomingData}) { 44 | const parsedData = parseData(incomingData); // generated each render 45 | 46 | return parsedData; // Will result in loops passed as a dependency in other hooks(e.g. useMemo, useCallback, useEffect). 47 | } 48 | ``` 49 | ***Correct*** 50 | ```js 51 | function useMyData({incomingData}) { 52 | const parsedData = useMemo(() => parseData(incomingData), [incomingData]); // generated only when incomingData changes 53 | 54 | return parsedData; // Won't generate loops if used as a dependency in hooks. 55 | } 56 | ``` 57 | 58 | ### It is not recommended to use memoization in the following cases: 59 | 60 | - When the resulting value (expression or variable) is primitive (string, number, boolean). 61 | 62 | ***Incorrect*** 63 | ```js 64 | function Component() { 65 | const width = useMemo(() => someValue * 10, []); // results in integer, wouldn't break hooks' shallow comparison; Memoizing this would only reduce performance 66 | 67 | return 68 | } 69 | ``` 70 | ***Correct*** 71 | ```js 72 | function Component() { 73 | const width = someValue * 10; 74 | 75 | return 76 | } 77 | ``` 78 | 79 | - If you're passing props to a native component of the framework (e.g. Div, Touchable, etc), except in some instances in react-native (e.g. FlatList). 80 | 81 | ***Incorrect*** 82 | ```js 83 | function Component() { 84 | const onClick = useCallback(() => {}, []); 85 | 86 | return
87 | } 88 | ``` 89 | ***Correct*** 90 | ```js 91 | function Component() { 92 | const onClick = () => {}; 93 | 94 | return
95 | } 96 | ``` 97 | 98 | - Values that can be a global/context outside the react Context. 99 | ***Incorrect*** 100 | ```js 101 | function Component() { 102 | const breakpoints = [100]; 103 | 104 | return 105 | } 106 | ``` 107 | 108 | ***Correct*** 109 | ```js 110 | const breakpoints = [100]; 111 | 112 | function Component() { 113 | return 114 | } 115 | ``` 116 | 117 | 118 | 119 | 120 | ## Installation 121 | 122 | Install it with yarn: 123 | 124 | ``` 125 | yarn add @arthurgeron/eslint-plugin-react-usememo --dev 126 | ``` 127 | 128 | or npm: 129 | 130 | ``` 131 | npm install @arthurgeron/eslint-plugin-react-usememo --save-dev 132 | ``` 133 | 134 | ## Compatibility 135 | 136 | This plugin supports ESLint v8 (traditional configuration) and is preparing support for ESLint v9 (flat configuration). 137 | 138 | > **Important Note**: While the plugin exports the proper structure for ESLint v9, there are some compatibility issues with ESLint v9's new architecture. We're actively working on resolving these issues. For now, please continue using ESLint v8 with the traditional configuration. 139 | 140 | ### ESLint v8 Configuration (.eslintrc) 141 | 142 | Add the plugin and enable the rules in your `.eslintrc` file: 143 | 144 | ```json 145 | { 146 | "plugins": ["@arthurgeron/react-usememo"], 147 | "rules": { 148 | "@arthurgeron/react-usememo/require-usememo": "error", 149 | "@arthurgeron/react-usememo/require-memo": "error", 150 | "@arthurgeron/react-usememo/require-usememo-children": "error" 151 | } 152 | } 153 | ``` 154 | 155 | ### ESLint v9 Configuration (eslint.config.js) 156 | 157 | ESLint v9 support is in progress. Once fully compatible, you'll be able to use the plugin in the following ways: 158 | 159 | #### Option 1: Importing the default config 160 | 161 | ```js 162 | import plugin from '@arthurgeron/eslint-plugin-react-usememo'; 163 | export default [ 164 | { 165 | files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], 166 | languageOptions: { 167 | ecmaVersion: 2020, 168 | sourceType: 'module', 169 | parserOptions: { 170 | ecmaFeatures: { 171 | jsx: true 172 | } 173 | } 174 | }, 175 | plugins: { 176 | '@arthurgeron/react-usememo': plugin.flatConfig, 177 | }, 178 | rules: { 179 | '@arthurgeron/react-usememo/require-usememo': 'error', 180 | '@arthurgeron/react-usememo/require-memo': 'error', 181 | '@arthurgeron/react-usememo/require-usememo-children': 'error', 182 | }, 183 | }, 184 | ]; 185 | ``` 186 | 187 | #### Option 2: Using the recommended config 188 | 189 | ```js 190 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 191 | 192 | export default [ 193 | // Other configs... 194 | flatConfig.configs.recommended, 195 | ]; 196 | ``` 197 | 198 | #### Option 3: Using CommonJS syntax 199 | 200 | ```js 201 | const { flatConfig } = require('@arthurgeron/eslint-plugin-react-usememo'); 202 | 203 | module.exports = [ 204 | // Other configs... 205 | { 206 | plugins: { 207 | '@arthurgeron/react-usememo': flatConfig, 208 | }, 209 | rules: { 210 | '@arthurgeron/react-usememo/require-usememo': 'error', 211 | '@arthurgeron/react-usememo/require-memo': 'error', 212 | '@arthurgeron/react-usememo/require-usememo-children': 'error', 213 | }, 214 | }, 215 | ]; 216 | ``` 217 | 218 | For detailed migration guidance from ESLint v8 to ESLint v9, please refer to our [ESLint v9 Migration Guide](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/eslint-v9-migration-guide.md). 219 | 220 | ## Examples 221 | 222 | For working examples of both ESLint v8 (traditional config) and ESLint v9 (flat config) implementations, please check the [examples directory](https://github.com/arthurgeron/eslint-plugin-react-usememo/tree/main/examples). 223 | 224 | ## Rule #1: `require-usememo` ***(recommended)*** 225 | This rule requires complex values (objects, arrays, functions, and JSX) that get passed props or referenced as a hook dependency to be wrapped in useMemo() or useCallback(). 226 | 227 | One of the great features of this rule is its amazing autofix functionality. It intelligently wraps necessary components with useMemo() or useCallback(), making your code more efficient and saving you valuable time. 228 | 229 | For detailed examples, options available for this rule, and information about the autofix functionality, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-usememo.md). 230 | 231 | ## Rule #2: `require-memo` 232 | This rule requires all function components to be wrapped in `React.memo()`. 233 | 234 | For detailed examples and usage of this rule, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-memo.md) 235 | 236 | ## Rule #3: `require-usememo-children` 237 | This rule requires complex values (objects, arrays, functions, and JSX) that get passed as children to be wrapped in `useMemo()` or `useCallback()`. 238 | 239 | For detailed examples and options available for this rule, please refer to our [rules documentation](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-usememo-children.md). 240 | 241 | ## Conclusion 242 | By efficiently using `useMemo`, `useCallback`, and `React.memo()`, we can optimize our React and React Native applications. It allows us to control the re-calculation and re-rendering of components, offering better scalability and performance. 243 | -------------------------------------------------------------------------------- /__tests__/eslint-v8/ruleTester.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ESLint V8 Rule Tester Configuration 3 | * Use this for testing rules with ESLint v8 4 | */ 5 | import { RuleTester } from "eslint"; 6 | 7 | export const ruleTester = new RuleTester({ 8 | parser: require.resolve("@typescript-eslint/parser"), 9 | parserOptions: { 10 | ecmaVersion: 2020, 11 | sourceType: "module", 12 | ecmaFeatures: { jsx: true }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /__tests__/eslint-v8/rules.test.ts: -------------------------------------------------------------------------------- 1 | import { createRequireMemoTestCases } from "../testcases/requireMemo"; 2 | import { createRequireUseCallbackTestCases } from "../testcases/requireUseCallback"; 3 | import { createRequireUseMemoTestCases } from "../testcases/requireUseMemo"; 4 | import { createRequireUseMemoChildrenTestCases } from "../testcases/requireUseMemoChildren"; 5 | import { ruleTester } from "./ruleTester"; 6 | import requireMemo from "../../src/require-memo"; 7 | import requireUseCallback from "../../src/require-usememo"; 8 | import requireUseMemo from "../../src/require-usememo"; 9 | import requireUseMemoChildren from "../../src/require-usememo-children"; 10 | import type { Rule } from "eslint"; 11 | 12 | const { 13 | validTestCases: validMemoTestCases, 14 | invalidTestCases: invalidMemoTestCases, 15 | } = createRequireMemoTestCases(); 16 | 17 | const { 18 | validTestCases: validUseCallbackTestCases, 19 | invalidTestCases: invalidUseCallbackTestCases, 20 | } = createRequireUseCallbackTestCases(); 21 | 22 | const { 23 | validTestCases: validUseMemoTestCases, 24 | invalidTestCases: invalidUseMemoTestCases, 25 | } = createRequireUseMemoTestCases(); 26 | 27 | const { 28 | validTestCases: validUseMemoChildrenTestCases, 29 | invalidTestCases: invalidUseMemoChildrenTestCases, 30 | } = createRequireUseMemoChildrenTestCases(); 31 | 32 | describe("ESLint v8", () => { 33 | describe("Require", () => { 34 | ruleTester.run("Memo", requireMemo as unknown as Rule.RuleModule, { 35 | valid: validMemoTestCases as any, 36 | invalid: invalidMemoTestCases as any, 37 | }); 38 | 39 | ruleTester.run( 40 | "useCallback", 41 | requireUseCallback as unknown as Rule.RuleModule, 42 | { 43 | valid: validUseCallbackTestCases as any, 44 | invalid: invalidUseCallbackTestCases as any, 45 | }, 46 | ); 47 | 48 | ruleTester.run("useMemo", requireUseMemo as unknown as Rule.RuleModule, { 49 | valid: validUseMemoTestCases as any, 50 | invalid: invalidUseMemoTestCases as any, 51 | }); 52 | 53 | ruleTester.run( 54 | "useMemo children", 55 | requireUseMemoChildren as unknown as Rule.RuleModule, 56 | { 57 | valid: validUseMemoChildrenTestCases as any, 58 | invalid: invalidUseMemoChildrenTestCases as any, 59 | }, 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/eslint-v9/ruleTester.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ESLint V9 Rule Tester Configuration 3 | * Use this for testing rules with ESLint v9 flat config format 4 | * 5 | * Note: When running tests with ESLint v9, you need to use ESLint v9 installed 6 | * This file is designed to work when you switch to ESLint v9 7 | */ 8 | import { RuleTester } from "eslint-v9"; 9 | 10 | export const createRuleTesterV9 = () => { 11 | // For ESLint v9, we need to use the flat config format with jsx support 12 | return new RuleTester({ 13 | languageOptions: { 14 | parser: require('@typescript-eslint/parser'), 15 | ecmaVersion: 2020, 16 | sourceType: 'module', 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true 20 | } 21 | } 22 | } 23 | }); 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /__tests__/eslint-v9/rules.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { createRequireMemoTestCases } from "../testcases/requireMemo"; 3 | import { createRequireUseCallbackTestCases } from "../testcases/requireUseCallback"; 4 | import { createRequireUseMemoTestCases } from "../testcases/requireUseMemo"; 5 | import { createRequireUseMemoChildrenTestCases } from "../testcases/requireUseMemoChildren"; 6 | import { createRuleTesterV9 } from "./ruleTester"; 7 | import requireMemo from "../../src/require-memo"; 8 | import requireUseCallback from "../../src/require-usememo"; 9 | import requireUseMemo from "../../src/require-usememo"; 10 | import requireUseMemoChildren from "../../src/require-usememo-children"; 11 | import type { Rule } from "eslint-v9"; 12 | 13 | const ruleTesterV9 = createRuleTesterV9(); 14 | 15 | const { 16 | validTestCases: validMemoTestCases, 17 | invalidTestCases: invalidMemoTestCases, 18 | } = createRequireMemoTestCases(); 19 | const { 20 | validTestCases: validUseCallbackTestCases, 21 | invalidTestCases: invalidUseCallbackTestCases, 22 | } = createRequireUseCallbackTestCases(); 23 | const { 24 | validTestCases: validUseMemoTestCases, 25 | invalidTestCases: invalidUseMemoTestCases, 26 | } = createRequireUseMemoTestCases(); 27 | const { 28 | validTestCases: validUseMemoChildrenTestCases, 29 | invalidTestCases: invalidUseMemoChildrenTestCases, 30 | } = createRequireUseMemoChildrenTestCases(); 31 | 32 | describe("ESLint v9", () => { 33 | describe("Require", () => { 34 | ruleTesterV9.run( 35 | "Memo", 36 | requireMemo as unknown as Rule.RuleModule, 37 | { 38 | valid: validMemoTestCases as any, 39 | invalid: invalidMemoTestCases as any, 40 | }, 41 | ); 42 | 43 | ruleTesterV9.run( 44 | "useCallback", 45 | requireUseCallback as unknown as Rule.RuleModule, 46 | { 47 | valid: validUseCallbackTestCases as any, 48 | invalid: invalidUseCallbackTestCases as any, 49 | }, 50 | ); 51 | 52 | ruleTesterV9.run("useMemo", requireUseMemo as unknown as Rule.RuleModule, { 53 | valid: validUseMemoTestCases as any, 54 | invalid: invalidUseMemoTestCases as any, 55 | }); 56 | 57 | ruleTesterV9.run( 58 | "useMemo children", 59 | requireUseMemoChildren as unknown as Rule.RuleModule, 60 | { 61 | valid: validUseMemoChildrenTestCases as any, 62 | invalid: invalidUseMemoChildrenTestCases as any, 63 | }, 64 | ); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/testcases/requireMemo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared test cases for ESLint plugin rules 3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations 4 | */ 5 | 6 | // Helper to create a standard set of test cases for require-memo 7 | export const createRequireMemoTestCases = () => { 8 | const validTestCases = [ 9 | // Valid case: Component wrapped in memo 10 | { 11 | code: ` 12 | import React, { memo } from 'react'; 13 | const Component = memo(() =>
); 14 | export default Component; 15 | `, 16 | }, 17 | // Valid case: Default export wrapped in memo 18 | { 19 | code: ` 20 | import React, { memo } from 'react'; 21 | export default memo(() =>
); 22 | `, 23 | }, 24 | // Valid case: Named export wrapped in memo 25 | { 26 | code: ` 27 | import React, { memo } from 'react'; 28 | export const Component = memo(() =>
); 29 | `, 30 | }, 31 | // Valid case: Expression wrapped in memo 32 | { 33 | code: ` 34 | import React, { memo } from 'react'; 35 | const Component = memo(() => { 36 | return
; 37 | }); 38 | export default Component; 39 | `, 40 | }, 41 | // Valid case: Alternative memo import 42 | { 43 | code: ` 44 | import { memo } from 'react'; 45 | export default memo(() =>
); 46 | `, 47 | }, 48 | // Valid case: Full React import with memo 49 | { 50 | code: ` 51 | import React from 'react'; 52 | export default React.memo(() =>
); 53 | `, 54 | }, 55 | // Additional cases from require-memo.test.ts 56 | { code: "export const TestMap = {};" }, 57 | { code: "const TestMap = {}; export default TestMap;" }, 58 | { code: "export default {};" }, 59 | { code: "export const SomethingWeird = func()();" }, 60 | { code: "export const Component = useRef(() =>
)" }, 61 | { code: "export const Component = useRef(function() { return
; })" }, 62 | { code: "export const variable = func()();" }, 63 | { code: "export const Component = React.memo(() =>
)" }, 64 | { code: "export const Component = memo(() =>
)" }, 65 | { code: "const Component = memo(() =>
); export default Component;" }, 66 | { code: "export default memo(function Component() { return
; })" }, 67 | { code: "export const Component = memo(useRef(() =>
))" }, 68 | { code: "const Component = React.useRef(React.memo(() =>
))" }, 69 | { code: "export const myFunction = () =>
" }, 70 | { code: "export const myFunction = wrapper(() =>
)" }, 71 | { code: "export const Component = React.memo(function() { return
; });" }, 72 | { code: "export const Component = memo(function Component() { return
; });" }, 73 | { code: "function myFunction() { return
; }" }, 74 | { code: "const myFunction = wrapper(function() { return
})" }, 75 | { 76 | code: "export default function() { return
};", 77 | filename: "dir/myFunction.js", 78 | }, 79 | { code: "const Component = () =>
; export default memo(Component);" }, 80 | { 81 | code: "export const Component = () =>
", 82 | options: [{ 83 | ignoredComponents: { 84 | 'Component': true, 85 | } 86 | }] 87 | }, 88 | ]; 89 | 90 | const invalidTestCases = [ 91 | // Invalid case: Component not wrapped in memo 92 | { 93 | code: ` 94 | import React from 'react'; 95 | const Component = () =>
; 96 | export default Component; 97 | `, 98 | errors: [{ messageId: 'memo-required' }], 99 | }, 100 | // Invalid case: Default export not wrapped in memo 101 | { 102 | code: ` 103 | import React from 'react'; 104 | export default () =>
; 105 | `, 106 | errors: [{ messageId: 'memo-required' }], 107 | }, 108 | // Invalid case: Named export not wrapped in memo 109 | { 110 | code: ` 111 | import React from 'react'; 112 | export const Component = () =>
; 113 | `, 114 | errors: [{ messageId: 'memo-required' }], 115 | }, 116 | // Invalid case: Component with expression not wrapped in memo 117 | { 118 | code: ` 119 | import React from 'react'; 120 | const Component = () => { 121 | return
; 122 | }; 123 | export default Component; 124 | `, 125 | errors: [{ messageId: 'memo-required' }], 126 | }, 127 | // Additional cases from require-memo.test.ts 128 | { 129 | code: "export const Component = () =>
", 130 | errors: [{ messageId: "memo-required" }], 131 | }, 132 | { 133 | code: "export const ListItem = () =>
", 134 | errors: [{ messageId: "memo-required" }], 135 | options: [{ 136 | ignoredComponents: { 137 | '*Item': false, 138 | '*': true 139 | } 140 | }] 141 | }, 142 | { 143 | code: "const Component = () =>
; export default Component;", 144 | errors: [{ messageId: "memo-required" }], 145 | }, 146 | { 147 | code: "export const Component = function Component() { return
; }", 148 | errors: [{ messageId: "memo-required" }], 149 | }, 150 | { 151 | code: "export function Component() { return
; }", 152 | errors: [{ messageId: "memo-required" }], 153 | }, 154 | { 155 | code: "export default function Component() { return
; }", 156 | errors: [{ messageId: "memo-required" }], 157 | }, 158 | ]; 159 | 160 | return { validTestCases, invalidTestCases }; 161 | }; 162 | 163 | -------------------------------------------------------------------------------- /__tests__/testcases/requireUseCallback.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared test cases for ESLint plugin rules 3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations 4 | */ 5 | 6 | // Helper to create a standard set of test cases for require-usecallback 7 | export const createRequireUseCallbackTestCases = () => { 8 | const validTestCases = [ 9 | { 10 | code: `const Component = () => { 11 | const myFn = useCallback(function() {}, []); 12 | return ; 13 | }`, 14 | }, 15 | { 16 | code: `const Component = () => { 17 | const myFn = useCallback(() => {}, []); 18 | return ; 19 | }`, 20 | }, 21 | { 22 | code: `const Component = () => { 23 | const myFn = function() {}; 24 | return
; 25 | }`, 26 | }, 27 | { 28 | code: `class Component { 29 | render() { 30 | const myFn = function() {}; 31 | return
; 32 | } 33 | }`, 34 | }, 35 | { 36 | code: ` 37 | const myFn = function() {}; 38 | const Component = () => { 39 | return ; 40 | }`, 41 | }, 42 | { 43 | code: ` 44 | const myFn = function() {}; 45 | class Component { 46 | render() { 47 | return ; 48 | } 49 | }`, 50 | }, 51 | { 52 | code: `const Component = () => { 53 | const myFn = () => {}; 54 | return
; 55 | }`, 56 | }, 57 | { 58 | code: ` 59 | const myFn = () => {}; 60 | const Component = () => { 61 | return
; 62 | }`, 63 | }, 64 | { 65 | code: `const Component = () => { 66 | const myFn1 = useCallback(() => [], []); 67 | const myFn2 = useCallback(() => myFn1, [myFn1]); 68 | return ; 69 | }`, 70 | }, 71 | { 72 | code: ` 73 | class Component { 74 | myFn() {} 75 | render() { 76 | return ; 77 | } 78 | }`, 79 | }, 80 | { 81 | code: `const Component = () => { 82 | const myFn = memoize(() => {}); 83 | return ; 84 | }`, 85 | }, 86 | { 87 | code: `const Component = () => { 88 | const myFn = lodash.memoize(() => []); 89 | return ; 90 | }`, 91 | }, 92 | { 93 | code:`const Component = () => { 94 | const myFn1 = () => []; 95 | const myFn2 = useCallback(() => myFn1, [myFn1]); 96 | return ; 97 | }`, 98 | }, 99 | { 100 | code: `const Component = () => { 101 | const myFn = useMemo(() => function() {}, []); 102 | return ; 103 | }`, 104 | } 105 | ]; 106 | 107 | const invalidTestCases = [ 108 | { 109 | code: ` 110 | const Component = () => { 111 | const myFn = function myFn() {}; 112 | return ; 113 | }`, 114 | output: `import { useCallback } from 'react'; 115 | 116 | const Component = () => { 117 | const myFn = useCallback(() => {}, []); 118 | return ; 119 | }`, 120 | errors: [{ messageId: "function-usecallback-props" }], 121 | }, 122 | { 123 | code: ` 124 | const Component = () => { 125 | const myFn = () => {}; 126 | return ; 127 | }`, 128 | output: `import { useCallback } from 'react'; 129 | 130 | const Component = () => { 131 | const myFn = useCallback(() => {}, []); 132 | return ; 133 | }`, 134 | errors: [{ messageId: "function-usecallback-props" }], 135 | }, 136 | { 137 | code: ` 138 | const Component = () => { 139 | let myFn = useCallback(() => ({})); 140 | myFn = () => ({}); 141 | return ; 142 | }`, 143 | output: ` 144 | const Component = () => { 145 | const myFn = useCallback(() => ({})); 146 | myFn = () => ({}); 147 | return ; 148 | }`, 149 | errors: [{ messageId: "usememo-const" }], 150 | }, 151 | { 152 | code: ` 153 | const Component = () => { 154 | return {}} />; 155 | }`, 156 | errors: [{ messageId: "function-usecallback-props" }], 157 | output: `import { useCallback } from 'react'; 158 | 159 | const Component = () => { 160 | const prop = useCallback(() => {}, []); 161 | return ; 162 | }`, 163 | }, 164 | { 165 | code: ` 166 | const Component = () => { 167 | return []} />; 168 | }`, 169 | errors: [{ messageId: "function-usecallback-props" }], 170 | output: `import { useCallback } from 'react'; 171 | 172 | const Component = () => { 173 | const prop = useCallback(() => [], []); 174 | return ; 175 | }`, 176 | }, 177 | { 178 | code: `class Component { 179 | return () { 180 | return []} />; 181 | } 182 | }`, 183 | errors: [{ messageId: "instance-class-memo-props" }], 184 | }, 185 | { 186 | code: ` 187 | const Component = () => { 188 | const myFn1 = () => []; 189 | const myFn2 = useCallback(() => [], []); 190 | return ; 191 | }`, 192 | output: `import { useCallback } from 'react'; 193 | 194 | const Component = () => { 195 | const myFn1 = useCallback(() => [], []); 196 | const myFn2 = useCallback(() => [], []); 197 | return ; 198 | }`, 199 | errors: [{ messageId: "function-usecallback-props" }], 200 | }, 201 | { 202 | code: `const Component = () => { 203 | const myFn = memoize(() => {}); 204 | return ; 205 | }`, 206 | options: [{ strict: true }], 207 | errors: [{ messageId: "unknown-usememo-props" }], 208 | }, 209 | { 210 | code: `const Component = () => { 211 | const myFn = lodash.memoize(() => []); 212 | return ; 213 | }`, 214 | options: [{ strict: true }], 215 | errors: [{ messageId: "unknown-usememo-props" }], 216 | }, 217 | { 218 | code: `class Component { 219 | render() { 220 | const myFn = lodash.memoize(() => []); 221 | return ; 222 | } 223 | }`, 224 | options: [{ strict: true }], 225 | errors: [{ messageId: "unknown-class-memo-props" }], 226 | }, 227 | { 228 | code: ` 229 | const Component = () => { 230 | const myFn = function test() {}; 231 | return ; 232 | }`, 233 | output: `import { useCallback } from 'react'; 234 | 235 | const Component = () => { 236 | const myFn = useCallback(() => {}, []); 237 | return ; 238 | }`, 239 | errors: [{ messageId: "function-usecallback-props" }], 240 | }, 241 | { 242 | code: ` 243 | const Component = () => { 244 | const myFn = () => []; 245 | return ; 246 | }`, 247 | output: `import { useCallback } from 'react'; 248 | 249 | const Component = () => { 250 | const myFn = useCallback(() => [], []); 251 | return ; 252 | }`, 253 | errors: [{ messageId: "function-usecallback-props" }], 254 | }, 255 | { 256 | code: `import { useMemo } from 'react'; 257 | 258 | const Component = () => { 259 | const myFn = () => []; 260 | const myFn2 = () => []; 261 | return ; 262 | }`, 263 | output: `import { useMemo, useCallback } from 'react'; 264 | 265 | const Component = () => { 266 | const myFn = useCallback(() => [], []); 267 | const myFn2 = useCallback(() => [], []); 268 | return ; 269 | }`, 270 | errors: [{ messageId: "function-usecallback-props" }, { messageId: "function-usecallback-props" }], 271 | }, 272 | { 273 | code: ` 274 | const Component = () => { 275 | const myFn = async function test() {}; 276 | return ; 277 | }`, 278 | output: `import { useCallback } from 'react'; 279 | 280 | const Component = () => { 281 | const myFn = useCallback(async () => {}, []); 282 | return ; 283 | }`, 284 | errors: [{ messageId: "function-usecallback-props" }], 285 | }, 286 | { 287 | code: ` 288 | const Component = () => { 289 | const myFn = async () => []; 290 | return ; 291 | }`, 292 | output: `import { useCallback } from 'react'; 293 | 294 | const Component = () => { 295 | const myFn = useCallback(async () => [], []); 296 | return ; 297 | }`, 298 | errors: [{ messageId: "function-usecallback-props" }], 299 | }, 300 | { 301 | code: ` 302 | const Component = () => { 303 | return []} />; 304 | }`, 305 | output: `import { useCallback } from 'react'; 306 | 307 | const Component = () => { 308 | const prop = useCallback(async () => [], []); 309 | return ; 310 | }`, 311 | errors: [{ messageId: "function-usecallback-props" }], 312 | } 313 | ]; 314 | 315 | return { validTestCases, invalidTestCases }; 316 | }; 317 | -------------------------------------------------------------------------------- /__tests__/testcases/requireUseMemo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared test cases for ESLint plugin rules 3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations 4 | */ 5 | 6 | // Helper to create a standard set of test cases for require-usememo 7 | export const createRequireUseMemoTestCases = () => { 8 | const validTestCases = [ 9 | { 10 | code: `function renderItem({ 11 | field, 12 | fieldState, 13 | }) { 14 | return ( 15 | ); 20 | }`, 21 | }, 22 | { 23 | code: `function Component() { 24 | let myObject = 'hi'; 25 | return ; 26 | }`, 27 | }, 28 | { 29 | code: `function Component({index = 0}) { 30 | const isNotFirst = index > 0; 31 | return ; 32 | }`, 33 | }, 34 | { 35 | code: `function Component() { 36 | return {/* Empty Expression Should Not Error :) */}; 37 | }`, 38 | }, 39 | { 40 | code: `const Component = () => { 41 | const myObject = useMemo(() => ({}), []); 42 | return ; 43 | }`, 44 | }, 45 | { 46 | code: `const Component = () => { 47 | const myArray = useMemo(() => [], []); 48 | return ; 49 | }` 50 | }, 51 | { 52 | code: ` 53 | function Component({data}) { 54 | return ; 55 | }`, 56 | }, 57 | { 58 | code: ` 59 | function x() {} 60 | function Component() { 61 | return ; 62 | }`, 63 | }, 64 | { 65 | code: ` 66 | function userTest() { 67 | const y = (data) => {}; 68 | return y; 69 | }`, 70 | }, 71 | { 72 | code: ` 73 | function hi() { 74 | const y = (data) => {}; 75 | return y; 76 | }`, 77 | }, 78 | { 79 | code: `const Component = () => { 80 | const myArray = useMemo(() => new Object(), []); 81 | return ; 82 | }` 83 | }, 84 | { 85 | code: ` 86 | const labels = ['data'] 87 | const Content = 88 | const Component = () => { 89 | const myArray = useMemo(() => new Object(), []); 90 | return ; 91 | }` 92 | }, 93 | { 94 | code: `function Component() { 95 | const myArray = useMemo(() => new Object(), []); 96 | return ; 97 | }` 98 | }, 99 | { 100 | code: ` 101 | const myArray = new Object(); 102 | class Component { 103 | render() { 104 | return ; 105 | } 106 | }`, 107 | }, 108 | { 109 | code: ` 110 | const myArray = new Object(); 111 | function Component() { 112 | return ; 113 | }`, 114 | }, 115 | { 116 | code: `class Component { 117 | constructor(props){ 118 | super(props); 119 | this.state = { 120 | myData: new Object(), 121 | }; 122 | } 123 | render() { 124 | const {myData} = this.state; 125 | return ; 126 | } 127 | }`, 128 | }, 129 | { 130 | code: `class Component { 131 | constructor(props){ 132 | super(props); 133 | this.state = { 134 | myArray: [], 135 | }; 136 | } 137 | render() { 138 | const {myArray} = this.state; 139 | return ; 140 | } 141 | }`, 142 | }, 143 | { 144 | code: ` 145 | const myArray = []; 146 | class Component { 147 | render() { 148 | return ; 149 | } 150 | }`, 151 | }, 152 | { 153 | code: ` 154 | function test() {} 155 | class Component { 156 | render() { 157 | return ; 158 | } 159 | }`, 160 | }, 161 | { 162 | code: `const Component = () => { 163 | const myObject = {}; 164 | return
; 165 | }`, 166 | }, 167 | { 168 | code: `const Component = () => { 169 | const myArray = []; 170 | return
; 171 | }`, 172 | }, 173 | { 174 | code: `class Component { 175 | render() { 176 | const myArray = []; 177 | return
; 178 | } 179 | }`, 180 | }, 181 | { 182 | code: `const Component = () => { 183 | const myNumber1 = 123; 184 | const myNumber2 = 123 + 456; 185 | const myString1 = 'abc'; 186 | const myString2 = \`abc\`; 187 | return
; 188 | }`, 189 | }, 190 | { 191 | code: `const Component = () => { 192 | const myObject = memoize({}); 193 | return ; 194 | }`, 195 | }, 196 | { 197 | code: ` 198 | function test() {} 199 | const Component = () => { 200 | return ; 201 | }`, 202 | }, 203 | { 204 | code: ` 205 | function test() {} 206 | function Component() { 207 | return ; 208 | }`, 209 | }, 210 | { 211 | code: `const Component = () => { 212 | const myArray = lodash.memoize([]); 213 | return ; 214 | }`, 215 | }, 216 | { 217 | code: `const Component = () => { 218 | const myBool = false; 219 | return ; 220 | }`, 221 | }, 222 | { 223 | code: `const Component = () => { 224 | const myString = 'test'; 225 | return ; 226 | }`, 227 | }, 228 | { 229 | code: `const Component = () => { 230 | const myComplexString = css\`color: red;\`; 231 | return ; 232 | }`, 233 | }, 234 | { 235 | code: `function useTest() { 236 | const myBool = false; 237 | return myBool; 238 | }`, 239 | }, 240 | { 241 | code: ` 242 | const x = {}; 243 | function useTest() { 244 | return {x}; 245 | }`, 246 | }, 247 | { 248 | code: `function useTesty() { 249 | const myString = ''; 250 | return myString; 251 | }`, 252 | }, 253 | { 254 | code: `function useTesty() { 255 | const myBool = useMemo(() => !!{}, []); 256 | return myBool; 257 | }`, 258 | }, 259 | { 260 | code: `function useTesty() { 261 | const x = {}; 262 | const myBool = useMemo(() => x, [x]); 263 | return myBool; 264 | }`, 265 | }, 266 | { 267 | code: ` 268 | function useTesty() { 269 | const x = {}; 270 | return useData(x); 271 | }`, 272 | options: [{ checkHookReturnObject: true, checkHookCalls: false }], 273 | }, 274 | { 275 | code: ` 276 | function useTesty() { 277 | const x = {}; 278 | return use(x); 279 | }`, 280 | }, 281 | { 282 | code: ` 283 | function useTesty() { 284 | const x = {}; 285 | return useDataManager(x); 286 | }`, 287 | options: [{ checkHookReturnObject: true, ignoredHookCallsNames: {"!useDate*": true} }], 288 | }, 289 | { 290 | code: `const Component = () => { 291 | const myArray1 = []; 292 | const myArray2 = useMemo(() => myArray1, [myArray1]); 293 | return ; 294 | }`, 295 | }, 296 | { 297 | code: `function useTest() { 298 | // @ts-ignore 299 | const y: boolean | undefined = false; 300 | const x = useMemo(() => x, [y]); 301 | return {x}; 302 | }`, 303 | }, 304 | { 305 | code: `function useTest({data}: {data: boolean | undefined}) { 306 | const x = useMemo(() => !data, [data]); 307 | return {x}; 308 | }`, 309 | }, 310 | { 311 | code: `function useTest() { 312 | let y = ''; 313 | const x = useMemo(() => '', []); 314 | return {x, y}; 315 | }`, 316 | }, 317 | // ignoredPropNames 318 | { 319 | code: `const Component = () => { 320 | const myObject = {}; 321 | return ; 322 | }`, 323 | options: [{ ignoredPropNames: ["ignoreProp"] }], 324 | }, 325 | { 326 | code: `const Component = () => { 327 | const myCallback = () => {}; 328 | return ; 329 | }`, 330 | options: [{ ignoredPropNames: ["onClick"] }], 331 | }, 332 | { 333 | code: `const Component = () => { 334 | return
; 335 | }`, 336 | options: [{ ignoredPropNames: ["style"] }], 337 | }, 338 | { 339 | code: `const Component = () => { 340 | return ; 341 | }`, 342 | options: [{ ignoredPropNames: ["onClick"] }], 343 | }, 344 | ]; 345 | 346 | const invalidTestCases = [ 347 | { 348 | code: ` 349 | import React, { useMemo } from 'react'; 350 | const MyComponent = () => { 351 | const items = [1, 2]; 352 | return ( 353 |
354 | {items.map(item => )} 355 | {items.forEach(item => )} 356 | {items.reduce((acc, item) => )} 357 | {items.some((acc, item) => )} 358 | {items.every((acc, item) => )} 359 | {items.find((acc, item) => )} 360 | {items.findIndex((acc, item) => )} 361 |
362 | ); 363 | }; 364 | `, 365 | errors: [ 366 | { messageId: "error-in-invalid-context" }, 367 | { messageId: "error-in-invalid-context" }, 368 | { messageId: "error-in-invalid-context" }, 369 | { messageId: "error-in-invalid-context" }, 370 | { messageId: "error-in-invalid-context" }, 371 | { messageId: "error-in-invalid-context" }, 372 | { messageId: "error-in-invalid-context" } 373 | ], 374 | }, 375 | { 376 | code: ` 377 | import React, { useMemo } from 'react'; 378 | const MyComponent = () => { 379 | const itemsJSX = useMemo(() => , []); 380 | const itemsParsed = useMemo(() => items.map(item => ), []); 381 | 382 | return null; 383 | }; 384 | `, 385 | errors: [ 386 | { messageId: "error-in-invalid-context" }, 387 | { messageId: "error-in-invalid-context" } 388 | ], 389 | }, 390 | { 391 | code: ` 392 | import React, { useCallback } from 'react'; 393 | const MyComponent = () => { 394 | const handleClick = useCallback(() => { 395 | return ; 396 | }, []); 397 | return
Click me
; 398 | }; 399 | `, 400 | errors: [{ messageId: "error-in-invalid-context" }], 401 | }, 402 | { 403 | code: ` 404 | const Component = () => { 405 | return } />; 406 | }`, 407 | errors: [{ messageId: "jsx-usememo-props" }], 408 | output: `import { useMemo } from 'react'; 409 | 410 | const Component = () => { 411 | const prop = useMemo(() => (), []); 412 | return ; 413 | }`, 414 | }, 415 | { 416 | code: ` 417 | const Component = () => { 418 | const myObject = {}; 419 | return ; 420 | }`, 421 | errors: [{ messageId: "object-usememo-props" }], 422 | output: `import { useMemo } from 'react'; 423 | 424 | const Component = () => { 425 | const myObject = useMemo(() => ({}), []); 426 | return ; 427 | }`, 428 | }, 429 | { 430 | code: ` 431 | const Component = () => { 432 | const myArray = []; 433 | return ; 434 | }`, 435 | output: `import { useMemo } from 'react'; 436 | 437 | const Component = () => { 438 | const myArray = useMemo(() => [], []); 439 | return ; 440 | }`, 441 | errors: [{ messageId: "array-usememo-props" }], 442 | }, 443 | { 444 | code: ` 445 | const Component = () => { 446 | const myArray = []; 447 | return ; 448 | }`, 449 | output: ` 450 | const Component = () => { 451 | const myArray = useMemo(() => [], []); 452 | return ; 453 | }`, 454 | options: [{ fix: { addImports: false } }], 455 | errors: [{ messageId: "array-usememo-props" }], 456 | }, 457 | { 458 | code: ` 459 | const Component = () => { 460 | const myInstance = new Object(); 461 | return ; 462 | }`, 463 | output: `import { useMemo } from 'react'; 464 | 465 | const Component = () => { 466 | const myInstance = useMemo(() => new Object(), []); 467 | return ; 468 | }`, 469 | errors: [{ messageId: "instance-usememo-props" }], 470 | }, 471 | { 472 | code: `class Component { 473 | render() { 474 | const myInstance = new Object(); 475 | return ; 476 | } 477 | }`, 478 | errors: [{ messageId: "instance-class-memo-props" }], 479 | }, 480 | { 481 | code: `import { useCallback } from 'react'; 482 | 483 | const Component = () => { 484 | const firstInstance = useMemo(() => new Object(), []); 485 | const second = new Object(); 486 | return ; 487 | }`, 488 | output: `import { useCallback, useMemo } from 'react'; 489 | 490 | const Component = () => { 491 | const firstInstance = useMemo(() => new Object(), []); 492 | const second = useMemo(() => new Object(), []); 493 | return ; 494 | }`, 495 | errors: [{ messageId: "instance-usememo-props" }], 496 | }, 497 | { 498 | code: ` 499 | const Component = () => { 500 | const firstInstance = useMemo(() => new Object(), []); 501 | const second = new Object(); 502 | return ; 503 | }`, 504 | output: `import { useMemo } from 'react'; 505 | 506 | const Component = () => { 507 | const firstInstance = useMemo(() => new Object(), []); 508 | const second = useMemo(() => new Object(), []); 509 | return ; 510 | }`, 511 | errors: [{ messageId: "instance-usememo-props" }], 512 | }, 513 | { 514 | code: ` 515 | const Component = () => { 516 | let myObject = useMemo(() => ({}), []); 517 | myObject = {a: 'b'}; 518 | return ; 519 | }`, 520 | output: ` 521 | const Component = () => { 522 | const myObject = useMemo(() => ({}), []); 523 | myObject = {a: 'b'}; 524 | return ; 525 | }`, 526 | errors: [{ messageId: "usememo-const" }], 527 | }, 528 | { 529 | code: ` 530 | import { useCallback } from 'react'; 531 | 532 | const Component = () => { 533 | return ; 534 | }`, 535 | output: ` 536 | import { useCallback, useMemo } from 'react'; 537 | 538 | const Component = () => { 539 | const userData = useMemo(() => ({}), []); 540 | return ; 541 | }`, 542 | errors: [{ messageId: "object-usememo-props" }], 543 | }, 544 | { 545 | code: ` 546 | const Component = () => { 547 | const userData = undefined; 548 | return ; 549 | }`, 550 | output: `import { useMemo } from 'react'; 551 | 552 | const Component = () => { 553 | const userData = undefined; 554 | const _userData = useMemo(() => ({}), []); 555 | return ; 556 | }`, 557 | errors: [{ messageId: "object-usememo-props" }], 558 | }, 559 | { 560 | code: ` 561 | const Component = () => { 562 | const userData = undefined; 563 | const _userData = undefined; 564 | return ; 565 | }`, 566 | output: `import { useMemo } from 'react'; 567 | 568 | const Component = () => { 569 | const userData = undefined; 570 | const _userData = undefined; 571 | const __userData = useMemo(() => ({}), []); 572 | return ; 573 | }`, 574 | errors: [{ messageId: "object-usememo-props" }], 575 | }, 576 | { 577 | code: ` 578 | const Component = () => { 579 | const userData = undefined; 580 | const _userData = undefined; 581 | const __userData = undefined; 582 | const ___userData = undefined; 583 | const ____userData = undefined; 584 | const _____userData = undefined; 585 | return ; 586 | }`, 587 | output: `import { useMemo } from 'react'; 588 | 589 | const Component = () => { 590 | const userData = undefined; 591 | const _userData = undefined; 592 | const __userData = undefined; 593 | const ___userData = undefined; 594 | const ____userData = undefined; 595 | const _____userData = undefined; 596 | const renameMe = useMemo(() => ({}), []); 597 | return ; 598 | }`, 599 | errors: [{ messageId: "object-usememo-props" }], 600 | }, 601 | { 602 | code: ` 603 | const Component = () => { 604 | const userData = undefined; 605 | const _userData = undefined; 606 | const __userData = undefined; 607 | const ___userData = undefined; 608 | const ____userData = undefined; 609 | const _____userData = undefined; 610 | const renameMe = undefined; 611 | return ; 612 | }`, 613 | output: `import { useMemo } from 'react'; 614 | 615 | const Component = () => { 616 | const userData = undefined; 617 | const _userData = undefined; 618 | const __userData = undefined; 619 | const ___userData = undefined; 620 | const ____userData = undefined; 621 | const _____userData = undefined; 622 | const renameMe = undefined; 623 | const renameMe_99c32a94 = useMemo(() => ({}), []); 624 | return ; 625 | }`, 626 | errors: [{ messageId: "object-usememo-props" }], 627 | }, 628 | { 629 | code: ` 630 | const Component = () => { 631 | let x = {}; 632 | return ; 633 | }`, 634 | output: `import { useMemo } from 'react'; 635 | 636 | const Component = () => { 637 | const x = useMemo(() => ({}), []); 638 | return ; 639 | }`, 640 | errors: [{ messageId: "object-usememo-props" }], 641 | }, 642 | { 643 | code: `import { useMemo } from 'react'; 644 | function useTesty() { 645 | const x = {}; 646 | return useDataManager(x); 647 | }`, 648 | options: [{ checkHookReturnObject: true, ignoredHookCallsNames: {"useData*": false} }], 649 | output: `import { useMemo } from 'react'; 650 | function useTesty() { 651 | const x = useMemo(() => ({}), []); 652 | return useDataManager(x); 653 | }`, 654 | errors: [{ messageId: "object-usememo-deps" }], 655 | }, 656 | { 657 | code: ` 658 | const useTest = () => { 659 | // @ts-ignore 660 | const x: boolean | undefined = false; 661 | function y() {} 662 | return {x, y}; 663 | }`, 664 | output: `import { useCallback } from 'react'; 665 | 666 | const useTest = () => { 667 | // @ts-ignore 668 | const x: boolean | undefined = false; 669 | const y = useCallback(() => {}, []); 670 | return {x, y}; 671 | }`, 672 | errors: [{ messageId: "function-usecallback-hook" }], 673 | }, 674 | { 675 | code: ` 676 | function Component() { 677 | const component = ; 678 | return ; 679 | }`, 680 | errors: [{messageId: "jsx-usememo-props"}], 681 | output: `import { useMemo } from 'react'; 682 | 683 | function Component() { 684 | const component = useMemo(() => (), []); 685 | return ; 686 | }` 687 | }, 688 | { 689 | code: ` 690 | import {} from 'react'; 691 | function useTesty() { 692 | const user = {}; 693 | const x = { 694 | renderCell: (user) => , 695 | }; 696 | return useData(x); 697 | }`, 698 | output: ` 699 | import { useMemo } from 'react'; 700 | function useTesty() { 701 | const user = {}; 702 | const x = { 703 | renderCell: (user) => ['role'], [])} user={user} />, 704 | }; 705 | return useData(x); 706 | }`, 707 | errors: [{ messageId: "object-usememo-deps" }, { messageId: "array-usememo-props" }, { messageId: "unknown-usememo-hook" }], 708 | options: [{ strict: true, 709 | checkHookReturnObject: true, 710 | fix: { addImports: true }, 711 | checkHookCalls: true, 712 | ignoredHookCallsNames: {}, 713 | }], 714 | }, 715 | { 716 | code: `import type { ComponentProps } from 'react'; 717 | import React from 'react'; 718 | 719 | const Component = () => { 720 | const myArray = []; 721 | return ; 722 | }`, 723 | output: `import type { ComponentProps } from 'react'; 724 | import React, { useMemo } from 'react'; 725 | 726 | const Component = () => { 727 | const myArray = useMemo(() => [], []); 728 | return ; 729 | }`, 730 | errors: [{ messageId: "array-usememo-props" }], 731 | }, 732 | { 733 | code: `import type { ComponentProps } from 'react'; 734 | 735 | const Component = () => { 736 | const myArray = []; 737 | return ; 738 | }`, 739 | output: `import { useMemo } from 'react'; 740 | import type { ComponentProps } from 'react'; 741 | 742 | const Component = () => { 743 | const myArray = useMemo(() => [], []); 744 | return ; 745 | }`, 746 | errors: [{ messageId: "array-usememo-props" }], 747 | }, 748 | { 749 | code: `import type { ComponentProps } from 'react'; 750 | import React, { useRef } from 'react'; 751 | 752 | const Component = () => { 753 | const myRef = useRef(); 754 | const myArray = []; 755 | return ; 756 | }`, 757 | output: `import type { ComponentProps } from 'react'; 758 | import React, { useRef, useMemo } from 'react'; 759 | 760 | const Component = () => { 761 | const myRef = useRef(); 762 | const myArray = useMemo(() => [], []); 763 | return ; 764 | }`, 765 | errors: [{ messageId: "array-usememo-props" }], 766 | }, 767 | { 768 | code: ` 769 | function useTest() { 770 | return { x: 1 }; 771 | }`, 772 | output: `import { useMemo } from 'react'; 773 | 774 | function useTest() { 775 | return useMemo(() => ({ x: 1 }), []); 776 | }`, 777 | errors: [{ messageId: "object-usememo-hook" }], 778 | options: [{ checkHookReturnObject: true, strict: true }], 779 | }, 780 | { 781 | code: ` 782 | function useTest() { 783 | return (data) => {}; 784 | }`, 785 | output: `import { useCallback } from 'react'; 786 | 787 | function useTest() { 788 | return useCallback((data) => {}, []); 789 | }`, 790 | errors: [{ messageId: "function-usecallback-hook" }], 791 | options: [{ checkHookReturnObject: true, strict: true }], 792 | }, 793 | { 794 | code: ` 795 | function useTest() { 796 | const y = { x: 1 }; 797 | return y; 798 | }`, 799 | output: `import { useMemo } from 'react'; 800 | 801 | function useTest() { 802 | const y = useMemo(() => ({ x: 1 }), []); 803 | return y; 804 | }`, 805 | errors: [{ messageId: "object-usememo-hook" }], 806 | options: [{ checkHookReturnObject: true, strict: true }], 807 | }, 808 | { 809 | code: ` 810 | function useTest() { 811 | const y = (data) => {}; 812 | return y; 813 | }`, 814 | output: `import { useCallback } from 'react'; 815 | 816 | function useTest() { 817 | const y = useCallback((data) => {}, []); 818 | return y; 819 | }`, 820 | errors: [{ messageId: "function-usecallback-hook" }], 821 | options: [{ checkHookReturnObject: true, strict: true }], 822 | }, 823 | { 824 | code: `import { anything } from 'react' 825 | 826 | const useMyHook = () => { 827 | useAnotherHook( 828 | () => {}, 829 | [] 830 | ); 831 | };`, 832 | output: `import { anything, useCallback } from 'react' 833 | 834 | const useMyHook = () => { 835 | useAnotherHook( 836 | useCallback(() => {}, []), 837 | [] 838 | ); 839 | };`, 840 | errors: [{ messageId: "function-usecallback-deps" }, { messageId: "array-usememo-deps" }], 841 | }, 842 | ]; 843 | 844 | return { validTestCases, invalidTestCases }; 845 | }; 846 | -------------------------------------------------------------------------------- /__tests__/testcases/requireUseMemoChildren.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared test cases for ESLint plugin rules 3 | * This file contains test cases that can be used by both ESLint v8 and v9 test configurations 4 | */ 5 | 6 | // Helper to create a standard set of test cases for require-usememo-children 7 | export const createRequireUseMemoChildrenTestCases = () => { 8 | const validTestCases = [ 9 | { 10 | code: `const Component = () => { 11 | const children = React.useMemo(() =>
, []); 12 | return {children}; 13 | }`, 14 | }, 15 | { 16 | code: `const Component = () => { 17 | return
; 18 | }`, 19 | }, 20 | { 21 | code: `const Component = () => { 22 | const renderFn = React.useCallback(() =>
, []); 23 | return {renderFn}; 24 | }`, 25 | } 26 | ]; 27 | 28 | const invalidTestCases = [ 29 | { 30 | code: `const Component = () => { 31 | const children = React.useMemo(() =>
, []); 32 | return 33 | <> 34 | {children} 35 | 36 | ; 37 | }`, 38 | errors: [{ messageId: "jsx-usememo-children" }], 39 | }, 40 | { 41 | code: `const Component = () => { 42 | const children =
; 43 | return {children} 44 | }`, 45 | errors: [{ messageId: "jsx-usememo-children" }], 46 | }, 47 | { 48 | code: `const Component = () => { 49 | const children = [
, , ]; 50 | return {children} 51 | }`, 52 | errors: [{ messageId: "array-usememo-children" }], 53 | }, 54 | { 55 | code: `const Component = () => { 56 | return 57 | {() =>
} 58 | 59 | }`, 60 | errors: [{ messageId: "function-usecallback-children" }], 61 | } 62 | ]; 63 | 64 | return { validTestCases, invalidTestCases }; 65 | }; 66 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /docs/rules/eslint-v9-migration-guide.md: -------------------------------------------------------------------------------- 1 | # ESLint v9 Migration Guide 2 | 3 | Starting from version 2.5.0, `@arthurgeron/eslint-plugin-react-usememo` exports configurations compatible with both ESLint v8 (traditional config) and ESLint v9 (flat config). This guide will help you migrate to ESLint v9 once full compatibility is achieved. 4 | 5 | ## Current Status 6 | 7 | **Important Note:** While the plugin exports the proper structure for ESLint v9 compatibility, there are some underlying compatibility issues with the current rule implementations and ESLint v9's new architecture. We're actively working on resolving these issues for complete ESLint v9 compatibility. 8 | 9 | For now, please continue using ESLint v8 with the traditional configuration. 10 | 11 | ## Prerequisites 12 | 13 | 1. Install ESLint v9 (once full compatibility is released): 14 | ```bash 15 | npm install eslint@^9.0.0 --save-dev 16 | ``` 17 | 18 | 2. Ensure your `@arthurgeron/eslint-plugin-react-usememo` version is at least 2.5.0: 19 | ```bash 20 | npm install @arthurgeron/eslint-plugin-react-usememo@^2.5.0 --save-dev 21 | ``` 22 | 23 | ## Migration Steps 24 | 25 | ### 1. Create a New Configuration File 26 | 27 | Once full compatibility is achieved, create a new `eslint.config.js` file in your project root: 28 | 29 | ```js 30 | // eslint.config.js 31 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 32 | import js from '@eslint/js'; 33 | // Import other plugins as needed 34 | import reactPlugin from 'eslint-plugin-react'; 35 | import reactHooksPlugin from 'eslint-plugin-react-hooks'; 36 | 37 | export default [ 38 | js.configs.recommended, 39 | { 40 | files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], 41 | plugins: { 42 | 'react': reactPlugin, 43 | 'react-hooks': reactHooksPlugin, 44 | '@arthurgeron/react-usememo': flatConfig 45 | }, 46 | languageOptions: { 47 | ecmaVersion: 2022, 48 | sourceType: 'module', 49 | parserOptions: { 50 | ecmaFeatures: { 51 | jsx: true 52 | } 53 | } 54 | }, 55 | rules: { 56 | // React rules 57 | 'react/react-in-jsx-scope': 'off', 58 | 'react-hooks/rules-of-hooks': 'error', 59 | 'react-hooks/exhaustive-deps': 'warn', 60 | 61 | // React useMemo plugin rules 62 | '@arthurgeron/react-usememo/require-usememo': 'error', 63 | '@arthurgeron/react-usememo/require-memo': 'error', 64 | '@arthurgeron/react-usememo/require-usememo-children': 'error' 65 | } 66 | } 67 | ]; 68 | ``` 69 | 70 | ### 2. Using the Recommended Configuration 71 | 72 | If you prefer to use the recommended configuration, you can simplify your config: 73 | 74 | ```js 75 | // eslint.config.js 76 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 77 | import js from '@eslint/js'; 78 | import reactPlugin from 'eslint-plugin-react'; 79 | 80 | export default [ 81 | js.configs.recommended, 82 | // Your other configs... 83 | flatConfig.configs.recommended 84 | ]; 85 | ``` 86 | 87 | ### 3. CommonJS Support 88 | 89 | If you're using CommonJS modules: 90 | 91 | ```js 92 | // eslint.config.js 93 | const { flatConfig } = require('@arthurgeron/eslint-plugin-react-usememo'); 94 | const js = require('@eslint/js'); 95 | 96 | module.exports = [ 97 | js.configs.recommended, 98 | // Your other configs... 99 | { 100 | plugins: { 101 | '@arthurgeron/react-usememo': flatConfig 102 | }, 103 | rules: { 104 | '@arthurgeron/react-usememo/require-usememo': 'error', 105 | '@arthurgeron/react-usememo/require-memo': 'error', 106 | '@arthurgeron/react-usememo/require-usememo-children': 'error' 107 | } 108 | } 109 | ]; 110 | ``` 111 | 112 | ### 4. Remove the Old Configuration 113 | 114 | After confirming that the new configuration works correctly, you can remove the old `.eslintrc.*` file. 115 | 116 | ## Key Differences 117 | 118 | ### Traditional Config (v8) 119 | 120 | ```json 121 | { 122 | "plugins": ["@arthurgeron/react-usememo"], 123 | "rules": { 124 | "@arthurgeron/react-usememo/require-usememo": "error", 125 | "@arthurgeron/react-usememo/require-memo": "error", 126 | "@arthurgeron/react-usememo/require-usememo-children": "error" 127 | } 128 | } 129 | ``` 130 | 131 | ### Flat Config (v9) 132 | 133 | ```js 134 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 135 | 136 | export default [ 137 | { 138 | plugins: { 139 | '@arthurgeron/react-usememo': flatConfig 140 | }, 141 | rules: { 142 | '@arthurgeron/react-usememo/require-usememo': 'error', 143 | '@arthurgeron/react-usememo/require-memo': 'error', 144 | '@arthurgeron/react-usememo/require-usememo-children': 'error' 145 | } 146 | } 147 | ]; 148 | ``` 149 | 150 | ## Example Projects 151 | 152 | For complete working examples of both configurations, check out the example projects in the [examples directory](https://github.com/arthurgeron/eslint-plugin-react-usememo/tree/main/examples): 153 | 154 | 1. **v8-traditional** - Example with ESLint v8 traditional config 155 | 2. **v9-flat** - Example with ESLint v9 flat config structure 156 | 157 | ## Additional Resources 158 | 159 | - [ESLint v9 Flat Config Documentation](https://eslint.org/docs/latest/use/configure/configuration-files-new) 160 | - [ESLint v9 Migration Guide](https://eslint.org/docs/latest/use/migrate-to-9.0.0) -------------------------------------------------------------------------------- /docs/rules/require-memo.md: -------------------------------------------------------------------------------- 1 | # Rule: require-memo 2 | 3 | This rule enforces the use of `memo()` on function components. The objective is to optimize your component re-renders and avoid unnecessary render cycles when your component's props do not change. 4 | 5 | ## Rationale 6 | 7 | React’s rendering behavior ensures that whenever the parent component renders, the child component instances are re-rendered as well. When dealing with expensive computations or components, this could lead to performance issues. `memo()` is a higher order component which tells React to skip rendering the component if its props have not changed. 8 | 9 | When `memo()` wraps an exported component, then it will only re-render if the current and next props are not shallowly equal. 10 | 11 | ```jsx 12 | function MyComponent(props) { /* ... */ } 13 | 14 | export default memo(MyComponent); 15 | ``` 16 | 17 | This rule applies to function components, not class-based components as they should extend `React.PureComponent` or must implement `shouldComponentUpdate` lifecycle method for similar optimization. 18 | 19 | ## Rule Details 20 | This rule will enforce that all function components are wrapped in `memo()`. 21 | Only exported components are validated. 22 | 23 | ## Incorrect Code Examples 24 | 25 | Here are examples of incorrect code: 26 | 27 | ```js 28 | // Not using memo on function component 29 | function ComponentA(props) { 30 | return
{props.name}
; 31 | } 32 | 33 | export default ComponentA; 34 | ``` 35 | 36 | ## Correct Code Examples 37 | 38 | Here is an example of correct code pattern: 39 | 40 | ```js 41 | // Using memo on function component 42 | function ComponentB(props) { 43 | return
{props.name}
; 44 | } 45 | 46 | export default memo(ComponentB); 47 | ``` 48 | ## Options 49 | 50 | The rule takes an optional object: 51 | 52 | ```json 53 | "rules": { 54 | "@arthurgeron/react-usememo/require-memo": [2, { 55 | "ignoredComponents": { 56 | "IgnoreMe": true, 57 | "DontIgnoreMe": false, 58 | "!IgnoreEverythingButMe": true, 59 | } 60 | }] 61 | } 62 | ``` 63 | - `{ignoredComponents: Record}`: This allows you to add specific Component Names, thereby individually disabling or enabling them to be checked when used. Matching names with a `true` value will cause the checks to be ignored. 64 | You can use strict 1:1 comparisons (e.g., `"ComponentName"`) or employ Minimatch's Glob Pattern (e.g., `"*Item"`). 65 | > For more information on Minimatch, refer to its README [here](https://www.npmjs.com/package/minimatch). You may also find this [cheatsheet](https://github.com/motemen/minimatch-cheat-sheet) useful. 66 | 67 | 68 | ## When Not To Use It 69 | 70 | If the component always re-renders with different props or is not expensive in terms of performance, there is no real benefit to using `memo()`. In fact, using `memo()` for a large number of simple components could negatively impact performance as memoizing small components may cost more than re-rendering them. 71 | 72 | > For more examples and detailed explanation, refer to the eslint-plugin-react-memo [readme](https://github.com/myorg/eslint-plugin-react-memo). -------------------------------------------------------------------------------- /docs/rules/require-usememo-children.md: -------------------------------------------------------------------------------- 1 | # Rule: require-usememo-children 2 | 3 | This rule enforces the use of `useMemo()` around children in render methods. 4 | 5 | ## Rationale 6 | 7 | The React `useMemo` hook is used to memorize expensive computations, but it can also be beneficial when passing complex objects or large arrays as props to child components. 8 | 9 | By wrapping such data with useMemo, you ensure that the child component will not re-render unless the data has changed. It compares the previous result with the new one and if they're the same, it avoids the re-render. 10 | 11 | ```jsx 12 | function ParentComponent({ list }) { 13 | const memoizedList = React.useMemo(() => list, [list]); 14 | 15 | return ; 16 | } 17 | ``` 18 | This rule applies to any type of component (functional or class-based) as long as it involves passing complex children (objects, arrays) to another component. 19 | 20 | ## Rule Details 21 | This rule will enforce that all complex children are wrapped in `useMemo()`. 22 | 23 | ## Incorrect 24 | ```JavaScript 25 | function Component() { 26 | 27 | 28 | <> 29 | 30 | 31 | 32 | } 33 | ``` 34 | 35 | ## Correct 36 | ```JavaScript 37 | function Component() { 38 | 39 | const children = useMemo(() => (), []); 40 | 41 | {children} 42 | 43 | } 44 | ``` 45 | 46 | ## Options 47 | 48 | - `{strict: true}`: Fails even in cases where it is difficult to determine if the value in question is a primitive (string or number) or a complex value (object, array, etc.). 49 | 50 | ## When Not To Use It 51 | 52 | If the child elements are simple types (strings, numbers) or the parent doesn't often re-render, then using `useMemo()` might be overkill and could even lead to worse performance. Therefore, it's better to turn off this rule in such cases. 53 | 54 | > For more examples and detailed explanation, refer to the eslint-plugin-react-memo-children [readme](https://github.com/myorg/eslint-plugin-react-memo-children). -------------------------------------------------------------------------------- /docs/rules/require-usememo.md: -------------------------------------------------------------------------------- 1 | # Rule: require-usememo 2 | 3 | This rule requires complex values (objects, arrays, functions, and JSX) that get passed as props or referenced as a hook dependency to be wrapped in `useMemo()` or `useCallback()`. 4 | 5 | Not only does this rule enforce performance optimization practices, but it also comes with an **amazing autofix functionality**. It intelligently wraps necessary components with `useMemo()` or `useCallback()`, which results in a more efficient code and saves developers valuable time. 6 | 7 | This rule aims to ensure that calculations are not performed more often than necessary and a component's tree only recalculates or renders again when absolutely required. This controls expensive expressions and enhances your application's scalability and performance. 8 | 9 | ## Options 10 | 11 | The rule takes an optional object: 12 | 13 | ```json 14 | "rules": { 15 | "@arthurgeron/react-usememo/require-usememo": [2, { 16 | "strict": true, 17 | "checkHookReturnObject": true, 18 | "fix": { "addImports": true }, 19 | "checkHookCalls": true, 20 | "ignoredHookCallsNames": { "useStateManagement": false }, 21 | "ignoredPropNames": ["style"] 22 | }], 23 | } 24 | ``` 25 | 26 | Here is what each option means: 27 | 28 | - `{strict: true}`: Fails even in cases where it is difficult to determine if the value in question is a primitive (string or number) or a complex value (object, array, etc.). 29 | 30 | - `{checkHookReturnObject: true}`: Requires Object Expressions passed in return statements. 31 | 32 | - `{checkHookCalls: true}`: Requires objects/data passed to a non-native/Custom hook to be memoized. 33 | 34 | - `{ignoredHookCallsNames: Record}`: This allows you to add specific hook names, thereby individually disabling or enabling them to be checked when used. Matching names with a `true` value will cause the checks to be ignored. 35 | You can use strict 1:1 comparisons (e.g., `"useCustomHook"`) or employ Minimatch's Glob Pattern (e.g., `"useState*"`). 36 | > React's [use](https://react.dev/reference/react/use) hook is ignored here by default, while triggering async calls dinamically losely within a component's render cycle is bad, it does not affect the overall "performance" or behavior of the hook itself. This can be disabled with a `"use": true` entry. 37 | 38 | > For more information on Minimatch, refer to its README [here](https://www.npmjs.com/package/minimatch). You may also find this [cheatsheet](https://github.com/motemen/minimatch-cheat-sheet) useful. 39 | 40 | > If no strict names match and you have entries with Glob syntax, the algorithm will stop at the first match. 41 | 42 | 43 | - `fix`: Contains rules that only apply during eslint's fix routine. 44 | 45 | - `addImports`: Creates imports for useMemo and useCallback when one or both are added by this rule. Will increment to a existing import declaration or create a new one. Setting this to false disables it, defaults to true. 46 | 47 | - `ignoredPropNames`: This allows you to add specific prop name, thereby disabling them to be checked when used. 48 | 49 | ## Autofix Examples (Function Components & Hooks only) 50 | 51 | To illustrate the autofix feature in action, below are some examples with input code and the corresponding fixed output: 52 | 53 | #### Example 1 54 | Input: 55 | ```jsx 56 | const Component = () => { 57 | return {}} />; 58 | } 59 | ``` 60 | Fixed output: 61 | ```jsx 62 | import { useCallback } from 'react'; 63 | 64 | const Component = () => { 65 | const userData = useCallback(() => ({}), []); 66 | return ; 67 | } 68 | ``` 69 | 70 | > If an expression is unnamed, it adopts the name of the property. Plus, it scans the scope to make sure the naming doesn't conflict with any existing variables. You can find more examples in our [tests](https://github.com/arthurgeron/eslint-plugin-react-usememo/__tests__/require-usememo.test.ts). 71 | 72 | > Imports for useMemo and useCallback are created, if necessary. Can be disabled with option `{ fix: { addImports: false } }` 73 | 74 | #### Example 2 75 | Input: 76 | ```jsx 77 | const Component = () => { 78 | const myObject = {}; 79 | return ; 80 | } 81 | ``` 82 | Fixed output: 83 | ```jsx 84 | import { useMemo } from 'react'; 85 | 86 | const Component = () => { 87 | const myObject = useMemo(() => ({}), []); 88 | return ; 89 | } 90 | ``` 91 | 92 | #### Example 3 93 | Input: 94 | ```jsx 95 | const Component = () => { 96 | const myArray = []; 97 | return ; 98 | } 99 | ``` 100 | Fixed output: 101 | ```jsx 102 | import { useMemo } from 'react'; 103 | 104 | const Component = () => { 105 | const myArray = useMemo(() => [], []); 106 | return ; 107 | } 108 | ``` 109 | 110 | #### Example 4 111 | Input: 112 | ```jsx 113 | const Component = () => { 114 | const myInstance = new Object(); 115 | return ; 116 | } 117 | ``` 118 | Fixed output: 119 | ```jsx 120 | import { useMemo } from 'react'; 121 | 122 | const Component = () => { 123 | const myInstance = useMemo(() => new Object(), []); 124 | return ; 125 | } 126 | ``` 127 | 128 | > For all the example scenarios provided above, please check [`this guide`](https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/require-usememo.md). 129 | 130 | 131 | Absolutely! Here are examples illustrating incorrect and correct application of the `require-usememo` rule. 132 | 133 | ## Incorrect Code Examples 134 | 135 | ### Function Components 136 | 137 | The function defined within a component is re-declared on each render which can lead to undesirable side effects or unnecessary render cycles. 138 | 139 | ```js 140 | function MyComponent() { 141 | const myObject = {}; // This object is re-created on each render 142 | const myArray = []; // This array is re-created on each render 143 | 144 | return ; 145 | } 146 | ``` 147 | 148 | ### Hooks 149 | 150 | Objects defined within a hook will be re-declared on each render, which will cause undesirable side effects for other hooks and components using these objects. 151 | 152 | ```js 153 | function useData() { 154 | const myObject = {}; // This object is re-created on each render 155 | const myArray = []; // This array is re-created on each render 156 | 157 | return return {myObject, myArray}; 158 | } 159 | ``` 160 | 161 | ### Class Components 162 | 163 | Each time `render()` is called, a new object (or array or class instance) is created. A better approach would be to have this object created once and reused. 164 | 165 | ```js 166 | class MyClassComponent extends React.Component { 167 | render() { 168 | const myObject = {}; // This object is re-created on each render 169 | return ; 170 | } 171 | } 172 | ``` 173 | 174 | ## Correct Code Examples 175 | 176 | ### Function Components 177 | 178 | Below, all the potentially mutable objects are wrapped with `useMemo`, so they aren't re-created on each render. 179 | 180 | ```js 181 | import { useMemo } from 'react'; 182 | 183 | function MyComponent() { 184 | const myObject = useMemo(() => ({}), []); 185 | const myArray = useMemo(() => [], []); 186 | 187 | return ; 188 | } 189 | ``` 190 | 191 | ### Hooks 192 | 193 | Below, all the potentially mutable objects are wrapped with `useMemo`, so they aren't re-created on each render. 194 | 195 | ```js 196 | function useData() { 197 | const myObject = useMemo(() => ({}), []); 198 | const myArray = useMemo(() => [], []); 199 | 200 | return return {myObject, myArray}; 201 | } 202 | ``` 203 | 204 | ### Class Components 205 | 206 | Here, we're storing our object in the component's state. This ensures the new object isn't re-created every time the component is rendered. 207 | 208 | ```js 209 | class MyClassComponent extends React.Component { 210 | constructor() { 211 | this.state = { 212 | myObject: {} 213 | } 214 | } 215 | render() { 216 | return ; 217 | } 218 | } 219 | ``` 220 | 221 | Another example showing how to properly modify a incoming prop only when the reference changes, instead of changing it every render. 222 | 223 | ```js 224 | function getUserIdsInfo (users) { 225 | return users.reduce((acc, userData, index) => { 226 | const id = userData.id; 227 | const isFirstPosition = !index; 228 | acc.ids.push(id); 229 | acc.joinedKeys += isFirstPosition ? id : `,${id}`; 230 | return acc; 231 | }, { joinedKeys: '', ids: [] }); 232 | } 233 | 234 | class MyClassComponent extends React.Component { 235 | constructor(props) { 236 | super(props); 237 | const { joinedKeys, ids: userIds } = getUserIdsInfo(props.users); 238 | 239 | this.state = { 240 | userIds, 241 | joinedKeys, 242 | }; 243 | } 244 | 245 | static getDerivedStateFromProps(nextProps, prevState) { 246 | 247 | // Not optimal to do reduce every prop/state change 248 | // Optimally there'd be an "if" statement here to check if the prop we care about has changed, avoiding the reduce. 249 | // This is only an example 250 | const { joinedKeys, ids: userIds } = getUserIdsInfo(nextProps.users); 251 | 252 | if (joinedKeys !== prevState.joinedKeys) { 253 | return { 254 | userIds, 255 | joinedKeys, 256 | }; 257 | } 258 | // Return null to indicate no change to state 259 | return null; 260 | } 261 | 262 | render() { 263 | // Reference to object wont change between renders, will only update when incoming prop 'users' changes. 264 | const { userIds } = this.state; 265 | return ; 266 | } 267 | } 268 | ``` 269 | 270 | > For more examples and detailed explanation, refer to the eslint-plugin-react-usememo [readme](https://github.com/arthurgeron/eslint-plugin-react-usememo). -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # ESLint Plugin React UseMemo Examples 2 | 3 | This directory contains example React projects demonstrating how to use the `@arthurgeron/eslint-plugin-react-usememo` plugin with different ESLint configurations. 4 | 5 | ## Examples 6 | 7 | 1. **[v8-traditional](./v8-traditional/)** - Demonstrates the plugin with ESLint v8 using traditional configuration format (`.eslintrc.json`) 8 | 9 | 2. **[v9-flat](./v9-flat/)** - Demonstrates the plugin with ESLint v9 using the new flat configuration format (`eslint.config.js`) 10 | 11 | ## Testing Results 12 | 13 | ### ESLint v8 Traditional Config 14 | 15 | The v8 traditional config example has been successfully tested. The plugin correctly identifies and reports issues when: 16 | - Objects aren't wrapped in `useMemo()` 17 | - Functions aren't wrapped in `useCallback()` 18 | - Components aren't wrapped in `React.memo()` 19 | 20 | You can run the linter in this example with: 21 | ``` 22 | cd v8-traditional 23 | yarn install 24 | yarn lint 25 | ``` 26 | 27 | ### ESLint v9 Flat Config 28 | 29 | While the plugin exports the correct flat config structure for ESLint v9, there are some compatibility issues with the current implementation and ESLint v9's new architecture. The example tests verify that the plugin's structure is correctly exported but the full integration with ESLint v9 requires further updates to the rule implementations. 30 | 31 | The test script confirms that the plugin correctly exports: 32 | - All three rules: `require-memo`, `require-usememo`, and `require-usememo-children` 33 | - The recommended configuration 34 | 35 | You can run the basic structure test with: 36 | ``` 37 | cd v9-flat 38 | node test-plugin.js 39 | ``` 40 | 41 | ## Key Differences 42 | 43 | ### ESLint v8 (Traditional Config) 44 | 45 | In the traditional config (`v8-traditional` example), the plugin is configured in `.eslintrc.json` like this: 46 | 47 | ```json 48 | { 49 | "plugins": ["@arthurgeron/react-usememo"], 50 | "rules": { 51 | "@arthurgeron/react-usememo/require-usememo": "error", 52 | "@arthurgeron/react-usememo/require-memo": "error", 53 | "@arthurgeron/react-usememo/require-usememo-children": "error" 54 | } 55 | } 56 | ``` 57 | 58 | ### ESLint v9 (Flat Config) 59 | 60 | In the flat config (`v9-flat` example), the plugin is configured in `eslint.config.js` like this: 61 | 62 | ```js 63 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 64 | 65 | export default [ 66 | { 67 | // ... 68 | plugins: { 69 | '@arthurgeron/react-usememo': flatConfig 70 | }, 71 | rules: { 72 | '@arthurgeron/react-usememo/require-usememo': 'error', 73 | '@arthurgeron/react-usememo/require-memo': 'error', 74 | '@arthurgeron/react-usememo/require-usememo-children': 'error' 75 | } 76 | } 77 | ]; 78 | ``` 79 | 80 | Alternatively, you can use the plugin's recommended configuration: 81 | 82 | ```js 83 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 84 | 85 | export default [ 86 | // Other configs... 87 | flatConfig.configs.recommended, 88 | ]; 89 | ``` 90 | 91 | ## Functionality 92 | 93 | Both examples demonstrate proper usage of: 94 | - `useMemo` for complex objects 95 | - `useCallback` for function references 96 | - `React.memo` for component memoization 97 | 98 | The examples show how the ESLint plugin enforces these patterns to improve React application performance. -------------------------------------------------------------------------------- /examples/v8-traditional/.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | 3 | module.exports = { 4 | "env": { 5 | "browser": true, 6 | "es2021": true, 7 | "node": true 8 | }, 9 | "extends": [ 10 | "eslint:recommended", 11 | // "plugin:react/recommended" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": "latest", 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | // "react", 22 | // "react-hooks", 23 | "@arthurgeron/react-usememo" 24 | ], 25 | "rules": { 26 | // "react/react-in-jsx-scope": "off", 27 | // "react-hooks/rules-of-hooks": "error", 28 | // "react-hooks/exhaustive-deps": "warn", 29 | 30 | // require-usememo rule with custom options 31 | "@arthurgeron/react-usememo/require-usememo": ["error", { 32 | strict: true, 33 | checkHookReturnObject: true, 34 | fix: { addImports: true }, 35 | checkHookCalls: true, 36 | ignoredHookCallsNames: { "useStateManagement": true }, 37 | ignoredPropNames: ["style", "className"] 38 | }], 39 | 40 | // require-memo rule with custom options 41 | "@arthurgeron/react-usememo/require-memo": ["error", { 42 | ignoredComponents: { 43 | "Header": true, 44 | "Footer": true, 45 | "SimpleText": true 46 | } 47 | }], 48 | 49 | // require-usememo-children rule with custom options 50 | "@arthurgeron/react-usememo/require-usememo-children": ["error", { 51 | strict: true 52 | }] 53 | }, 54 | "settings": { 55 | "react": { 56 | "version": "detect" 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /examples/v8-traditional/README.md: -------------------------------------------------------------------------------- 1 | # ESLint Plugin React UseMemo - v8 Traditional Config Example 2 | 3 | This example project demonstrates how to use the `@arthurgeron/eslint-plugin-react-usememo` plugin with ESLint v8 using the traditional configuration format. 4 | 5 | ## Setup Details 6 | 7 | This project uses: 8 | - React 18 9 | - ESLint v8 10 | - Traditional ESLint configuration (`.eslintrc.json`) 11 | 12 | ## Key Files 13 | 14 | - `.eslintrc.json`: Contains the ESLint configuration that enables the react-usememo plugin rules 15 | - `src/App.js`: Demonstrates proper usage of `useMemo`, `useCallback`, and `React.memo` 16 | 17 | ## How to Use 18 | 19 | 1. Install dependencies: 20 | ``` 21 | npm install 22 | ``` 23 | 24 | 2. Run the linter: 25 | ``` 26 | npm run lint 27 | ``` 28 | 29 | 3. Start the development server: 30 | ``` 31 | npm start 32 | ``` 33 | 34 | ## ESLint Plugin Configuration 35 | 36 | The plugin is configured in `.eslintrc.json` with the following rules enabled: 37 | 38 | ```json 39 | { 40 | "@arthurgeron/react-usememo/require-usememo": "error", 41 | "@arthurgeron/react-usememo/require-memo": "error", 42 | "@arthurgeron/react-usememo/require-usememo-children": "error" 43 | } 44 | ``` 45 | 46 | This configuration ensures that: 47 | 1. Complex values (objects, arrays, functions) passed as props are wrapped in `useMemo`/`useCallback` 48 | 2. Function components are wrapped in `React.memo()` 49 | 3. Complex values passed as children are wrapped in `useMemo`/`useCallback` -------------------------------------------------------------------------------- /examples/v8-traditional/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-react-usememo-v8-example", 3 | "version": "1.0.0", 4 | "description": "Example React project with ESLint v8 traditional config", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "react-scripts start", 8 | "build": "react-scripts build", 9 | "test": "react-scripts test", 10 | "lint": "eslint src --resolve-plugins-relative-to=../../" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-scripts": "5.0.1" 16 | }, 17 | "devDependencies": { 18 | "@arthurgeron/eslint-plugin-react-usememo": "../../", 19 | "eslint": "^8.56.0", 20 | "eslint-plugin-react": "^7.33.2", 21 | "eslint-plugin-react-hooks": "^4.6.0" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /examples/v8-traditional/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useCallback } from 'react'; 2 | import { Custom } from './Custom'; 3 | 4 | // This component demonstrates a component that would benefit from useMemo and memo 5 | function CountDisplay({ count, label, onClick }) { 6 | console.log(`Rendering CountDisplay with count: ${count}`); 7 | 8 | // This complex object should be memoized to prevent unnecessary re-renders 9 | const styles = useMemo(() => ({ 10 | container: { 11 | padding: '20px', 12 | margin: '10px', 13 | border: '1px solid #ccc', 14 | borderRadius: '5px', 15 | backgroundColor: count % 2 === 0 ? '#f0f0f0' : '#e0e0e0' 16 | }, 17 | text: { 18 | color: count > 5 ? 'red' : 'blue', 19 | fontWeight: 'bold' 20 | } 21 | }), [count]); 22 | // This should trigger a lint error without useMemo because it creates a new object each render 23 | const data = { 24 | name: 'John', 25 | age: 30 26 | }; 27 | 28 | return ( 29 |
30 |

{label}: {count}

31 | 32 | 33 |
34 | ); 35 | } 36 | 37 | // Wrap with React.memo to prevent re-renders when props don't change 38 | const MemoizedCountDisplay = React.memo(CountDisplay); 39 | 40 | // This component is not wrapped in React.memo, which should trigger the require-memo rule 41 | export function UnmemoizedComponent({ value }) { 42 | // Add some complexity to make sure it triggers the require-memo rule 43 | const formattedValue = `Value: ${value}`; 44 | const styles = { 45 | container: { 46 | padding: '10px', 47 | margin: '5px', 48 | border: '1px solid #ddd', 49 | borderRadius: '3px' 50 | } 51 | }; 52 | 53 | return ( 54 |
55 |

{formattedValue}

56 | This component should be memoized 57 |
58 | ); 59 | } 60 | 61 | // Won't trigger ESLint errors because it's in ignoredComponents 62 | export function Header({ title }) { 63 | return ( 64 |
65 |

{title}

66 |
67 | ); 68 | } 69 | 70 | // Won't trigger ESLint errors because it's in ignoredComponents 71 | export function Footer() { 72 | return ( 73 |
74 |

© 2023 My Application

75 |
76 | ); 77 | } 78 | 79 | // Won't trigger ESLint errors because it's in ignoredComponents 80 | export function SimpleText({ text }) { 81 | return

{text}

; 82 | } 83 | 84 | // A component with complex children that should trigger the require-usememo-children rule 85 | function ComplexChildContainer({ children }) { 86 | return ( 87 |
88 | {children} 89 |
90 | ); 91 | } 92 | 93 | function App() { 94 | const [count1, setCount1] = useState(0); 95 | const [count2, setCount2] = useState(0); 96 | 97 | // Using useCallback to memoize function references 98 | const incrementCount1 = useCallback(() => { 99 | setCount1(prevCount => prevCount + 1); 100 | }, []); 101 | 102 | const incrementCount2 = useCallback(() => { 103 | setCount2(prevCount => prevCount + 1); 104 | }, []); 105 | 106 | // Example of a complex calculation that should be memoized 107 | const total = useMemo(() => { 108 | console.log('Calculating total...'); 109 | // Simulating expensive calculation 110 | return count1 + count2; 111 | }, [count1, count2]); 112 | 113 | // This could trigger a lint error without useMemo because it creates a new object each render 114 | const userData = useMemo(() => ({ 115 | counts: { 116 | first: count1, 117 | second: count2, 118 | total 119 | }, 120 | lastUpdated: new Date().toISOString() 121 | }), [count1, count2, total]); 122 | 123 | // These objects would normally require useMemo, but won't throw errors because their props are in ignoredPropNames 124 | const customStyle = { color: 'blue', fontWeight: 'bold' }; 125 | const customClassName = { primary: true, secondary: false }; 126 | 127 | // Calling a hook that would normally require its arguments to be memoized 128 | const useStateManagementResult = useStateManagement({ count1, count2 }); 129 | 130 | // This complex JSX structure should be memoized when used as children 131 | const complexChildren = ( 132 |
133 |

Complex Children

134 |

These children should be memoized with useMemo

135 |
    136 |
  • Item 1
  • 137 |
  • Item 2
  • 138 |
139 |
140 | ); 141 | 142 | // This function should use useCallback 143 | const handleClick = () => { 144 | console.log('Clicked!'); 145 | }; 146 | 147 | return ( 148 |
149 |

ESLint React useMemo Plugin Example (v8)

150 |
151 |

Total: {total}

152 |

Last Updated: {userData.lastUpdated}

153 | 154 | 159 | 160 | 165 | 166 | 167 | 168 |
169 | This div has a style prop that would normally require useMemo 170 |
171 | 172 |
173 | This div has a className prop that would normally require useMemo 174 |
175 | 176 | 177 | 178 |
{useStateManagementResult}
179 | 180 | 181 | {complexChildren} 182 | 183 | 184 | 185 | 186 |
187 |
188 | ); 189 | } 190 | 191 | // Mock hook for demonstration 192 | function useStateManagement(state) { 193 | return JSON.stringify(state); 194 | } 195 | 196 | export { MemoizedCountDisplay, ComplexChildContainer }; 197 | export default App; -------------------------------------------------------------------------------- /examples/v8-traditional/src/Custom.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Custom = React.memo(({ data }) => { 4 | return ( 5 |
6 |

Custom Component

7 |

Name: {data.name}

8 |

Age: {data.age}

9 |
10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/v8-traditional/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')); 6 | root.render( 7 | 8 | 9 | 10 | ); -------------------------------------------------------------------------------- /examples/v9-flat/README.md: -------------------------------------------------------------------------------- 1 | # ESLint Plugin React UseMemo - v9 Flat Config Example 2 | 3 | This example project demonstrates how to use the `@arthurgeron/eslint-plugin-react-usememo` plugin with ESLint v9 using the new flat configuration format. 4 | 5 | ## Setup Details 6 | 7 | This project uses: 8 | - React 18 9 | - ESLint v9 10 | - Flat ESLint configuration (`eslint.config.js`) 11 | 12 | ## Key Files 13 | 14 | - `eslint.config.js`: Contains the ESLint flat configuration that enables the react-usememo plugin rules 15 | - `src/App.js`: Demonstrates proper usage of `useMemo`, `useCallback`, and `React.memo` 16 | 17 | ## How to Use 18 | 19 | 1. Install dependencies: 20 | ``` 21 | npm install 22 | ``` 23 | 24 | 2. Run the linter: 25 | ``` 26 | npm run lint 27 | ``` 28 | 29 | 3. Start the development server: 30 | ``` 31 | npm start 32 | ``` 33 | 34 | ## ESLint Plugin Configuration 35 | 36 | The plugin is configured in `eslint.config.js` using the flat configuration format: 37 | 38 | ```js 39 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 40 | 41 | export default [ 42 | // Other configs... 43 | { 44 | // ... 45 | plugins: { 46 | '@arthurgeron/react-usememo': flatConfig 47 | }, 48 | rules: { 49 | '@arthurgeron/react-usememo/require-usememo': 'error', 50 | '@arthurgeron/react-usememo/require-memo': 'error', 51 | '@arthurgeron/react-usememo/require-usememo-children': 'error', 52 | } 53 | } 54 | ]; 55 | ``` 56 | 57 | Alternatively, you can use the plugin's recommended configuration: 58 | 59 | ```js 60 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 61 | 62 | export default [ 63 | // Other configs... 64 | flatConfig.configs.recommended, 65 | ]; 66 | ``` 67 | 68 | These configurations ensure that: 69 | 1. Complex values (objects, arrays, functions) passed as props are wrapped in `useMemo`/`useCallback` 70 | 2. Function components are wrapped in `React.memo()` 71 | 3. Complex values passed as children are wrapped in `useMemo`/`useCallback` -------------------------------------------------------------------------------- /examples/v9-flat/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { flatConfig } from '@arthurgeron/eslint-plugin-react-usememo'; 2 | import js from '@eslint/js'; 3 | import reactPlugin from 'eslint-plugin-react'; 4 | import reactHooksPlugin from 'eslint-plugin-react-hooks'; 5 | 6 | export default [ 7 | js.configs.recommended, 8 | // React configuration 9 | { 10 | files: ['**/*.js', '**/*.jsx'], 11 | plugins: { 12 | react: reactPlugin, 13 | 'react-hooks': reactHooksPlugin 14 | }, 15 | languageOptions: { 16 | ecmaVersion: 2022, 17 | sourceType: 'module', 18 | globals: { 19 | // Common browser globals 20 | document: 'readonly', 21 | window: 'readonly', 22 | console: 'readonly' 23 | }, 24 | // Parser options for JSX 25 | parserOptions: { 26 | ecmaFeatures: { 27 | jsx: true 28 | } 29 | }, 30 | }, 31 | settings: { 32 | react: { 33 | version: 'detect' 34 | } 35 | }, 36 | rules: { 37 | // React rules 38 | 'react/react-in-jsx-scope': 'off', 39 | 'react-hooks/rules-of-hooks': 'error', 40 | 'react-hooks/exhaustive-deps': 'warn' 41 | } 42 | }, 43 | // The plugin's flat config recommended settings 44 | { 45 | files: ['**/*.js', '**/*.jsx'], 46 | plugins: { 47 | '@arthurgeron/react-usememo': flatConfig 48 | }, 49 | rules: { 50 | // require-usememo rule with custom options 51 | '@arthurgeron/react-usememo/require-usememo': ['error', { 52 | strict: true, 53 | checkHookReturnObject: true, 54 | fix: { addImports: true }, 55 | checkHookCalls: true, 56 | ignoredHookCallsNames: { 'useStateManagement': true }, 57 | ignoredPropNames: ['style', 'className'] 58 | }], 59 | 60 | // require-memo rule with custom options 61 | '@arthurgeron/react-usememo/require-memo': ['warn', { 62 | ignoredComponents: { 63 | 'Header': true, 64 | 'Footer': true, 65 | 'SimpleText': true 66 | } 67 | }], 68 | 69 | // require-usememo-children rule with custom options 70 | '@arthurgeron/react-usememo/require-usememo-children': ['warn', { 71 | strict: true 72 | }] 73 | } 74 | } 75 | ]; -------------------------------------------------------------------------------- /examples/v9-flat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-react-usememo-v9-example", 3 | "version": "1.0.0", 4 | "description": "Example React project with ESLint v9 flat config", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "react-scripts start", 9 | "build": "react-scripts build", 10 | "test": "react-scripts test", 11 | "lint": "eslint src" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-scripts": "5.0.1" 17 | }, 18 | "devDependencies": { 19 | "@arthurgeron/eslint-plugin-react-usememo": "../../", 20 | "@eslint/js": "^9.0.0", 21 | "eslint": "^9.0.0", 22 | "eslint-plugin-react": "^7.33.2", 23 | "eslint-plugin-react-hooks": "^4.6.0" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /examples/v9-flat/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useCallback } from 'react'; 2 | import { Custom } from './Custom'; 3 | // This component demonstrates a component that would benefit from useMemo and memo 4 | function CountDisplay({ count, label, onClick }) { 5 | console.log(`Rendering CountDisplay with count: ${count}`); 6 | 7 | // This complex object should be memoized to prevent unnecessary re-renders 8 | const styles = useMemo(() => ({ 9 | container: { 10 | padding: '20px', 11 | margin: '10px', 12 | border: '1px solid #ccc', 13 | borderRadius: '5px', 14 | backgroundColor: count % 2 === 0 ? '#f0f0f0' : '#e0e0e0' 15 | }, 16 | text: { 17 | color: count > 5 ? 'red' : 'blue', 18 | fontWeight: 'bold' 19 | } 20 | }), [count]); 21 | 22 | // This should trigger a lint error without useMemo because it creates a new object each render 23 | const data = { 24 | name: 'John', 25 | age: 30 26 | }; 27 | 28 | // This should trigger a lint error - a function that should use useCallback 29 | const handleClick = () => { 30 | console.log('Clicked!', count); 31 | onClick(); 32 | }; 33 | 34 | // This should trigger a lint error - an array that should use useMemo 35 | const items = ['item1', 'item2', 'item3']; 36 | 37 | return ( 38 |
39 |

{label}: {count}

40 | 41 | 42 |
    43 | {items.map(item =>
  • {item}
  • )} 44 |
45 |
46 | ); 47 | } 48 | 49 | // Wrap with React.memo to prevent re-renders when props don't change 50 | const MemoizedCountDisplay = React.memo(CountDisplay); 51 | 52 | // This component should trigger errors because it's not wrapped in React.memo 53 | export function UnmemoizedComponent({ value }) { 54 | // Add some complexity to make sure it triggers the require-memo rule 55 | const formattedValue = `Value: ${value}`; 56 | const styles = { 57 | container: { 58 | padding: '10px', 59 | margin: '5px', 60 | border: '1px solid #ddd', 61 | borderRadius: '3px' 62 | } 63 | }; 64 | 65 | return ( 66 |
67 |

{formattedValue}

68 | This component should be memoized 69 |
70 | ); 71 | } 72 | 73 | // Won't trigger ESLint errors because it's in ignoredComponents 74 | export function Header({ title }) { 75 | return ( 76 |
77 |

{title}

78 |
79 | ); 80 | } 81 | // Won't trigger ESLint errors because it's in ignoredComponents 82 | export function Footer() { 83 | return ( 84 |
85 |

© 2023 My Application

86 |
87 | ); 88 | } 89 | 90 | export function SimpleText({ text }) { 91 | return

{text}

; 92 | } 93 | 94 | // A component with complex children that should trigger the require-usememo-children rule 95 | function ComplexChildContainer({ children }) { 96 | return ( 97 |
98 | {children} 99 |
100 | ); 101 | } 102 | 103 | function App() { 104 | const [count1, setCount1] = useState(0); 105 | const [count2, setCount2] = useState(0); 106 | 107 | // Using useCallback to memoize function references 108 | const incrementCount1 = useCallback(() => { 109 | setCount1(prevCount => prevCount + 1); 110 | }, []); 111 | 112 | const incrementCount2 = useCallback(() => { 113 | setCount2(prevCount => prevCount + 1); 114 | }, []); 115 | 116 | // Example of a complex calculation that should be memoized 117 | const total = useMemo(() => { 118 | console.log('Calculating total...'); 119 | // Simulating expensive calculation 120 | return count1 + count2; 121 | }, [count1, count2]); 122 | 123 | // This could trigger a lint error without useMemo because it creates a new object each render 124 | const userData = useMemo(() => ({ 125 | counts: { 126 | first: count1, 127 | second: count2, 128 | total 129 | }, 130 | lastUpdated: new Date().toISOString() 131 | }), [count1, count2, total]); 132 | 133 | // These objects would normally require useMemo, but won't throw errors because their props are in ignoredPropNames 134 | const customStyle = { color: 'blue', fontWeight: 'bold' }; 135 | const customClassName = { primary: true, secondary: false }; 136 | 137 | // Calling a hook that would normally require its arguments to be memoized 138 | const useStateManagementResult = useStateManagement({ count1, count2 }); 139 | 140 | // This complex JSX structure should be memoized when used as children 141 | const complexChildren = ( 142 |
143 |

Complex Children

144 |

These children should be memoized with useMemo

145 |
    146 |
  • Item 1
  • 147 |
  • Item 2
  • 148 |
149 |
150 | ); 151 | 152 | return ( 153 |
154 |

ESLint React useMemo Plugin Example (v9 Flat Config)

155 |
156 |

Total: {total}

157 |

Last Updated: {userData.lastUpdated}

158 | 159 | 164 | 165 | 170 | 171 | 172 | 173 |
174 | This div has a style prop that would normally require useMemo 175 |
176 | 177 |
178 | This div has a className prop that would normally require useMemo 179 |
180 | 181 | 182 | 183 |
{useStateManagementResult}
184 | 185 | 186 | {complexChildren} 187 | 188 | 189 |
190 |
191 | ); 192 | } 193 | 194 | // Mock hook for demonstration 195 | function useStateManagement(state) { 196 | return JSON.stringify(state); 197 | } 198 | 199 | export { MemoizedCountDisplay, ComplexChildContainer }; 200 | export default App; -------------------------------------------------------------------------------- /examples/v9-flat/src/Custom.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Custom = React.memo(({ data }) => { 4 | return ( 5 |
6 |

Custom Component

7 |

Name: {data.name}

8 |

Age: {data.age}

9 |
10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/v9-flat/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')); 6 | root.render( 7 | 8 | 9 | 10 | ); -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | globals: { 5 | 'ts-jest': { 6 | diagnostics: false, 7 | }, 8 | }, 9 | moduleNameMapper: { 10 | "src/(.*)": "/src/$1" 11 | }, 12 | testPathIgnorePatterns : [ 13 | "/src/index.ts", 14 | "/__tests__/(.*)/ruleTester.ts", 15 | "/__tests__/testcases/(.*)", 16 | ], 17 | collectCoverage: true, 18 | collectCoverageFrom: [ 19 | '/src/*', 20 | '/src/**/*', 21 | '!/src/index.{js,ts}', 22 | '!/src/**/constants.{js,ts}', 23 | '!/src/**/flat-config.{js,ts}', 24 | '!/src/**/traditional-config.{js,ts}', 25 | ], 26 | cacheDirectory: '.jest-cache', 27 | coverageThreshold: { 28 | global: { 29 | branches: 75, 30 | functions: 80, 31 | lines: 80, 32 | statements: 75, 33 | }, 34 | }, 35 | }; -------------------------------------------------------------------------------- /knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/webpro/knip/raw/main/schema.json", 3 | "entry": ["src/index.ts"], 4 | "project": "**/*.ts", 5 | "ignoreUnusedDependencies": ["estree"], 6 | "ignoreFiles": [ 7 | "src/flat-config.ts", 8 | "src/traditional-config.ts", 9 | "__tests__/testcases/*.ts" 10 | ], 11 | "rules": { 12 | "exports": "error" 13 | }, 14 | "ignoreExportsUsedInFile": true 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@arthurgeron/eslint-plugin-react-usememo", 3 | "version": "2.5.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "author": "Stefano J. Attardi & Arthur Geron =3.0.3" 53 | }, 54 | "exports": { 55 | ".": { 56 | "import": "./dist/index.js", 57 | "require": "./dist/index.cjs" 58 | } 59 | }, 60 | "eslintConfig": { 61 | "plugins": [ 62 | "@arthurgeron/react-usememo" 63 | ], 64 | "rules": { 65 | "@arthurgeron/react-usememo/require-usememo": "error", 66 | "@arthurgeron/react-usememo/require-memo": "error", 67 | "@arthurgeron/react-usememo/require-usememo-children": "error" 68 | } 69 | }, 70 | "peerDependencies": { 71 | "eslint": "^8.0.0 || ^9.0.0" 72 | } 73 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | 5 | export default [ 6 | { 7 | input: 'src/index.ts', 8 | output: [ 9 | { 10 | file: 'dist/index.js', 11 | format: 'esm', 12 | sourcemap: true, 13 | }, 14 | { 15 | file: 'dist/index.cjs', 16 | format: 'cjs', 17 | sourcemap: true, 18 | }, 19 | ], 20 | plugins: [ 21 | resolve(), 22 | commonjs(), 23 | typescript({ 24 | exclude: ['**/__tests__/**', '**/*.test.ts', '**/testcases/**'] 25 | }) 26 | ], 27 | external: ['eslint', '@typescript-eslint/types'], 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /src/constants/messages.ts: -------------------------------------------------------------------------------- 1 | export const MessagesRequireUseMemo = { 2 | "error-in-invalid-context": "An error was identified inside this expression, but it can't be fixed because it'd break the rule of hooks. JSX logic should be extracted into a separate component.", 3 | "object-usememo-props": 4 | "Object literal should be wrapped in useMemo() or be static when used as a prop", 5 | "object-class-memo-props": 6 | "Object literal should come from state or be static when used as a prop", 7 | "object-usememo-hook": 8 | "Object literal should come from state or be static when returned from a hook", 9 | "object-usememo-deps": 10 | "Object literal should be wrapped in useMemo() or be static when used as a hook dependency", 11 | "array-usememo-props": 12 | "Array literal should be wrapped in useMemo() or be static when used as a prop", 13 | "array-usememo-hook": 14 | "Array literal should be wrapped in useMemo() or be static when returned from a hook", 15 | "array-class-memo-props": 16 | "Array literal should be from state and declared in state, constructor, getDerivedStateFromProps or statically when used as a prop", 17 | "array-usememo-deps": 18 | "Array literal should be wrapped in useMemo() or be static when used as a hook dependency", 19 | "instance-usememo-props": 20 | "Object instantiation should be wrapped in useMemo() or be static when used as a prop", 21 | "instance-usememo-hook": 22 | "Object instantiation should be wrapped in useMemo() or be static when returned from a hook", 23 | "instance-class-memo-props": 24 | "Object instantiation should be done in state, constructor, getDerivedStateFromProps or statically when used as a prop", 25 | "instance-usememo-deps": 26 | "Object instantiation should be wrapped in useMemo() or be static when used as a hook dependency", 27 | "jsx-usememo-props": 28 | "JSX should be wrapped in useMemo() when used as a prop", 29 | "jsx-usememo-hook": 30 | "JSX should be wrapped in useMemo() when returned from a hook", 31 | "jsx-usememo-deps": 32 | "JSX should be wrapped in useMemo() when used as a hook dependency", 33 | "function-usecallback-props": 34 | "Function definition should be wrapped in useCallback() or be static when used as a prop", 35 | "function-usecallback-hook": 36 | "Function definition should be wrapped in useCallback() or be static when returned from a hook", 37 | "function-class-props": 38 | "Function definition should declared as a class property or statically when used as a prop", 39 | "function-usecallback-deps": 40 | "Function definition should be wrapped in useCallback() or be static when used as a hook dependency", 41 | "unknown-usememo-props": 42 | "Unknown value may need to be wrapped in useMemo() when used as a prop", 43 | "unknown-usememo-hook": 44 | "Unknown value may need to be wrapped in useMemo() when returned from a hook", 45 | "unknown-class-memo-props": 46 | "Unknown value should be declared in state, constructor, getDerivedStateFromProps or statically when used as a prop", 47 | "unknown-usememo-deps": 48 | "Unknown value may need to be wrapped in useMemo() when used as a hook dependency", 49 | "usememo-const": 50 | "useMemo/useCallback return value should be assigned to a `const` to prevent reassignment", 51 | } as const; 52 | 53 | export const MessagesRequireUseMemoChildren = { 54 | "object-usememo-children": 55 | "Object literal should be wrapped in React's useMemo() when used as children", 56 | "array-usememo-children": 57 | "Array literal should be wrapped in React's useMemo() when used as children", 58 | "instance-usememo-children": 59 | "Object instantiation should be wrapped in React's useMemo() when used as children", 60 | "jsx-usememo-children": 61 | "JSX should be wrapped in React's useMemo() when used as children", 62 | "function-usecallback-children": 63 | "Function definition should be wrapped in useCallback() when used as children", 64 | "unknown-usememo-children": 65 | "Unknown value may need to be wrapped in React's useMemo() when used as children", 66 | "usememo-const": 67 | "useMemo/useCallback return value should be assigned to a `const` to prevent reassignment", 68 | }; 69 | 70 | -------------------------------------------------------------------------------- /src/flat-config.ts: -------------------------------------------------------------------------------- 1 | import requireMemoRule from "./require-memo"; 2 | import requireUseMemoRule from "./require-usememo/index"; 3 | import requireUseMemoChildrenRule from "./require-usememo-children"; 4 | 5 | /** 6 | * Plugin definition for ESLint flat config format (ESLint V9) 7 | */ 8 | export const flatConfig = { 9 | meta: { 10 | name: '@arthurgeron/eslint-plugin-react-usememo', 11 | version: '2.5.0' 12 | }, 13 | rules: { 14 | 'require-memo': requireMemoRule, 15 | 'require-usememo': requireUseMemoRule, 16 | 'require-usememo-children': requireUseMemoChildrenRule, 17 | } 18 | }; 19 | 20 | // Add configs after defining the plugin to reference the plugin itself 21 | Object.defineProperty(flatConfig, 'configs', { 22 | value: { 23 | recommended: { 24 | plugins: { 25 | '@arthurgeron/react-usememo': flatConfig 26 | }, 27 | rules: { 28 | '@arthurgeron/react-usememo/require-usememo': 'error', 29 | '@arthurgeron/react-usememo/require-memo': 'error', 30 | '@arthurgeron/react-usememo/require-usememo-children': 'error', 31 | }, 32 | }, 33 | } 34 | }); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { configs, rules } from "./traditional-config"; 2 | import { flatConfig } from "./flat-config"; 3 | 4 | export { configs, rules } from "./traditional-config"; 5 | export { flatConfig } from "./flat-config"; 6 | 7 | export default { 8 | rules, 9 | configs, 10 | flatConfig, 11 | }; -------------------------------------------------------------------------------- /src/require-memo/index.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint-v9"; 2 | import type { Rule as RuleV8 } from "eslint"; 3 | import { findVariable, isComponentName } from "../utils"; 4 | import { checkVariableDeclaration, checkFunction } from "./utils"; 5 | import type { MemoFunctionDeclaration, MemoFunctionExpression } from "./types"; 6 | import type { TSESTree } from "@typescript-eslint/types"; 7 | import { getCompatibleScope, type CompatibleNode } from "../utils/compatibility"; 8 | import type { CompatibleRuleModule } from "../utils/compatibility"; 9 | 10 | const rule: CompatibleRuleModule = { 11 | meta: { 12 | messages: { 13 | "memo-required": "Component definition not wrapped in React.memo()", 14 | }, 15 | schema: [ 16 | { 17 | type: "object", 18 | properties: { ignoredComponents: { type: "object" } }, 19 | additionalProperties: false, 20 | }, 21 | ], 22 | }, 23 | create: (context: RuleV8.RuleContext | Rule.RuleContext) => { 24 | return { 25 | ExportNamedDeclaration(node: CompatibleNode) { 26 | // Use type assertions to help TypeScript understand the node structure 27 | const tsNode = node as TSESTree.ExportNamedDeclaration; 28 | // Check if the node has declaration and is a function 29 | if ( 30 | tsNode.declaration && 31 | tsNode.declaration.type === "VariableDeclaration" 32 | ) { 33 | const declarations = tsNode.declaration.declarations; 34 | for (const declaration of declarations) { 35 | if ( 36 | isComponentName((declaration?.id as TSESTree.Identifier)?.name) 37 | ) { 38 | checkVariableDeclaration( 39 | context, 40 | declaration, 41 | ); 42 | } 43 | } 44 | return; 45 | } 46 | if (tsNode.declaration?.type === "FunctionDeclaration") { 47 | checkFunction(context, tsNode.declaration as MemoFunctionDeclaration); 48 | } 49 | }, 50 | ExportDefaultDeclaration(node: CompatibleNode) { 51 | // Use type assertions to help TypeScript understand the node structure 52 | const tsNode = node as TSESTree.ExportDefaultDeclaration; 53 | 54 | // Handle default export of arrow functions and function expressions directly 55 | if ( 56 | tsNode.declaration.type === "ArrowFunctionExpression" || 57 | tsNode.declaration.type === "FunctionExpression" 58 | ) { 59 | // Direct export default of a function should use memo 60 | context.report({ 61 | node: tsNode.declaration as any, 62 | messageId: "memo-required", 63 | }); 64 | return; 65 | } 66 | 67 | // It was exported in one place but declared elsewhere 68 | if (tsNode.declaration.type === "Identifier") { 69 | const scope = getCompatibleScope(context, node); 70 | if (scope) { 71 | const variable = findVariable(scope, tsNode.declaration); 72 | 73 | if (variable?.defs[0]?.type === "Variable") { 74 | const variableNode = variable.defs[0].node; 75 | 76 | if ( 77 | isComponentName((variableNode.id as TSESTree.Identifier).name) 78 | ) { 79 | checkVariableDeclaration( 80 | context, 81 | variableNode, 82 | ); 83 | } 84 | } 85 | } 86 | return; 87 | } 88 | if (tsNode.declaration.type === "FunctionDeclaration") { 89 | checkFunction(context, tsNode.declaration as MemoFunctionDeclaration); 90 | } 91 | }, 92 | }; 93 | }, 94 | }; 95 | 96 | export default rule; 97 | -------------------------------------------------------------------------------- /src/require-memo/types.ts: -------------------------------------------------------------------------------- 1 | import type * as ESTree from "estree"; 2 | import type { Rule } from "eslint"; 3 | 4 | export type MemoVariableIdentifier = ESTree.Identifier & Rule.NodeParentExtension; 5 | export type MemoFunctionExpression = (ESTree.FunctionExpression | ESTree.ArrowFunctionExpression) & Rule.NodeParentExtension; 6 | export type MemoFunctionDeclaration = ESTree.FunctionDeclaration & Rule.NodeParentExtension; -------------------------------------------------------------------------------- /src/require-memo/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint"; 2 | import type { Rule as RuleV9 } from "eslint-v9"; 3 | import type * as ESTree from "estree"; 4 | import { isComponentName, shouldIgnoreNode } from "../utils"; 5 | import * as path from "node:path"; 6 | import { getCompatibleScope, getCompatibleFilename } from "../utils/compatibility"; 7 | 8 | import type { MemoFunctionExpression, MemoVariableIdentifier } from "./types"; 9 | import type { ESNode } from "src/types"; 10 | 11 | function isMemoCallExpression(node: Rule.Node) { 12 | if (node.type !== "CallExpression") return false; 13 | if (node.callee?.type === "MemberExpression") { 14 | const { 15 | callee: { object, property }, 16 | } = node; 17 | if ( 18 | object.type === "Identifier" && 19 | property.type === "Identifier" && 20 | object.name === "React" && 21 | property.name === "memo" 22 | ) { 23 | return true; 24 | } 25 | } else if ( 26 | node.callee?.type === "Identifier" && 27 | node.callee?.name === "memo" 28 | ) { 29 | return true; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | export function checkFunction( 36 | context: Rule.RuleContext | RuleV9.RuleContext, 37 | node: ( 38 | | ESTree.ArrowFunctionExpression 39 | | ESTree.FunctionExpression 40 | | ESTree.FunctionDeclaration 41 | | ESTree.Identifier 42 | ) & 43 | (Rule.NodeParentExtension | RuleV9.NodeParentExtension), 44 | ) { 45 | const ignoredNames = context.options?.[0]?.ignoredComponents; 46 | let currentNode = node.type === "FunctionDeclaration" ? node : node.parent; 47 | while (currentNode.type === "CallExpression") { 48 | if (isMemoCallExpression(currentNode)) { 49 | return; 50 | } 51 | 52 | currentNode = currentNode.parent; 53 | } 54 | 55 | if ( 56 | currentNode.type === "VariableDeclarator" || 57 | currentNode.type === "FunctionDeclaration" 58 | ) { 59 | const { id } = currentNode; 60 | if (id?.type === "Identifier") { 61 | if ( 62 | isComponentName(id?.name) && 63 | (!ignoredNames || 64 | !shouldIgnoreNode(id as unknown as ESNode, ignoredNames)) 65 | ) { 66 | context.report({ node, messageId: "memo-required" }); 67 | } 68 | } 69 | } else if ( 70 | node.type === "FunctionDeclaration" && 71 | currentNode.type === "Program" 72 | ) { 73 | if ( 74 | ignoredNames && 75 | !shouldIgnoreNode(node as unknown as ESNode, ignoredNames) 76 | ) { 77 | return; 78 | } 79 | if (node.id !== null && isComponentName(node.id?.name)) { 80 | context.report({ node, messageId: "memo-required" }); 81 | } else { 82 | if (getCompatibleFilename(context) === "") return; 83 | const filename = path.basename(getCompatibleFilename(context)); 84 | if (isComponentName(filename)) { 85 | context.report({ node, messageId: "memo-required" }); 86 | } 87 | } 88 | } 89 | } 90 | 91 | export function checkVariableDeclaration( 92 | context: Rule.RuleContext | RuleV9.RuleContext, 93 | declaration: any, // Using any to bypass type incompatibilities between ESLint v8 and v9 94 | ) { 95 | if (declaration.init) { 96 | if (declaration.init.type === "CallExpression") { 97 | const declarationProperties = ( 98 | (declaration.init.callee as MemoVariableIdentifier).name 99 | ? declaration.init.callee 100 | : (declaration.init.callee as ESTree.MemberExpression).property 101 | ) as MemoVariableIdentifier; 102 | if (declarationProperties?.name === "memo") { 103 | checkFunction( 104 | context, 105 | declaration.init.arguments[0] as MemoVariableIdentifier, 106 | ); 107 | return; 108 | } 109 | } else if ( 110 | declaration.init.type === "ArrowFunctionExpression" || 111 | declaration.init.type === "FunctionExpression" 112 | ) { 113 | checkFunction(context, declaration.init as MemoFunctionExpression); 114 | return; 115 | } 116 | } 117 | } 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/require-usememo-children.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint"; 2 | import type { Rule as RuleV9 } from "eslint-v9"; 3 | import type * as ESTree from "estree"; 4 | import type { TSESTree } from "@typescript-eslint/types"; 5 | import { 6 | getExpressionMemoStatus, 7 | isComplexComponent, 8 | } from "./utils"; 9 | import { MessagesRequireUseMemoChildren } from "./constants/messages"; 10 | import { MemoStatus } from "src/types"; 11 | import type { CompatibleContext } from "./require-usememo/utils"; 12 | import type { CompatibleNode, CompatibleRuleModule } from "./utils/compatibility"; 13 | 14 | const rule: CompatibleRuleModule = { 15 | meta: { 16 | messages: MessagesRequireUseMemoChildren, 17 | schema: [ 18 | { 19 | type: "object", 20 | properties: { strict: { type: "boolean" } }, 21 | additionalProperties: false, 22 | }, 23 | ], 24 | }, 25 | create: (context: CompatibleContext) => { 26 | let isClass = false; 27 | function report(node: CompatibleNode, messageId: keyof typeof MessagesRequireUseMemoChildren) { 28 | context.report({ node: node as any, messageId: messageId as string }); 29 | } 30 | 31 | return { 32 | ClassDeclaration: () => { 33 | isClass = true; 34 | }, 35 | 36 | JSXElement: (node: CompatibleNode) => { 37 | const tsNode = node as unknown as TSESTree.JSXElement; 38 | const { 39 | children, 40 | openingElement, 41 | } = tsNode; 42 | if (isClass || !isComplexComponent(openingElement)) return; 43 | 44 | for (const child of children) { 45 | if (child.type === "JSXElement" || child.type === "JSXFragment") { 46 | report(node, "jsx-usememo-children"); 47 | return; 48 | } 49 | if (child.type === "JSXExpressionContainer") { 50 | const { expression } = child; 51 | if (expression.type !== "JSXEmptyExpression") { 52 | const statusData = getExpressionMemoStatus(context, expression); 53 | switch (statusData?.status) { 54 | case MemoStatus.UnmemoizedObject: 55 | report(node, "object-usememo-children"); 56 | break; 57 | case MemoStatus.UnmemoizedArray: 58 | report(node, "array-usememo-children"); 59 | break; 60 | case MemoStatus.UnmemoizedNew: 61 | report(node, "instance-usememo-children"); 62 | break; 63 | case MemoStatus.UnmemoizedFunction: 64 | report(node, "function-usecallback-children"); 65 | break; 66 | case MemoStatus.UnmemoizedFunctionCall: 67 | case MemoStatus.UnmemoizedOther: 68 | if (context.options?.[0]?.strict) { 69 | report(node, "unknown-usememo-children"); 70 | } 71 | break; 72 | case MemoStatus.UnmemoizedJSX: 73 | report(node, "jsx-usememo-children"); 74 | break; 75 | } 76 | } 77 | } 78 | } 79 | }, 80 | }; 81 | }, 82 | }; 83 | 84 | export default rule; 85 | -------------------------------------------------------------------------------- /src/require-usememo/constants.ts: -------------------------------------------------------------------------------- 1 | import { MemoStatus } from 'src/types'; 2 | import type { ExpressionData, MemoErrorHookDictionary } from './types'; 3 | 4 | export const defaultImportRangeStart = `import { `; 5 | export const nameGeneratorUUID = 'b32a4d70-4f64-11eb-89d5-33e6ce8a6c99'; 6 | export const jsxEmptyExpressionClassData: ExpressionData = { 7 | [MemoStatus.UnmemoizedObject.toString()]: "object-class-memo-props", 8 | [MemoStatus.UnmemoizedArray.toString()]: "array-class-memo-props", 9 | [MemoStatus.UnmemoizedNew.toString()]: "instance-class-memo-props", 10 | [MemoStatus.UnmemoizedFunction.toString()]: 'instance-class-memo-props', 11 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-class-memo-props", 12 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-class-memo-props", 13 | [MemoStatus.UnsafeLet.toString()]: "usememo-const", 14 | } 15 | 16 | export const jsxEmptyExpressionData: ExpressionData = { 17 | [MemoStatus.UnmemoizedObject.toString()]: "object-usememo-props", 18 | [MemoStatus.UnmemoizedArray.toString()]: "array-usememo-props", 19 | [MemoStatus.UnmemoizedNew.toString()]: "instance-usememo-props", 20 | [MemoStatus.UnmemoizedFunction.toString()]: "function-usecallback-props", 21 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-usememo-props", 22 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-usememo-props", 23 | [MemoStatus.UnmemoizedJSX.toString()]: "jsx-usememo-props", 24 | [MemoStatus.UnsafeLet.toString()]: "usememo-const", 25 | } 26 | 27 | export const hookReturnExpressionData: ExpressionData = { 28 | [MemoStatus.UnmemoizedObject.toString()]: "object-usememo-hook", 29 | [MemoStatus.UnmemoizedArray.toString()]: "array-usememo-hook", 30 | [MemoStatus.UnmemoizedNew.toString()]: "instance-usememo-hook", 31 | [MemoStatus.UnmemoizedFunction.toString()]: "function-usecallback-hook", 32 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-usememo-hook", 33 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-usememo-hook", 34 | [MemoStatus.UnmemoizedJSX.toString()]: "jsx-usememo-hook", 35 | [MemoStatus.UnsafeLet.toString()]: "usememo-const", 36 | } 37 | 38 | export const callExpressionData: ExpressionData = { 39 | [MemoStatus.UnmemoizedObject.toString()]: "object-usememo-deps", 40 | [MemoStatus.UnmemoizedArray.toString()]: "array-usememo-deps", 41 | [MemoStatus.UnmemoizedNew.toString()]: "instance-usememo-deps", 42 | [MemoStatus.UnmemoizedFunction.toString()]: "function-usecallback-deps", 43 | [MemoStatus.UnmemoizedFunctionCall.toString()]: "unknown-usememo-deps", 44 | [MemoStatus.UnmemoizedOther.toString()]: "unknown-usememo-deps", 45 | [MemoStatus.UnmemoizedJSX.toString()]: "jsx-usememo-deps", 46 | [MemoStatus.UnsafeLet.toString()]: "usememo-const", 47 | } 48 | 49 | export const defaultReactHookNames: Record = { 50 | "useContext": true, 51 | "useState": true, 52 | "useReducer": true, 53 | "useRef": true, 54 | "useLayoutEffect": true, 55 | "useEffect": true, 56 | "useImperativeHandle": true, 57 | "useCallback": true, 58 | "useMemo": true, 59 | "useDebugValue": true, 60 | "useDeferredValue": true, 61 | "useTransition": true, 62 | "useId": true, 63 | "useInsertionEffect": true, 64 | "useSyncExternalStore": true, 65 | "useQuery": true, 66 | "useMutation": true, 67 | "useQueryClient": true, 68 | "useInfiniteQuery": true, 69 | } 70 | 71 | export const messageIdToHookDict: MemoErrorHookDictionary = { 72 | 'function-usecallback-props': 'useCallback', 73 | 'function-usecallback-hook': 'useCallback', 74 | 'function-usecallback-deps': 'useCallback', 75 | 'object-usememo-props': 'useMemo', 76 | 'usememo-const': 'useMemo', 77 | }; 78 | -------------------------------------------------------------------------------- /src/require-usememo/index.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint-v9"; 2 | import type { Rule as RuleV8 } from "eslint"; 3 | import type { TSESTree } from "@typescript-eslint/types"; 4 | import { defaultReactHookNames, jsxEmptyExpressionClassData, jsxEmptyExpressionData, callExpressionData, hookReturnExpressionData } from './constants'; 5 | import { MessagesRequireUseMemo } from '../constants/messages'; 6 | import { 7 | getExpressionMemoStatus, 8 | isComplexComponent, 9 | shouldIgnoreNode, 10 | } from "../utils"; 11 | import type {ExpressionTypes, NodeType, ExpressionData, ReactImportInformation, ImportNode} from './types'; 12 | import { checkForErrors, fixBasedOnMessageId, getIsHook, type CompatibleContext } from './utils'; 13 | import { type ESNode, MemoStatus } from "src/types"; 14 | import { getCompatibleScope, type CompatibleNode } from "../utils/compatibility"; 15 | import type { CompatibleRuleModule } from "../utils/compatibility"; 16 | 17 | const rule: CompatibleRuleModule = { 18 | meta: { 19 | type: 'problem', 20 | messages: MessagesRequireUseMemo, 21 | docs: { 22 | description: 'Detects shallow comparison fails in React', 23 | recommended: true, 24 | }, 25 | fixable: 'code', 26 | schema: [ 27 | { 28 | type: "object", 29 | properties: { strict: { type: "boolean" }, checkHookReturnObject: { type: "boolean" }, checkHookCalls: { type: "boolean"}, ignoredHookCallsNames: {type: "object"}, fix: { 30 | addImports: "boolean", 31 | }, ignoredPropNames: { type: "array" } }, 32 | additionalProperties: false, 33 | }, 34 | ], 35 | }, 36 | create: (context: CompatibleContext) => { 37 | let isClass = false; 38 | const importData: ReactImportInformation = { 39 | reactImported: false, 40 | useMemoImported: false, 41 | useCallbackImported: false, 42 | } 43 | 44 | function report(node: T, messageId: keyof typeof MessagesRequireUseMemo) { 45 | context.report({ 46 | node: node as unknown as Rule.Node, 47 | messageId, 48 | fix(fixer) { 49 | const disableFixer = isClass || messageId === MemoStatus.ErrorInvalidContext; 50 | return disableFixer ? null : fixBasedOnMessageId(node as Rule.Node, messageId, fixer, context, importData); 51 | } 52 | }); 53 | } 54 | 55 | function process(node: NodeType, _expression?: ExpressionTypes, expressionData?: ExpressionData, checkContext = false) { 56 | const isGlobalScope = getCompatibleScope(context, node)?.block.type === 'Program'; 57 | 58 | if (checkContext && isGlobalScope) { 59 | return; 60 | } 61 | 62 | const expression = _expression ?? (node.value && Object.prototype.hasOwnProperty.call(node.value, 'expression') ? (node.value as unknown as TSESTree.JSXExpressionContainer).expression : node.value) ; 63 | switch(expression?.type) { 64 | case 'LogicalExpression': 65 | !expression.left ? true : process(node, (expression as TSESTree.LogicalExpression).left); 66 | !expression.right ? true : process(node, (expression as TSESTree.LogicalExpression).right); 67 | return; 68 | case 'JSXEmptyExpression': 69 | return; 70 | default: 71 | checkForErrors( 72 | expressionData || (isClass ? jsxEmptyExpressionClassData : jsxEmptyExpressionData), 73 | getExpressionMemoStatus(context as Rule.RuleContext, expression as TSESTree.Expression, checkContext), 74 | context, 75 | node, 76 | report 77 | ); 78 | return; 79 | } 80 | } 81 | 82 | function JSXAttribute(node: CompatibleNode) { 83 | const ignoredPropNames = context.options?.[0]?.ignoredPropNames ?? []; 84 | const tsNode = node as unknown as TSESTree.JSXAttribute; 85 | 86 | const { parent, value } = tsNode; 87 | if (value === null) return null; 88 | if (parent && !isComplexComponent(parent as TSESTree.JSXIdentifier)) return null; 89 | if ((value.type as string) === "JSXExpressionContainer") { 90 | if (ignoredPropNames.includes(tsNode.name?.name)) { 91 | return null; 92 | } 93 | process(node as unknown as NodeType, undefined, undefined, true); 94 | } 95 | return null; 96 | } 97 | 98 | return { 99 | JSXAttribute, 100 | 101 | ClassDeclaration: () => { 102 | isClass = true; 103 | }, 104 | 105 | ImportDeclaration(node: CompatibleNode) { 106 | const tsNode = node as TSESTree.ImportDeclaration; 107 | if (tsNode.source.value === 'react' && tsNode.importKind !== 'type') { 108 | importData.reactImported = true; 109 | const specifiers = Array.isArray(tsNode.specifiers) ? tsNode.specifiers.slice() : tsNode.specifiers; 110 | importData.importDeclaration = Object.assign({}, tsNode, {specifiers}) as TSESTree.ImportDeclaration; 111 | importData.useMemoImported = specifiers.some(specifier => specifier.local.name === 'useMemo'); 112 | importData.useCallbackImported = specifiers.some(specifier => specifier.local.name === 'useCallback'); 113 | } 114 | }, 115 | 116 | ReturnStatement(node: CompatibleNode) { 117 | const tsNode = node as TSESTree.ReturnStatement; 118 | const functionDeclarationNode = tsNode.parent?.parent?.type === 'FunctionDeclaration' && tsNode?.parent?.parent?.id; 119 | const anonFuncVariableDeclarationNode = tsNode.parent?.parent?.type === 'ArrowFunctionExpression' && tsNode?.parent?.parent?.parent?.type === 'VariableDeclarator' && tsNode?.parent?.parent?.parent?.id; 120 | const validNode = functionDeclarationNode || anonFuncVariableDeclarationNode; 121 | 122 | if (validNode && getIsHook(validNode as TSESTree.Identifier) && tsNode.argument) { 123 | if (tsNode.argument.type === 'ObjectExpression') { 124 | if (context.options?.[0]?.checkHookReturnObject) { 125 | report(node as unknown as Rule.NodeParentExtension, "object-usememo-hook"); 126 | return; 127 | } 128 | const objExp = (tsNode.argument as TSESTree.ObjectExpression); 129 | 130 | // Replace forEach with for...of to fix linting error 131 | for (const _node of objExp.properties) { 132 | process( 133 | _node as unknown as NodeType, 134 | (_node as TSESTree.Property).value as unknown as ExpressionTypes, 135 | hookReturnExpressionData 136 | ); 137 | } 138 | return; 139 | } 140 | process( 141 | node as unknown as NodeType, 142 | tsNode.argument as unknown as ExpressionTypes, 143 | hookReturnExpressionData 144 | ); 145 | } 146 | }, 147 | 148 | CallExpression: (node: CompatibleNode) => { 149 | const tsNode = node as TSESTree.CallExpression; 150 | const { callee } = tsNode; 151 | const ignoredNames = context.options?.[0]?.ignoredHookCallsNames ?? {}; 152 | 153 | if ( 154 | context.options?.[0]?.checkHookCalls === false 155 | || !getIsHook(callee as ESNode) 156 | ) { 157 | return; 158 | } 159 | 160 | if(!shouldIgnoreNode(node as ESNode, {...defaultReactHookNames, ...ignoredNames})) { 161 | for (const argument of tsNode.arguments) { 162 | if (argument.type !== 'SpreadElement') { 163 | checkForErrors( 164 | callExpressionData, 165 | getExpressionMemoStatus(context as Rule.RuleContext, (argument as TSESTree.Expression)), 166 | context, 167 | node as Rule.Node, 168 | report 169 | ); 170 | } 171 | } 172 | } 173 | }, 174 | }; 175 | }, 176 | }; 177 | 178 | export default rule; -------------------------------------------------------------------------------- /src/require-usememo/types.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint-v9"; 2 | import type { TSESTree } from "@typescript-eslint/types"; 3 | import type { MessagesRequireUseMemo} from '../constants/messages'; 4 | 5 | export type ExpressionTypes = TSESTree.ArrowFunctionExpression | TSESTree.JSXExpressionContainer | TSESTree.Expression | TSESTree.ObjectExpression | TSESTree.ArrayExpression | TSESTree.Identifier | TSESTree.LogicalExpression | TSESTree.JSXEmptyExpression; 6 | 7 | export type NodeType = TSESTree.MethodDefinitionComputedName; 8 | export type ExpressionData = Record; 9 | type OptionalRecord = { 10 | [P in K]?: T; 11 | }; 12 | type PartialKeyOfMessages = keyof typeof MessagesRequireUseMemo; 13 | export type MemoErrorHookDictionary = OptionalRecord; 14 | export type ImportNode = TSESTree.ImportDeclaration & Rule.NodeParentExtension; 15 | export type ReactImportInformation = { 16 | reactImported: boolean; 17 | useMemoImported: boolean; 18 | useCallbackImported: boolean; 19 | importDeclaration?: TSESTree.ImportDeclaration; 20 | }; -------------------------------------------------------------------------------- /src/require-usememo/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint"; 2 | import type { Rule as RuleV9 } from "eslint-v9"; 3 | import type { TSESTree } from "@typescript-eslint/types"; 4 | import type * as ESTree from "estree"; 5 | import type { MessagesRequireUseMemo } from "../constants/messages"; 6 | import type { ExpressionData, ReactImportInformation } from "./types"; 7 | import { MemoStatus, type MemoStatusToReport } from "src/types"; 8 | import { 9 | messageIdToHookDict, 10 | nameGeneratorUUID, 11 | defaultImportRangeStart, 12 | } from "./constants"; 13 | import getVariableInScope from "src/utils/getVariableInScope"; 14 | import { v5 as uuidV5 } from "uuid"; 15 | import type { CompatibleNode } from "../utils/compatibility"; 16 | 17 | export type CompatibleContext = Rule.RuleContext | RuleV9.RuleContext; 18 | 19 | export function isImpossibleToFix( 20 | node: Rule.NodeParentExtension | RuleV9.NodeParentExtension, 21 | context: CompatibleContext, 22 | ) { 23 | let current: TSESTree.Node | undefined = node as TSESTree.Node; 24 | 25 | while (current) { 26 | if (current.type === "CallExpression") { 27 | const callee = current.callee; 28 | const isInsideIteration = 29 | callee.type === "MemberExpression" && 30 | callee.property.type === "Identifier" && 31 | callee.property.name in Array.prototype; 32 | const isInsideOtherHook = 33 | callee.type === "Identifier" && 34 | (callee.name === "useMemo" || callee.name === "useCallback"); 35 | return { result: isInsideIteration || isInsideOtherHook, node: callee }; 36 | } 37 | current = current.parent; 38 | } 39 | 40 | return { result: false }; 41 | } 42 | 43 | export function checkForErrors< 44 | T, 45 | Y extends 46 | | Rule.NodeParentExtension 47 | | RuleV9.NodeParentExtension 48 | | TSESTree.MethodDefinitionComputedName, 49 | >( 50 | data: ExpressionData, 51 | statusData: MemoStatusToReport, 52 | context: CompatibleContext, 53 | node: Y | undefined, 54 | report: (node: Y, error: keyof typeof MessagesRequireUseMemo) => void, 55 | ) { 56 | if (!statusData) { 57 | return; 58 | } 59 | if (statusData.status === MemoStatus.ErrorInvalidContext) { 60 | report((statusData.node ?? node) as Y, MemoStatus.ErrorInvalidContext); 61 | } 62 | const errorName = data?.[statusData.status.toString()]; 63 | if (errorName) { 64 | const strict = errorName.includes("unknown"); 65 | if (!strict || (strict && context.options?.[0]?.strict)) { 66 | report((statusData.node ?? node) as Y, errorName); 67 | } 68 | } 69 | } 70 | 71 | function addReactImports( 72 | context: CompatibleContext, 73 | kind: "useMemo" | "useCallback", 74 | reactImportData: ReactImportInformation, 75 | fixer: Rule.RuleFixer | RuleV9.RuleFixer, 76 | ) { 77 | const importsDisabled = context.options?.[0]?.fix?.addImports === false; 78 | let specifier: TSESTree.ImportClause | undefined = undefined; 79 | 80 | if (importsDisabled) { 81 | return; 82 | } 83 | 84 | if (!reactImportData[`${kind}Imported`]) { 85 | // Create a new ImportSpecifier for useMemo/useCallback hook. 86 | specifier = { 87 | type: "ImportSpecifier", 88 | imported: { type: "Identifier", name: kind }, 89 | local: { type: "Identifier", name: kind }, 90 | } as TSESTree.ImportSpecifier; 91 | 92 | if (reactImportData.importDeclaration?.specifiers) { 93 | const specifiers = reactImportData.importDeclaration.specifiers; 94 | const hasDefaultExport = 95 | specifiers?.[0]?.type === "ImportDefaultSpecifier"; 96 | const isEmpty = !specifiers.length; 97 | // Default export counts as a specifier too 98 | const shouldCreateSpecifiersBracket = 99 | specifiers.length <= 1 && hasDefaultExport; 100 | const hasCurrentSpecifier = 101 | !isEmpty && 102 | !shouldCreateSpecifiersBracket && 103 | specifiers.find((x) => x.local.name === kind); 104 | 105 | if (shouldCreateSpecifiersBracket) { 106 | specifiers.push(specifier); 107 | return fixer.insertTextAfter(specifiers[0], `, { ${kind} }`); 108 | } 109 | 110 | if (isEmpty) { 111 | const importDeclaration = 112 | reactImportData.importDeclaration as TSESTree.ImportDeclaration; 113 | const fixRange = 114 | importDeclaration.range[0] + defaultImportRangeStart.length - 1; 115 | 116 | return fixer.insertTextAfterRange([fixRange, fixRange], ` ${kind} `); 117 | } 118 | 119 | if (!hasCurrentSpecifier) { 120 | specifiers.push(specifier); 121 | const insertPosition = specifiers.find( 122 | (specifier) => 123 | !!specifier.range && 124 | (!hasDefaultExport || specifier.type !== "ImportDefaultSpecifier"), 125 | ); 126 | 127 | if (insertPosition) { 128 | return fixer.insertTextAfter(insertPosition, `, ${kind}`); 129 | } 130 | return; 131 | } 132 | } 133 | } 134 | 135 | // If React is not imported, create a new ImportDeclaration for it. 136 | if (!reactImportData.reactImported && !reactImportData.importDeclaration) { 137 | reactImportData.importDeclaration = { 138 | type: "ImportDeclaration", 139 | specifiers: [ 140 | { 141 | ...specifier, 142 | range: [ 143 | defaultImportRangeStart.length, 144 | defaultImportRangeStart.length + kind.length, 145 | ], 146 | }, 147 | ], 148 | source: { type: "Literal", value: "react" }, 149 | } as TSESTree.ImportDeclaration; 150 | reactImportData.reactImported = true; 151 | reactImportData[`${kind}Imported`] = true; 152 | 153 | // Add an extra new line before const component and use indentSpace for proper spacing. 154 | return fixer.insertTextBeforeRange( 155 | [0, 0], 156 | `${defaultImportRangeStart}${kind} } from 'react';\n`, 157 | ); 158 | } 159 | return; 160 | } 161 | 162 | export function getIsHook(node: TSESTree.Node | TSESTree.Identifier) { 163 | if (node.type === "Identifier") { 164 | const { name } = node; 165 | return ( 166 | name === "use" || 167 | ((name?.length ?? 0) >= 4 && 168 | name[0] === "u" && 169 | name[1] === "s" && 170 | name[2] === "e" && 171 | name[3] === name[3]?.toUpperCase?.()) 172 | ); 173 | } 174 | 175 | if ( 176 | node.type === "MemberExpression" && 177 | !node.computed && 178 | getIsHook(node.property) 179 | ) { 180 | const { object: obj } = node; // Utilizing Object destructuring 181 | return obj.type === "Identifier" && obj.name === "React"; 182 | } 183 | 184 | return false; 185 | } 186 | 187 | // Helper function to find parent of a specified type. 188 | export function findParentType( 189 | node: Rule.Node, 190 | type: string, 191 | ): Rule.Node | undefined { 192 | let parent = node.parent; 193 | 194 | while (parent) { 195 | if (parent.type === type) return parent; 196 | 197 | parent = parent.parent; 198 | } 199 | 200 | return undefined; 201 | } 202 | 203 | function fixFunction( 204 | node: 205 | | TSESTree.FunctionDeclaration 206 | | TSESTree.FunctionExpression 207 | | TSESTree.ArrowFunctionExpression, 208 | context: Rule.RuleContext, 209 | shouldSetName?: boolean, 210 | ) { 211 | const sourceCode = context.getSourceCode(); 212 | const { body, params = [] } = node; 213 | const funcBody = sourceCode.getText(body as unknown as ESTree.Node); 214 | const funcParams = (params as Array).map((node) => 215 | sourceCode.getText(node), 216 | ); 217 | let fixedCode = `useCallback(${node.async ? "async " : ""}(${funcParams.join(", ")}) => ${funcBody}, [])${shouldSetName ? ";" : ""}`; 218 | if (shouldSetName && node?.id?.name) { 219 | const name = node?.id?.name; 220 | fixedCode = `const ${name} = ${fixedCode}`; 221 | } 222 | return fixedCode; 223 | } 224 | 225 | function getSafeVariableName( 226 | context: Rule.RuleContext, 227 | node: CompatibleNode | undefined, 228 | name: string, 229 | attempts = 0, 230 | ): string { 231 | const tempVarPlaceholder = "renameMe"; 232 | 233 | if (node && !getVariableInScope(context, node, name)) { 234 | return name; 235 | } 236 | if (attempts >= 5) { 237 | const nameExtensionIfExists = 238 | node && getVariableInScope(context, node, tempVarPlaceholder) 239 | ? uuidV5(name, nameGeneratorUUID).split("-")[0] 240 | : ""; 241 | return `${tempVarPlaceholder}${nameExtensionIfExists ? `_${nameExtensionIfExists}` : ""}`; 242 | } 243 | return getSafeVariableName(context, node, `_${name}`, ++attempts); 244 | } 245 | 246 | // Eslint Auto-fix logic, functional components/hooks only 247 | export function fixBasedOnMessageId( 248 | node: CompatibleNode, 249 | messageId: keyof typeof MessagesRequireUseMemo, 250 | fixer: Rule.RuleFixer | RuleV9.RuleFixer, 251 | context: CompatibleContext, 252 | reactImportData: ReactImportInformation, 253 | ) { 254 | // Get source code in a way that works with both v8 and v9 255 | const sourceCode = 256 | "getSourceCode" in context ? context.getSourceCode() : (context as RuleV9.RuleContext)?.sourceCode; 257 | 258 | const hook = messageIdToHookDict[messageId] || "useMemo"; 259 | const isObjExpression = node.type === "ObjectExpression"; 260 | const isJSXElement = 261 | (node as unknown as TSESTree.JSXElement).type === "JSXElement"; 262 | const isArrowFunctionExpression = node.type === "ArrowFunctionExpression"; 263 | const isFunctionExpression = node.type === "FunctionExpression"; 264 | const isCorrectableFunctionExpression = 265 | isFunctionExpression || isArrowFunctionExpression; 266 | const fixes: Array = []; 267 | 268 | // Determine what type of behavior to follow according to the error message 269 | switch (messageId) { 270 | case "function-usecallback-hook": 271 | if ( 272 | node.type === "FunctionExpression" || 273 | node.type === "ArrowFunctionExpression" 274 | ) { 275 | const importStatementFixes = addReactImports( 276 | context, 277 | "useCallback", 278 | reactImportData, 279 | fixer, 280 | ); 281 | const fixed = fixFunction( 282 | node as TSESTree.FunctionExpression, 283 | context as any, 284 | ); 285 | importStatementFixes && fixes.push(importStatementFixes); 286 | // Use any cast to bypass type checking 287 | fixes.push(fixer.replaceText(node as any, fixed)); 288 | return fixes; 289 | } 290 | break; 291 | case "object-usememo-hook": { 292 | const _returnNode = node as TSESTree.ReturnStatement; 293 | // An undefined node.argument means returned value is not an expression, but most probably a variable which should not be handled here, which falls under default, simpler fix logic. 294 | if (_returnNode.argument) { 295 | const importStatementFixes = addReactImports( 296 | context, 297 | "useMemo", 298 | reactImportData, 299 | fixer, 300 | ); 301 | const fixed = `useMemo(() => (${sourceCode.getText(_returnNode.argument as any)}), [])`; 302 | importStatementFixes && fixes.push(importStatementFixes); 303 | // Use any cast to bypass type checking 304 | fixes.push(fixer.replaceText(_returnNode.argument as any, fixed)); 305 | return fixes; 306 | } 307 | break; 308 | } 309 | case "function-usecallback-props": 310 | case "object-usememo-props": 311 | case "jsx-usememo-props": 312 | case "usememo-const": { 313 | const variableDeclaration = 314 | node.type === "VariableDeclaration" 315 | ? node 316 | : (findParentType( 317 | node as any, 318 | "VariableDeclaration", 319 | ) as TSESTree.VariableDeclaration); 320 | 321 | // Check if it is a hook being stored in let/var, change to const if so 322 | if (variableDeclaration && variableDeclaration.kind !== "const") { 323 | const tokens = sourceCode.getTokens(variableDeclaration as any); 324 | const letKeywordToken = tokens?.[0]; 325 | if (letKeywordToken?.value !== "const") { 326 | fixes.push(fixer.replaceTextRange(letKeywordToken.range, "const")); 327 | } 328 | } 329 | // If it's an dynamic object - Add useMemo/Callback 330 | if (isObjExpression || isJSXElement || isCorrectableFunctionExpression) { 331 | const importStatementFixes = addReactImports( 332 | context, 333 | isCorrectableFunctionExpression ? "useCallback" : "useMemo", 334 | reactImportData, 335 | fixer, 336 | ); 337 | importStatementFixes && fixes.push(importStatementFixes); 338 | const fixed = isCorrectableFunctionExpression 339 | ? fixFunction(node as any, context as any) 340 | : `useMemo(() => (${sourceCode.getText(node as any)}), [])`; 341 | const parent = 342 | node.parent as unknown as TSESTree.JSXExpressionContainer; 343 | // Means we have a object expression declared directly in jsx 344 | if (parent.type === "JSXExpressionContainer") { 345 | const parentPropName = ( 346 | parent?.parent as TSESTree.JSXAttribute 347 | )?.name?.name.toString(); 348 | const newVarName = getSafeVariableName( 349 | context as any, 350 | parent.parent, 351 | parentPropName, 352 | ); 353 | const returnStatement = findParentType( 354 | node as any, 355 | "ReturnStatement", 356 | ) as TSESTree.ReturnStatement; 357 | 358 | if (returnStatement) { 359 | const indentationLevel = 360 | sourceCode.lines[returnStatement.loc.start.line - 1].search(/\S/); 361 | const indentation = " ".repeat(indentationLevel); 362 | // Creates a declaration for the variable and inserts it before the return statement 363 | fixes.push( 364 | fixer.insertTextBeforeRange( 365 | returnStatement.range, 366 | `const ${newVarName} = ${fixed};\n${indentation}`, 367 | ), 368 | ); 369 | // Replaces the old inline object expression with the variable name 370 | fixes.push(fixer.replaceText(node as any, newVarName)); 371 | } 372 | } else { 373 | fixes.push(fixer.replaceText(node as any, fixed)); 374 | } 375 | } 376 | 377 | return !fixes.length ? null : fixes; 378 | } 379 | // Unknown cases are usually complex issues or false positives, so we ignore them 380 | case "unknown-class-memo-props": 381 | case "unknown-usememo-hook": 382 | case "unknown-usememo-deps": 383 | case "unknown-usememo-props": 384 | case "error-in-invalid-context": 385 | return null; 386 | } 387 | 388 | // Simpler cases bellow, all of them are just adding useMemo/Callback 389 | const functionPrefix = isArrowFunctionExpression ? "" : "() => "; 390 | const expressionPrefix = isObjExpression || isJSXElement ? "(" : ""; 391 | const coreExpression = sourceCode.getText(node as any); 392 | const expressionSuffix = isObjExpression ? ")" : ""; 393 | 394 | let fixed = `${hook}(${functionPrefix}${expressionPrefix}${coreExpression}${expressionSuffix}, [])`; 395 | const importStatementFixes = addReactImports( 396 | context, 397 | hook, 398 | reactImportData, 399 | fixer, 400 | ); 401 | importStatementFixes && fixes.push(importStatementFixes); 402 | 403 | if (node.type === "FunctionDeclaration") { 404 | const _node = node as TSESTree.FunctionDeclaration; 405 | if (_node && _node?.id?.type === "Identifier") { 406 | fixed = fixFunction(_node, context as any, true); 407 | } 408 | } 409 | 410 | if ( 411 | "computed" in node && 412 | (node as any)?.computed?.type === "ArrowFunctionExpression" 413 | ) { 414 | fixes.push(fixer.replaceText((node as any).computed, fixed)); 415 | } else { 416 | fixes.push(fixer.replaceText(node as any, fixed)); 417 | } 418 | return fixes; 419 | } 420 | -------------------------------------------------------------------------------- /src/traditional-config.ts: -------------------------------------------------------------------------------- 1 | import requireMemoRule from "./require-memo"; 2 | import requireUseMemoRule from "./require-usememo"; 3 | import requireUseMemoChildrenRule from "./require-usememo-children"; 4 | import type { Rule } from "eslint"; 5 | import type { CompatibleRuleModule } from "./utils/compatibility"; 6 | 7 | export const rules: Record = { 8 | "require-memo": requireMemoRule, 9 | "require-usememo": requireUseMemoRule, 10 | "require-usememo-children": requireUseMemoChildrenRule, 11 | }; 12 | 13 | // Legacy configs export (for ESLint v8 and below) 14 | export const configs = { 15 | recommended: { 16 | plugins: ["@arthurgeron/react-usememo"], 17 | rules: { 18 | "@arthurgeron/react-usememo/require-usememo": "error", 19 | "@arthurgeron/react-usememo/require-memo": "error", 20 | "@arthurgeron/react-usememo/require-usememo-children": "error", 21 | }, 22 | }, 23 | }; 24 | 25 | // Add meta information to each rule 26 | for (const ruleName of Object.keys(rules)) { 27 | const rule = rules[ruleName] as CompatibleRuleModule; 28 | rule.meta = { 29 | ...rule.meta, 30 | docs: { 31 | ...rule.meta.docs, 32 | url: `https://github.com/arthurgeron/eslint-plugin-react-usememo/blob/main/docs/rules/${ruleName}.md` 33 | } 34 | }; 35 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint"; 2 | import type { TSESTree } from "@typescript-eslint/types"; 3 | 4 | export type MemoStatusToReport = { 5 | node?: Rule.RuleContext | TSESTree.Node, 6 | status: MemoStatus 7 | } | undefined; 8 | 9 | export enum MemoStatus { 10 | Memoized, 11 | UnmemoizedObject, 12 | UnmemoizedArray, 13 | UnmemoizedNew, 14 | UnmemoizedFunction, 15 | UnmemoizedFunctionCall, 16 | UnmemoizedJSX, 17 | UnmemoizedOther, 18 | UnsafeLet, 19 | ErrorInvalidContext = 'error-in-invalid-context' 20 | } 21 | 22 | export type ESNode = TSESTree.CallExpression & Rule.NodeParentExtension -------------------------------------------------------------------------------- /src/utils/compatibility.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint"; 2 | import type { Rule as RuleV9 } from "eslint-v9"; 3 | import type { TSESTree } from "@typescript-eslint/types"; 4 | 5 | // Type that represents a node from either ESLint v8 or v9 6 | export type CompatibleNode = Rule.Node | RuleV9.Node | TSESTree.Node; 7 | 8 | /** 9 | * Utility to safely get scope in both ESLint v8 and v9 10 | */ 11 | export function getCompatibleScope( 12 | context: Rule.RuleContext | RuleV9.RuleContext, 13 | node?: CompatibleNode, 14 | ) { 15 | if (typeof context.getScope === "function") { 16 | // ESLint v8 approach 17 | return context.getScope(); 18 | } 19 | 20 | const v9Context = context as unknown as RuleV9.RuleContext; 21 | if ( 22 | v9Context.sourceCode && 23 | typeof v9Context.sourceCode.getScope === "function" 24 | ) { 25 | // ESLint v9 approach 26 | 27 | if (!node) { 28 | throw new Error("Node is required for ESLint v9"); 29 | } 30 | return v9Context.sourceCode.getScope(node as RuleV9.Node); 31 | } 32 | 33 | throw new Error("Failed to fetch scope method"); 34 | } 35 | 36 | /** 37 | * Safely get the filename in a way that works with both ESLint v8 and v9 38 | */ 39 | export function getCompatibleFilename( 40 | context: Rule.RuleContext | RuleV9.RuleContext, 41 | ): string { 42 | if (typeof context.getFilename === "function") { 43 | // ESLint v8 approach 44 | return context.getFilename(); 45 | } 46 | 47 | const v9Context = context as RuleV9.RuleContext; 48 | return v9Context.filename || ""; 49 | } 50 | 51 | /** 52 | * Create a type that works for both ESLint v8 and v9 rule modules 53 | * This relaxes the typing to allow both versions to work together 54 | */ 55 | export type CompatibleRuleModule = { 56 | meta: { 57 | messages?: Record; 58 | type?: string; 59 | docs?: { 60 | description?: string; 61 | category?: string; 62 | recommended?: boolean; 63 | url?: string; 64 | }; 65 | fixable?: "code" | "whitespace"; 66 | schema?: unknown[]; 67 | }; 68 | create: ( 69 | context: Rule.RuleContext | RuleV9.RuleContext, 70 | ) => Record unknown>; 71 | }; 72 | -------------------------------------------------------------------------------- /src/utils/getVariableInScope.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from "eslint-v9"; 2 | import type { Rule as RuleV8 } from "eslint"; 3 | import { CompatibleNode, getCompatibleScope } from "../utils/compatibility"; 4 | import type { CompatibleContext } from "../require-usememo/utils"; 5 | 6 | /** 7 | * Get a variable in the current scope by name 8 | * This is a wrapper around context.getScope().variables.find() 9 | * that uses getCompatibleScope to handle both ESLint v8 and v9 10 | */ 11 | export default function getVariableInScope(context: CompatibleContext, node: CompatibleNode, name: string) { 12 | try { 13 | // Try to get scope without a node parameter (ESLint v8 style) 14 | return getCompatibleScope(context, node)?.variables.find((variable) => { 15 | return variable.name === name; 16 | }); 17 | } catch (error) { 18 | // If that fails, we might not be able to get a global scope directly 19 | return undefined; 20 | } 21 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { Rule, Scope } from "eslint"; 2 | import type { Rule as RuleV9 } from "eslint-v9"; 3 | import type { TSESTree } from "@typescript-eslint/types"; 4 | import type * as ESTree from "estree"; 5 | import { type ESNode, MemoStatus, type MemoStatusToReport } from "src/types"; 6 | import { getIsHook, isImpossibleToFix } from "src/require-usememo/utils"; 7 | import getVariableInScope from "src/utils/getVariableInScope"; 8 | import { Minimatch } from "minimatch"; 9 | import type { CompatibleContext } from "src/require-usememo/utils"; 10 | 11 | export function isComplexComponent( 12 | node: TSESTree.JSXOpeningElement | TSESTree.JSXIdentifier, 13 | ) { 14 | if (node?.type !== "JSXOpeningElement") return false; 15 | if (node?.name?.type !== "JSXIdentifier") return false; 16 | const firstCharacterLowerCase = node?.name?.name?.[0]?.toLowerCase(); 17 | return ( 18 | !!firstCharacterLowerCase && 19 | firstCharacterLowerCase !== node?.name?.name?.[0] 20 | ); 21 | } 22 | 23 | export function isComponentName(name: string | undefined) { 24 | return ( 25 | typeof name === "string" && !!name && name?.[0] === name?.[0]?.toUpperCase() 26 | ); 27 | } 28 | 29 | function isCallExpression( 30 | node: TSESTree.CallExpression, 31 | name: "useMemo" | "useCallback", 32 | ) { 33 | if (node?.callee?.type === "MemberExpression") { 34 | const { 35 | callee: { object, property }, 36 | } = node; 37 | if ( 38 | object.type === "Identifier" && 39 | property.type === "Identifier" && 40 | object.name === "React" && 41 | property.name === name 42 | ) { 43 | return true; 44 | } 45 | } else if (node?.callee?.type === "Identifier" && node.callee.name === name) { 46 | return true; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | function getIdentifierMemoStatus( 53 | context: CompatibleContext, 54 | identifierNode: TSESTree.Identifier, 55 | ): MemoStatusToReport { 56 | const { name } = identifierNode; 57 | const variableInScope = getVariableInScope(context, identifierNode, name); 58 | if (variableInScope === undefined) return { status: MemoStatus.Memoized }; 59 | const [{ node }] = variableInScope.defs; 60 | const isProps = 61 | node?.id?.type === "Identifier" && 62 | (isComponentName(node.id.name) || getIsHook(node.id)); 63 | if (isProps) { 64 | return; 65 | } 66 | 67 | const isFunctionParameter = node?.id?.name !== name; 68 | if (node.type === "FunctionDeclaration") 69 | return { 70 | node: node, 71 | status: isFunctionParameter 72 | ? MemoStatus.Memoized 73 | : MemoStatus.UnmemoizedFunction, 74 | }; 75 | if (node.type !== "VariableDeclarator") 76 | return { node: node, status: MemoStatus.Memoized }; 77 | if ( 78 | node?.parent?.kind === "let" && 79 | node?.init?.type === "CallExpression" && 80 | getIsHook(node?.init?.callee) 81 | ) { 82 | return { node: node.parent, status: MemoStatus.UnsafeLet }; 83 | } 84 | return getExpressionMemoStatus(context, node.init); 85 | } 86 | 87 | function getInvalidContextReport( 88 | context: CompatibleContext, 89 | expression: TSESTree.Expression, 90 | ) { 91 | const impossibleFix = isImpossibleToFix( 92 | expression as Rule.NodeParentExtension, 93 | context, 94 | ); 95 | if (impossibleFix?.result) { 96 | return { node: impossibleFix.node, status: MemoStatus.ErrorInvalidContext }; 97 | } 98 | return false; 99 | } 100 | 101 | export function getExpressionMemoStatus( 102 | context: CompatibleContext, 103 | expression: TSESTree.Expression, 104 | checkContext = false, 105 | ): MemoStatusToReport { 106 | switch (expression?.type) { 107 | case undefined: 108 | case "ObjectExpression": 109 | return ( 110 | (checkContext && getInvalidContextReport(context, expression)) || { 111 | node: expression, 112 | status: MemoStatus.UnmemoizedObject, 113 | } 114 | ); 115 | case "ArrayExpression": 116 | return ( 117 | (checkContext && getInvalidContextReport(context, expression)) || { 118 | node: expression, 119 | status: MemoStatus.UnmemoizedArray, 120 | } 121 | ); 122 | case "NewExpression": 123 | return ( 124 | (checkContext && getInvalidContextReport(context, expression)) || { 125 | node: expression, 126 | status: MemoStatus.UnmemoizedNew, 127 | } 128 | ); 129 | case "FunctionExpression": 130 | case "ArrowFunctionExpression": { 131 | return ( 132 | (checkContext && getInvalidContextReport(context, expression)) || { 133 | node: expression, 134 | status: MemoStatus.UnmemoizedFunction, 135 | } 136 | ); 137 | } 138 | case "JSXElement": 139 | return ( 140 | (checkContext && getInvalidContextReport(context, expression)) || { 141 | node: expression, 142 | status: MemoStatus.UnmemoizedJSX, 143 | } 144 | ); 145 | case "CallExpression": { 146 | const validCallExpression = 147 | isCallExpression(expression, "useMemo") || 148 | isCallExpression(expression, "useCallback"); 149 | 150 | return ( 151 | (validCallExpression && 152 | checkContext && 153 | getInvalidContextReport(context, expression)) || { 154 | node: expression, 155 | status: validCallExpression 156 | ? MemoStatus.Memoized 157 | : MemoStatus.UnmemoizedFunctionCall, 158 | } 159 | ); 160 | } 161 | case "Identifier": 162 | return getIdentifierMemoStatus(context, expression); 163 | case "BinaryExpression": 164 | return { node: expression, status: MemoStatus.Memoized }; 165 | default: 166 | return { node: expression, status: MemoStatus.UnmemoizedOther }; 167 | } 168 | } 169 | 170 | export function findVariable( 171 | scope: Scope.Scope, 172 | node: ESTree.Identifier, 173 | ): Scope.Variable | undefined { 174 | if (scope.variables.some((variable) => variable.name === node.name)) { 175 | return scope.variables.find((variable) => variable.name === node.name); 176 | } 177 | 178 | if (scope.upper) { 179 | return findVariable(scope.upper, node); 180 | } 181 | 182 | return undefined; 183 | } 184 | 185 | export function shouldIgnoreNode( 186 | node: ESNode, 187 | ignoredNames: Record, 188 | ) { 189 | const nodeName = (node as TSESTree.Node as TSESTree.Identifier)?.name; 190 | const nodeCalleeName = (node?.callee as TSESTree.Identifier)?.name; 191 | const nodeCalleePropertyName = ( 192 | (node?.callee as TSESTree.MemberExpression)?.property as TSESTree.Identifier 193 | )?.name; 194 | const nameToCheck = nodeName || nodeCalleeName || nodeCalleePropertyName; 195 | 196 | const matchedValue = ignoredNames[nameToCheck]; 197 | 198 | if (matchedValue !== undefined) { 199 | return matchedValue; 200 | } 201 | 202 | if (nameToCheck === "use") { 203 | return true; 204 | } 205 | 206 | let shouldIgnore: boolean | undefined; 207 | 208 | Object.keys(ignoredNames).find((key) => { 209 | const value = ignoredNames[key]; 210 | const miniMatch = new Minimatch(key); 211 | if (miniMatch.hasMagic()) { 212 | const isMatch = nameToCheck && miniMatch.match(nameToCheck); 213 | if (isMatch) { 214 | shouldIgnore = !!value; 215 | return true; 216 | } 217 | } 218 | return false; 219 | }); 220 | 221 | return !!shouldIgnore; 222 | } 223 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "dist", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "baseUrl": "./", 8 | "noEmitOnError": false 9 | }, 10 | } 11 | --------------------------------------------------------------------------------