├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── README_CONTENT.md └── workflows │ └── ci.yaml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .release-it.json ├── CHANGELOG.md ├── DEPENDENCIES.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── src ├── config.ts ├── guard.ts ├── index.ts ├── rule.ts └── writer.ts ├── test ├── rule.spec.ts └── scenarios.ts ├── tsconfig.json └── vitest.config.mts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.js] 4 | charset = utf-8 5 | indent_size = 2 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JamieMason 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Explain how to reproduce a Bug 4 | --- 5 | 6 | ## Description 7 | 8 | 13 | 14 | ## Suggested Solution 15 | 16 | 20 | 21 | ## Help Needed 22 | 23 | 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ## Description 7 | 8 | 14 | 15 | ## Suggested Solution 16 | 17 | 21 | 22 | ## Help Needed 23 | 24 | 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description (What) 2 | 3 | 9 | 10 | ## Justification (Why) 11 | 12 | 18 | 19 | ## How Can This Be Tested? 20 | 21 | 24 | -------------------------------------------------------------------------------- /.github/README_CONTENT.md: -------------------------------------------------------------------------------- 1 | ## ☁️ Installation 2 | 3 | ``` 4 | npm install --save-dev eslint eslint-plugin-prefer-arrow-functions 5 | ``` 6 | 7 | ## 🏓 Playground 8 | 9 | Try it yourself at 10 | [ASTExplorer.net](https://astexplorer.net/#/gist/7c36fe8c604945df27df210cf79dcc3c/12f01bed4dcf08f32a85f72db0851440b7e45cdd) 11 | by pasting code snippets in the top left panel, the results will appear in the 12 | bottom right panel. 13 | 14 | ## ⚖️ Configuration 15 | 16 | Add the plugin to the `plugins` section and the rule to the `rules` section in 17 | your .eslintrc. The default values for options are listed in this example. 18 | 19 | ```json 20 | { 21 | "plugins": ["prefer-arrow-functions"], 22 | "rules": { 23 | "prefer-arrow-functions/prefer-arrow-functions": [ 24 | "warn", 25 | { 26 | "classPropertiesAllowed": false, 27 | "disallowPrototype": false, 28 | "returnStyle": "unchanged", 29 | "singleReturnOnly": false 30 | } 31 | ] 32 | } 33 | } 34 | ``` 35 | 36 | ## 🤔 Options 37 | 38 | ### `classPropertiesAllowed` 39 | 40 | When `true`, functions defined as 41 | [class instance fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Field_declarations) 42 | will be converted to arrow functions when doing so would not alter or break 43 | their behaviour. 44 | 45 | ### `disallowPrototype` 46 | 47 | When `true`, functions assigned to a `prototype` will be converted to arrow 48 | functions when doing so would not alter or break their behaviour. 49 | 50 | ### `returnStyle` 51 | 52 | - When `"implicit"`, arrow functions such as `x => { return x; }` will be 53 | converted to `x => x`. 54 | - When `"explicit"`, arrow functions such as `x => x` will be converted to 55 | `x => { return x; }`. 56 | - When `"unchanged"` or not set, arrow functions will be left as they were. 57 | 58 | ### `singleReturnOnly` 59 | 60 | When `true`, only `function` declarations which _only_ contain a return 61 | statement will be converted. Functions containing block statements will be 62 | ignored. 63 | 64 | > This option works well in conjunction with ESLint's built-in 65 | > [arrow-body-style](http://eslint.org/docs/rules/arrow-body-style) set to 66 | > `as-needed`. 67 | 68 | ## 👏🏻 Credits 69 | 70 | This project is a fork of https://github.com/TristonJ/eslint-plugin-prefer-arrow 71 | by [Triston Jones](https://github.com/TristonJ). 72 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build, Lint, Test 2 | 3 | defaults: 4 | run: 5 | shell: bash 6 | working-directory: ./ 7 | 8 | on: 9 | pull_request: 10 | push: 11 | branches: [main, dev] 12 | 13 | env: 14 | FORCE_COLOR: 3 15 | TERM: xterm-256color 16 | 17 | jobs: 18 | all: 19 | name: Build, Lint, Test 20 | runs-on: ubuntu-latest 21 | timeout-minutes: 10 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: pnpm/action-setup@v4 25 | with: 26 | version: 9 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version-file: 'package.json' 30 | cache: 'pnpm' 31 | - uses: actions/cache@v4 32 | id: cache-npm 33 | with: 34 | path: node_modules 35 | key: src-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} 36 | - run: pnpm install 37 | if: steps.cache-npm.outputs.cache-hit != 'true' 38 | - run: npm run build 39 | - run: npm run lint 40 | - run: npm run test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.log.* 3 | coverage 4 | dist 5 | node_modules 6 | .idea -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 120, 4 | "proseWrap": "always", 5 | "singleQuote": true, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "changelog": "npm exec auto-changelog -- --stdout --commit-limit false --unreleased --breaking-pattern 'BREAKING CHANGE:' --template https://raw.githubusercontent.com/release-it/release-it/main/templates/changelog-compact.hbs", 4 | "commitMessage": "chore(release): ${version}", 5 | "push": false, 6 | "requireBranch": "main", 7 | "tag": true 8 | }, 9 | "github": { 10 | "release": true, 11 | "releaseName": "${version}" 12 | }, 13 | "hooks": { 14 | "after:bump": "npm exec auto-changelog -- -p --breaking-pattern 'BREAKING CHANGE:' && git add CHANGELOG.md" 15 | }, 16 | "npm": { 17 | "publish": false 18 | }, 19 | "plugins": { 20 | "@release-it/conventional-changelog": { 21 | "preset": { 22 | "name": "conventionalcommits" 23 | }, 24 | "strictSemver": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC. 4 | 5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 6 | 7 | #### [3.6.2](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.6.1...3.6.2) 8 | 9 | - fix: change export to fix #50 [`#50`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/50) [`#50`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/50) [`#54`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/54) 10 | 11 | #### [3.6.1](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.6.0...3.6.1) 12 | 13 | > 12 January 2025 14 | 15 | - test: add scenario from issue #35 [`c08cac1`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/c08cac1284769b89118ff4aa121393bda2d04a09) 16 | - chore(release): 3.6.1 [`b53da74`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/b53da7478ca04ad14148a019f0a1ad0fda3e8f9d) 17 | - fix: restore semver range in peerDependencies [`6e04024`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/6e04024efdc2a9385a2c8ddd7572173c283f6635) 18 | 19 | #### [3.6.0](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.5.0...3.6.0) 20 | 21 | > 9 January 2025 22 | 23 | - feat: allow methods on objects [`#15`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/15) 24 | - chore(release): 3.6.0 [`ca7bcd0`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/ca7bcd0537848f22816c854ebd01c4890f8bf6f7) 25 | 26 | #### [3.5.0](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.4.2...3.5.0) 27 | 28 | > 9 January 2025 29 | 30 | - feat: allow functions with certain names [`#9`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/9) 31 | - refactor: improve internal typings [`949b6e3`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/949b6e384b8fe3d9bc18668bd52857e27e5b935b) 32 | - refactor: move writer to a module [`7625a59`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/7625a593a51d45d95c2764dfd73db106f4f01c2c) 33 | - refactor: move guards to a module [`32c9fa7`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/32c9fa70b5b9ac2157a68ed5ed1c6da0104fd159) 34 | 35 | #### [3.4.2](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.4.1...3.4.2) 36 | 37 | > 9 January 2025 38 | 39 | - fix: format static private methods correctly with classPropertiesAllowed: true [`#46`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/46) [`#47`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/47) 40 | - fix: update npm dependencies (patch and minor) [`ace5f32`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/ace5f327bf273b7b42211d9d5b056d4041f6d08e) 41 | - chore(release): 3.4.2 [`8b086f2`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/8b086f2596a895d3111480ee345ee5e81295bf8c) 42 | - chore: credit contributors [`143ef3e`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/143ef3ee3ed844113844a3e642e80d9f8be2c167) 43 | 44 | #### [3.4.1](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.4.0...3.4.1) 45 | 46 | > 14 August 2024 47 | 48 | - fix(npm): set eslint peer to v8 [`#43`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/43) 49 | - chore(release): 3.4.1 [`e39d1b0`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/e39d1b02775f0ac390d21863306dbfd505197c26) 50 | 51 | #### [3.4.0](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.3.2...3.4.0) 52 | 53 | > 11 August 2024 54 | 55 | - feat: upgrade to eslint v9 [`#34`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/34) 56 | - fix: adjusts fix for generic type arguments on TSX files [`#27`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/27) 57 | - feat: do not flag constructors without `this` [`#29`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/29) 58 | - chore: set minimum node version to 18 [`#41`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/41) 59 | - fix: TypeError on function f() { return; } [`#38`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/38) 60 | - chore: switch to pnpm [`a8b8c93`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/a8b8c939649d8310b17d104d51fa4b4f73c1235e) 61 | - fix: update npm dependencies [`6fa62ec`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/6fa62ec02d27bfdacabdb75dd06b203d4802753a) 62 | - chore: various fixes and breaking changes [`ab198e5`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/ab198e53b553669a23eeea9f1f14d8e86c603e4a) 63 | 64 | #### [3.3.2](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.3.1...3.3.2) 65 | 66 | > 27 February 2024 67 | 68 | - fix: handle missing case for function overloads [`#31`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/pull/31) 69 | - chore(release): 3.3.2 [`2778c2d`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/2778c2d289c9e0db6d42dafd26e761c50d306a15) 70 | 71 | #### [3.3.1](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.3.0...3.3.1) 72 | 73 | > 25 February 2024 74 | 75 | - fix: preserve $ characters in function bodies [`#28`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/28) 76 | - chore(release): 3.3.1 [`a34f8f9`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/a34f8f9f53ce9a3c09603c80968cefdb9ae9b060) 77 | - test: add case for $ in function body [`78342f5`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/78342f545702efb0f6c3629f74d36ca7ba65ca36) 78 | - style(format): format source [`7d5dd71`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/7d5dd71ed48602b7f36742bbc612bcc96a103eb7) 79 | 80 | #### [3.3.0](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.2.4...3.3.0) 81 | 82 | > 25 February 2024 83 | 84 | - fix: protect static modifier on static methods [`#30`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/pull/30) 85 | - feat: support function overloads [`#26`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/pull/26) 86 | - feat: support assertion functions [`#25`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/pull/25) 87 | - feat: support function overloads (#26) [`#17`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/17) 88 | - feat: support assertion functions (#25) [`#22`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/22) 89 | - chore(github): add release-it [`93e6f71`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/93e6f7185a2161e864f40659c23b12761ef74de5) 90 | - chore(release): 3.3.0 [`3051a81`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/3051a813ad4e2e093965a2c343ca14df001fe6af) 91 | - style(format): format source [`1b140aa`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/1b140aaf6408ed2d084ceb30ce31705b5e554ea3) 92 | 93 | #### [3.2.4](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.1.4...3.2.4) 94 | 95 | > 23 October 2023 96 | 97 | - chore(github): update funding.yml [`#18`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/18) [`#19`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/19) 98 | - feat(config): add support for allowNamedFunctions [`50fb3b6`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/50fb3b68d9b2eaf48617404bec8f9ed1b4e6a511) 99 | - chore(release): 3.2.4 [`9b76409`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/9b76409f17deaa5d820c92adbb32cdc462fb3ec4) 100 | 101 | #### [3.1.4](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.1.3...3.1.4) 102 | 103 | > 13 November 2021 104 | 105 | - fix(yarn): regenerate lockfile [`b0afea1`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/b0afea135a4de55e9c0e3525fc98caa88d81292e) 106 | - chore(codeclimate): remove codeclimate [`0f338c2`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/0f338c275c3cdc1e835b8da9ed78195f35f6521c) 107 | - chore(release): 3.1.4 [`4520ba6`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/4520ba6591a7bc489f5af967f5ffc508d70cb6c7) 108 | 109 | #### [3.1.3](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.0.1...3.1.3) 110 | 111 | > 13 November 2021 112 | 113 | - feat(types): add support for generics [`#10`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/10) 114 | - fix(types): ensure return type is not lost [`#14`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/14) 115 | - fix(npm): update dependencies [`30a64e1`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/30a64e1cd12c5ae502ff7fdaa83f1515934e0358) 116 | - style(format): format source [`1a01195`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/1a011957ceedb341272c8de28a5439bccb781b7a) 117 | - docs(readme): update readme template [`7d8b273`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/7d8b2735d7247e1e9e8cc9981b3c2805e5dbf8e6) 118 | 119 | #### [3.0.1](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/compare/3.0.0...3.0.1) 120 | 121 | > 4 August 2019 122 | 123 | - fix(npm): update dependencies [`274db0d`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/274db0de3fcc4b191942273f78978a6bc9720382) 124 | - chore(readme): move sponsorship info [`18af90f`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/18af90f1694b6a91cb5fad33ca546a0be7e25111) 125 | - docs: add link to try it yourself [`bb899c4`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/bb899c44e744cd3b811c14c1ecd2e7d6121904ad) 126 | 127 | #### 3.0.0 128 | 129 | > 7 May 2019 130 | 131 | - Add support for ESLint >=2 [`#6`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/pull/6) 132 | - Add support for ESLint 4 [`#5`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/pull/5) 133 | - Allow functions with reference to 'this' [`#4`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/pull/4) 134 | - Skip generator functions. Fixes #8 [`#8`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/8) 135 | - **Breaking change:** feat: add formal support for async functions [`45b597b`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/45b597b39af451de96abb74d6b52082da2a7aad1) 136 | - **Breaking change:** feat: skip functions containing this, arguments, super, and new.target [`f1cee8c`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/f1cee8cb91fd60494a49d226cb4d1b0e726b162f) 137 | - refactor: transpile using typescript [`370d0b6`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/commit/370d0b60c749199a02821ffb857fc136cb13a98c) 138 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-prefer-arrow-functions 2 | 3 | Convert functions to arrow functions 4 | 5 | ## Installation 6 | 7 | This is a [Node.js](https://nodejs.org/) module available through the 8 | [npm registry](https://www.npmjs.com/). It can be installed using the 9 | [`npm`](https://docs.npmjs.com/getting-started/installing-npm-packages-locally) 10 | or 11 | [`yarn`](https://yarnpkg.com/en/) 12 | command line tools. 13 | 14 | ```sh 15 | npm install eslint-plugin-prefer-arrow-functions --save 16 | ``` 17 | 18 | ## Tests 19 | 20 | ```sh 21 | npm install 22 | npm test 23 | ``` 24 | 25 | ## Dependencies 26 | 27 | None 28 | 29 | ## Dev Dependencies 30 | 31 | - [@types/jest](https://ghub.io/@types/jest): TypeScript definitions for Jest 32 | - [@types/node](https://ghub.io/@types/node): TypeScript definitions for Node.js 33 | - [@typescript-eslint/eslint-plugin](https://ghub.io/@typescript-eslint/eslint-plugin): TypeScript plugin for ESLint 34 | - [@typescript-eslint/parser](https://ghub.io/@typescript-eslint/parser): An ESLint custom parser which leverages TypeScript ESTree 35 | - [eslint](https://ghub.io/eslint): An AST-based pattern checker for JavaScript. 36 | - [jest](https://ghub.io/jest): Delightful JavaScript Testing. 37 | - [prettier](https://ghub.io/prettier): Prettier is an opinionated code formatter 38 | - [rimraf](https://ghub.io/rimraf): A deep deletion module for node (like `rm -rf`) 39 | - [ts-jest](https://ghub.io/ts-jest): A Jest transformer with source map support that lets you use Jest to test projects written in TypeScript 40 | - [typescript](https://ghub.io/typescript): TypeScript is a language for application scale JavaScript development 41 | 42 | ## License 43 | 44 | MIT -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Triston Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-prefer-arrow-functions 2 | 3 | > An ESLint Plugin to Lint and auto-fix plain Functions into Arrow Functions, in all cases where conversion would result in the same behaviour (Arrow Functions do not support `this`, `arguments`, or `new.target` for example). 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install --save-dev eslint eslint-plugin-prefer-arrow-functions 9 | ``` 10 | 11 | ## Configuration 12 | 13 | Add the plugin to the `plugins` section and the rule to the `rules` section in your .eslintrc. The default values for options are listed in this example. 14 | 15 | ```json 16 | { 17 | "plugins": ["prefer-arrow-functions"], 18 | "rules": { 19 | "prefer-arrow-functions/prefer-arrow-functions": [ 20 | "warn", 21 | { 22 | "allowedNames": [], 23 | "allowNamedFunctions": false, 24 | "allowObjectProperties": false, 25 | "classPropertiesAllowed": false, 26 | "disallowPrototype": false, 27 | "returnStyle": "unchanged", 28 | "singleReturnOnly": false 29 | } 30 | ] 31 | } 32 | } 33 | ``` 34 | 35 | ## Options 36 | 37 | ### `allowedNames` 38 | 39 | An optional array of function names to ignore. When set, the rule won't report named functions such as `function foo() {}` whose name is identical to a member of this array. 40 | 41 | ### `allowNamedFunctions` 42 | 43 | If set to true, the rule won't report named functions such as `function foo() {}`. Anonymous function such as `const foo = function() {}` will still be reported. 44 | 45 | ### `allowObjectProperties` 46 | 47 | If set to true, the rule won't report named methods such as 48 | 49 | ```js 50 | const myObj = { 51 | hello() {} 52 | } 53 | ``` 54 | 55 | ### `classPropertiesAllowed` 56 | 57 | When `true`, functions defined as [class instance fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Field_declarations) will be converted to arrow functions when doing so would not alter or break their behaviour. 58 | 59 | ### `disallowPrototype` 60 | 61 | When `true`, functions assigned to a `prototype` will be converted to arrow functions when doing so would not alter or break their behaviour. 62 | 63 | ### `returnStyle` 64 | 65 | - When `"implicit"`, arrow functions such as `x => { return x; }` will be converted to `x => x`. 66 | - When `"explicit"`, arrow functions such as `x => x` will be converted to `x => { return x; }`. 67 | - When `"unchanged"` or not set, arrow functions will be left as they were. 68 | 69 | ### `singleReturnOnly` 70 | 71 | When `true`, only `function` declarations which _only_ contain a return statement will be converted. Functions containing block statements will be ignored. 72 | 73 | > This option works well in conjunction with ESLint's built-in [arrow-body-style](http://eslint.org/docs/rules/arrow-body-style) set to `as-needed`. 74 | 75 | ## Credits 76 | 77 | This project is a fork of by [Triston Jones](https://github.com/TristonJ). 78 | 79 | ## Badges 80 | 81 | - [![support on ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/C0C4PY4P) 82 | - [![NPM version](http://img.shields.io/npm/v/eslint-plugin-prefer-arrow-functions.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-prefer-arrow-functions) 83 | - [![NPM downloads](http://img.shields.io/npm/dm/eslint-plugin-prefer-arrow-functions.svg?style=flat-square)](https://www.npmjs.com/package/eslint-plugin-prefer-arrow-functions) 84 | - [![Build Status](https://img.shields.io/github/actions/workflow/status/JamieMason/eslint-plugin-prefer-arrow-functions/ci.yaml?branch=main)](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/actions) 85 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import globals from 'globals'; 3 | import tsParser from '@typescript-eslint/parser'; 4 | import path from 'node:path'; 5 | import { fileURLToPath } from 'node:url'; 6 | import js from '@eslint/js'; 7 | import { FlatCompat } from '@eslint/eslintrc'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default [ 18 | ...compat.extends( 19 | 'eslint:recommended', 20 | 'plugin:@typescript-eslint/recommended', 21 | ), 22 | { 23 | plugins: { 24 | '@typescript-eslint': typescriptEslint, 25 | }, 26 | languageOptions: { 27 | globals: { 28 | ...globals.node, 29 | }, 30 | parser: tsParser, 31 | ecmaVersion: 6, 32 | sourceType: 'module', 33 | }, 34 | }, 35 | { 36 | files: ['**/*.ts'], 37 | rules: { 38 | '@typescript-eslint/no-var-requires': 0, 39 | }, 40 | }, 41 | { 42 | files: ['**/*.spec.ts'], 43 | languageOptions: { 44 | globals: { 45 | ...globals.jest, 46 | }, 47 | }, 48 | }, 49 | { 50 | ignores: ['dist/**/*.js'], 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-prefer-arrow-functions", 3 | "description": "Convert functions to arrow functions", 4 | "version": "3.6.2", 5 | "author": "Jamie Mason (https://github.com/JamieMason)", 6 | "bugs": "https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues", 7 | "contributors": [ 8 | "Anders Kaseorg (https://github.com/andersk)", 9 | "Gabriel Montes (https://github.com/gabmontes)", 10 | "Gürgün Dayıoğlu (https://github.com/gurgunday)", 11 | "Harel Moshe (https://github.com/harelmo)", 12 | "Keith Lea (https://github.com/keithkml)", 13 | "Lou Cyx (https://github.com/loucyx)", 14 | "Marek Dědič (https://github.com/marekdedic)", 15 | "Michael Gallagher (https://github.com/mikeapr4)", 16 | "Mitchell Merry (https://github.com/mitchell-merry)", 17 | "Pablo Enrici (https://github.com/pablen)", 18 | "Renato Böhler (https://github.com/renato-bohler)", 19 | "Triston Jones (https://github.com/TristonJ)" 20 | ], 21 | "dependencies": { 22 | "@typescript-eslint/types": "8.19.1", 23 | "@typescript-eslint/utils": "8.19.1" 24 | }, 25 | "exports": { 26 | ".": { 27 | "types": "./dist/index.d.ts", 28 | "default": "./dist/index.js" 29 | } 30 | }, 31 | "devDependencies": { 32 | "@eslint/eslintrc": "3.2.0", 33 | "@eslint/js": "9.17.0", 34 | "@release-it/conventional-changelog": "8.0.2", 35 | "@types/eslint": "9.6.1", 36 | "@types/node": "22.10.5", 37 | "@typescript-eslint/eslint-plugin": "8.19.1", 38 | "@typescript-eslint/parser": "8.19.1", 39 | "@typescript-eslint/rule-tester": "8.19.1", 40 | "@vitest/coverage-v8": "2.1.8", 41 | "auto-changelog": "2.5.0", 42 | "eslint": "9.17.0", 43 | "globals": "15.14.0", 44 | "prettier": "3.4.2", 45 | "release-it": "17.11.0", 46 | "rimraf": "6.0.1", 47 | "typescript": "5.7.3", 48 | "vitest": "2.1.8" 49 | }, 50 | "engines": { 51 | "node": ">=18.0.0" 52 | }, 53 | "files": [ 54 | "dist" 55 | ], 56 | "homepage": "https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions", 57 | "keywords": [ 58 | "es6", 59 | "eslint", 60 | "eslint-plugin" 61 | ], 62 | "license": "MIT", 63 | "main": "dist/index.js", 64 | "peerDependencies": { 65 | "eslint": ">=9.17.0" 66 | }, 67 | "repository": "JamieMason/eslint-plugin-prefer-arrow-functions", 68 | "scripts": { 69 | "build": "tsc --project .", 70 | "format": "prettier --write src test", 71 | "lint": "eslint .", 72 | "prebuild": "rimraf ./dist", 73 | "prelint": "npm run format", 74 | "prepack": "npm run build", 75 | "release": "release-it", 76 | "test": "vitest run", 77 | "test:watch": "vitest watch" 78 | }, 79 | "type": "commonjs" 80 | } 81 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { TSESTree } from '@typescript-eslint/types'; 2 | import { TSESLint } from '@typescript-eslint/utils'; 3 | 4 | export type AnyFunctionBody = TSESTree.BlockStatement | TSESTree.Expression; 5 | export type AnyFunction = TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression; 6 | export type NameableFunction = TSESTree.FunctionDeclaration | TSESTree.FunctionExpression; 7 | export type NamedFunction = NameableFunction & { id: TSESTree.Identifier }; 8 | export type GeneratorFunction = NameableFunction & { generator: true }; 9 | export type WithTypeParameters = T & { typeParameters: TSESTree.TSTypeParameterDeclaration }; 10 | export type MessageId = keyof typeof MESSAGES_BY_ID; 11 | export type Options = [ActualOptions]; 12 | 13 | export interface Scope { 14 | isTsx: boolean; 15 | options: ActualOptions; 16 | sourceCode: TSESLint.RuleContext['sourceCode']; 17 | } 18 | 19 | export interface ActualOptions { 20 | allowedNames: string[]; 21 | allowNamedFunctions: boolean; 22 | allowObjectProperties: boolean; 23 | classPropertiesAllowed: boolean; 24 | disallowPrototype: boolean; 25 | returnStyle: 'explicit' | 'implicit' | 'unchanged'; 26 | singleReturnOnly: boolean; 27 | } 28 | 29 | export const DEFAULT_OPTIONS: ActualOptions = { 30 | allowedNames: [], 31 | allowNamedFunctions: false, 32 | allowObjectProperties: false, 33 | classPropertiesAllowed: false, 34 | disallowPrototype: false, 35 | returnStyle: 'unchanged', 36 | singleReturnOnly: false, 37 | }; 38 | 39 | export const MESSAGES_BY_ID = { 40 | USE_ARROW_WHEN_FUNCTION: 'Prefer using arrow functions over plain functions', 41 | USE_ARROW_WHEN_SINGLE_RETURN: 'Prefer using arrow functions when the function contains only a return', 42 | USE_EXPLICIT: 'Prefer using explicit returns when the arrow function contain only a return', 43 | USE_IMPLICIT: 'Prefer using implicit returns when the arrow function contain only a return', 44 | } as const; 45 | -------------------------------------------------------------------------------- /src/guard.ts: -------------------------------------------------------------------------------- 1 | import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; 2 | import { AnyFunction, AnyFunctionBody, Scope, GeneratorFunction, NamedFunction, WithTypeParameters } from './config'; 3 | 4 | export class Guard { 5 | isTsx: Scope['isTsx']; 6 | options: Scope['options']; 7 | sourceCode: Scope['sourceCode']; 8 | 9 | constructor(scope: Scope) { 10 | this.isTsx = scope.isTsx; 11 | this.options = scope.options; 12 | this.sourceCode = scope.sourceCode; 13 | } 14 | 15 | isAnyFunction(value: TSESTree.Node): value is AnyFunction { 16 | return [ 17 | AST_NODE_TYPES.FunctionDeclaration, 18 | AST_NODE_TYPES.FunctionExpression, 19 | AST_NODE_TYPES.ArrowFunctionExpression, 20 | ].includes(value.type); 21 | } 22 | 23 | isReturnStatement(value: unknown): value is TSESTree.ReturnStatement { 24 | return (value as TSESTree.Node)?.type === AST_NODE_TYPES.ReturnStatement; 25 | } 26 | 27 | isBlockStatementWithSingleReturn(body: AnyFunctionBody): body is TSESTree.BlockStatement & { 28 | body: [TSESTree.ReturnStatement & { argument: TSESTree.Expression }]; 29 | } { 30 | return ( 31 | body.type === AST_NODE_TYPES.BlockStatement && 32 | body.body.length === 1 && 33 | this.isReturnStatement(body.body[0]) && 34 | body.body[0].argument !== null 35 | ); 36 | } 37 | 38 | hasImplicitReturn(body: AnyFunctionBody): body is Exclude { 39 | return body.type !== AST_NODE_TYPES.BlockStatement; 40 | } 41 | 42 | returnsImmediately(fn: AnyFunction): boolean { 43 | return this.isBlockStatementWithSingleReturn(fn.body) || this.hasImplicitReturn(fn.body); 44 | } 45 | 46 | isExportedAsNamedExport(node: AnyFunction): boolean { 47 | return node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration; 48 | } 49 | 50 | getPreviousNode(fn: AnyFunction): TSESTree.Node | null { 51 | const node = this.isExportedAsNamedExport(fn) ? fn.parent : fn; 52 | const tokenBefore = this.sourceCode.getTokenBefore(node); 53 | if (!tokenBefore) return null; 54 | return this.sourceCode.getNodeByRangeIndex(tokenBefore.range[0]); 55 | } 56 | 57 | isOverloadedFunction(fn: AnyFunction): boolean { 58 | const previousNode = this.getPreviousNode(fn); 59 | return ( 60 | previousNode?.type === AST_NODE_TYPES.TSDeclareFunction || 61 | (previousNode?.type === AST_NODE_TYPES.ExportNamedDeclaration && 62 | previousNode.declaration?.type === AST_NODE_TYPES.TSDeclareFunction) 63 | ); 64 | } 65 | 66 | hasTypeParameters(fn: T): fn is WithTypeParameters { 67 | return Boolean(fn.typeParameters); 68 | } 69 | 70 | isAsyncFunction(node: AnyFunction): boolean { 71 | return node.async === true; 72 | } 73 | 74 | isGeneratorFunction(fn: AnyFunction): fn is GeneratorFunction { 75 | return fn.generator === true; 76 | } 77 | 78 | isAssertionFunction(fn: T): fn is T & { returnType: TSESTree.TSTypeAnnotation } { 79 | return ( 80 | fn.returnType?.typeAnnotation.type === AST_NODE_TYPES.TSTypePredicate && fn.returnType?.typeAnnotation.asserts 81 | ); 82 | } 83 | 84 | containsToken(type: string, value: string, node: TSESTree.Node): boolean { 85 | return this.sourceCode.getTokens(node).some((token) => token.type === type && token.value === value); 86 | } 87 | 88 | containsSuper(node: TSESTree.Node): boolean { 89 | return this.containsToken('Keyword', 'super', node); 90 | } 91 | 92 | containsThis(node: TSESTree.Node): boolean { 93 | return this.containsToken('Keyword', 'this', node); 94 | } 95 | 96 | containsArguments(node: TSESTree.Node): boolean { 97 | return this.containsToken('Identifier', 'arguments', node); 98 | } 99 | 100 | containsTokenSequence(sequence: [string, string][], node: TSESTree.Node): boolean { 101 | return this.sourceCode.getTokens(node).some((_, tokenIndex, tokens) => { 102 | return sequence.every(([expectedType, expectedValue], i) => { 103 | const actual = tokens[tokenIndex + i]; 104 | return actual && actual.type === expectedType && actual.value === expectedValue; 105 | }); 106 | }); 107 | } 108 | 109 | containsNewDotTarget(node: TSESTree.Node): boolean { 110 | return this.containsTokenSequence( 111 | [ 112 | ['Keyword', 'new'], 113 | ['Punctuator', '.'], 114 | ['Identifier', 'target'], 115 | ], 116 | node, 117 | ); 118 | } 119 | 120 | isPrototypeAssignment(node: AnyFunction): boolean { 121 | return this.sourceCode 122 | .getAncestors(node) 123 | .reverse() 124 | .some((ancestor) => { 125 | const isPropertyOfReplacementPrototypeObject = 126 | ancestor.type === AST_NODE_TYPES.AssignmentExpression && 127 | ancestor.left && 128 | 'property' in ancestor.left && 129 | ancestor.left.property && 130 | 'name' in ancestor.left.property && 131 | ancestor.left.property.name === 'prototype'; 132 | const isMutationOfExistingPrototypeObject = 133 | ancestor.type === AST_NODE_TYPES.AssignmentExpression && 134 | ancestor.left && 135 | 'object' in ancestor.left && 136 | ancestor.left.object && 137 | 'property' in ancestor.left.object && 138 | ancestor.left.object.property && 139 | 'name' in ancestor.left.object.property && 140 | ancestor.left.object.property.name === 'prototype'; 141 | return isPropertyOfReplacementPrototypeObject || isMutationOfExistingPrototypeObject; 142 | }); 143 | } 144 | 145 | isWithinClassBody(node: TSESTree.Node): boolean { 146 | return this.sourceCode 147 | .getAncestors(node) 148 | .reverse() 149 | .some((ancestor) => { 150 | return ancestor.type === AST_NODE_TYPES.ClassBody; 151 | }); 152 | } 153 | 154 | isNamedFunction(fn: AnyFunction): fn is NamedFunction { 155 | return fn.id !== null && fn.id.name !== null; 156 | } 157 | 158 | hasNameAndIsExportedAsDefaultExport(fn: AnyFunction): fn is NamedFunction { 159 | return this.isNamedFunction(fn) && fn.parent.type === AST_NODE_TYPES.ExportDefaultDeclaration; 160 | } 161 | 162 | isIgnored(fn: AnyFunction): boolean { 163 | return this.isNamedFunction(fn) && this.options.allowedNames.includes(fn.id.name); 164 | } 165 | 166 | isObjectProperty(fn: AnyFunction): boolean { 167 | return this.sourceCode 168 | .getAncestors(fn) 169 | .reverse() 170 | .some((ancestor) => { 171 | return ancestor.type === AST_NODE_TYPES.Property; 172 | }); 173 | } 174 | 175 | isSafeTransformation(fn: TSESTree.Node): fn is AnyFunction { 176 | const isSafe = 177 | this.isAnyFunction(fn) && 178 | !this.isGeneratorFunction(fn) && 179 | !this.isAssertionFunction(fn) && 180 | !this.isOverloadedFunction(fn) && 181 | !this.containsThis(fn) && 182 | !this.containsSuper(fn) && 183 | !this.containsArguments(fn) && 184 | !this.containsNewDotTarget(fn); 185 | if (!isSafe) return false; 186 | if (this.isIgnored(fn)) return false; 187 | if (this.options.allowNamedFunctions && this.isNamedFunction(fn)) return false; 188 | if (!this.options.disallowPrototype && this.isPrototypeAssignment(fn)) return false; 189 | if (this.options.singleReturnOnly && !this.returnsImmediately(fn)) return false; 190 | if (this.isObjectProperty(fn) && this.options.allowObjectProperties) return false; 191 | if (this.hasNameAndIsExportedAsDefaultExport(fn)) return false; 192 | return true; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { TSESLint } from '@typescript-eslint/utils'; 2 | import { preferArrowFunctions } from './rule'; 3 | 4 | const { name, version } = 5 | // `import`ing here would bypass the TSConfig's `"rootDir": "src"` 6 | // eslint-disable-next-line @typescript-eslint/no-require-imports 7 | require('../package.json') as typeof import('../package.json'); 8 | 9 | const plugin: TSESLint.FlatConfig.Plugin = { 10 | meta: { name, version }, 11 | rules: { 12 | 'prefer-arrow-functions': preferArrowFunctions, 13 | }, 14 | }; 15 | 16 | export const { meta, rules } = plugin; 17 | -------------------------------------------------------------------------------- /src/rule.ts: -------------------------------------------------------------------------------- 1 | import { TSESTree, ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils'; 2 | import { Options, MessageId, MESSAGES_BY_ID, DEFAULT_OPTIONS, AnyFunction, Scope } from './config'; 3 | import { Guard } from './guard'; 4 | import { Writer } from './writer'; 5 | 6 | const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/JamieMason/${name}`); 7 | 8 | export const preferArrowFunctions = createRule({ 9 | name: 'prefer-arrow-functions', 10 | meta: { 11 | type: 'suggestion', 12 | docs: { 13 | description: 14 | 'Auto-fix plain Functions into Arrow Functions, in all cases where conversion would result in the same behaviour', 15 | }, 16 | fixable: 'code', 17 | messages: MESSAGES_BY_ID, 18 | schema: [ 19 | { 20 | additionalProperties: false, 21 | properties: { 22 | allowedNames: { 23 | default: DEFAULT_OPTIONS.allowedNames, 24 | items: { 25 | type: 'string', 26 | }, 27 | type: 'array', 28 | }, 29 | allowNamedFunctions: { 30 | default: DEFAULT_OPTIONS.allowNamedFunctions, 31 | type: 'boolean', 32 | }, 33 | allowObjectProperties: { 34 | default: DEFAULT_OPTIONS.allowObjectProperties, 35 | type: 'boolean', 36 | }, 37 | classPropertiesAllowed: { 38 | default: DEFAULT_OPTIONS.classPropertiesAllowed, 39 | type: 'boolean', 40 | }, 41 | disallowPrototype: { 42 | default: DEFAULT_OPTIONS.disallowPrototype, 43 | type: 'boolean', 44 | }, 45 | returnStyle: { 46 | default: DEFAULT_OPTIONS.returnStyle, 47 | pattern: '^(explicit|implicit|unchanged)$', 48 | type: 'string', 49 | }, 50 | singleReturnOnly: { 51 | default: DEFAULT_OPTIONS.singleReturnOnly, 52 | type: 'boolean', 53 | }, 54 | }, 55 | type: 'object', 56 | }, 57 | ], 58 | }, 59 | defaultOptions: [DEFAULT_OPTIONS], 60 | create: (ctx, [options]) => { 61 | const isTsx = ctx.physicalFilename?.endsWith('.tsx'); 62 | const sourceCode = ctx.sourceCode; 63 | const scope: Scope = { 64 | isTsx, 65 | options, 66 | sourceCode, 67 | }; 68 | 69 | const guard = new Guard(scope); 70 | const writer = new Writer(scope, guard); 71 | 72 | const getMessageId = (node: AnyFunction): MessageId => { 73 | return options.singleReturnOnly && guard.returnsImmediately(node) 74 | ? 'USE_ARROW_WHEN_SINGLE_RETURN' 75 | : 'USE_ARROW_WHEN_FUNCTION'; 76 | }; 77 | 78 | return { 79 | 'ExportDefaultDeclaration > FunctionDeclaration': (node: TSESTree.FunctionDeclaration) => { 80 | if (guard.isSafeTransformation(node)) { 81 | ctx.report({ 82 | fix: (fixer) => fixer.replaceText(node, writer.writeArrowFunction(node) + ';'), 83 | messageId: getMessageId(node), 84 | node, 85 | }); 86 | } 87 | }, 88 | ':matches(ClassProperty, MethodDefinition, Property)[key.name][value.type="FunctionExpression"][kind!=/^(get|set|constructor)$/]': 89 | (node: TSESTree.MethodDefinition | TSESTree.Property) => { 90 | const fn = node.value; 91 | if (guard.isSafeTransformation(fn) && (!guard.isWithinClassBody(fn) || options.classPropertiesAllowed)) { 92 | const name = 'name' in node.key ? node.key.name : ''; 93 | const propName = node.key.type === AST_NODE_TYPES.PrivateIdentifier ? `#${name}` : name; 94 | const staticModifier = 'static' in node && node.static ? 'static ' : ''; 95 | ctx.report({ 96 | fix: (fixer) => 97 | fixer.replaceText( 98 | node, 99 | guard.isWithinClassBody(node) 100 | ? `${staticModifier}${propName} = ${writer.writeArrowFunction(fn)};` 101 | : `${staticModifier}${propName}: ${writer.writeArrowFunction(fn)}`, 102 | ), 103 | messageId: getMessageId(fn), 104 | node: fn, 105 | }); 106 | } 107 | }, 108 | 'ArrowFunctionExpression[body.type!="BlockStatement"]': (node: TSESTree.ArrowFunctionExpression) => { 109 | if (options.returnStyle === 'explicit' && guard.isSafeTransformation(node)) { 110 | ctx.report({ 111 | fix: (fixer) => fixer.replaceText(node, writer.writeArrowFunction(node)), 112 | messageId: 'USE_EXPLICIT', 113 | node, 114 | }); 115 | } 116 | }, 117 | 'ArrowFunctionExpression[body.body.length=1][body.body.0.type="ReturnStatement"]': ( 118 | node: TSESTree.ArrowFunctionExpression, 119 | ) => { 120 | if (options.returnStyle === 'implicit' && guard.isSafeTransformation(node)) { 121 | ctx.report({ 122 | fix: (fixer) => fixer.replaceText(node, writer.writeArrowFunction(node)), 123 | messageId: 'USE_IMPLICIT', 124 | node, 125 | }); 126 | } 127 | }, 128 | 'FunctionExpression[parent.type!=/^(ClassProperty|MethodDefinition|Property)$/]': ( 129 | node: TSESTree.FunctionExpression, 130 | ) => { 131 | if (guard.isSafeTransformation(node)) { 132 | ctx.report({ 133 | fix: (fixer) => { 134 | return fixer.replaceText(node, writer.writeArrowFunction(node)); 135 | }, 136 | messageId: getMessageId(node), 137 | node, 138 | }); 139 | } 140 | }, 141 | 'FunctionDeclaration[parent.type!="ExportDefaultDeclaration"]': (node: TSESTree.FunctionDeclaration) => { 142 | if (guard.isSafeTransformation(node)) { 143 | ctx.report({ 144 | fix: (fixer) => fixer.replaceText(node, writer.writeArrowConstant(node) + ';'), 145 | messageId: getMessageId(node), 146 | node, 147 | }); 148 | } 149 | }, 150 | }; 151 | }, 152 | }); 153 | -------------------------------------------------------------------------------- /src/writer.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; 2 | import { AnyFunction, Scope } from './config'; 3 | import { Guard } from './guard'; 4 | 5 | export class Writer { 6 | guard: Guard; 7 | isTsx: Scope['isTsx']; 8 | options: Scope['options']; 9 | sourceCode: Scope['sourceCode']; 10 | 11 | constructor(scope: Scope, guard: Guard) { 12 | this.guard = guard; 13 | this.isTsx = scope.isTsx; 14 | this.options = scope.options; 15 | this.sourceCode = scope.sourceCode; 16 | } 17 | 18 | getBodySource({ body }: AnyFunction): string { 19 | if (this.options.returnStyle !== 'explicit' && this.guard.isBlockStatementWithSingleReturn(body)) { 20 | const returnValue = body.body[0].argument; 21 | const source = this.sourceCode.getText(returnValue); 22 | return returnValue.type === AST_NODE_TYPES.ObjectExpression ? `(${source})` : source; 23 | } 24 | if (this.guard.hasImplicitReturn(body) && this.options.returnStyle !== 'implicit') { 25 | return `{ return ${this.sourceCode.getText(body)} }`; 26 | } 27 | return this.sourceCode.getText(body); 28 | } 29 | 30 | getParamsSource(params: TSESTree.Parameter[]): string[] { 31 | return params.map((param) => this.sourceCode.getText(param)); 32 | } 33 | 34 | getFunctionName(node: AnyFunction): string { 35 | return node.id && node.id.name ? node.id.name : ''; 36 | } 37 | 38 | getGenericSource(fn: AnyFunction): string { 39 | if (!this.guard.hasTypeParameters(fn)) return ''; 40 | const genericSource = this.sourceCode.getText(fn.typeParameters); 41 | if (!this.isTsx) return genericSource; 42 | const params = fn.typeParameters.params; 43 | if (params.length === 1) return `<${params[0].name.name},>`; 44 | return genericSource; 45 | } 46 | 47 | getReturnType(node: AnyFunction): string | undefined { 48 | return node.returnType && node.returnType.range && this.sourceCode.getText().substring(...node.returnType.range); 49 | } 50 | 51 | writeArrowFunction(node: AnyFunction): string { 52 | const fn = this.getFunctionDescriptor(node); 53 | const ASYNC = fn.isAsync ? 'async ' : ''; 54 | const GENERIC = fn.isGeneric ? fn.generic : ''; 55 | const BODY = fn.body; 56 | const RETURN_TYPE = fn.returnType ? fn.returnType : ''; 57 | const PARAMS = fn.params.join(', '); 58 | return `${ASYNC}${GENERIC}(${PARAMS})${RETURN_TYPE} => ${BODY}`; 59 | } 60 | 61 | writeArrowConstant(node: TSESTree.FunctionDeclaration): string { 62 | const fn = this.getFunctionDescriptor(node); 63 | return `const ${fn.name} = ${this.writeArrowFunction(node)}`; 64 | } 65 | 66 | getFunctionDescriptor(node: AnyFunction) { 67 | return { 68 | body: this.getBodySource(node), 69 | isAsync: this.guard.isAsyncFunction(node), 70 | isGenerator: this.guard.isGeneratorFunction(node), 71 | isGeneric: this.guard.hasTypeParameters(node), 72 | name: this.getFunctionName(node), 73 | generic: this.getGenericSource(node), 74 | params: this.getParamsSource(node.params), 75 | returnType: this.getReturnType(node), 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/rule.spec.ts: -------------------------------------------------------------------------------- 1 | import * as vitest from 'vitest'; 2 | import { RuleTester } from '@typescript-eslint/rule-tester'; 3 | import { preferArrowFunctions as rule } from '../src/rule'; 4 | import * as scenarios from './scenarios'; 5 | import { ActualOptions, MessageId, Options } from '../src/config'; 6 | 7 | type Runner = typeof ruleTester.run; 8 | type ValidTestCase = Exclude[2]['valid'][0], string>; 9 | type InvalidTestCase = Exclude[2]['invalid'][0], string>; 10 | type TestCase = ValidTestCase | InvalidTestCase; 11 | 12 | RuleTester.afterAll = vitest.afterAll; 13 | RuleTester.it = vitest.it; 14 | RuleTester.itOnly = vitest.it.only; 15 | RuleTester.describe = vitest.describe; 16 | 17 | const { describe } = vitest; 18 | const ruleTester = new RuleTester(); 19 | 20 | const withOptions = (extraOptions: Partial) => (object) => ({ 21 | ...object, 22 | options: [ 23 | { 24 | ...(object.options ? object.options[0] : {}), 25 | ...(extraOptions as ActualOptions), 26 | }, 27 | ], 28 | }); 29 | 30 | const withErrors = (errors: MessageId[]) => (object) => ({ 31 | ...object, 32 | errors: errors.map((messageId) => ({ messageId })), 33 | }); 34 | 35 | const withTsx = 36 | () => 37 | (object: T): T => ({ 38 | ...object, 39 | filename: '/some/path/Component.tsx', 40 | languageOptions: { 41 | parserOptions: { 42 | ecmaFeatures: { 43 | jsx: true, 44 | }, 45 | }, 46 | }, 47 | }); 48 | 49 | describe('when function is already an arrow function, or cannot be converted to an arrow function', () => { 50 | describe('it considers the function valid', () => { 51 | ruleTester.run('prefer-arrow-functions', rule, { 52 | valid: [ 53 | { 54 | code: 'var foo = (bar) => bar;', 55 | }, 56 | { 57 | code: 'var foo = async (bar) => bar;', 58 | }, 59 | { 60 | code: 'var foo = bar => bar;', 61 | }, 62 | { 63 | code: 'var foo = async bar => bar;', 64 | }, 65 | { 66 | code: 'var foo = bar => { return bar; }', 67 | }, 68 | { 69 | code: 'var foo = async bar => { return bar; }', 70 | }, 71 | { 72 | code: 'var foo = () => 1;', 73 | }, 74 | { 75 | code: 'var foo = async () => 1;', 76 | }, 77 | { 78 | code: 'var foo = (bar, fuzz) => bar + fuzz', 79 | }, 80 | { 81 | code: 'var foo = async (bar, fuzz) => bar + fuzz', 82 | }, 83 | { 84 | code: '["Hello", "World"].reduce((p, a) => p + " " + a);', 85 | }, 86 | { 87 | code: '["Hello", "World"].reduce(async (p, a) => p + " " + a);', 88 | }, 89 | { 90 | code: 'var foo = (...args) => args', 91 | }, 92 | { 93 | code: 'var foo = async (...args) => args', 94 | }, 95 | { 96 | code: 'class obj {constructor(foo){this.foo = foo;}}; obj.prototype.func = function() {};', 97 | }, 98 | { 99 | code: 'class obj {constructor(foo){this.foo = foo;}}; obj.prototype = {func: function() {}};', 100 | }, 101 | { 102 | code: 'class obj {constructor(private readonly foo: number){}};', 103 | }, 104 | { 105 | code: 'var foo = function() { return this.bar; };', 106 | }, 107 | { 108 | code: 'function * testGenerator() { return yield 1; }', 109 | }, 110 | { 111 | code: 'const foo = { get bar() { return "test"; } }', 112 | }, 113 | { 114 | code: 'const foo = { set bar(xyz) {} }', 115 | }, 116 | { 117 | code: 'class foo { get bar() { return "test"; } }', 118 | }, 119 | { 120 | code: 'class foo { set bar(xyz) { } }', 121 | }, 122 | // arguments is unavailable in arrow functions 123 | { 124 | code: 'function bar () {return arguments}', 125 | }, 126 | { 127 | code: 'var foo = function () {return arguments}', 128 | }, 129 | { 130 | code: 'function bar () {console.log(arguments);}', 131 | }, 132 | { 133 | code: 'var foo = function () {console.log(arguments);}', 134 | }, 135 | // super() is unavailable in arrow functions 136 | { 137 | code: 'class foo extends bar { constructor() {return super()} }', 138 | }, 139 | { 140 | code: 'class foo extends bar { constructor() {console.log(super())} }', 141 | }, 142 | // new.target is unavailable in arrow functions 143 | { 144 | code: 'function Foo() {if (!new.target) throw "Foo() must be called with new";}', 145 | }, 146 | // assertion functions are unavailable in arrow functions 147 | { 148 | code: 'function foo(val: any): asserts val is string {}', 149 | }, 150 | // function overloading is unavailable in arrow functions 151 | { 152 | code: 'function foo(): any;', 153 | }, 154 | { 155 | code: 'function foo(): any; function foo() {}', 156 | }, 157 | { 158 | code: 'function foo(val: string): void; function foo(val: number): void; function foo(val: string | number): void {}', 159 | }, 160 | { 161 | code: 'const foo = () => { function bar(val: string): void; function bar(val: number): void; function bar(val: string | number): void {} }', 162 | }, 163 | { 164 | code: 'export function foo(): any;', 165 | }, 166 | { 167 | code: 'export function foo(): any; export function foo() {}', 168 | }, 169 | { 170 | code: 'export function foo(val: string): void; export function foo(val: number): void; export function foo(val: string | number): void {}', 171 | }, 172 | // export { x }; has node.declaration === null - regression test for this case 173 | { 174 | code: 'export { foo }; export function bar(val: number): void; export function bar(val: string | number): void {}', 175 | }, 176 | ], 177 | invalid: [], 178 | }); 179 | }); 180 | }); 181 | 182 | describe('when classPropertiesAllowed is true', () => { 183 | describe('when property cannot be converted to an arrow function', () => { 184 | describe('it considers the function valid', () => { 185 | ruleTester.run('prefer-arrow-functions', rule, { 186 | valid: [ 187 | { 188 | code: 'class obj {constructor(private readonly foo: number){}};', 189 | }, 190 | ].map(withOptions({ classPropertiesAllowed: true })), 191 | invalid: [], 192 | }); 193 | }); 194 | }); 195 | }); 196 | 197 | describe('issue #35', () => { 198 | ruleTester.run('prefer-arrow-functions', rule, { 199 | valid: [ 200 | { 201 | code: 'export default async function fetchFoo() { return await fetch("/foo"); }', 202 | }, 203 | ], 204 | invalid: [], 205 | }); 206 | }); 207 | 208 | describe('allowObjectProperties', () => { 209 | describe('when property can be converted to an arrow function', () => { 210 | describe('leaves the method as is when allowObjectProperties is true', () => { 211 | ruleTester.run('prefer-arrow-functions', rule, { 212 | valid: [ 213 | { 214 | code: 'const foo = { render(a, b) { console.log(3); } }', 215 | }, 216 | { 217 | code: 'export default { data(){ return 4 } }', 218 | }, 219 | ].map(withOptions({ allowObjectProperties: true })), 220 | invalid: [ 221 | { 222 | code: 'const foo = { render(a, b) { return a + b; } }', 223 | output: 'const foo = { render: (a, b) => a + b }', 224 | }, 225 | { 226 | code: 'export default { data(){ return 4 } }', 227 | output: 'export default { data: () => 4 }', 228 | }, 229 | ] 230 | .map(withOptions({ allowObjectProperties: false })) 231 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION'])), 232 | }); 233 | }); 234 | }); 235 | }); 236 | 237 | describe('when singleReturnOnly is true', () => { 238 | describe('when function should be an arrow function', () => { 239 | describe('when function does not contain only a return statement', () => { 240 | describe('it considers the function valid', () => { 241 | ruleTester.run('prefer-arrow-functions', rule, { 242 | valid: [ 243 | ...scenarios.invalidAndHasBlockStatement, 244 | ...scenarios.invalidAndHasBlockStatementWithMultipleMatches, 245 | ...scenarios.validWhenSingleReturnOnly, 246 | ].map(withOptions({ singleReturnOnly: true })), 247 | invalid: [], 248 | }); 249 | }); 250 | }); 251 | describe('when function contains only a return statement', () => { 252 | describe('it fixes the function', () => { 253 | ruleTester.run('prefer-arrow-functions', rule, { 254 | valid: [], 255 | invalid: scenarios.invalidAndHasSingleReturn 256 | .map(withOptions({ singleReturnOnly: true })) 257 | .map(withErrors(['USE_ARROW_WHEN_SINGLE_RETURN'])), 258 | }); 259 | }); 260 | }); 261 | }); 262 | describe('when two functions are featured: one returns immediately and the other has a block statement', () => { 263 | describe('it fixes the function which returns and considers the other valid', () => { 264 | ruleTester.run('prefer-arrow-functions', rule, { 265 | valid: [], 266 | invalid: scenarios.invalidAndHasSingleReturnWithMultipleMatches 267 | .map(withOptions({ singleReturnOnly: true })) 268 | .map(withErrors(['USE_ARROW_WHEN_SINGLE_RETURN'])), 269 | }); 270 | }); 271 | }); 272 | }); 273 | 274 | describe('when singleReturnOnly is false', () => { 275 | describe('when function should be an arrow function', () => { 276 | describe('it fixes the function', () => { 277 | ruleTester.run('prefer-arrow-functions', rule, { 278 | valid: [], 279 | invalid: [ 280 | ...scenarios.invalidAndHasSingleReturn, 281 | ...scenarios.invalidAndHasBlockStatement, 282 | ...scenarios.invalidAndHasBlockStatementWithMultipleMatches, 283 | ] 284 | .map(withOptions({ singleReturnOnly: false })) 285 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION'])), 286 | }); 287 | }); 288 | describe('when function has a block statement', () => { 289 | describe('when returnStyle is "explicit"', () => { 290 | ruleTester.run('prefer-arrow-functions', rule, { 291 | valid: [], 292 | invalid: scenarios.invalidAndHasBlockStatement 293 | .map(withOptions({ returnStyle: 'explicit', singleReturnOnly: false })) 294 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION'])), 295 | }); 296 | }); 297 | describe('when returnStyle is "implicit"', () => { 298 | ruleTester.run('prefer-arrow-functions', rule, { 299 | valid: [], 300 | invalid: scenarios.invalidAndHasBlockStatement 301 | .map(withOptions({ returnStyle: 'implicit', singleReturnOnly: false })) 302 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION'])), 303 | }); 304 | }); 305 | describe('when returnStyle is "unchanged" or not set', () => { 306 | ruleTester.run('prefer-arrow-functions', rule, { 307 | valid: [], 308 | invalid: scenarios.invalidAndHasBlockStatement 309 | .map( 310 | withOptions({ 311 | returnStyle: 'unchanged', 312 | singleReturnOnly: false, 313 | }), 314 | ) 315 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION'])), 316 | }); 317 | }); 318 | }); 319 | // I think this is actually correct but the test runner is now only 320 | // returning the source for the outer function, before it has processed the 321 | // inner one 322 | describe.skip('when two functions are featured: one returns immediately and the other has a block statement', () => { 323 | describe('it fixes both functions', () => { 324 | ruleTester.run('prefer-arrow-functions', rule, { 325 | valid: [], 326 | invalid: scenarios.invalidAndHasSingleReturnWithMultipleMatches 327 | .map(withOptions({ singleReturnOnly: false })) 328 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION', 'USE_ARROW_WHEN_FUNCTION'])), 329 | }); 330 | }); 331 | }); 332 | }); 333 | }); 334 | 335 | describe('when disallowPrototype is true', () => { 336 | describe('when function should be an arrow function', () => { 337 | describe('when function is assigned to a prototype', () => { 338 | describe('it fixes the function', () => { 339 | ruleTester.run('prefer-arrow-functions', rule, { 340 | valid: [ 341 | { 342 | code: 'class obj {constructor(foo){this.foo = foo;}}; obj.prototype.func = (): void => {};', 343 | }, 344 | ].map(withOptions({ disallowPrototype: true })), 345 | invalid: [ 346 | { 347 | code: 'class obj {constructor(foo){this.foo = foo;}}; obj.prototype.func = function(): void {};', 348 | output: 'class obj {constructor(foo){this.foo = foo;}}; obj.prototype.func = (): void => {};', 349 | errors: [{ messageId: 'USE_ARROW_WHEN_FUNCTION' }], 350 | }, 351 | ].map(withOptions({ disallowPrototype: true })), 352 | }); 353 | }); 354 | }); 355 | }); 356 | }); 357 | 358 | describe('when returnStyle is "implicit"', () => { 359 | describe('when function is an arrow function with a block statement containing an immediate return', () => { 360 | describe('it fixes the function to have an implicit return', () => { 361 | ruleTester.run('prefer-arrow-functions', rule, { 362 | valid: [], 363 | invalid: [ 364 | { 365 | code: 'var foo = bar => { return bar(); }', 366 | output: 'var foo = (bar) => bar()', 367 | }, 368 | { 369 | code: 'var foo: (fn: () => number) => number = (bar: () => number): number => { return bar() }', 370 | output: 'var foo: (fn: () => number) => number = (bar: () => number): number => bar()', 371 | }, 372 | { 373 | code: 'var foo = async bar => { return bar(); }', 374 | output: 'var foo = async (bar) => bar()', 375 | }, 376 | ] 377 | .map(withOptions({ returnStyle: 'implicit' })) 378 | .map(withErrors(['USE_IMPLICIT'])), 379 | }); 380 | }); 381 | }); 382 | }); 383 | 384 | describe('when returnStyle is "explicit"', () => { 385 | describe('when function is an arrow function with an implicit return', () => { 386 | describe('it fixes the function to have a block statement containing an immediate return', () => { 387 | ruleTester.run('prefer-arrow-functions', rule, { 388 | valid: [], 389 | invalid: [ 390 | { 391 | code: 'var foo = (bar) => bar()', 392 | output: 'var foo = (bar) => { return bar() }', 393 | }, 394 | { 395 | code: 'var foo: (fn: () => number) => number = (bar: () => number): number => bar()', 396 | output: 'var foo: (fn: () => number) => number = (bar: () => number): number => { return bar() }', 397 | }, 398 | { 399 | code: 'var foo = async (bar) => bar()', 400 | output: 'var foo = async (bar) => { return bar() }', 401 | }, 402 | ] 403 | .map(withOptions({ returnStyle: 'explicit' })) 404 | .map(withErrors(['USE_EXPLICIT'])), 405 | }); 406 | }); 407 | }); 408 | }); 409 | 410 | describe('when allowNamedFunctions is true', () => { 411 | describe("it doesn't report named functions", () => { 412 | ruleTester.run('prefer-arrow-functions', rule, { 413 | valid: [ 414 | { code: '() => { function foo() { return "bar"; } }' }, 415 | { code: '() => { function * fooGen() { return yield "bar"; } }' }, 416 | { code: '() => { async function foo() { return await "bar"; } }' }, 417 | { code: '() => { function foo() { return () => "bar"; } }' }, 418 | { code: 'class FooClass { foo() { return "bar" }}' }, 419 | { 420 | code: 'export default () => { function foo() { return "bar"; } }', 421 | }, 422 | { 423 | // Make sure "allowNamedFunctions" works with typescript 424 | code: '() => { function foo(a: string): string { return `bar ${a}`;} }', 425 | }, 426 | ].map(withOptions({ allowNamedFunctions: true })), 427 | invalid: [ 428 | // Invalid tests for "allowNamedFunctions" option 429 | { 430 | code: '() => { var foo = function() { return "bar"; }; }', 431 | output: '() => { var foo = () => "bar"; }', 432 | }, 433 | { 434 | code: '() => { var foo = async function() { return await "bar"; }; }', 435 | output: '() => { var foo = async () => await "bar"; }', 436 | }, 437 | { 438 | code: '() => { var foo = function() { return () => "bar"; }; }', 439 | output: '() => { var foo = () => () => "bar"; }', 440 | }, 441 | { 442 | code: 'module.exports = () => { var foo = function() { return "bar"; }; }', 443 | output: 'module.exports = () => { var foo = () => "bar"; }', 444 | }, 445 | { 446 | code: 'module.exports.foo = () => { var bar = function() { return "baz"; }; }', 447 | output: 'module.exports.foo = () => { var bar = () => "baz"; }', 448 | }, 449 | { 450 | code: '() => { exports.foo = function() { return "bar"; }; }', 451 | output: '() => { exports.foo = () => "bar"; }', 452 | }, 453 | { 454 | code: 'exports = function() { return "bar"; };', 455 | output: 'exports = () => "bar";', 456 | }, 457 | { 458 | code: 'export default () => { var foo = function() { return "bar"; }; }', 459 | output: 'export default () => { var foo = () => "bar"; }', 460 | }, 461 | { 462 | // Using multiple lines to check that it only errors on the inner function 463 | code: `function top() { 464 | return function() { return "bar"; }; 465 | }`, 466 | output: `function top() { 467 | return () => "bar"; 468 | }`, 469 | }, 470 | { 471 | // Make sure "allowNamedFunctions" works with typescript 472 | code: `function foo(a: string): () => string { 473 | return function() { return \`bar \${a}\`; }; 474 | }`, 475 | output: `function foo(a: string): () => string { 476 | return () => \`bar \${a}\`; 477 | }`, 478 | }, 479 | ] 480 | .map(withOptions({ allowNamedFunctions: true })) 481 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION'])), 482 | }); 483 | }); 484 | }); 485 | 486 | describe('when file is TSX', () => { 487 | describe('it properly fixes generic type arguments', () => { 488 | ruleTester.run('prefer-arrow-functions', rule, { 489 | valid: [ 490 | { 491 | code: 'const Component = () =>
test
;', 492 | }, 493 | { 494 | code: 'const Component = () =>
test
;', 495 | }, 496 | ].map(withTsx()), 497 | invalid: [ 498 | { 499 | code: 'function Component() { return
test
}', 500 | output: 'const Component = () =>
test
;', 501 | }, 502 | { 503 | code: 'function Component() { return
test
}', 504 | output: 'const Component = () =>
test
;', 505 | }, 506 | { 507 | code: 'function Component() { return
test
}', 508 | output: 'const Component = () =>
test
;', 509 | }, 510 | ] 511 | .map(withTsx()) 512 | .map(withErrors(['USE_ARROW_WHEN_FUNCTION'])), 513 | }); 514 | }); 515 | }); 516 | -------------------------------------------------------------------------------- /test/scenarios.ts: -------------------------------------------------------------------------------- 1 | export const validWhenSingleReturnOnly = [ 2 | { 3 | code: 'var foo = (bar) => {return bar();}', 4 | }, 5 | { 6 | code: 'var foo = async (bar) => {return bar();}', 7 | }, 8 | { 9 | code: 'function foo(bar) {bar()}', 10 | }, 11 | { 12 | code: 'async function foo(bar) {bar()}', 13 | }, 14 | { 15 | code: 'var x = function foo(bar) {bar()}', 16 | }, 17 | { 18 | code: 'var x = async function foo(bar) {bar()}', 19 | }, 20 | { 21 | code: 'var x = function(bar) {bar()}', 22 | }, 23 | { 24 | code: 'var x = async function(bar) {bar()}', 25 | }, 26 | { 27 | code: 'function foo(bar) {/* yo */ bar()}', 28 | }, 29 | { 30 | code: 'async function foo(bar) {/* yo */ bar()}', 31 | }, 32 | { 33 | code: 'function foo() {}', 34 | }, 35 | { 36 | code: 'async function foo() {}', 37 | }, 38 | { 39 | code: 'function foo(bar) {bar(); return bar()}', 40 | }, 41 | { 42 | code: 'async function foo(bar) {bar(); return bar()}', 43 | }, 44 | { 45 | code: 'class MyClass { foo(bar) {bar(); return bar()} }', 46 | }, 47 | { 48 | code: 'class MyClass { async foo(bar) {bar(); return bar()} }', 49 | }, 50 | { 51 | code: 'class MyClass { static foo(bar) {bar(); return bar()} }', 52 | }, 53 | { 54 | code: 'class MyClass { static async foo(bar) {bar(); return bar()} }', 55 | }, 56 | { 57 | code: 'class MyClass {constructor(foo){this.foo = foo;}}; MyClass.prototype.func = function() {this.foo = "bar";};', 58 | }, 59 | { 60 | code: 'var MyClass = { foo(bar) {bar(); return bar()} }', 61 | }, 62 | { 63 | code: 'var MyClass = { async foo(bar) {bar(); return bar()} }', 64 | }, 65 | { 66 | code: 'export default function xyz() { return 3; }', 67 | }, 68 | { 69 | code: 'export default async function xyz() { return 3; }', 70 | }, 71 | { 72 | code: 'class MyClass { render(a, b) { return 3; } }', 73 | }, 74 | { 75 | code: 'class MyClass { async render(a, b) { return 3; } }', 76 | }, 77 | ]; 78 | 79 | export const invalidAndHasSingleReturn = [ 80 | // ES6 classes & functions declared in object literals 81 | { 82 | code: 'class MyClass { render(a, b) { return 3; } }', 83 | output: 'class MyClass { render = (a, b) => 3; }', 84 | options: [{ classPropertiesAllowed: true }], 85 | }, 86 | { 87 | code: 'class MyClass { render(a: number, b: number): number { return 3; } }', 88 | output: 'class MyClass { render = (a: number, b: number): number => 3; }', 89 | options: [{ classPropertiesAllowed: true }], 90 | }, 91 | { 92 | code: 'var MyClass = { render(a, b) { return 3; }, b: false }', 93 | output: 'var MyClass = { render: (a, b) => 3, b: false }', 94 | }, 95 | 96 | // named function declarations 97 | { 98 | code: 'function foo() { return 3; }', 99 | output: 'const foo = () => 3;', 100 | }, 101 | { 102 | code: 'function foo(): number { return 3; }', 103 | output: 'const foo = (): number => 3;', 104 | }, 105 | { 106 | code: 'async function foo() { return 3; }', 107 | output: 'const foo = async () => 3;', 108 | }, 109 | { 110 | code: 'function foo(a) { return 3 }', 111 | output: 'const foo = (a) => 3;', 112 | }, 113 | { 114 | code: 'async function foo(a) { return 3 }', 115 | output: 'const foo = async (a) => 3;', 116 | }, 117 | { 118 | code: 'function foo(a) { return 3; }', 119 | output: 'const foo = (a) => 3;', 120 | }, 121 | { 122 | code: 'async function foo(a) { return 3; }', 123 | output: 'const foo = async (a) => 3;', 124 | }, 125 | { 126 | code: 'function identity(t: T): T { return t; }', 127 | output: 'const identity = (t: T): T => t;', 128 | }, 129 | { 130 | code: 'function identity(t: T) { return t; }', 131 | output: 'const identity = (t: T) => t;', 132 | }, 133 | 134 | // Eslint treats export default as a special form of function declaration 135 | { 136 | code: 'export default function() { return 3; }', 137 | output: 'export default () => 3;', 138 | }, 139 | { 140 | code: 'export default function(): number { return 3; }', 141 | output: 'export default (): number => 3;', 142 | }, 143 | { 144 | code: 'export default async function() { return 3; }', 145 | output: 'export default async () => 3;', 146 | }, 147 | 148 | // Sanity check complex logic 149 | { 150 | code: 'function foo(a) { return a && (3 + a()) ? true : 99; }', 151 | output: 'const foo = (a) => a && (3 + a()) ? true : 99;', 152 | }, 153 | { 154 | code: 'async function foo(a) { return a && (3 + a()) ? true : 99; }', 155 | output: 'const foo = async (a) => a && (3 + a()) ? true : 99;', 156 | }, 157 | 158 | // function expressions 159 | { 160 | code: 'var foo = function(bar) { return bar(); }', 161 | output: 'var foo = (bar) => bar()', 162 | }, 163 | { 164 | code: 'var foo = function() { return "World"; }', 165 | output: 'var foo = () => "World"', 166 | }, 167 | { 168 | code: 'var foo = async function() { return "World"; }', 169 | output: 'var foo = async () => "World"', 170 | }, 171 | { 172 | code: 'var foo = function() { return "World"; };', 173 | output: 'var foo = () => "World";', 174 | }, 175 | { 176 | code: 'var foo = async function() { return "World"; };', 177 | output: 'var foo = async () => "World";', 178 | }, 179 | { 180 | code: 'var foo = function x() { return "World"; };', 181 | output: 'var foo = () => "World";', 182 | }, 183 | { 184 | code: 'var foo = async function x() { return "World"; };', 185 | output: 'var foo = async () => "World";', 186 | }, 187 | 188 | // wrap object literal returns in parens 189 | { 190 | code: 'var foo = function() { return {a: false} }', 191 | output: 'var foo = () => ({a: false})', 192 | }, 193 | { 194 | code: 'var foo = async function() { return {a: false} }', 195 | output: 'var foo = async () => ({a: false})', 196 | }, 197 | { 198 | code: 'var foo = function() { return {a: false}; }', 199 | output: 'var foo = () => ({a: false})', 200 | }, 201 | { 202 | code: 'var foo = async function() { return {a: false}; }', 203 | output: 'var foo = async () => ({a: false})', 204 | }, 205 | { 206 | code: 'function foo(a) { return {a: false}; }', 207 | output: 'const foo = (a) => ({a: false});', 208 | }, 209 | { 210 | code: 'async function foo(a) { return {a: false}; }', 211 | output: 'const foo = async (a) => ({a: false});', 212 | }, 213 | { 214 | code: 'function foo(a) { return {a: false} }', 215 | output: 'const foo = (a) => ({a: false});', 216 | }, 217 | { 218 | code: 'async function foo(a) { return {a: false} }', 219 | output: 'const foo = async (a) => ({a: false});', 220 | }, 221 | 222 | // treat inner functions properly 223 | { 224 | code: '["Hello", "World"].reduce(function(a, b) { return a + " " + b; })', 225 | output: '["Hello", "World"].reduce((a, b) => a + " " + b)', 226 | }, 227 | { 228 | code: 'var foo = function () { return () => false }', 229 | output: 'var foo = () => () => false', 230 | }, 231 | { 232 | code: 'var foo = async function () { return async () => false }', 233 | output: 'var foo = async () => async () => false', 234 | }, 235 | 236 | // don't obliterate whitespace and only remove newlines when appropriate 237 | { 238 | code: 'var foo = function() {\n return "World";\n}', 239 | output: 'var foo = () => "World"', 240 | }, 241 | { 242 | code: 'var foo = async function() {\n return "World";\n}', 243 | output: 'var foo = async () => "World"', 244 | }, 245 | { 246 | code: 'var foo = function() {\n return "World"\n}', 247 | output: 'var foo = () => "World"', 248 | }, 249 | { 250 | code: 'var foo = async function() {\n return "World"\n}', 251 | output: 'var foo = async () => "World"', 252 | }, 253 | { 254 | code: 'function foo(a) {\n return 3;\n}', 255 | output: 'const foo = (a) => 3;', 256 | }, 257 | { 258 | code: 'async function foo(a) {\n return 3;\n}', 259 | output: 'const foo = async (a) => 3;', 260 | }, 261 | { 262 | code: 'function foo(a) {\n return 3\n}', 263 | output: 'const foo = (a) => 3;', 264 | }, 265 | { 266 | code: 'async function foo(a) {\n return 3\n}', 267 | output: 'const foo = async (a) => 3;', 268 | }, 269 | 270 | // don't mess up inner generator functions 271 | { 272 | code: 'function foo() { return function * gen() { return yield 1; }; }', 273 | output: 'const foo = () => function * gen() { return yield 1; };', 274 | }, 275 | { 276 | code: 'async function foo() { return function * gen() { return yield 1; }; }', 277 | output: 'const foo = async () => function * gen() { return yield 1; };', 278 | }, 279 | 280 | // don't mess with the semicolon in for statements 281 | { 282 | code: 'function withLoop() { return () => { for (i = 0; i < 5; i++) {}}}', 283 | output: 'const withLoop = () => () => { for (i = 0; i < 5; i++) {}};', 284 | }, 285 | { 286 | code: 'async function withLoop() { return async () => { for (i = 0; i < 5; i++) {}}}', 287 | output: 'const withLoop = async () => async () => { for (i = 0; i < 5; i++) {}};', 288 | }, 289 | { 290 | code: 'var withLoop = function() { return () => { for (i = 0; i < 5; i++) {}}}', 291 | output: 'var withLoop = () => () => { for (i = 0; i < 5; i++) {}}', 292 | }, 293 | { 294 | code: 'var withLoop = async function() { return async () => { for (i = 0; i < 5; i++) {}}}', 295 | output: 'var withLoop = async () => async () => { for (i = 0; i < 5; i++) {}}', 296 | }, 297 | 298 | // function overloading - don't mislabel as overload 299 | // export { x }; has node.declaration === null - regression test for this case 300 | { 301 | code: 'export { foo }; export async function bar() { return false; }', 302 | output: 'export { foo }; export const bar = async () => false;', 303 | }, 304 | ]; 305 | 306 | export const invalidAndHasSingleReturnWithMultipleMatches = [ 307 | { 308 | code: 'var foo = function () { return function(a) { a() } }', 309 | output: 'var foo = () => function(a) { a() }', 310 | }, 311 | { 312 | code: 'var foo = async function () { return async function(a) { a() } }', 313 | output: 'var foo = async () => async function(a) { a() }', 314 | }, 315 | ]; 316 | 317 | export const invalidAndHasBlockStatement = [ 318 | // ES6 classes & functions declared in object literals 319 | { 320 | code: 'class MyClass { render(a, b) { console.log(3); } }', 321 | output: 'class MyClass { render = (a, b) => { console.log(3); }; }', 322 | options: [{ classPropertiesAllowed: true }], 323 | }, 324 | { 325 | code: 'class MyClass { #render(a, b) { console.log(3); } }', 326 | output: 'class MyClass { #render = (a, b) => { console.log(3); }; }', 327 | options: [{ classPropertiesAllowed: true }], 328 | }, 329 | { 330 | code: 'class MyClass { static #render(a, b) { console.log(3); } }', 331 | output: 'class MyClass { static #render = (a, b) => { console.log(3); }; }', 332 | options: [{ classPropertiesAllowed: true }], 333 | }, 334 | { 335 | code: 'class MyClass { static render(a, b) { console.log(3); } }', 336 | output: 'class MyClass { static render = (a, b) => { console.log(3); }; }', 337 | options: [{ classPropertiesAllowed: true }], 338 | }, 339 | { 340 | code: 'class MyClass { static async render(a, b) { console.log(3); } }', 341 | output: 'class MyClass { static render = async (a, b) => { console.log(3); }; }', 342 | options: [{ classPropertiesAllowed: true }], 343 | }, 344 | { 345 | code: 'var MyClass = { render(a, b) { console.log(3); }, b: false }', 346 | output: 'var MyClass = { render: (a, b) => { console.log(3); }, b: false }', 347 | }, 348 | 349 | // named function declarations 350 | { 351 | code: 'function foo() { console.log(3); }', 352 | output: 'const foo = () => { console.log(3); };', 353 | }, 354 | { 355 | code: 'async function foo() { console.log(3); }', 356 | output: 'const foo = async () => { console.log(3); };', 357 | }, 358 | { 359 | code: 'function foo(a) { console.log(3); }', 360 | output: 'const foo = (a) => { console.log(3); };', 361 | }, 362 | { 363 | code: 'async function foo(a) { console.log(3); }', 364 | output: 'const foo = async (a) => { console.log(3); };', 365 | }, 366 | 367 | // https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions/issues/28 368 | { 369 | code: `export function foo() { const bar = '$'; return bar; }`, 370 | output: `export const foo = () => { const bar = '$'; return bar; };`, 371 | }, 372 | 373 | // Eslint treats export default as a special form of function declaration 374 | { 375 | code: 'export default function() { console.log(3); }', 376 | output: 'export default () => { console.log(3); };', 377 | }, 378 | { 379 | code: 'export default async function() { console.log(3); }', 380 | output: 'export default async () => { console.log(3); };', 381 | }, 382 | 383 | // Sanity check complex logic 384 | { 385 | code: 'function foo(a) { console.log(a && (3 + a()) ? true : 99); }', 386 | output: 'const foo = (a) => { console.log(a && (3 + a()) ? true : 99); };', 387 | }, 388 | { 389 | code: 'function foo(a): boolean | number { console.log(a && (3 + a()) ? true : 99); }', 390 | output: 'const foo = (a): boolean | number => { console.log(a && (3 + a()) ? true : 99); };', 391 | }, 392 | { 393 | code: 'async function foo(a) { console.log(a && (3 + a()) ? true : 99); }', 394 | output: 'const foo = async (a) => { console.log(a && (3 + a()) ? true : 99); };', 395 | }, 396 | 397 | // function expressions 398 | { 399 | code: 'var foo = function() { console.log("World"); }', 400 | output: 'var foo = () => { console.log("World"); }', 401 | }, 402 | { 403 | code: 'var foo = function(): void { console.log("World"); }', 404 | output: 'var foo = (): void => { console.log("World"); }', 405 | }, 406 | { 407 | code: 'var foo = async function() { console.log("World"); }', 408 | output: 'var foo = async () => { console.log("World"); }', 409 | }, 410 | { 411 | code: 'var foo = function() { console.log("World"); };', 412 | output: 'var foo = () => { console.log("World"); };', 413 | }, 414 | { 415 | code: 'var foo = async function() { console.log("World"); };', 416 | output: 'var foo = async () => { console.log("World"); };', 417 | }, 418 | { 419 | code: 'var foo = function x() { console.log("World"); };', 420 | output: 'var foo = () => { console.log("World"); };', 421 | }, 422 | { 423 | code: 'var foo = async function x() { console.log("World"); };', 424 | output: 'var foo = async () => { console.log("World"); };', 425 | }, 426 | 427 | // wrap object literal console.log(in parens 428 | { 429 | code: 'var foo = function() { console.log({a: false}); }', 430 | output: 'var foo = () => { console.log({a: false}); }', 431 | }, 432 | { 433 | code: 'var foo = function(): void { console.log({a: false}); }', 434 | output: 'var foo = (): void => { console.log({a: false}); }', 435 | }, 436 | { 437 | code: 'var foo = async function() { console.log({a: false}); }', 438 | output: 'var foo = async () => { console.log({a: false}); }', 439 | }, 440 | { 441 | code: 'function foo(a) { console.log({a: false}); }', 442 | output: 'const foo = (a) => { console.log({a: false}); };', 443 | }, 444 | { 445 | code: 'async function foo(a) { console.log({a: false}); }', 446 | output: 'const foo = async (a) => { console.log({a: false}); };', 447 | }, 448 | 449 | // don't obliterate whitespace and only remove newlines when appropriate 450 | { 451 | code: 'var foo = function() {\n console.log("World");\n}', 452 | output: 'var foo = () => {\n console.log("World");\n}', 453 | }, 454 | { 455 | code: 'var foo = function(): void {\n console.log("World");\n}', 456 | output: 'var foo = (): void => {\n console.log("World");\n}', 457 | }, 458 | { 459 | code: 'var foo = async function() {\n console.log("World");\n}', 460 | output: 'var foo = async () => {\n console.log("World");\n}', 461 | }, 462 | { 463 | code: 'function foo(a) {\n console.log(3);\n}', 464 | output: 'const foo = (a) => {\n console.log(3);\n};', 465 | }, 466 | { 467 | code: 'async function foo(a) {\n console.log(3);\n}', 468 | output: 'const foo = async (a) => {\n console.log(3);\n};', 469 | }, 470 | 471 | // don't mess with the semicolon in for statements 472 | { 473 | code: 'function withLoop() { console.log(() => { for (i = 0; i < 5; i++) {}}) }', 474 | output: 'const withLoop = () => { console.log(() => { for (i = 0; i < 5; i++) {}}) };', 475 | }, 476 | { 477 | code: 'function withLoop(): void { console.log(() => { for (i = 0; i < 5; i++) {}}) }', 478 | output: 'const withLoop = (): void => { console.log(() => { for (i = 0; i < 5; i++) {}}) };', 479 | }, 480 | { 481 | code: 'async function withLoop() { console.log(async () => { for (i = 0; i < 5; i++) {}}) }', 482 | output: 'const withLoop = async () => { console.log(async () => { for (i = 0; i < 5; i++) {}}) };', 483 | }, 484 | { 485 | code: 'var withLoop = function() { console.log(() => { for (i = 0; i < 5; i++) {}}) }', 486 | output: 'var withLoop = () => { console.log(() => { for (i = 0; i < 5; i++) {}}) }', 487 | }, 488 | { 489 | code: 'var withLoop = async function() { console.log(async () => { for (i = 0; i < 5; i++) {}}) }', 490 | output: 'var withLoop = async () => { console.log(async () => { for (i = 0; i < 5; i++) {}}) }', 491 | }, 492 | ]; 493 | 494 | export const invalidAndHasBlockStatementWithMultipleMatches = [ 495 | // treat inner functions properly 496 | { 497 | code: '["Hello", "World"].forEach(function(a, b) { console.log(a + " " + b); })', 498 | output: '["Hello", "World"].forEach((a, b) => { console.log(a + " " + b); })', 499 | }, 500 | { 501 | code: '["Hello", "World"].forEach(function(a: number, b: number): void { console.log(a + " " + b); })', 502 | output: '["Hello", "World"].forEach((a: number, b: number): void => { console.log(a + " " + b); })', 503 | }, 504 | { 505 | code: 'var foo = function () { console.log(() => false); }', 506 | output: 'var foo = () => { console.log(() => false); }', 507 | }, 508 | { 509 | code: 'var foo = async function () { console.log(async () => false) }', 510 | output: 'var foo = async () => { console.log(async () => false) }', 511 | }, 512 | 513 | // don't mess up inner generator functions 514 | { 515 | code: 'function foo() { console.log(function * gen() { yield 1; }); }', 516 | output: 'const foo = () => { console.log(function * gen() { yield 1; }); };', 517 | }, 518 | { 519 | code: 'function foo() { console.log(function * gen(): number { yield 1; }); }', 520 | output: 'const foo = () => { console.log(function * gen(): number { yield 1; }); };', 521 | }, 522 | { 523 | code: 'async function foo() { console.log(function * gen() { yield 1; }); }', 524 | output: 'const foo = async () => { console.log(function * gen() { yield 1; }); };', 525 | }, 526 | ]; 527 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "module": "NodeNext", 6 | "outDir": "dist", 7 | "resolveJsonModule": true, 8 | "rootDir": "src", 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "ES2022" 13 | }, 14 | "include": [ 15 | "src" 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['test/**/*.spec.ts'], 6 | hideSkippedTests: true, 7 | }, 8 | }); 9 | --------------------------------------------------------------------------------