├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md ├── actions │ └── prepare │ │ └── action.yml ├── codecov.yml ├── labels.yml ├── renovate.json └── workflows │ ├── accessibility-alt-text-bot.yml │ ├── build.yml │ ├── done-label.yml │ ├── lint-docs.yml │ ├── lint-js.yml │ ├── lint-knip.yml │ ├── lint-markdown.yml │ ├── lint-packages.yml │ ├── lint-spelling.yml │ ├── lint-yaml.yml │ ├── release.yml │ ├── semantic-pr.yml │ ├── stale.yml │ ├── sync-labels.yml │ ├── test-js.yml │ └── typecheck.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .lintstagedrc.yml ├── .markdownlint-cli2.jsonc ├── .npmrc ├── .nvmrc ├── .releaserc.yml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DONATIONS.md ├── GETTING_STARTED.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cspell.config.yml ├── docs ├── assets │ ├── eslint-functional-logo.pdn │ ├── eslint-functional-logo.png │ └── eslint-logo.svg ├── rules │ ├── functional-parameters.md │ ├── immutable-data.md │ ├── no-class-inheritance.md │ ├── no-classes.md │ ├── no-conditional-statements.md │ ├── no-expression-statements.md │ ├── no-let.md │ ├── no-loop-statements.md │ ├── no-mixed-types.md │ ├── no-promise-reject.md │ ├── no-return-void.md │ ├── no-this-expressions.md │ ├── no-throw-statements.md │ ├── no-try-statements.md │ ├── prefer-immutable-types.md │ ├── prefer-property-signatures.md │ ├── prefer-readonly-type.md │ ├── prefer-tacit.md │ ├── readonly-type.md │ ├── settings │ │ └── immutability.md │ └── type-declaration-immutability.md └── user-guide │ └── migrating-from-tslint.md ├── eslint-doc-generator.config.ts ├── eslint.config.js ├── knip.jsonc ├── package.json ├── pnpm-lock.yaml ├── project-dictionary.txt ├── rollup.config.ts ├── src ├── configs │ ├── all.ts │ ├── currying.ts │ ├── disable-type-checked.ts │ ├── external-typescript-recommended.ts │ ├── external-vanilla-recommended.ts │ ├── lite.ts │ ├── no-exceptions.ts │ ├── no-mutations.ts │ ├── no-other-paradigms.ts │ ├── no-statements.ts │ ├── off.ts │ ├── recommended.ts │ ├── strict.ts │ └── stylistic.ts ├── index.ts ├── options │ ├── ignore.ts │ ├── index.ts │ └── overrides.ts ├── rules │ ├── functional-parameters.ts │ ├── immutable-data.ts │ ├── index.ts │ ├── no-class-inheritance.ts │ ├── no-classes.ts │ ├── no-conditional-statements.ts │ ├── no-expression-statements.ts │ ├── no-let.ts │ ├── no-loop-statements.ts │ ├── no-mixed-types.ts │ ├── no-promise-reject.ts │ ├── no-return-void.ts │ ├── no-this-expressions.ts │ ├── no-throw-statements.ts │ ├── no-try-statements.ts │ ├── prefer-immutable-types.ts │ ├── prefer-property-signatures.ts │ ├── prefer-readonly-type.ts │ ├── prefer-tacit.ts │ ├── readonly-type.ts │ └── type-declaration-immutability.ts ├── settings │ ├── immutability.ts │ └── index.ts ├── tsconfig.build.json ├── tsconfig.json └── utils │ ├── conditional-imports │ ├── ts-api-utils.ts │ └── typescript.ts │ ├── constants.ts │ ├── misc.ts │ ├── node-types.ts │ ├── rule.ts │ ├── schemas.ts │ ├── tree.ts │ ├── type-guards.ts │ └── type-specifier.ts ├── tests ├── configs.test.ts ├── fixture │ ├── file.ts │ └── tsconfig.json ├── index.test.ts ├── rules │ ├── __snapshots__ │ │ ├── functional-parameters.test.ts.snap │ │ ├── no-class-inheritance.test.ts.snap │ │ ├── no-classes.test.ts.snap │ │ ├── no-conditional-statements.test.ts.snap │ │ ├── no-expression-statements.test.ts.snap │ │ ├── no-let.test.ts.snap │ │ ├── no-loop-statements.test.ts.snap │ │ ├── no-mixed-types.test.ts.snap │ │ ├── no-promise-reject.test.ts.snap │ │ ├── no-return-void.test.ts.snap │ │ ├── no-this-expressions.test.ts.snap │ │ ├── no-throw-statements.test.ts.snap │ │ ├── no-try-statements.test.ts.snap │ │ ├── prefer-property-signatures.test.ts.snap │ │ ├── prefer-readonly-type.test.ts.snap │ │ ├── prefer-tacit.test.ts.snap │ │ └── type-declaration-immutability.test.ts.snap │ ├── functional-parameters.test.ts │ ├── immutable-data │ │ ├── __snapshots__ │ │ │ ├── array.test.ts.snap │ │ │ ├── map.test.ts.snap │ │ │ ├── object.test.ts.snap │ │ │ └── set.test.ts.snap │ │ ├── array.test.ts │ │ ├── map.test.ts │ │ ├── object.test.ts │ │ └── set.test.ts │ ├── index.test.ts │ ├── no-class-inheritance.test.ts │ ├── no-classes.test.ts │ ├── no-conditional-statements.test.ts │ ├── no-expression-statements.test.ts │ ├── no-let.test.ts │ ├── no-loop-statements.test.ts │ ├── no-mixed-types.test.ts │ ├── no-promise-reject.test.ts │ ├── no-return-void.test.ts │ ├── no-this-expressions.test.ts │ ├── no-throw-statements.test.ts │ ├── no-try-statements.test.ts │ ├── prefer-immutable-types │ │ ├── __snapshots__ │ │ │ ├── parameters.test.ts.snap │ │ │ ├── return-types.test.ts.snap │ │ │ └── variables.test.ts.snap │ │ ├── parameters.test.ts │ │ ├── return-types.test.ts │ │ └── variables.test.ts │ ├── prefer-property-signatures.test.ts │ ├── prefer-readonly-type.test.ts │ ├── prefer-tacit.test.ts │ └── type-declaration-immutability.test.ts └── utils │ └── configs.ts ├── tsconfig.json ├── typings ├── es.d.ts └── node.d.ts └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = LF 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.js linguist-detectable=false 4 | *.mjs linguist-detectable=false 5 | *.cjs linguist-detectable=false 6 | /.husky/** linguist-detectable=false 7 | 8 | **/tsconfig.json linguist-language=jsonc 9 | **/tsconfig.*.json linguist-language=jsonc 10 | /.vscode/*.json linguist-language=jsonc 11 | 12 | /CHANGELOG.md linguist-generated 13 | 14 | /docs/** linguist-documentation 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | tidelift: npm/eslint-plugin-functional 2 | ko_fi: rebeccastevens 3 | issuehunt: eslint-functional/eslint-plugin-functional 4 | custom: https://github.com/eslint-functional/eslint-plugin-functional/blob/main/DONATIONS.md 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Report a bug to help us fix it 4 | title: '' 5 | labels: 'Type: Bug, Status: Triage' 6 | assignees: '' 7 | --- 8 | 9 | ## Bug Report 10 | 11 | 12 | 13 | ### Expected behavior 14 | 15 | 16 | 17 | ### Actual behavior 18 | 19 | 20 | 21 | ### Steps to reproduce 22 | 23 | 24 | 25 | ### Proposed changes 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: Suggest an idea 4 | title: '' 5 | labels: 'Type: Idea, Status: Triage' 6 | assignees: '' 7 | --- 8 | 9 | ## Suggestion 10 | -------------------------------------------------------------------------------- /.github/actions/prepare/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare 2 | description: Prepares the repo for a typical CI job 3 | 4 | inputs: 5 | node-version: 6 | required: false 7 | description: "`node-version` passed to `actions/setup-node`." 8 | default: "latest" 9 | 10 | runs: 11 | using: "composite" 12 | steps: 13 | - name: Configure Git 14 | run: | 15 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 16 | git config --global user.name ${GITHUB_ACTOR} 17 | shell: bash 18 | - uses: pnpm/action-setup@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ inputs.node-version }} 22 | cache: "pnpm" 23 | - run: pnpm install --frozen-lockfile --ignore-scripts 24 | shell: bash 25 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":ignoreModulesAndTests", 5 | ":prConcurrentLimitNone", 6 | ":prHourlyLimitNone", 7 | ":semanticCommits", 8 | "group:allNonMajor", 9 | "group:monorepos", 10 | "group:recommended", 11 | "helpers:disableTypesNodeMajor", 12 | "replacements:all", 13 | "schedule:monthly", 14 | "workarounds:all" 15 | ], 16 | "automerge": true, 17 | "automergeStrategy": "fast-forward", 18 | "automergeType": "branch", 19 | "labels": ["Type: Maintenance", ":blue_heart:"], 20 | "rebaseWhen": "conflicted", 21 | "postUpdateOptions": ["pnpmDedupe"], 22 | "packageRules": [ 23 | { 24 | "major": { 25 | "automerge": false, 26 | "semanticCommitType": "build", 27 | "semanticCommitScope": "deps-major", 28 | "rebaseWhen": "behind-base-branch" 29 | }, 30 | "matchDepTypes": ["dependencies"], 31 | "matchManagers": ["npm"], 32 | "rangeStrategy": "update-lockfile", 33 | "semanticCommitScope": "deps", 34 | "semanticCommitType": "chore" 35 | }, 36 | { 37 | "matchDepTypes": ["devDependencies"], 38 | "matchManagers": ["npm"], 39 | "rangeStrategy": "pin", 40 | "semanticCommitScope": "dev-deps", 41 | "semanticCommitType": "chore" 42 | }, 43 | { 44 | "major": { 45 | "semanticCommitType": "build" 46 | }, 47 | "matchDepTypes": ["peerDependencies"], 48 | "matchManagers": ["npm"], 49 | "rangeStrategy": "widen", 50 | "semanticCommitScope": "peer-deps", 51 | "semanticCommitType": "chore" 52 | }, 53 | { 54 | "matchManagers": ["github-actions"], 55 | "rangeStrategy": "replace", 56 | "semanticCommitScope": "dev-deps", 57 | "semanticCommitType": "ci" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/accessibility-alt-text-bot.yml: -------------------------------------------------------------------------------- 1 | name: Accessibility Alt Text Bot 2 | 3 | on: 4 | issue_comment: 5 | types: 6 | - created 7 | - edited 8 | issues: 9 | types: 10 | - edited 11 | - opened 12 | pull_request: 13 | types: 14 | - edited 15 | - opened 16 | 17 | permissions: 18 | issues: write 19 | pull-requests: write 20 | 21 | jobs: 22 | accessibility_alt_text_bot: 23 | if: ${{ !endsWith(github.actor, '[bot]') }} 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: github/accessibility-alt-text-bot@v1.7.1 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run build 15 | -------------------------------------------------------------------------------- /.github/workflows/done-label.yml: -------------------------------------------------------------------------------- 1 | name: Apply Done Label 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | run: 10 | name: "Update Labels" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: RebeccaStevens/issue-closed-labeler-action@v1 14 | with: 15 | rules: '[{"condition":"Type: Bug","add":"Resolution: Fixed","remove":["Status: Triage","Status: Investigation Needed","Status: On Hold","Status: In Progress","Status: Stale","Status: Awaiting Response","Status: Awaiting Feedback","Status: Blocked"]},{"condition":["some",["Type: Feature","Type: Enhancement"]],"add":"Resolution: Added","remove":["Status: Triage","Status: Investigation Needed","Status: On Hold","Status: In Progress","Status: Stale","Status: Awaiting Response","Status: Awaiting Feedback","Status: Blocked"]}]' 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-docs.yml: -------------------------------------------------------------------------------- 1 | name: Lint Code 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | lint_docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run build:node 15 | - run: pnpm run lint:eslint-docs 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-js.yml: -------------------------------------------------------------------------------- 1 | name: Lint Code 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | lint_js: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run build:node 15 | - run: pnpm run lint:js 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-knip.yml: -------------------------------------------------------------------------------- 1 | name: Lint Knip 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | lint_knip: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run lint:knip 15 | -------------------------------------------------------------------------------- /.github/workflows/lint-markdown.yml: -------------------------------------------------------------------------------- 1 | name: Lint Markdown 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | lint_markdown: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run build:node 15 | - run: pnpm run lint:md-full 16 | -------------------------------------------------------------------------------- /.github/workflows/lint-packages.yml: -------------------------------------------------------------------------------- 1 | name: Lint Packages 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | lint_packages: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run lint:packages 15 | -------------------------------------------------------------------------------- /.github/workflows/lint-spelling.yml: -------------------------------------------------------------------------------- 1 | name: Lint Spelling 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | lint_spelling: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run lint:spelling 15 | -------------------------------------------------------------------------------- /.github/workflows/lint-yaml.yml: -------------------------------------------------------------------------------- 1 | name: Lint Yaml 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | lint_yaml: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run build:node 15 | - run: pnpm run lint:yaml 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | 9 | permissions: 10 | contents: write 11 | issues: write 12 | pull-requests: write 13 | 14 | concurrency: 15 | group: Release 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | lint_js: 20 | uses: ./.github/workflows/lint-js.yml 21 | lint_docs: 22 | uses: ./.github/workflows/lint-docs.yml 23 | lint_knip: 24 | uses: ./.github/workflows/lint-knip.yml 25 | lint_markdown: 26 | uses: ./.github/workflows/lint-markdown.yml 27 | lint_packages: 28 | uses: ./.github/workflows/lint-packages.yml 29 | lint_spelling: 30 | uses: ./.github/workflows/lint-spelling.yml 31 | lint_yaml: 32 | uses: ./.github/workflows/lint-yaml.yml 33 | test_js: 34 | uses: ./.github/workflows/test-js.yml 35 | typecheck: 36 | uses: ./.github/workflows/typecheck.yml 37 | 38 | release: 39 | needs: 40 | - lint_js 41 | - lint_docs 42 | - lint_knip 43 | - lint_markdown 44 | - lint_packages 45 | - lint_spelling 46 | - lint_yaml 47 | - test_js 48 | - typecheck 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | with: 53 | fetch-depth: 0 54 | persist-credentials: false 55 | - uses: ./.github/actions/prepare 56 | with: 57 | node-version: v20 58 | 59 | - name: Build 60 | run: pnpm run build 61 | 62 | - name: Release 63 | run: pnpm run release 64 | env: 65 | GITHUB_TOKEN: ${{ github.token }} 66 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 67 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Semantic PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5.5.3 16 | env: 17 | GITHUB_TOKEN: ${{ github.token }} 18 | with: 19 | types: | 20 | feat 21 | fix 22 | perf 23 | refactor 24 | style 25 | docs 26 | test 27 | build 28 | ci 29 | chore 30 | part 31 | scopes: | 32 | functional-parameters 33 | immutable-data 34 | no-classes 35 | no-class-inheritance 36 | no-conditional-statements 37 | no-expression-statements 38 | no-let 39 | no-loop-statements 40 | no-mixed-types 41 | no-promise-reject 42 | no-return-void 43 | no-this-expressions 44 | no-throw-statements 45 | no-try-statements 46 | prefer-immutable-types 47 | prefer-property-signatures 48 | prefer-readonly-types 49 | prefer-tacit 50 | readonly-type 51 | type-declaration-immutability 52 | deps 53 | dev-deps 54 | peer-deps 55 | release-patch 56 | release-minor 57 | release-major 58 | requireScope: false 59 | subjectPattern: ^(?![A-Z]).+$ # Don't start with an uppercase character. 60 | subjectPatternError: | 61 | The subject "{subject}" should not start with an uppercase character. 62 | headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?!?: (.*)$' 63 | headerPatternCorrespondence: type, scope, subject 64 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues and PRs" 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/stale@v9 13 | with: 14 | days-before-issue-stale: 60 15 | days-before-issue-close: 7 16 | days-before-pr-stale: 60 17 | days-before-pr-close: -1 18 | 19 | remove-stale-when-updated: true 20 | 21 | any-of-labels: "Status: Awaiting Response,Resolution: Not Reproducible" 22 | stale-issue-label: "Status: Stale" 23 | stale-pr-label: "Status: Stale" 24 | 25 | stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days." 26 | close-issue-message: "This issue was closed because it has been stalled for 7 days with no activity." 27 | stale-pr-message: "This PR is stale because it has been open 60 days with no activity." 28 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync labels 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - .github/labels.yml 9 | workflow_dispatch: 10 | 11 | permissions: 12 | pull-requests: write 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: micnncim/action-label-syncer@v1 21 | with: 22 | manifest: .github/labels.yml 23 | env: 24 | GITHUB_TOKEN: ${{ github.token }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test-js.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | test_js: 10 | name: "Test - Node: ${{ matrix.node_version }} - TS: ${{ matrix.ts_version }} - OS: ${{ matrix.os }}" 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: 15 | - "ubuntu-latest" 16 | node_version: 17 | - "18.18" 18 | - "20" 19 | - "latest" 20 | ts_version: 21 | # - "next" 22 | - "latest" 23 | # - "4.7.4" 24 | # - "JS" 25 | runs-on: ${{ matrix.os }} 26 | continue-on-error: ${{ matrix.ts_version == 'next' }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ./.github/actions/prepare 30 | 31 | - name: Setup NodeJs ${{ matrix.node_version }} for testing 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node_version }} 35 | 36 | # - name: Add TypeScript "${{ matrix.ts_version }}" for testing 37 | # run: pnpm add -D typescript@"${{ matrix.ts_version }}" 38 | 39 | - name: Run Tests 40 | run: pnpm test:js-run 41 | 42 | - name: Report coverage 43 | uses: codecov/codecov-action@v5.4.2 44 | with: 45 | file: coverage/lcov.info 46 | flags: ${{ matrix.ts_version }} 47 | -------------------------------------------------------------------------------- /.github/workflows/typecheck.yml: -------------------------------------------------------------------------------- 1 | name: Type Check 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | workflow_call: 7 | 8 | jobs: 9 | typecheck: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: ./.github/actions/prepare 14 | - run: pnpm run typecheck 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /coverage/ 4 | /lib/ 5 | 6 | *.log 7 | 8 | .DS_Store 9 | thumbs.db 10 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm exec lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "*.{json,yml}": 2 | - eslint --fix 3 | - cspell lint --no-progress --show-suggestions --show-context --no-must-find-files --dot 4 | 5 | "*.?([cm])[jt]s?(x)": 6 | - eslint --fix 7 | - cspell lint --no-progress --show-suggestions --show-context --no-must-find-files --dot 8 | 9 | "src/**/*.?([cm])[jt]s?(x)": 10 | - tsc-files -p src/tsconfig.json --noEmit 11 | 12 | "./*.?([cm])[jt]s?(x)": 13 | - tsc-files -p tsconfig.json --noEmit 14 | 15 | "*.md": 16 | - markdownlint-cli2 --fix 17 | - eslint --fix 18 | - cspell lint --no-progress --show-suggestions --show-context --no-must-find-files --dot 19 | 20 | pnpm-lock.yaml: 21 | - "pnpm dedupe && :" 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-prefix="" 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | shell-emulator=true 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.15.0 2 | -------------------------------------------------------------------------------- /.releaserc.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | - main 3 | - name: next 4 | channel: next 5 | prerelease: next 6 | plugins: 7 | - - "@semantic-release/commit-analyzer" 8 | - preset: angular 9 | parserOpts: 10 | headerPattern: '^(\w*)(?:\((.*)\))?!?: (.*)$' 11 | breakingHeaderPattern: '^(\w*)(?:\((.*)\))?!: (.*)$' 12 | releaseRules: 13 | - breaking: true 14 | release: major 15 | - revert: true 16 | release: patch 17 | - type: feat 18 | release: minor 19 | - type: fix 20 | release: patch 21 | - type: perf 22 | release: patch 23 | - type: build 24 | scope: deps-major 25 | breaking: true 26 | release: major 27 | - type: build 28 | scope: deps 29 | release: patch 30 | - type: build 31 | scope: peer-deps 32 | release: patch 33 | - type: build 34 | scope: release-patch 35 | release: patch 36 | - type: build 37 | scope: release-minor 38 | release: minor 39 | - type: build 40 | scope: release-major 41 | release: major 42 | - - "@semantic-release/release-notes-generator" 43 | - preset: angular 44 | parserOpts: 45 | headerPattern: '^(\w*)(?:\((.*)\))?!?: (.*)$' 46 | breakingHeaderPattern: '^(\w*)(?:\((.*)\))?!: (.*)$' 47 | - - "@semantic-release/changelog" 48 | - changelogTitle: "# Changelog 49 | 50 | All notable changes to this project will be documented in this file. Dates are displayed in UTC." 51 | - - "semantic-release-replace-plugin" 52 | - replacements: 53 | - files: 54 | - "./lib/index.js" 55 | from: "0\\.0\\.0\\-development" 56 | to: "${nextRelease.version}" 57 | results: 58 | - file: "./lib/index.js" 59 | hasChanged: true 60 | numMatches: 1 61 | numReplacements: 1 62 | countMatches: true 63 | - - "@semantic-release/git" 64 | - assets: 65 | - CHANGELOG.md 66 | - "@semantic-release/npm" 67 | - - "@semantic-release/github" 68 | - releasedLabels: 69 | - "<%= nextRelease.channel === 'next' ? 'Status: Beta Released' : 'Status: Released' %>" 70 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "DavidAnson.vscode-markdownlint", 4 | "dbaeumer.vscode-eslint", 5 | "editorconfig.editorconfig", 6 | "esbenp.prettier-vscode", 7 | "streetsidesoftware.code-spell-checker" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Current Test File", 9 | "type": "node", 10 | "request": "launch", 11 | 12 | // Run tester. 13 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 14 | "args": ["run", "${relativeFile}"], 15 | "autoAttachChildProcesses": true, 16 | 17 | "smartStep": true, 18 | "sourceMaps": true, 19 | 20 | /* 21 | * Open terminal when debugging starts. 22 | * Useful to see console.logs 23 | */ 24 | "console": "integratedTerminal", 25 | "internalConsoleOptions": "neverOpen", 26 | 27 | // Files to exclude from debugger. 28 | "skipFiles": [ 29 | // Node.js internal core modules. 30 | "/**", 31 | 32 | // Ignore all dependencies. 33 | "${workspaceFolder}/node_modules/**" 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll": "explicit" 7 | }, 8 | "files.trimTrailingWhitespace": true, 9 | 10 | // Silent the stylistic rules in you IDE, but still auto fix them 11 | "eslint.rules.customizations": [ 12 | { "rule": "style/*", "severity": "off" }, 13 | { "rule": "*-indent", "severity": "off" }, 14 | { "rule": "*-spacing", "severity": "off" }, 15 | { "rule": "*-spaces", "severity": "off" }, 16 | { "rule": "*-order", "severity": "off" }, 17 | { "rule": "*-dangle", "severity": "off" }, 18 | { "rule": "*-newline", "severity": "off" }, 19 | { "rule": "*quotes", "severity": "off" }, 20 | { "rule": "*semi", "severity": "off" } 21 | ], 22 | 23 | // Enable eslint for all supported languages 24 | "eslint.validate": [ 25 | "javascript", 26 | "javascriptreact", 27 | "typescript", 28 | "typescriptreact", 29 | "vue", 30 | "html", 31 | "markdown", 32 | "json", 33 | "jsonc", 34 | "yaml" 35 | ], 36 | 37 | "editor.rulers": [120], 38 | "typescript.tsdk": "node_modules/typescript/lib", 39 | "json.schemas": [ 40 | { 41 | "fileMatch": ["**/*.jsonc", "**/tsconfig.json", "**/tsconfig.*.json", ".vscode/*.json"], 42 | "schema": { 43 | "allowTrailingCommas": true 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How to 4 | 5 | For new features file an issue. For bugs, file an issue and optionally file a PR with a failing test. 6 | 7 | ## How to develop 8 | 9 | To execute the tests run `pnpm test`. 10 | 11 | To learn about ESLint plugin development see the 12 | [relevant section](https://eslint.org/docs/developer-guide/working-with-plugins) of the ESLint docs. 13 | You can also checkout the [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) repo which has 14 | some more information specific to TypeScript. 15 | 16 | In order to know which AST nodes are created for a snippet of TypeScript code you can use 17 | [AST explorer](https://astexplorer.net/) with options JavaScript and @typescript-eslint/parser. 18 | 19 | ### How to publish 20 | 21 | Publishing is handled by [semantic release](https://github.com/semantic-release/semantic-release#readme) - 22 | there shouldn't be any need to publish manually. 23 | -------------------------------------------------------------------------------- /DONATIONS.md: -------------------------------------------------------------------------------- 1 | # Donations 2 | 3 | Any donations would be much appreciated. 😄 4 | 5 | ## Enterprise Users 6 | 7 | `eslint-plugin-functional` is available as part of the 8 | [Tidelift Subscription](https://tidelift.com/subscription/pkg/npm-eslint-plugin-functional). 9 | 10 | ## Real money 11 | 12 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/rebeccastevens) 13 | 14 | ## Cryptocurrencies 15 | 16 |
17 | 18 | Bitcoin Logo 19 | Bitcoin 20 | 21 | 22 | ![bitcoin address QR code](https://raw.githubusercontent.com/RebeccaStevens/RebeccaStevens/main/assets/cryptocurrencies/wallets/bitcoin.png)\ 23 | bc1qgr2xwvkpztsaq9kplud84r3dfz4g3e7d5c5lxm 24 | 25 |
26 | 27 |
28 | 29 | Ethereum Logo 30 | Ethereum 31 | 32 | 33 | ![ethereum address QR code](https://raw.githubusercontent.com/RebeccaStevens/RebeccaStevens/main/assets/cryptocurrencies/wallets/ethereum.png)\ 34 | 0x643769d1DD2Cb912656dAA27C1b97e5A81EF9fd2 35 | 36 |
37 | 38 |
39 | 40 | Litecoin Logo 41 | Litecoin 42 | 43 | 44 | ![litecoin address QR code](https://raw.githubusercontent.com/RebeccaStevens/RebeccaStevens/main/assets/cryptocurrencies/wallets/litecoin.png)\ 45 | ltc1qxr7p6z4hrh87g9mjjk67chyduwrh2nfrpxksjv 46 | 47 |
48 | -------------------------------------------------------------------------------- /GETTING_STARTED.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | ### JavaScript 6 | 7 | ```sh 8 | # Install with npm 9 | npm install -D eslint eslint-plugin-functional 10 | 11 | # Install with yarn 12 | yarn add -D eslint eslint-plugin-functional 13 | 14 | # Install with pnpm 15 | pnpm add -D eslint eslint-plugin-functional 16 | ``` 17 | 18 | ### TypeScript 19 | 20 | ```sh 21 | # Install with npm 22 | npm install -D eslint typescript-eslint eslint-plugin-functional 23 | 24 | # Install with yarn 25 | yarn add -D eslint typescript-eslint eslint-plugin-functional 26 | 27 | # Install with pnpm 28 | pnpm add -D eslint typescript-eslint eslint-plugin-functional 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### With TypeScript 34 | 35 | In your `eslint.config.js` file, import `typescript-eslint` and `eslint-plugin-functional` and configure them as you wish. 36 | 37 | ```js 38 | // eslint.config.js 39 | import functional from "eslint-plugin-functional"; 40 | import tseslint from "typescript-eslint"; 41 | 42 | export default tseslint.config({ 43 | files: ["**/*.ts"], 44 | extends: [ 45 | functional.configs.externalTypeScriptRecommended, 46 | functional.configs.recommended, 47 | functional.configs.stylistic, 48 | // your other plugin configs here 49 | ], 50 | languageOptions: { 51 | parser: tseslint.parser, 52 | parserOptions: { 53 | projectService: true, 54 | }, 55 | }, 56 | rules: { 57 | // any rule configs here 58 | }, 59 | }); 60 | ``` 61 | 62 | ### Without TypeScript 63 | 64 | In your `eslint.config.js` file, import `eslint-plugin-functional` and configure it as you wish. 65 | 66 | If you're not using TypeScript, be sure to include the `disableTypeChecked` config after the other configs to 67 | disable rules that require TypeScript. 68 | 69 | ```js 70 | // eslint.config.js 71 | import functional from "eslint-plugin-functional"; 72 | 73 | export default [ 74 | functional.configs.externalVanillaRecommended, 75 | functional.configs.recommended, 76 | functional.configs.stylistic, 77 | functional.configs.disableTypeChecked, 78 | // your other plugin configs here 79 | { 80 | rules: { 81 | // any rule configs here 82 | }, 83 | }, 84 | ]; 85 | ``` 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jonas Kello 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Security contact information 4 | 5 | To report a security vulnerability, please use the 6 | [Tidelift security contact](https://tidelift.com/security). 7 | Tidelift will coordinate the fix and disclosure. 8 | -------------------------------------------------------------------------------- /cspell.config.yml: -------------------------------------------------------------------------------- 1 | $schema: https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json 2 | version: "0.2" 3 | 4 | language: en-US 5 | 6 | dictionaryDefinitions: 7 | - name: project-dictionary 8 | path: ./project-dictionary.txt 9 | addWords: true 10 | 11 | allowCompoundWords: true 12 | caseSensitive: false 13 | useGitignore: true 14 | 15 | dictionaries: 16 | - en_US 17 | - bash 18 | - filetypes 19 | - fonts 20 | - html 21 | - misc 22 | - node 23 | - npm 24 | - softwareTerms 25 | - typescript 26 | - project-dictionary 27 | 28 | import: 29 | - "@cspell/dict-cryptocurrencies/cspell-ext.json" 30 | 31 | ignorePaths: 32 | - .git 33 | - .gitattributes 34 | - .gitignore 35 | - .husky 36 | - .lintstagedrc.yml 37 | - .markdownlint.json 38 | - .npmrc 39 | - .prettierignore 40 | - .prettierrc.yml 41 | - .vscode 42 | - "*.pdn" 43 | - CHANGELOG.md 44 | - coverage 45 | - cspell.config.yml 46 | - lib 47 | - node_modules 48 | - package.json 49 | - patches 50 | - pnpm-lock.yaml 51 | - project-dictionary.txt 52 | 53 | ignoreRegExpList: 54 | - /\b[a-f0-9]{6}\b/ui # ignore hex color codes 55 | - /\b[a-z0-9]{32,}\b/ui # ignore long string of hex characters 56 | - /`[^`]*`/u # ignore things in `...` 57 | - /```[\w\W]*?```/u # ignore things in ```...``` 58 | 59 | overrides: 60 | - filename: "**/*.yml" 61 | ignoreRegExpList: 62 | - /^\s*(?:[a-z0-9]|-|_|\"|')+:/ui # ignore keys 63 | - /@[a-z0-9-]+\/[a-z0-9-]+/u # scoped packages 64 | 65 | - filename: ".github/(actions|workflows)/*.yml" 66 | ignoreRegExpList: 67 | - /\b(?:[A-Za-z0-9\-_])+\/(?:[A-Za-z0-9\-_])+@v\d+(?:\.\d+){0,2}\b/ui # ignore action 68 | -------------------------------------------------------------------------------- /docs/assets/eslint-functional-logo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eslint-functional/eslint-plugin-functional/f02eca24f28e16db29ec3ecf67afe5ff472f2e42/docs/assets/eslint-functional-logo.pdn -------------------------------------------------------------------------------- /docs/assets/eslint-functional-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eslint-functional/eslint-plugin-functional/f02eca24f28e16db29ec3ecf67afe5ff472f2e42/docs/assets/eslint-functional-logo.png -------------------------------------------------------------------------------- /docs/assets/eslint-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/rules/no-class-inheritance.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow inheritance in classes (`functional/no-class-inheritance`) 5 | 6 | 💼 This rule is enabled in the following configs: ☑️ `lite`, `noOtherParadigms`, ✅ `recommended`, 🔒 `strict`. 7 | 8 | 9 | 10 | 11 | 12 | Disallow use of inheritance for classes. 13 | 14 | ## Rule Details 15 | 16 | ### ❌ Incorrect 17 | 18 | 19 | 20 | ```js 21 | /* eslint functional/no-class-inheritance: "error" */ 22 | 23 | abstract class Animal { 24 | constructor(name, age) { 25 | this.name = name; 26 | this.age = age; 27 | } 28 | } 29 | 30 | class Dog extends Animal { 31 | constructor(name, age) { 32 | super(name, age); 33 | } 34 | 35 | get ageInDogYears() { 36 | return 7 * this.age; 37 | } 38 | } 39 | 40 | const dogA = new Dog("Jasper", 2); 41 | 42 | console.log(`${dogA.name} is ${dogA.ageInDogYears} in dog years.`); 43 | ``` 44 | 45 | ### ✅ Correct 46 | 47 | ```js 48 | /* eslint functional/no-class-inheritance: "error" */ 49 | 50 | class Animal { 51 | constructor(name, age) { 52 | this.name = name; 53 | this.age = age; 54 | } 55 | } 56 | 57 | class Dog { 58 | constructor(name, age) { 59 | this.animal = new Animal(name, age); 60 | } 61 | 62 | get ageInDogYears() { 63 | return 7 * this.animal.age; 64 | } 65 | } 66 | 67 | console.log(`${dogA.name} is ${getAgeInDogYears(dogA.age)} in dog years.`); 68 | ``` 69 | 70 | ## Options 71 | 72 | This rule accepts an options object of the following type: 73 | 74 | ```ts 75 | type Options = { 76 | ignoreIdentifierPattern?: string[] | string; 77 | ignoreCodePattern?: string[] | string; 78 | }; 79 | ``` 80 | 81 | ### Default Options 82 | 83 | ```ts 84 | const defaults = {}; 85 | ``` 86 | 87 | ### `ignoreIdentifierPattern` 88 | 89 | This option takes a RegExp string or an array of RegExp strings. 90 | It allows for the ability to ignore violations based on the class's name. 91 | 92 | ### `ignoreCodePattern` 93 | 94 | This option takes a RegExp string or an array of RegExp strings. 95 | It allows for the ability to ignore violations based on the code itself. 96 | -------------------------------------------------------------------------------- /docs/rules/no-classes.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow classes (`functional/no-classes`) 5 | 6 | 💼🚫 This rule is enabled in the following configs: `noOtherParadigms`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the ☑️ `lite` config. 7 | 8 | 9 | 10 | 11 | 12 | Disallow use of the `class` keyword. 13 | 14 | ## Rule Details 15 | 16 | ### ❌ Incorrect 17 | 18 | 19 | 20 | ```js 21 | /* eslint functional/no-classes: "error" */ 22 | 23 | class Dog { 24 | constructor(name, age) { 25 | this.name = name; 26 | this.age = age; 27 | } 28 | 29 | get ageInDogYears() { 30 | return 7 * this.age; 31 | } 32 | } 33 | 34 | const dogA = new Dog("Jasper", 2); 35 | 36 | console.log(`${dogA.name} is ${dogA.ageInDogYears} in dog years.`); 37 | ``` 38 | 39 | ### ✅ Correct 40 | 41 | ```js 42 | /* eslint functional/no-classes: "error" */ 43 | 44 | function getAgeInDogYears(age) { 45 | return 7 * age; 46 | } 47 | 48 | const dogA = { 49 | name: "Jasper", 50 | age: 2, 51 | }; 52 | 53 | console.log(`${dogA.name} is ${getAgeInDogYears(dogA.age)} in dog years.`); 54 | ``` 55 | 56 | ## Options 57 | 58 | This rule accepts an options object of the following type: 59 | 60 | ```ts 61 | type Options = { 62 | ignoreIdentifierPattern?: string[] | string; 63 | ignoreCodePattern?: string[] | string; 64 | }; 65 | ``` 66 | 67 | ### Default Options 68 | 69 | ```ts 70 | const defaults = {}; 71 | ``` 72 | 73 | ### `ignoreIdentifierPattern` 74 | 75 | This option takes a RegExp string or an array of RegExp strings. 76 | It allows for the ability to ignore violations based on the class's name. 77 | 78 | ### `ignoreCodePattern` 79 | 80 | This option takes a RegExp string or an array of RegExp strings. 81 | It allows for the ability to ignore violations based on the code itself. 82 | -------------------------------------------------------------------------------- /docs/rules/no-conditional-statements.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow conditional statements (`functional/no-conditional-statements`) 5 | 6 | 💼🚫 This rule is enabled in the following configs: `noStatements`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the following configs: `disableTypeChecked`, ☑️ `lite`. 7 | 8 | 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). 9 | 10 | 11 | 12 | 13 | 14 | This rule disallows conditional statements such as `if` and `switch`. 15 | 16 | ## Rule Details 17 | 18 | Conditional statements are not a good fit for functional style programming as they are not expressions and do not return 19 | a value. Instead consider using the 20 | [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) 21 | which is an expression that returns a value: 22 | 23 | For more background see this [blog post](https://hackernoon.com/rethinking-javascript-the-if-statement-b158a61cd6cb) 24 | and discussion in [tslint-immutable #54](https://github.com/jonaskello/tslint-immutable/issues/54). 25 | 26 | ### ❌ Incorrect 27 | 28 | 29 | 30 | ```js 31 | /* eslint functional/no-conditional-statements: "error" */ 32 | 33 | let x; 34 | if (i === 1) { 35 | x = 2; 36 | } else { 37 | x = 3; 38 | } 39 | ``` 40 | 41 | ### ✅ Correct 42 | 43 | ```js 44 | /* eslint functional/no-conditional-statements: "error" */ 45 | 46 | const x = i === 1 ? 2 : 3; 47 | ``` 48 | 49 | ```js 50 | /* eslint functional/no-conditional-statements: "error" */ 51 | 52 | function foo(x, y) { 53 | return x === y // if 54 | ? 0 55 | : x > y // else if 56 | ? 1 57 | : -1; // else 58 | } 59 | ``` 60 | 61 | ## Options 62 | 63 | This rule accepts an options object of the following type: 64 | 65 | ```ts 66 | type Options = { 67 | allowReturningBranches: boolean | "ifExhaustive"; 68 | ignoreCodePattern?: ReadonlyArray | string; 69 | }; 70 | ``` 71 | 72 | ### Default Options 73 | 74 | ```ts 75 | const defaults = { 76 | allowReturningBranches: false, 77 | }; 78 | ``` 79 | 80 | ### Preset Overrides 81 | 82 | #### `recommended` and `lite` 83 | 84 | ```ts 85 | const recommendedAndLiteOptions = { 86 | allowReturningBranches: true, 87 | }; 88 | ``` 89 | 90 | ### `allowReturningBranches` 91 | 92 | #### `true` 93 | 94 | Allows conditional statements but only if all defined branches end with a return statement or other terminal. 95 | This allows early escapes to be used. 96 | 97 | ```js 98 | function foo(error, data) { 99 | if (error) { 100 | return; 101 | } 102 | 103 | // ... - Do stuff with data. 104 | } 105 | ``` 106 | 107 | #### `"ifExhaustive"` 108 | 109 | This will only allow conditional statements to exist if every case is taken into account and each has a return statement 110 | or other terminal. In other words, every `if` must have an `else` and every `switch` must have a default case. 111 | This allows conditional statements to be used like [do expressions](https://github.com/tc39/proposal-do-expressions). 112 | 113 | ```js 114 | const x = (() => { 115 | switch (y) { 116 | case "a": 117 | return 1; 118 | case "b": 119 | return 2; 120 | default: 121 | return 0; 122 | } 123 | })(); 124 | ``` 125 | 126 | Note: Currently this option is not useable with the [no-else-return](https://eslint.org/docs/rules/no-else-return) rule; 127 | `else` statements must contain a return statement. 128 | 129 | ### `ignoreCodePattern` 130 | 131 | This option takes a RegExp string or an array of RegExp strings. 132 | It allows for the ability to ignore violations based on the test condition of the `if` 133 | statement. 134 | 135 | Note: This option has no effect on `switch` statements. 136 | -------------------------------------------------------------------------------- /docs/rules/no-expression-statements.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow expression statements (`functional/no-expression-statements`) 5 | 6 | 💼🚫 This rule is enabled in the following configs: `noStatements`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the following configs: `disableTypeChecked`, ☑️ `lite`. 7 | 8 | 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). 9 | 10 | 11 | 12 | 13 | 14 | This rule checks that the value of an expression is assigned to a variable and thus helps promote side-effect free 15 | (pure) functions. 16 | 17 | ## Rule Details 18 | 19 | When you call a function and don’t use it’s return value, chances are high that it is being called for its side effect. e.g. 20 | 21 | ### ❌ Incorrect 22 | 23 | 24 | 25 | ```js 26 | /* eslint functional/no-expression-statements: "error" */ 27 | 28 | console.log("Hello world!"); 29 | ``` 30 | 31 | 32 | 33 | ```js 34 | /* eslint functional/no-expression-statements: "error" */ 35 | 36 | array.push(3); 37 | ``` 38 | 39 | 40 | 41 | ```js 42 | /* eslint functional/no-expression-statements: "error" */ 43 | 44 | foo(bar); 45 | ``` 46 | 47 | ### ✅ Correct 48 | 49 | ```js 50 | /* eslint functional/no-expression-statements: "error" */ 51 | 52 | const baz = foo(bar); 53 | ``` 54 | 55 | ```js 56 | /* eslint functional/no-expression-statements: ["error", { "ignoreVoid": true }] */ 57 | 58 | console.log("hello world"); 59 | ``` 60 | 61 | ## Options 62 | 63 | This rule accepts an options object of the following type: 64 | 65 | ```ts 66 | type Options = { 67 | ignoreCodePattern?: string[] | string; 68 | ignoreVoid?: boolean; 69 | ignoreSelfReturning?: boolean; 70 | }; 71 | ``` 72 | 73 | ### Default Options 74 | 75 | ```ts 76 | const defaults = { 77 | ignoreVoid: false, 78 | ignoreSelfReturning: false, 79 | }; 80 | ``` 81 | 82 | ### `ignoreVoid` 83 | 84 | When enabled, expression of type `void` and `Promise` are not flagged as violations. 85 | This options requires TypeScript in order to work. 86 | 87 | ### `ignoreSelfReturning` 88 | 89 | Like `ignoreVoid` but instead does not flag function calls that always only return `this`. 90 | 91 | Limitation: The function declaration must explicitly use `return this`; equivalents 92 | (such as assign this to a variable first, that is then returned) won't be considered valid. 93 | 94 | ### `ignoreCodePattern` 95 | 96 | This option takes a RegExp string or an array of RegExp strings. 97 | It allows for the ability to ignore violations based on the code itself. 98 | -------------------------------------------------------------------------------- /docs/rules/no-let.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow mutable variables (`functional/no-let`) 5 | 6 | 💼 This rule is enabled in the following configs: ☑️ `lite`, `noMutations`, ✅ `recommended`, 🔒 `strict`. 7 | 8 | 9 | 10 | 11 | 12 | This rule should be combined with ESLint's built-in `no-var` rule to enforce that all variables are declared as `const`. 13 | 14 | ## Rule Details 15 | 16 | In functional programming variables should not be mutable; use `const` instead. 17 | 18 | ### ❌ Incorrect 19 | 20 | 21 | 22 | ```js 23 | /* eslint functional/no-let: "error" */ 24 | 25 | let x = 5; 26 | ``` 27 | 28 | 29 | 30 | ```js 31 | /* eslint functional/no-let: "error" */ 32 | 33 | for (let i = 0; i < array.length; i++) {} 34 | ``` 35 | 36 | ### ✅ Correct 37 | 38 | ```js 39 | /* eslint functional/no-let: "error" */ 40 | 41 | const x = 5; 42 | ``` 43 | 44 | ```js 45 | /* eslint functional/no-let: "error" */ 46 | 47 | for (const element of array) { 48 | } 49 | ``` 50 | 51 | ```js 52 | /* eslint functional/no-let: "error" */ 53 | 54 | for (const [index, element] of array.entries()) { 55 | } 56 | ``` 57 | 58 | ## Options 59 | 60 | This rule accepts an options object of the following type: 61 | 62 | ```ts 63 | type Options = { 64 | allowInFunctions: boolean; 65 | ignoreIdentifierPattern?: string[] | string; 66 | }; 67 | ``` 68 | 69 | ### Default Options 70 | 71 | ```ts 72 | const defaults = { 73 | allowInForLoopInit: false, 74 | allowInFunctions: false, 75 | }; 76 | ``` 77 | 78 | ### Preset Overrides 79 | 80 | #### `recommended` and `lite` 81 | 82 | ```ts 83 | const recommendedAndLiteOptions = { 84 | allowInForLoopInit: true, 85 | }; 86 | ``` 87 | 88 | ### `allowInForLoopInit` 89 | 90 | If set, `let`s inside of for a loop initializer are allowed. This does not include for...of or for...in loops as they 91 | should use `const` instead. 92 | 93 | #### ❌ Incorrect 94 | 95 | 96 | 97 | ```js 98 | /* eslint functional/no-let: ["error", { "allowInForLoopInit": true } ] */ 99 | 100 | for (let element of array) { 101 | } 102 | ``` 103 | 104 | 105 | 106 | ```js 107 | /* eslint functional/no-let: ["error", { "allowInForLoopInit": true } ] */ 108 | 109 | for (let [index, element] of array.entries()) { 110 | } 111 | ``` 112 | 113 | #### ✅ Correct 114 | 115 | 116 | 117 | ```js 118 | /* eslint functional/no-let: ["error", { "allowInForLoopInit": true } ] */ 119 | 120 | for (let i = 0; i < array.length; i++) {} 121 | ``` 122 | 123 | ### `allowInFunctions` 124 | 125 | If true, the rule will not flag any statements that are inside of function bodies. 126 | 127 | ### `ignoreIdentifierPattern` 128 | 129 | This option takes a RegExp string or an array of RegExp strings. 130 | It allows for the ability to ignore violations based on a variable's name. 131 | -------------------------------------------------------------------------------- /docs/rules/no-loop-statements.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow imperative loops (`functional/no-loop-statements`) 5 | 6 | 💼 This rule is enabled in the following configs: ☑️ `lite`, `noStatements`, ✅ `recommended`, 🔒 `strict`. 7 | 8 | 9 | 10 | 11 | 12 | This rule disallows for loop statements, including `for`, `for...of`, `for...in`, `while`, and `do...while`. 13 | 14 | ## Rule Details 15 | 16 | In functional programming we want everything to be an expression that returns a value. 17 | Loops in JavaScript are statements so they are not a good fit for a functional programming style. 18 | Instead consider using `map`, `reduce` or similar. 19 | For more background see this 20 | [blog post](https://hackernoon.com/rethinking-javascript-death-of-the-for-loop-c431564c84a8) and discussion in 21 | [tslint-immutable #54](https://github.com/jonaskello/tslint-immutable/issues/54). 22 | 23 | ### ❌ Incorrect 24 | 25 | 26 | 27 | ```js 28 | /* eslint functional/no-loop-statements: "error" */ 29 | 30 | const numbers = [1, 2, 3]; 31 | const double = []; 32 | for (let i = 0; i < numbers.length; i++) { 33 | double[i] = numbers[i] * 2; 34 | } 35 | ``` 36 | 37 | 38 | 39 | ```js 40 | /* eslint functional/no-loop-statements: "error" */ 41 | 42 | const numbers = [1, 2, 3]; 43 | let sum = 0; 44 | for (const number of numbers) { 45 | sum += number; 46 | } 47 | ``` 48 | 49 | ### ✅ Correct 50 | 51 | ```js 52 | /* eslint functional/no-loop-statements: "error" */ 53 | const numbers = [1, 2, 3]; 54 | const double = numbers.map((n) => n * 2); 55 | ``` 56 | 57 | ```js 58 | /* eslint functional/no-loop-statements: "error" */ 59 | 60 | const numbers = [1, 2, 3]; 61 | const sum = numbers.reduce((carry, number) => carry + number, 0); 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/rules/no-mixed-types.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Restrict types so that only members of the same kind are allowed in them (`functional/no-mixed-types`) 5 | 6 | 💼🚫 This rule is enabled in the following configs: ☑️ `lite`, `noOtherParadigms`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the `disableTypeChecked` config. 7 | 8 | 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). 9 | 10 | 11 | 12 | 13 | 14 | This rule enforces that an aliased type literal or an interface only has one type of members, eg. only data properties 15 | or only functions. 16 | 17 | ## Rule Details 18 | 19 | Mixing functions and data properties in the same type is a sign of object-orientation style. 20 | 21 | ### ❌ Incorrect 22 | 23 | 24 | 25 | ```ts 26 | /* eslint functional/no-mixed-types: "error" */ 27 | 28 | type Foo = { 29 | prop1: string; 30 | prop2: () => string; 31 | }; 32 | ``` 33 | 34 | ### ✅ Correct 35 | 36 | ```ts 37 | /* eslint functional/no-mixed-types: "error" */ 38 | 39 | type Foo = { 40 | prop1: string; 41 | prop2: number; 42 | }; 43 | ``` 44 | 45 | 46 | 47 | ```ts 48 | /* eslint functional/no-mixed-types: "error" */ 49 | 50 | type Foo = { 51 | prop1: () => string; 52 | prop2(): number; 53 | }; 54 | ``` 55 | 56 | ## Limitations 57 | 58 | This rule will only check alias type literal declarations and interface declarations. Advanced types will not be checked. 59 | For example union and intersection types will not be checked. 60 | 61 | ## Options 62 | 63 | This rule accepts an options object of the following type: 64 | 65 | ```ts 66 | type Options = { 67 | checkInterfaces: boolean; 68 | checkTypeLiterals: boolean; 69 | }; 70 | ``` 71 | 72 | ### Default Options 73 | 74 | ```ts 75 | const defaults = { 76 | checkInterfaces: true, 77 | checkTypeLiterals: true, 78 | }; 79 | ``` 80 | 81 | ### checkInterfaces 82 | 83 | If true, interface declarations will be checked. 84 | 85 | ### checkTypeLiterals 86 | 87 | If true, aliased type literal declarations will be checked. 88 | -------------------------------------------------------------------------------- /docs/rules/no-promise-reject.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow rejecting promises (`functional/no-promise-reject`) 5 | 6 | 7 | 8 | 9 | 10 | This rule disallows rejecting promises. 11 | 12 | ## Rule Details 13 | 14 | It is useful when using an `Option` type (something like `{ value: T } | { error: Error }`) 15 | for handling errors. In this case a promise should always resolve with an `Option` and never reject. 16 | 17 | ### ❌ Incorrect 18 | 19 | 20 | 21 | ```js 22 | /* eslint functional/no-promise-reject: "error" */ 23 | 24 | async function divide(x, y) { 25 | const [xv, yv] = await Promise.all([x, y]); 26 | 27 | return yv === 0 28 | ? Promise.reject(new Error("Cannot divide by zero.")) 29 | : xv / yv; 30 | } 31 | ``` 32 | 33 | ### ✅ Correct 34 | 35 | ```js 36 | /* eslint functional/no-promise-reject: "error" */ 37 | 38 | async function divide(x, y) { 39 | const [xv, yv] = await Promise.all([x, y]); 40 | 41 | return yv === 0 42 | ? { error: new Error("Cannot divide by zero.") } 43 | : { value: xv / yv }; 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/rules/no-return-void.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow functions that don't return anything (`functional/no-return-void`) 5 | 6 | 💼🚫 This rule is enabled in the following configs: ☑️ `lite`, `noStatements`, ✅ `recommended`, 🔒 `strict`. This rule is _disabled_ in the `disableTypeChecked` config. 7 | 8 | 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). 9 | 10 | 11 | 12 | 13 | 14 | Disallow functions that are declared as returning nothing. 15 | 16 | ## Rule Details 17 | 18 | In functional programming functions must return something, they cannot return nothing. 19 | 20 | By default, this rule allows function to return `undefined` and `null`. 21 | 22 | Note: For performance reasons, this rule does not check implicit return types. 23 | We recommend using the rule 24 | [@typescript-eslint/explicit-function-return-type](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/explicit-function-return-type.md) 25 | in conjunction with this rule. 26 | 27 | ### ❌ Incorrect 28 | 29 | 30 | 31 | ```ts 32 | /* eslint functional/no-return-void: "error" */ 33 | 34 | function updateText(): void {} 35 | ``` 36 | 37 | ### ✅ Correct 38 | 39 | ```ts 40 | /* eslint functional/no-return-void: "error" */ 41 | 42 | function updateText(value: string): string {} 43 | ``` 44 | 45 | ## Options 46 | 47 | This rule accepts an options object of the following type: 48 | 49 | ```ts 50 | type Options = { 51 | allowNull: boolean; 52 | allowUndefined: boolean; 53 | ignoreInferredTypes: boolean; 54 | }; 55 | ``` 56 | 57 | ### Default Options 58 | 59 | ```ts 60 | const defaults = { 61 | allowNull: true, 62 | allowUndefined: true, 63 | ignoreInferredTypes: false, 64 | }; 65 | ``` 66 | 67 | ### `allowNull` 68 | 69 | If true allow returning null. 70 | 71 | ### `allowUndefined` 72 | 73 | If true allow returning undefined. 74 | 75 | ### `ignoreInferredTypes` 76 | 77 | If true ignore functions that don't explicitly specify a return type. 78 | -------------------------------------------------------------------------------- /docs/rules/no-this-expressions.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow this access (`functional/no-this-expressions`) 5 | 6 | 💼🚫 This rule is enabled in the following configs: `noOtherParadigms`, 🔒 `strict`. This rule is _disabled_ in the following configs: ☑️ `lite`, ✅ `recommended`. 7 | 8 | 9 | 10 | 11 | 12 | ## Rule Details 13 | 14 | This rule is a companion rule to the [no-classes](./no-classes.md) rule. 15 | See the its docs for more info. 16 | 17 | ### ❌ Incorrect 18 | 19 | 20 | 21 | ```js 22 | /* eslint functional/no-this-expressions: "error" */ 23 | 24 | const foo = this.value + 17; 25 | ``` 26 | 27 | ### ✅ Correct 28 | 29 | ```js 30 | /* eslint functional/no-this-expressions: "error" */ 31 | 32 | const foo = object.value + 17; 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/rules/no-throw-statements.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow throwing exceptions (`functional/no-throw-statements`) 5 | 6 | 💼 This rule is enabled in the following configs: ☑️ `lite`, `noExceptions`, ✅ `recommended`, 🔒 `strict`. 7 | 8 | 9 | 10 | 11 | 12 | This rule disallows the `throw` keyword. 13 | 14 | ## Rule Details 15 | 16 | Exceptions are not part of functional programming. 17 | As an alternative a function should return an error or in the case of an async function, a rejected promise. 18 | 19 | ### ❌ Incorrect 20 | 21 | 22 | 23 | ```js 24 | /* eslint functional/no-throw-statements: "error" */ 25 | 26 | throw new Error("Something went wrong."); 27 | ``` 28 | 29 | ### ✅ Correct 30 | 31 | ```js 32 | /* eslint functional/no-throw-statements: "error" */ 33 | 34 | function divide(x, y) { 35 | return y === 0 ? new Error("Cannot divide by zero.") : x / y; 36 | } 37 | ``` 38 | 39 | ```js 40 | /* eslint functional/no-throw-statements: "error" */ 41 | 42 | async function divide(x, y) { 43 | const [xv, yv] = await Promise.all([x, y]); 44 | 45 | return yv === 0 46 | ? Promise.reject(new Error("Cannot divide by zero.")) 47 | : xv / yv; 48 | } 49 | ``` 50 | 51 | ## Options 52 | 53 | This rule accepts an options object of the following type: 54 | 55 | ```ts 56 | type Options = { 57 | allowToRejectPromises: boolean; 58 | }; 59 | ``` 60 | 61 | ### Default Options 62 | 63 | ```ts 64 | const defaults = { 65 | allowToRejectPromises: false, 66 | }; 67 | ``` 68 | 69 | ### Preset Overrides 70 | 71 | #### `recommended` and `lite` 72 | 73 | ```ts 74 | const recommendedAndLiteOptions = { 75 | allowToRejectPromises: true, 76 | }; 77 | ``` 78 | 79 | ### `allowToRejectPromises` 80 | 81 | If true, throw statements will be allowed when they are used to reject a promise, such when in an async function.\ 82 | This essentially allows throw statements to be used as return statements for errors. 83 | 84 | #### ✅ Correct 85 | 86 | ```js 87 | /* eslint functional/no-throw-statements: ["error", { "allowToRejectPromises": true }] */ 88 | 89 | async function divide(x, y) { 90 | const [xv, yv] = await Promise.all([x, y]); 91 | 92 | if (yv === 0) { 93 | throw new Error("Cannot divide by zero."); 94 | } 95 | return xv / yv; 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /docs/rules/no-try-statements.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Disallow try-catch[-finally] and try-finally patterns (`functional/no-try-statements`) 5 | 6 | 💼🚫 This rule is enabled in the following configs: `noExceptions`, 🔒 `strict`. This rule is _disabled_ in the following configs: ☑️ `lite`, ✅ `recommended`. 7 | 8 | 9 | 10 | 11 | 12 | This rule disallows the `try` keyword. 13 | 14 | ## Rule Details 15 | 16 | Try statements are not part of functional programming. See [no-throw-statements](./no-throw-statements.md) for more information. 17 | 18 | ### ❌ Incorrect 19 | 20 | 21 | 22 | ```js 23 | /* eslint functional/no-try-statements: "error" */ 24 | 25 | try { 26 | doSomethingThatMightGoWrong(); // <-- Might throw an exception. 27 | } catch (error) { 28 | // Handle error. 29 | } 30 | ``` 31 | 32 | ### ✅ Correct 33 | 34 | ```js 35 | /* eslint functional/no-try-statements: "error" */ 36 | 37 | doSomethingThatMightGoWrong() // <-- Returns a Promise 38 | .catch((error) => { 39 | // Handle error. 40 | }); 41 | ``` 42 | 43 | ## Options 44 | 45 | This rule accepts an options object of the following type: 46 | 47 | ```ts 48 | type Options = { 49 | allowCatch: boolean; 50 | allowFinally: boolean; 51 | }; 52 | ``` 53 | 54 | ### Default Options 55 | 56 | ```ts 57 | const defaults = { 58 | allowCatch: false, 59 | allowFinally: false, 60 | }; 61 | ``` 62 | 63 | ### `allowCatch` 64 | 65 | If true, try-catch statements are allowed. 66 | 67 | ### `allowFinally` 68 | 69 | If true, try-finally statements are allowed. 70 | -------------------------------------------------------------------------------- /docs/rules/prefer-property-signatures.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Prefer property signatures over method signatures (`functional/prefer-property-signatures`) 5 | 6 | 💼🚫 This rule is enabled in the 🎨 `stylistic` config. This rule is _disabled_ in the `disableTypeChecked` config. 7 | 8 | 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). 9 | 10 | 11 | 12 | 13 | 14 | ## Rule Details 15 | 16 | There are two ways function members can be declared in interfaces and type aliases; `MethodSignature` and `PropertySignature`. 17 | 18 | The `MethodSignature` and the `PropertySignature` forms seem equivalent, but only the `PropertySignature` form can have 19 | a `readonly` modifier. 20 | Because of this any `MethodSignature` will be mutable unless wrapped in the `Readonly` type. 21 | 22 | It should be noted however that the `PropertySignature` form does not support overloading. 23 | 24 | ### ❌ Incorrect 25 | 26 | 27 | 28 | ```ts 29 | /* eslint functional/prefer-property-signatures: "error" */ 30 | 31 | type Foo = { 32 | bar(): string; 33 | }; 34 | ``` 35 | 36 | ### ✅ Correct 37 | 38 | 39 | 40 | ```ts 41 | /* eslint functional/prefer-property-signatures: "error" */ 42 | 43 | type Foo = { 44 | bar: () => string; 45 | }; 46 | 47 | type Foo = { 48 | readonly bar: () => string; 49 | }; 50 | ``` 51 | 52 | ## Options 53 | 54 | This rule accepts an options object of the following type: 55 | 56 | ```ts 57 | type Options = { 58 | ignoreIfReadonlyWrapped: boolean; 59 | }; 60 | ``` 61 | 62 | ### Default Options 63 | 64 | ```ts 65 | const defaults = { 66 | ignoreIfReadonlyWrapped: false, 67 | }; 68 | ``` 69 | 70 | ### `ignoreIfReadonlyWrapped` 71 | 72 | If set to `true`, method signatures wrapped in the `Readonly` type will not be flagged as violations. 73 | 74 | #### ✅ Correct 75 | 76 | ```ts 77 | /* eslint functional/prefer-property-signatures: ["error", { "ignoreIfReadonlyWrapped": true } ] */ 78 | 79 | type Foo = Readonly<{ 80 | bar(): string; 81 | }>; 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/rules/prefer-tacit.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Replaces `x => f(x)` with just `f` (`functional/prefer-tacit`) 5 | 6 | ⚠️🚫 This rule _warns_ in the 🎨 `stylistic` config. This rule is _disabled_ in the `disableTypeChecked` config. 7 | 8 | 💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). 9 | 10 | 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). 11 | 12 | 13 | 14 | 15 | 16 | This rule enforces using functions directly if they can be without wrapping them. 17 | 18 | ## Rule Details 19 | 20 | If a function can be used directly without being in a callback wrapper, then it's generally better to use it directly. 21 | Extra inline lambdas can slow the runtime down. 22 | 23 | ⚠️ Warning ⚠️: Use with caution as if not all parameters should be passed to the function, a wrapper function is then required. 24 | 25 | ### ❌ Incorrect 26 | 27 | 28 | 29 | ```ts 30 | /* eslint functional/prefer-tacit: "error" */ 31 | 32 | function f(x) { 33 | return x + 1; 34 | } 35 | 36 | const foo = [1, 2, 3].map((x) => f(x)); 37 | ``` 38 | 39 | ### ✅ Correct 40 | 41 | ```ts 42 | /* eslint functional/prefer-tacit: "error" */ 43 | 44 | function f(x) { 45 | return x + 1; 46 | } 47 | 48 | const foo = [1, 2, 3].map(f); 49 | 50 | const bar = { f }; 51 | const baz = [1, 2, 3].map((x) => bar.f(x)); // Allowed unless using `checkMemberExpressions` 52 | ``` 53 | 54 | ## Options 55 | 56 | This rule accepts an options object of the following type: 57 | 58 | ```ts 59 | type Options = { 60 | checkMemberExpressions: boolean; 61 | }; 62 | ``` 63 | 64 | ### Default Options 65 | 66 | ```ts 67 | type Options = { 68 | checkMemberExpressions: false; 69 | }; 70 | ``` 71 | 72 | ### `checkMemberExpressions` 73 | 74 | If `true`, calls of member expressions are checked as well. 75 | If `false`, only calls of identifiers are checked. 76 | 77 | #### ❌ Incorrect 78 | 79 | 80 | 81 | ```ts 82 | /* eslint functional/prefer-tacit: ["error", { "checkMemberExpressions": true }] */ 83 | 84 | const bar = { 85 | f(x) { 86 | return x + 1; 87 | }, 88 | }; 89 | 90 | const foo = [1, 2, 3].map((x) => bar.f(x)); 91 | ``` 92 | 93 | #### ✅ Correct 94 | 95 | ```ts 96 | /* eslint functional/prefer-tacit: ["error", { "checkMemberExpressions": true }] */ 97 | 98 | const bar = { 99 | f(x) { 100 | return x + 1; 101 | }, 102 | }; 103 | 104 | const foo = [1, 2, 3].map(bar.f.bind(bar)); 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/rules/readonly-type.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Require consistently using either `readonly` keywords or `Readonly` (`functional/readonly-type`) 5 | 6 | 💼🚫 This rule is enabled in the 🎨 `stylistic` config. This rule is _disabled_ in the `disableTypeChecked` config. 7 | 8 | 🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). 9 | 10 | 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). 11 | 12 | 13 | 14 | 15 | 16 | This rule enforces consistently using either `readonly` keywords or `Readonly`. 17 | 18 | ## Rule Details 19 | 20 | There are two ways to declare type literals as readonly, either by specifying that each 21 | property of the type is readonly using the `readonly` keyword, or by wrapping the type 22 | in `Readonly`. 23 | 24 | This rule is designed to enforce a consistent way of doing this. 25 | 26 | ### ❌ Incorrect 27 | 28 | 29 | 30 | ```ts 31 | /* eslint functional/readonly-type: ["error", "keyword"] */ 32 | 33 | type Foo = Readonly<{ 34 | bar: string; 35 | baz: number; 36 | }>; 37 | ``` 38 | 39 | 40 | 41 | ```ts 42 | /* eslint functional/readonly-type: ["error", "generic"] */ 43 | 44 | type Foo = { 45 | readonly bar: string; 46 | readonly baz: number; 47 | }; 48 | ``` 49 | 50 | ### ✅ Correct 51 | 52 | ```ts 53 | /* eslint functional/readonly-type: ["error", "keyword"] */ 54 | 55 | type Foo = { 56 | readonly bar: string; 57 | readonly baz: number; 58 | }; 59 | 60 | type Foo2 = { 61 | readonly bar: string; 62 | baz: number; 63 | }; 64 | ``` 65 | 66 | ```ts 67 | /* eslint functional/readonly-type: ["error", "generic"] */ 68 | 69 | type Foo = Readonly<{ 70 | bar: string; 71 | baz: number; 72 | }>; 73 | 74 | // No issue as it's not fully readonly. 75 | type Foo2 = { 76 | readonly bar: string; 77 | baz: number; 78 | }; 79 | ``` 80 | 81 | ## Options 82 | 83 | This rule takes a single string option, either `generic` | `keyword`. 84 | 85 | ### Default Options 86 | 87 | ```ts 88 | const defaults = "generic"; 89 | ``` 90 | 91 | ### `generic` 92 | 93 | Enforce using `Readonly` instead of marking each property as readonly with the `readonly` keyword. 94 | 95 | ### `keyword` 96 | 97 | Enforce using `readonly` keyword for each property instead of wrapping with `Readonly`. 98 | -------------------------------------------------------------------------------- /docs/rules/settings/immutability.md: -------------------------------------------------------------------------------- 1 | # Using the `immutability` setting 2 | 3 | We are using the 4 | [is-immutable-type](https://www.npmjs.com/package/is-immutable-type) library to 5 | determine the immutability of types. This library can be configure for all rules 6 | at once using a shared setting. 7 | 8 | ## Overrides 9 | 10 | For details see [the overrides 11 | section](https://github.com/RebeccaStevens/is-immutable-type#overrides) of 12 | [is-immutable-type](https://www.npmjs.com/package/is-immutable-type). 13 | 14 | ### Example of configuring immutability overrides 15 | 16 | In this example, we are configuring 17 | [is-immutable-type](https://www.npmjs.com/package/is-immutable-type) to treat 18 | any readonly array (regardless of the syntax used) as immutable in the case 19 | where it was found to be deeply readonly. If it was only found to be shallowly 20 | readonly, then no override will be applied. 21 | 22 | ```jsonc 23 | // .eslintrc.json 24 | { 25 | // ... 26 | "settings": { 27 | "immutability": { 28 | "overrides": [ 29 | { 30 | "type": { 31 | "from": "lib", 32 | "name": "ReadonlyArray", 33 | }, 34 | "to": "Immutable", 35 | "from": "ReadonlyDeep", 36 | }, 37 | ], 38 | }, 39 | }, 40 | "rules": { 41 | // ... 42 | }, 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/user-guide/migrating-from-tslint.md: -------------------------------------------------------------------------------- 1 | # Migrating from TSLint 2 | 3 | [TSLint is now deprecate](https://github.com/palantir/tslint/issues/4534). 4 | This guide is intended to help those who are using tslint-immutable to migrate their settings and projects to use eslint-plugin-functional. 5 | 6 | ## Configuration File 7 | 8 | The ESLint version of `tslint.json` (the configuration file) is `.eslintrc`. 9 | See [ESLint's docs](https://eslint.org/docs/user-guide/configuring) for more information on this file. 10 | 11 | Out of the box, ESLint does not understand TypeScript. To get ESLint to understand it we need to change the default 12 | parser to one that understands it. This is where 13 | [@typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) comes in. In the config file, we can 14 | specify the parser to be used with the "parser" key. Any extra parser configuration can then be specified under the 15 | "parserOptions" key. In order for the parser to have access to type information, it needs access to your 16 | `tsconfig.json`; you'll need to specify this under "parserOptions" -> "project". 17 | 18 | ### Example config 19 | 20 | ```jsonc 21 | { 22 | "root": true, 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "ecmaVersion": 10, 26 | "project": "./tsconfig.json", 27 | "sourceType": "module", 28 | }, 29 | "plugins": ["functional"], 30 | "env": { 31 | "es6": true, 32 | }, 33 | "extends": ["plugin:functional/recommended"], 34 | "rules": { 35 | // These rules will be applied to all linted file. 36 | }, 37 | "overrides": [ 38 | { 39 | "files": ["*.ts", "*.tsx"], 40 | "rules": { 41 | // These rules will only be applied to ts file. 42 | }, 43 | }, 44 | ], 45 | } 46 | ``` 47 | 48 | ## Rules 49 | 50 | Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-immutable` equivalents. 51 | 52 | | `eslint-plugin-functional` Rule | Equivalent `tslint-immutable` Rules | 53 | | ------------------------------------------------------------------------------- | ------------------------------------------------------- | 54 | | [`functional/prefer-readonly-type`](../rules/prefer-readonly-type.md) | `readonly-keyword` & `readonly-array` | 55 | | [`functional/no-let`](../rules/no-let.md) | `no-let` | 56 | | [`functional/immutable-data`](../rules/immutable-data.md) | `no-object-mutation`, `no-array-mutation` & `no-delete` | 57 | | [`functional/no-method-signature`](../rules/no-method-signature.md) | `no-method-signature` | 58 | | [`functional/no-this-expressions`](../rules/no-this-expressions.md) | `no-this` | 59 | | [`functional/no-classes`](../rules/no-classes.md) | `no-classes` | 60 | | [`functional/no-mixed-types`](../rules/no-mixed-types.md) | `no-mixed-interface` | 61 | | [`functional/no-expression-statements`](../rules/no-expression-statements.md) | `no-expression-statements` | 62 | | [`functional/no-conditional-statements`](../rules/no-conditional-statements.md) | `no-if-statement` | 63 | | [`functional/no-loop-statements`](../rules/no-loop-statements.md) | `no-loop-statements` | 64 | | [`functional/no-return-void`](../rules/no-return-void.md) | - | 65 | | [`functional/no-throw-statements`](../rules/no-throw-statements.md) | `no-throw` | 66 | | [`functional/no-try-statements`](../rules/no-try-statements.md) | `no-try` | 67 | | [`functional/no-promise-reject`](../rules/no-promise-reject.md) | `no-reject` | 68 | | [`functional/functional-parameters`](../rules/functional-parameters.md) | - | 69 | -------------------------------------------------------------------------------- /eslint-doc-generator.config.ts: -------------------------------------------------------------------------------- 1 | import type { GenerateOptions } from "eslint-doc-generator"; 2 | import { format } from "prettier"; 3 | 4 | export default { 5 | configEmoji: [["lite", "☑️"]], 6 | ignoreConfig: ["all", "off", "disable-type-checked"], 7 | ruleDocSectionInclude: ["Rule Details"], 8 | ruleListSplit: "meta.docs.category", 9 | postprocess: (doc) => 10 | format(doc, { 11 | parser: "markdown", 12 | }), 13 | } satisfies GenerateOptions; 14 | -------------------------------------------------------------------------------- /knip.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/knip/schema-jsonc.json", 3 | "entry": ["src/index.ts!"], 4 | "project": ["src/**/*.ts!", "tests/**/*.{js,ts}"], 5 | "ignore": ["tests/fixture/file.ts"], 6 | "ignoreDependencies": [ 7 | "@eslint/compat", 8 | "@semantic-release/commit-analyzer", 9 | "@semantic-release/github", 10 | "@semantic-release/npm", 11 | "@semantic-release/release-notes-generator", 12 | "tsc-files", 13 | ], 14 | "exclude": ["exports", "nsExports", "types", "nsTypes"], 15 | } 16 | -------------------------------------------------------------------------------- /project-dictionary.txt: -------------------------------------------------------------------------------- 1 | automerge 2 | deassert 3 | declarator 4 | declarators 5 | deepmerge 6 | IIFE 7 | jonaskello 8 | Kello 9 | klass 10 | knip 11 | lcov 12 | litecoin 13 | markdownlint 14 | noreply 15 | rebeccastevens 16 | ruleset 17 | rulesets 18 | sonarjs 19 | treeshake 20 | TSES 21 | uncompiled 22 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import rollupPluginReplace from "@rollup/plugin-replace"; 2 | import rollupPluginTypescript from "@rollup/plugin-typescript"; 3 | import type { RollupOptions } from "rollup"; 4 | import rollupPluginDeassert from "rollup-plugin-deassert"; 5 | import generateDtsBundle from "rollup-plugin-dts-bundle-generator-2"; 6 | 7 | import pkg from "./package.json" with { type: "json" }; 8 | 9 | type PackageJSON = typeof pkg & { 10 | dependencies?: Record; 11 | peerDependencies?: Record; 12 | }; 13 | 14 | const externalDependencies = [ 15 | ...Object.keys((pkg as PackageJSON).dependencies ?? {}), 16 | ...Object.keys((pkg as PackageJSON).peerDependencies ?? {}), 17 | ]; 18 | 19 | export default { 20 | input: "src/index.ts", 21 | 22 | output: [ 23 | { 24 | file: pkg.exports.default, 25 | format: "esm", 26 | sourcemap: false, 27 | importAttributesKey: "with", 28 | }, 29 | ], 30 | 31 | plugins: [ 32 | rollupPluginTypescript({ 33 | tsconfig: "src/tsconfig.build.json", 34 | }), 35 | rollupPluginReplace({ 36 | values: { 37 | "import.meta.vitest": "undefined", 38 | }, 39 | preventAssignment: true, 40 | }), 41 | rollupPluginDeassert({ 42 | include: ["**/*.{js,ts}"], 43 | }), 44 | generateDtsBundle({ 45 | compilation: { 46 | preferredConfigPath: "src/tsconfig.build.json", 47 | }, 48 | output: { 49 | exportReferencedTypes: false, 50 | inlineDeclareExternals: true, 51 | }, 52 | }), 53 | ], 54 | 55 | treeshake: { 56 | annotations: true, 57 | moduleSideEffects: [], 58 | propertyReadSideEffects: false, 59 | unknownGlobalSideEffects: false, 60 | }, 61 | 62 | external: (source) => { 63 | if ( 64 | source.startsWith("node:") || 65 | externalDependencies.some((dep) => dep === source || source.startsWith(`${dep}/`)) 66 | ) { 67 | return true; 68 | } 69 | return undefined; 70 | }, 71 | } satisfies RollupOptions; 72 | -------------------------------------------------------------------------------- /src/configs/all.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default { 7 | ...Object.fromEntries( 8 | Object.entries(rules) 9 | .filter(([, rule]) => rule.meta.deprecated !== true) 10 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 11 | ), 12 | } satisfies FlatConfig.Config["rules"] as NonNullable; 13 | -------------------------------------------------------------------------------- /src/configs/currying.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter( 9 | ([, rule]) => 10 | rule.meta.deprecated !== true && rule.meta.docs.category === "Currying" && rule.meta.docs.recommended !== false, 11 | ) 12 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 13 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 14 | -------------------------------------------------------------------------------- /src/configs/disable-type-checked.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter(([, rule]) => rule.meta.docs.requiresTypeChecking) 9 | .map(([name]) => [`${ruleNameScope}/${name}`, "off"]), 10 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 11 | -------------------------------------------------------------------------------- /src/configs/external-typescript-recommended.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import externalVanillaRecommended from "#/configs/external-vanilla-recommended"; 4 | 5 | const tsConfig = { 6 | "@typescript-eslint/prefer-readonly": "error", 7 | "@typescript-eslint/switch-exhaustiveness-check": "error", 8 | } satisfies FlatConfig.Config["rules"]; 9 | 10 | export default { 11 | ...externalVanillaRecommended, 12 | ...tsConfig, 13 | } satisfies FlatConfig.Config["rules"] as NonNullable; 14 | -------------------------------------------------------------------------------- /src/configs/external-vanilla-recommended.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | export default { 4 | "prefer-const": "error", 5 | "no-param-reassign": "error", 6 | "no-var": "error", 7 | } satisfies FlatConfig.Config["rules"] as NonNullable; 8 | -------------------------------------------------------------------------------- /src/configs/lite.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import * as functionalParameters from "#/rules/functional-parameters"; 4 | import * as immutableData from "#/rules/immutable-data"; 5 | import * as noClasses from "#/rules/no-classes"; 6 | import * as noConditionalStatements from "#/rules/no-conditional-statements"; 7 | import * as noExpressionStatements from "#/rules/no-expression-statements"; 8 | import * as preferImmutableTypes from "#/rules/prefer-immutable-types"; 9 | 10 | import recommended from "./recommended"; 11 | 12 | const overrides = { 13 | [functionalParameters.fullName]: [ 14 | "error", 15 | { 16 | enforceParameterCount: false, 17 | }, 18 | ], 19 | [immutableData.fullName]: ["error", { ignoreClasses: "fieldsOnly" }], 20 | [noClasses.fullName]: "off", 21 | [noConditionalStatements.fullName]: "off", 22 | [noExpressionStatements.fullName]: "off", 23 | [preferImmutableTypes.fullName]: [ 24 | "error", 25 | { 26 | enforcement: "None", 27 | overrides: [ 28 | { 29 | specifiers: { 30 | from: "file", 31 | }, 32 | options: { 33 | ignoreInferredTypes: true, 34 | parameters: { 35 | enforcement: "ReadonlyShallow", 36 | }, 37 | }, 38 | }, 39 | ], 40 | }, 41 | ], 42 | } satisfies FlatConfig.Config["rules"]; 43 | 44 | export default { 45 | ...recommended, 46 | ...overrides, 47 | } satisfies FlatConfig.Config["rules"] as NonNullable; 48 | -------------------------------------------------------------------------------- /src/configs/no-exceptions.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter( 9 | ([, rule]) => 10 | rule.meta.deprecated !== true && 11 | rule.meta.docs.category === "No Exceptions" && 12 | rule.meta.docs.recommended !== false, 13 | ) 14 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 15 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 16 | -------------------------------------------------------------------------------- /src/configs/no-mutations.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter( 9 | ([, rule]) => 10 | rule.meta.deprecated !== true && 11 | rule.meta.docs.category === "No Mutations" && 12 | rule.meta.docs.recommended !== false, 13 | ) 14 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 15 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 16 | -------------------------------------------------------------------------------- /src/configs/no-other-paradigms.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter( 9 | ([, rule]) => 10 | rule.meta.deprecated !== true && 11 | rule.meta.docs.category === "No Other Paradigms" && 12 | rule.meta.docs.recommended !== false, 13 | ) 14 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 15 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 16 | -------------------------------------------------------------------------------- /src/configs/no-statements.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter( 9 | ([, rule]) => 10 | rule.meta.deprecated !== true && 11 | rule.meta.docs.category === "No Statements" && 12 | rule.meta.docs.recommended !== false, 13 | ) 14 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 15 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 16 | -------------------------------------------------------------------------------- /src/configs/off.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules).map(([name]) => [`${ruleNameScope}/${name}`, "off"]), 8 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 9 | -------------------------------------------------------------------------------- /src/configs/strict.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter( 9 | ([, rule]) => 10 | rule.meta.deprecated !== true && 11 | rule.meta.docs.recommended !== false && 12 | rule.meta.docs.category !== "Stylistic", 13 | ) 14 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 15 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 16 | -------------------------------------------------------------------------------- /src/configs/stylistic.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import { rules } from "#/rules"; 4 | import { ruleNameScope } from "#/utils/misc"; 5 | 6 | export default Object.fromEntries( 7 | Object.entries(rules) 8 | .filter( 9 | ([, rule]) => 10 | rule.meta.deprecated !== true && 11 | rule.meta.docs.category === "Stylistic" && 12 | rule.meta.docs.recommended !== false, 13 | ) 14 | .map(([name, rule]) => [`${ruleNameScope}/${name}`, rule.meta.docs.recommendedSeverity]), 15 | ) satisfies FlatConfig.Config["rules"] as NonNullable; 16 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { FlatConfig } from "@typescript-eslint/utils/ts-eslint"; 2 | 3 | import all from "#/configs/all"; 4 | import currying from "#/configs/currying"; 5 | import disableTypeChecked from "#/configs/disable-type-checked"; 6 | import externalTypeScriptRecommended from "#/configs/external-typescript-recommended"; 7 | import externalVanillaRecommended from "#/configs/external-vanilla-recommended"; 8 | import lite from "#/configs/lite"; 9 | import noExceptions from "#/configs/no-exceptions"; 10 | import noMutations from "#/configs/no-mutations"; 11 | import noOtherParadigms from "#/configs/no-other-paradigms"; 12 | import noStatements from "#/configs/no-statements"; 13 | import off from "#/configs/off"; 14 | import recommended from "#/configs/recommended"; 15 | import strict from "#/configs/strict"; 16 | import stylistic from "#/configs/stylistic"; 17 | import { rules } from "#/rules"; 18 | import { __VERSION__ } from "#/utils/constants"; 19 | 20 | const meta = { 21 | name: "eslint-plugin-functional", 22 | version: __VERSION__ as string, 23 | } as const; 24 | 25 | const functional = { 26 | meta, 27 | rules, 28 | } satisfies FlatConfig.Plugin; 29 | 30 | const plugins = { functional } as const; 31 | 32 | const configs: Readonly<{ 33 | all: FlatConfig.Config; 34 | lite: FlatConfig.Config; 35 | recommended: FlatConfig.Config; 36 | strict: FlatConfig.Config; 37 | off: FlatConfig.Config; 38 | disableTypeChecked: FlatConfig.Config; 39 | externalVanillaRecommended: FlatConfig.Config; 40 | externalTypeScriptRecommended: FlatConfig.Config; 41 | currying: FlatConfig.Config; 42 | noExceptions: FlatConfig.Config; 43 | noMutations: FlatConfig.Config; 44 | noOtherParadigms: FlatConfig.Config; 45 | noStatements: FlatConfig.Config; 46 | stylistic: FlatConfig.Config; 47 | }> = { 48 | all: { plugins, rules: all }, 49 | lite: { plugins, rules: lite }, 50 | recommended: { plugins, rules: recommended }, 51 | strict: { plugins, rules: strict }, 52 | off: { plugins, rules: off }, 53 | disableTypeChecked: { 54 | plugins, 55 | rules: disableTypeChecked, 56 | }, 57 | externalVanillaRecommended: { 58 | plugins, 59 | rules: externalVanillaRecommended, 60 | }, 61 | externalTypeScriptRecommended: { 62 | plugins, 63 | rules: externalTypeScriptRecommended, 64 | }, 65 | currying: { plugins, rules: currying }, 66 | noExceptions: { plugins, rules: noExceptions }, 67 | noMutations: { plugins, rules: noMutations }, 68 | noOtherParadigms: { 69 | plugins, 70 | rules: noOtherParadigms, 71 | }, 72 | noStatements: { plugins, rules: noStatements }, 73 | stylistic: { plugins, rules: stylistic }, 74 | } satisfies Record; 75 | 76 | type EslintPluginFunctional = FlatConfig.Plugin & { 77 | meta: typeof meta; 78 | rules: typeof rules; 79 | configs: typeof configs; 80 | }; 81 | 82 | // eslint-disable-next-line functional/immutable-data 83 | (functional as EslintPluginFunctional).configs = configs; 84 | 85 | export default functional as EslintPluginFunctional; 86 | -------------------------------------------------------------------------------- /src/options/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ignore"; 2 | export * from "./overrides"; 3 | -------------------------------------------------------------------------------- /src/rules/index.ts: -------------------------------------------------------------------------------- 1 | import * as functionalParameters from "./functional-parameters"; 2 | import * as immutableData from "./immutable-data"; 3 | import * as noClassInheritance from "./no-class-inheritance"; 4 | import * as noClasses from "./no-classes"; 5 | import * as noConditionalStatements from "./no-conditional-statements"; 6 | import * as noExpressionStatements from "./no-expression-statements"; 7 | import * as noLet from "./no-let"; 8 | import * as noLoopStatements from "./no-loop-statements"; 9 | import * as noMixedTypes from "./no-mixed-types"; 10 | import * as noPromiseReject from "./no-promise-reject"; 11 | import * as noReturnVoid from "./no-return-void"; 12 | import * as noThisExpressions from "./no-this-expressions"; 13 | import * as noThrowStatements from "./no-throw-statements"; 14 | import * as noTryStatements from "./no-try-statements"; 15 | import * as preferImmutableTypes from "./prefer-immutable-types"; 16 | import * as preferPropertySignatures from "./prefer-property-signatures"; 17 | import * as preferReadonlyTypes from "./prefer-readonly-type"; 18 | import * as preferTacit from "./prefer-tacit"; 19 | import * as readonlyType from "./readonly-type"; 20 | import * as typeDeclarationImmutability from "./type-declaration-immutability"; 21 | 22 | /** 23 | * All of the custom rules. 24 | */ 25 | export const rules: Readonly<{ 26 | [functionalParameters.name]: typeof functionalParameters.rule; 27 | [immutableData.name]: typeof immutableData.rule; 28 | [noClasses.name]: typeof noClasses.rule; 29 | [noClassInheritance.name]: typeof noClassInheritance.rule; 30 | [noConditionalStatements.name]: typeof noConditionalStatements.rule; 31 | [noExpressionStatements.name]: typeof noExpressionStatements.rule; 32 | [noLet.name]: typeof noLet.rule; 33 | [noLoopStatements.name]: typeof noLoopStatements.rule; 34 | [noMixedTypes.name]: typeof noMixedTypes.rule; 35 | [noPromiseReject.name]: typeof noPromiseReject.rule; 36 | [noReturnVoid.name]: typeof noReturnVoid.rule; 37 | [noThisExpressions.name]: typeof noThisExpressions.rule; 38 | [noThrowStatements.name]: typeof noThrowStatements.rule; 39 | [noTryStatements.name]: typeof noTryStatements.rule; 40 | [preferImmutableTypes.name]: typeof preferImmutableTypes.rule; 41 | [preferPropertySignatures.name]: typeof preferPropertySignatures.rule; 42 | [preferReadonlyTypes.name]: typeof preferReadonlyTypes.rule; 43 | [preferTacit.name]: typeof preferTacit.rule; 44 | [readonlyType.name]: typeof readonlyType.rule; 45 | [typeDeclarationImmutability.name]: typeof typeDeclarationImmutability.rule; 46 | }> = { 47 | [functionalParameters.name]: functionalParameters.rule, 48 | [immutableData.name]: immutableData.rule, 49 | [noClasses.name]: noClasses.rule, 50 | [noClassInheritance.name]: noClassInheritance.rule, 51 | [noConditionalStatements.name]: noConditionalStatements.rule, 52 | [noExpressionStatements.name]: noExpressionStatements.rule, 53 | [noLet.name]: noLet.rule, 54 | [noLoopStatements.name]: noLoopStatements.rule, 55 | [noMixedTypes.name]: noMixedTypes.rule, 56 | [noPromiseReject.name]: noPromiseReject.rule, 57 | [noReturnVoid.name]: noReturnVoid.rule, 58 | [noThisExpressions.name]: noThisExpressions.rule, 59 | [noThrowStatements.name]: noThrowStatements.rule, 60 | [noTryStatements.name]: noTryStatements.rule, 61 | [preferImmutableTypes.name]: preferImmutableTypes.rule, 62 | [preferPropertySignatures.name]: preferPropertySignatures.rule, 63 | [preferReadonlyTypes.name]: preferReadonlyTypes.rule, 64 | [preferTacit.name]: preferTacit.rule, 65 | [readonlyType.name]: readonlyType.rule, 66 | [typeDeclarationImmutability.name]: typeDeclarationImmutability.rule, 67 | }; 68 | -------------------------------------------------------------------------------- /src/rules/no-class-inheritance.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; 2 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 3 | import { deepmerge } from "deepmerge-ts"; 4 | 5 | import { 6 | type IgnoreCodePatternOption, 7 | type IgnoreIdentifierPatternOption, 8 | ignoreCodePatternOptionSchema, 9 | ignoreIdentifierPatternOptionSchema, 10 | shouldIgnorePattern, 11 | } from "#/options"; 12 | import { ruleNameScope } from "#/utils/misc"; 13 | import type { ESClass } from "#/utils/node-types"; 14 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 15 | 16 | /** 17 | * The name of this rule. 18 | */ 19 | export const name = "no-class-inheritance"; 20 | 21 | /** 22 | * The full name of this rule. 23 | */ 24 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 25 | 26 | /** 27 | * The options this rule can take. 28 | */ 29 | type RawOptions = [IgnoreIdentifierPatternOption & IgnoreCodePatternOption]; 30 | 31 | /** 32 | * The schema for the rule options. 33 | */ 34 | const schema: JSONSchema4[] = [ 35 | { 36 | type: "object", 37 | properties: deepmerge(ignoreIdentifierPatternOptionSchema, ignoreCodePatternOptionSchema), 38 | additionalProperties: false, 39 | }, 40 | ]; 41 | 42 | /** 43 | * The default options for the rule. 44 | */ 45 | const defaultOptions: RawOptions = [{}]; 46 | 47 | /** 48 | * The possible error messages. 49 | */ 50 | const errorMessages = { 51 | abstract: "Unexpected abstract class.", 52 | extends: "Unexpected inheritance, use composition instead.", 53 | } as const; 54 | 55 | /** 56 | * The meta data for this rule. 57 | */ 58 | const meta: NamedCreateRuleCustomMeta = { 59 | type: "suggestion", 60 | docs: { 61 | category: "No Other Paradigms", 62 | description: "Disallow inheritance in classes.", 63 | recommended: "recommended", 64 | recommendedSeverity: "error", 65 | requiresTypeChecking: false, 66 | }, 67 | messages: errorMessages, 68 | schema, 69 | }; 70 | 71 | /** 72 | * Check if the given class node violates this rule. 73 | */ 74 | function checkClass( 75 | node: ESClass, 76 | context: Readonly>, 77 | options: Readonly, 78 | ): RuleResult { 79 | const [optionsObject] = options; 80 | const { ignoreIdentifierPattern, ignoreCodePattern } = optionsObject; 81 | 82 | const mut_descriptors: Array["descriptors"][number]> = []; 83 | 84 | if (!shouldIgnorePattern(node, context, ignoreIdentifierPattern, undefined, ignoreCodePattern)) { 85 | if (node.abstract) { 86 | const nodeText = context.sourceCode.getText(node); 87 | const abstractRelativeIndex = nodeText.indexOf("abstract"); 88 | const abstractIndex = context.sourceCode.getIndexFromLoc(node.loc.start) + abstractRelativeIndex; 89 | const start = context.sourceCode.getLocFromIndex(abstractIndex); 90 | const end = context.sourceCode.getLocFromIndex(abstractIndex + "abstract".length); 91 | 92 | mut_descriptors.push({ 93 | node, 94 | loc: { 95 | start, 96 | end, 97 | }, 98 | messageId: "abstract", 99 | }); 100 | } 101 | 102 | if (node.superClass !== null) { 103 | const nodeText = context.sourceCode.getText(node); 104 | const extendsRelativeIndex = nodeText.indexOf("extends"); 105 | const extendsIndex = context.sourceCode.getIndexFromLoc(node.loc.start) + extendsRelativeIndex; 106 | const start = context.sourceCode.getLocFromIndex(extendsIndex); 107 | const { end } = node.superClass.loc; 108 | 109 | mut_descriptors.push({ 110 | node, 111 | loc: { 112 | start, 113 | end, 114 | }, 115 | messageId: "extends", 116 | }); 117 | } 118 | } 119 | 120 | return { 121 | context, 122 | descriptors: mut_descriptors, 123 | }; 124 | } 125 | 126 | // Create the rule. 127 | export const rule: Rule = createRule( 128 | name, 129 | meta, 130 | defaultOptions, 131 | { 132 | ClassDeclaration: checkClass, 133 | ClassExpression: checkClass, 134 | }, 135 | ); 136 | -------------------------------------------------------------------------------- /src/rules/no-classes.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; 2 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 3 | import { deepmerge } from "deepmerge-ts"; 4 | 5 | import { 6 | type IgnoreCodePatternOption, 7 | type IgnoreIdentifierPatternOption, 8 | ignoreCodePatternOptionSchema, 9 | ignoreIdentifierPatternOptionSchema, 10 | shouldIgnorePattern, 11 | } from "#/options"; 12 | import { ruleNameScope } from "#/utils/misc"; 13 | import type { ESClass } from "#/utils/node-types"; 14 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 15 | 16 | /** 17 | * The name of this rule. 18 | */ 19 | export const name = "no-classes"; 20 | 21 | /** 22 | * The full name of this rule. 23 | */ 24 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 25 | 26 | /** 27 | * The options this rule can take. 28 | */ 29 | type RawOptions = [IgnoreIdentifierPatternOption & IgnoreCodePatternOption]; 30 | 31 | /** 32 | * The schema for the rule options. 33 | */ 34 | const schema: JSONSchema4[] = [ 35 | { 36 | type: "object", 37 | properties: deepmerge(ignoreIdentifierPatternOptionSchema, ignoreCodePatternOptionSchema), 38 | additionalProperties: false, 39 | }, 40 | ]; 41 | 42 | /** 43 | * The default options for the rule. 44 | */ 45 | const defaultOptions: RawOptions = [{}]; 46 | 47 | /** 48 | * The possible error messages. 49 | */ 50 | const errorMessages = { 51 | generic: "Unexpected class, use functions not classes.", 52 | } as const; 53 | 54 | /** 55 | * The meta data for this rule. 56 | */ 57 | const meta: NamedCreateRuleCustomMeta = { 58 | type: "suggestion", 59 | docs: { 60 | category: "No Other Paradigms", 61 | description: "Disallow classes.", 62 | recommended: "recommended", 63 | recommendedSeverity: "error", 64 | requiresTypeChecking: false, 65 | }, 66 | messages: errorMessages, 67 | schema, 68 | }; 69 | 70 | /** 71 | * Check if the given class node violates this rule. 72 | */ 73 | function checkClass( 74 | node: ESClass, 75 | context: Readonly>, 76 | options: Readonly, 77 | ): RuleResult { 78 | const [optionsObject] = options; 79 | const { ignoreIdentifierPattern, ignoreCodePattern } = optionsObject; 80 | 81 | if (shouldIgnorePattern(node, context, ignoreIdentifierPattern, undefined, ignoreCodePattern)) { 82 | return { 83 | context, 84 | descriptors: [], 85 | }; 86 | } 87 | 88 | return { context, descriptors: [{ node, messageId: "generic" }] }; 89 | } 90 | 91 | // Create the rule. 92 | export const rule: Rule = createRule( 93 | name, 94 | meta, 95 | defaultOptions, 96 | { 97 | ClassDeclaration: checkClass, 98 | ClassExpression: checkClass, 99 | }, 100 | ); 101 | -------------------------------------------------------------------------------- /src/rules/no-let.ts: -------------------------------------------------------------------------------- 1 | import type { TSESTree } from "@typescript-eslint/utils"; 2 | import type { JSONSchema4, JSONSchema4ObjectSchema } from "@typescript-eslint/utils/json-schema"; 3 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 4 | import { deepmerge } from "deepmerge-ts"; 5 | 6 | import { 7 | type IgnoreIdentifierPatternOption, 8 | ignoreIdentifierPatternOptionSchema, 9 | shouldIgnoreInFunction, 10 | shouldIgnorePattern, 11 | } from "#/options"; 12 | import { ruleNameScope } from "#/utils/misc"; 13 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 14 | import { isInForLoopInitializer } from "#/utils/tree"; 15 | 16 | /** 17 | * The name of this rule. 18 | */ 19 | export const name = "no-let"; 20 | 21 | /** 22 | * The full name of this rule. 23 | */ 24 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 25 | 26 | /** 27 | * The options this rule can take. 28 | */ 29 | type RawOptions = [ 30 | IgnoreIdentifierPatternOption & { 31 | allowInForLoopInit: boolean; 32 | allowInFunctions: boolean; 33 | }, 34 | ]; 35 | 36 | /** 37 | * The schema for the rule options. 38 | */ 39 | const schema: JSONSchema4[] = [ 40 | { 41 | type: "object", 42 | properties: deepmerge(ignoreIdentifierPatternOptionSchema, { 43 | allowInForLoopInit: { 44 | type: "boolean", 45 | }, 46 | allowInFunctions: { 47 | type: "boolean", 48 | }, 49 | } satisfies JSONSchema4ObjectSchema["properties"]), 50 | additionalProperties: false, 51 | }, 52 | ]; 53 | 54 | /** 55 | * The default options for the rule. 56 | */ 57 | const defaultOptions: RawOptions = [ 58 | { 59 | allowInForLoopInit: false, 60 | allowInFunctions: false, 61 | }, 62 | ]; 63 | 64 | /** 65 | * The possible error messages. 66 | */ 67 | const errorMessages = { 68 | generic: "Unexpected let, use const instead.", 69 | } as const; 70 | 71 | /** 72 | * The meta data for this rule. 73 | */ 74 | const meta: NamedCreateRuleCustomMeta = { 75 | type: "suggestion", 76 | docs: { 77 | category: "No Mutations", 78 | description: "Disallow mutable variables.", 79 | recommended: "recommended", 80 | recommendedSeverity: "error", 81 | requiresTypeChecking: false, 82 | }, 83 | messages: errorMessages, 84 | schema, 85 | }; 86 | 87 | /** 88 | * Check if the given VariableDeclaration violates this rule. 89 | */ 90 | function checkVariableDeclaration( 91 | node: TSESTree.VariableDeclaration, 92 | context: Readonly>, 93 | options: Readonly, 94 | ): RuleResult { 95 | const [optionsObject] = options; 96 | const { allowInForLoopInit, ignoreIdentifierPattern, allowInFunctions } = optionsObject; 97 | 98 | if ( 99 | node.kind !== "let" || 100 | shouldIgnoreInFunction(node, context, allowInFunctions) || 101 | shouldIgnorePattern(node, context, ignoreIdentifierPattern) || 102 | (allowInForLoopInit && isInForLoopInitializer(node)) 103 | ) { 104 | return { 105 | context, 106 | descriptors: [], 107 | }; 108 | } 109 | 110 | return { 111 | context, 112 | descriptors: [{ node, messageId: "generic" }], 113 | }; 114 | } 115 | 116 | // Create the rule. 117 | export const rule: Rule = createRule( 118 | name, 119 | meta, 120 | defaultOptions, 121 | { 122 | VariableDeclaration: checkVariableDeclaration, 123 | }, 124 | ); 125 | -------------------------------------------------------------------------------- /src/rules/no-loop-statements.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; 2 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 3 | 4 | import { ruleNameScope } from "#/utils/misc"; 5 | import type { ESLoop } from "#/utils/node-types"; 6 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 7 | 8 | /** 9 | * The name of this rule. 10 | */ 11 | export const name = "no-loop-statements"; 12 | 13 | /** 14 | * The full name of this rule. 15 | */ 16 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 17 | 18 | /** 19 | * The options this rule can take. 20 | */ 21 | type RawOptions = [{}]; 22 | 23 | /** 24 | * The schema for the rule options. 25 | */ 26 | const schema: JSONSchema4[] = []; 27 | 28 | /** 29 | * The default options for the rule. 30 | */ 31 | const defaultOptions: RawOptions = [{}]; 32 | 33 | /** 34 | * The possible error messages. 35 | */ 36 | const errorMessages = { 37 | generic: "Unexpected loop, use map or reduce instead.", 38 | } as const; 39 | 40 | /** 41 | * The meta data for this rule. 42 | */ 43 | const meta: NamedCreateRuleCustomMeta = { 44 | type: "suggestion", 45 | docs: { 46 | category: "No Statements", 47 | description: "Disallow imperative loops.", 48 | recommended: "recommended", 49 | recommendedSeverity: "error", 50 | requiresTypeChecking: false, 51 | }, 52 | messages: errorMessages, 53 | schema, 54 | }; 55 | 56 | /** 57 | * Check if the given loop violates this rule. 58 | */ 59 | function checkLoop( 60 | node: ESLoop, 61 | context: Readonly>, 62 | ): RuleResult { 63 | // All loops violate this rule. 64 | return { context, descriptors: [{ node, messageId: "generic" }] }; 65 | } 66 | 67 | // Create the rule. 68 | export const rule: Rule = createRule( 69 | name, 70 | meta, 71 | defaultOptions, 72 | { 73 | ForStatement: checkLoop, 74 | ForInStatement: checkLoop, 75 | ForOfStatement: checkLoop, 76 | WhileStatement: checkLoop, 77 | DoWhileStatement: checkLoop, 78 | }, 79 | ); 80 | -------------------------------------------------------------------------------- /src/rules/no-this-expressions.ts: -------------------------------------------------------------------------------- 1 | import type { TSESTree } from "@typescript-eslint/utils"; 2 | import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; 3 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 4 | 5 | import { ruleNameScope } from "#/utils/misc"; 6 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 7 | 8 | /** 9 | * The name of this rule. 10 | */ 11 | export const name = "no-this-expressions"; 12 | 13 | /** 14 | * The full name of this rule. 15 | */ 16 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 17 | 18 | /** 19 | * The options this rule can take. 20 | */ 21 | type RawOptions = [{}]; 22 | 23 | /** 24 | * The schema for the rule options. 25 | */ 26 | const schema: JSONSchema4[] = []; 27 | 28 | /** 29 | * The default options for the rule. 30 | */ 31 | const defaultOptions: RawOptions = [{}]; 32 | 33 | /** 34 | * The possible error messages. 35 | */ 36 | const errorMessages = { 37 | generic: "Unexpected this, use functions not classes.", 38 | } as const; 39 | 40 | /** 41 | * The meta data for this rule. 42 | */ 43 | const meta: NamedCreateRuleCustomMeta = { 44 | type: "suggestion", 45 | docs: { 46 | category: "No Other Paradigms", 47 | description: "Disallow this access.", 48 | recommended: "recommended", 49 | recommendedSeverity: "error", 50 | requiresTypeChecking: false, 51 | }, 52 | messages: errorMessages, 53 | schema, 54 | }; 55 | 56 | /** 57 | * Check if the given ThisExpression violates this rule. 58 | */ 59 | function checkThisExpression( 60 | node: TSESTree.ThisExpression, 61 | context: Readonly>, 62 | ): RuleResult { 63 | // All throw statements violate this rule. 64 | return { context, descriptors: [{ node, messageId: "generic" }] }; 65 | } 66 | 67 | // Create the rule. 68 | export const rule: Rule = createRule( 69 | name, 70 | meta, 71 | defaultOptions, 72 | { 73 | ThisExpression: checkThisExpression, 74 | }, 75 | ); 76 | -------------------------------------------------------------------------------- /src/rules/no-throw-statements.ts: -------------------------------------------------------------------------------- 1 | import type { TSESTree } from "@typescript-eslint/utils"; 2 | import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; 3 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 4 | 5 | import { ruleNameScope } from "#/utils/misc"; 6 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 7 | import { getEnclosingFunction, getEnclosingTryStatement, isInPromiseHandlerFunction } from "#/utils/tree"; 8 | 9 | /** 10 | * The name of this rule. 11 | */ 12 | export const name = "no-throw-statements"; 13 | 14 | /** 15 | * The full name of this rule. 16 | */ 17 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 18 | 19 | /** 20 | * The options this rule can take. 21 | */ 22 | type RawOptions = [ 23 | { 24 | allowToRejectPromises: boolean; 25 | }, 26 | ]; 27 | 28 | /** 29 | * The schema for the rule options. 30 | */ 31 | const schema: JSONSchema4[] = [ 32 | { 33 | type: "object", 34 | properties: { 35 | allowToRejectPromises: { 36 | type: "boolean", 37 | }, 38 | }, 39 | additionalProperties: false, 40 | }, 41 | ]; 42 | 43 | /** 44 | * The default options for the rule. 45 | */ 46 | const defaultOptions: RawOptions = [ 47 | { 48 | allowToRejectPromises: false, 49 | }, 50 | ]; 51 | 52 | /** 53 | * The possible error messages. 54 | */ 55 | const errorMessages = { 56 | generic: "Unexpected throw, throwing exceptions is not functional.", 57 | } as const; 58 | 59 | /** 60 | * The meta data for this rule. 61 | */ 62 | const meta: NamedCreateRuleCustomMeta = { 63 | type: "suggestion", 64 | docs: { 65 | category: "No Exceptions", 66 | description: "Disallow throwing exceptions.", 67 | recommended: "recommended", 68 | recommendedSeverity: "error", 69 | requiresTypeChecking: false, 70 | }, 71 | messages: errorMessages, 72 | schema, 73 | }; 74 | 75 | /** 76 | * Check if the given ThrowStatement violates this rule. 77 | */ 78 | function checkThrowStatement( 79 | node: TSESTree.ThrowStatement, 80 | context: Readonly>, 81 | options: Readonly, 82 | ): RuleResult { 83 | const [{ allowToRejectPromises }] = options; 84 | 85 | if (!allowToRejectPromises) { 86 | return { context, descriptors: [{ node, messageId: "generic" }] }; 87 | } 88 | 89 | if (isInPromiseHandlerFunction(node, context)) { 90 | return { context, descriptors: [] }; 91 | } 92 | 93 | const enclosingFunction = getEnclosingFunction(node); 94 | if (enclosingFunction?.async !== true) { 95 | return { context, descriptors: [{ node, messageId: "generic" }] }; 96 | } 97 | 98 | const enclosingTryStatement = getEnclosingTryStatement(node); 99 | if ( 100 | !( 101 | enclosingTryStatement === null || 102 | getEnclosingFunction(enclosingTryStatement) !== enclosingFunction || 103 | enclosingTryStatement.handler === null 104 | ) 105 | ) { 106 | return { context, descriptors: [{ node, messageId: "generic" }] }; 107 | } 108 | 109 | return { 110 | context, 111 | descriptors: [], 112 | }; 113 | } 114 | 115 | // Create the rule. 116 | export const rule: Rule = createRule( 117 | name, 118 | meta, 119 | defaultOptions, 120 | { 121 | ThrowStatement: checkThrowStatement, 122 | }, 123 | ); 124 | -------------------------------------------------------------------------------- /src/rules/no-try-statements.ts: -------------------------------------------------------------------------------- 1 | import type { TSESTree } from "@typescript-eslint/utils"; 2 | import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; 3 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 4 | 5 | import { ruleNameScope } from "#/utils/misc"; 6 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 7 | 8 | /** 9 | * The name of this rule. 10 | */ 11 | export const name = "no-try-statements"; 12 | 13 | /** 14 | * The full name of this rule. 15 | */ 16 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 17 | 18 | /** 19 | * The options this rule can take. 20 | */ 21 | type RawOptions = [ 22 | { 23 | allowCatch: boolean; 24 | allowFinally: boolean; 25 | }, 26 | ]; 27 | 28 | /** 29 | * The schema for the rule options. 30 | */ 31 | const schema: JSONSchema4[] = [ 32 | { 33 | type: "object", 34 | properties: { 35 | allowCatch: { 36 | type: "boolean", 37 | }, 38 | allowFinally: { 39 | type: "boolean", 40 | }, 41 | }, 42 | additionalProperties: false, 43 | }, 44 | ]; 45 | 46 | /** 47 | * The default options for the rule. 48 | */ 49 | const defaultOptions: RawOptions = [ 50 | { 51 | allowCatch: false, 52 | allowFinally: false, 53 | }, 54 | ]; 55 | 56 | /** 57 | * The possible error messages. 58 | */ 59 | const errorMessages = { 60 | catch: "Unexpected try-catch, this pattern is not functional.", 61 | finally: "Unexpected try-finally, this pattern is not functional.", 62 | } as const; 63 | 64 | /** 65 | * The meta data for this rule. 66 | */ 67 | const meta: NamedCreateRuleCustomMeta = { 68 | type: "suggestion", 69 | docs: { 70 | category: "No Exceptions", 71 | description: "Disallow try-catch[-finally] and try-finally patterns.", 72 | recommended: "recommended", 73 | recommendedSeverity: "error", 74 | requiresTypeChecking: false, 75 | }, 76 | messages: errorMessages, 77 | schema, 78 | }; 79 | 80 | /** 81 | * Check if the given TryStatement violates this rule. 82 | */ 83 | function checkTryStatement( 84 | node: TSESTree.TryStatement, 85 | context: Readonly>, 86 | options: Readonly, 87 | ): RuleResult { 88 | const [{ allowCatch, allowFinally }] = options; 89 | 90 | return { 91 | context, 92 | descriptors: 93 | !allowCatch && node.handler !== null 94 | ? [{ node, messageId: "catch" }] 95 | : !allowFinally && node.finalizer !== null 96 | ? [{ node, messageId: "finally" }] 97 | : [], 98 | }; 99 | } 100 | 101 | // Create the rule. 102 | export const rule: Rule = createRule( 103 | name, 104 | meta, 105 | defaultOptions, 106 | { 107 | TryStatement: checkTryStatement, 108 | }, 109 | ); 110 | -------------------------------------------------------------------------------- /src/rules/prefer-property-signatures.ts: -------------------------------------------------------------------------------- 1 | import type { TSESTree } from "@typescript-eslint/utils"; 2 | import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; 3 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 4 | 5 | import { ruleNameScope } from "#/utils/misc"; 6 | import { type NamedCreateRuleCustomMeta, type Rule, type RuleResult, createRule } from "#/utils/rule"; 7 | import { isInReadonly } from "#/utils/tree"; 8 | 9 | /** 10 | * The name of this rule. 11 | */ 12 | export const name = "prefer-property-signatures"; 13 | 14 | /** 15 | * The full name of this rule. 16 | */ 17 | export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameScope}/${name}`; 18 | 19 | /** 20 | * The options this rule can take. 21 | */ 22 | type RawOptions = [ 23 | { 24 | ignoreIfReadonlyWrapped: boolean; 25 | }, 26 | ]; 27 | 28 | /** 29 | * The schema for the rule options. 30 | */ 31 | const schema: JSONSchema4[] = [ 32 | { 33 | type: "object", 34 | properties: { 35 | ignoreIfReadonlyWrapped: { 36 | type: "boolean", 37 | default: false, 38 | }, 39 | }, 40 | additionalProperties: false, 41 | }, 42 | ]; 43 | 44 | /** 45 | * The default options for the rule. 46 | */ 47 | const defaultOptions: RawOptions = [ 48 | { 49 | ignoreIfReadonlyWrapped: false, 50 | }, 51 | ]; 52 | 53 | /** 54 | * The possible error messages. 55 | */ 56 | const errorMessages = { 57 | generic: "Use a property signature instead of a method signature", 58 | } as const; 59 | 60 | /** 61 | * The meta data for this rule. 62 | */ 63 | const meta: NamedCreateRuleCustomMeta = { 64 | type: "suggestion", 65 | docs: { 66 | category: "Stylistic", 67 | description: "Prefer property signatures over method signatures.", 68 | recommended: "recommended", 69 | recommendedSeverity: "error", 70 | requiresTypeChecking: true, 71 | }, 72 | messages: errorMessages, 73 | schema, 74 | }; 75 | 76 | /** 77 | * Check if the given TSMethodSignature violates this rule. 78 | */ 79 | function checkTSMethodSignature( 80 | node: TSESTree.TSMethodSignature, 81 | context: Readonly>, 82 | options: Readonly, 83 | ): RuleResult { 84 | const [{ ignoreIfReadonlyWrapped }] = options; 85 | 86 | if (ignoreIfReadonlyWrapped && isInReadonly(node)) { 87 | return { context, descriptors: [] }; 88 | } 89 | 90 | // All TS method signatures violate this rule. 91 | return { context, descriptors: [{ node, messageId: "generic" }] }; 92 | } 93 | 94 | // Create the rule. 95 | export const rule: Rule = createRule( 96 | name, 97 | meta, 98 | defaultOptions, 99 | { 100 | TSMethodSignature: checkTSMethodSignature, 101 | }, 102 | ); 103 | -------------------------------------------------------------------------------- /src/settings/immutability.ts: -------------------------------------------------------------------------------- 1 | import type { SharedConfigurationSettings } from "@typescript-eslint/utils"; 2 | import { 3 | Immutability, 4 | type ImmutabilityOverrides, 5 | type TypeSpecifier, 6 | getDefaultOverrides as getDefaultImmutabilityOverrides, 7 | } from "is-immutable-type"; 8 | 9 | declare module "@typescript-eslint/utils" { 10 | type OverridesSetting = { 11 | type: TypeSpecifier; 12 | to: Immutability | keyof typeof Immutability; 13 | from?: Immutability | keyof typeof Immutability; 14 | }; 15 | 16 | // eslint-disable-next-line ts/consistent-type-definitions, ts/no-shadow 17 | interface SharedConfigurationSettings { 18 | immutability?: { 19 | overrides?: 20 | | OverridesSetting[] 21 | | { 22 | keepDefault?: boolean; 23 | values?: OverridesSetting[]; 24 | }; 25 | }; 26 | } 27 | } 28 | 29 | /** 30 | * The settings that have been loaded - so we don't have to reload them. 31 | */ 32 | const cachedSettings = new WeakMap< 33 | NonNullable, 34 | ImmutabilityOverrides | undefined 35 | >(); 36 | 37 | /** 38 | * Get the immutability overrides defined in the settings. 39 | */ 40 | export function getImmutabilityOverrides({ 41 | immutability, 42 | }: Readonly): ImmutabilityOverrides | undefined { 43 | if (immutability === undefined) { 44 | return undefined; 45 | } 46 | if (!cachedSettings.has(immutability)) { 47 | const overrides = loadImmutabilityOverrides(immutability); 48 | 49 | cachedSettings.set(immutability, overrides); 50 | return overrides; 51 | } 52 | return cachedSettings.get(immutability); 53 | } 54 | 55 | /** 56 | * Get all the overrides and upgrade them. 57 | */ 58 | function loadImmutabilityOverrides( 59 | immutabilitySettings: SharedConfigurationSettings["immutability"], 60 | ): ImmutabilityOverrides | undefined { 61 | const overridesSetting = immutabilitySettings?.overrides; 62 | 63 | if (overridesSetting === undefined) { 64 | return undefined; 65 | } 66 | 67 | const raw = Array.isArray(overridesSetting) ? overridesSetting : (overridesSetting.values ?? []); 68 | 69 | const upgraded = raw.map((rawValue) => { 70 | const { type, to, from, ...rest } = rawValue; 71 | const value = { 72 | type, 73 | to: typeof to === "string" ? Immutability[to] : to, 74 | from: from === undefined ? undefined : typeof from === "string" ? Immutability[from] : from, 75 | } as ImmutabilityOverrides[number]; 76 | 77 | if (value.type === undefined) { 78 | throw new Error(`Override is missing required "type" property. Value: "${JSON.stringify(rawValue)}"`); 79 | } 80 | if (value.to === undefined) { 81 | throw new Error(`Override is missing required "to" property. Value: "${JSON.stringify(rawValue)}"`); 82 | } 83 | const restKeys = Object.keys(rest); 84 | if (restKeys.length > 0) { 85 | throw new Error( 86 | `Override is contains unknown property(s) "${restKeys.join(", ")}". Value: "${JSON.stringify(rawValue)}"`, 87 | ); 88 | } 89 | 90 | return value; 91 | }); 92 | 93 | const keepDefault = Array.isArray(overridesSetting) || overridesSetting.keepDefault !== false; 94 | 95 | return keepDefault ? [...getDefaultImmutabilityOverrides(), ...upgraded] : upgraded; 96 | } 97 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./immutability"; 2 | -------------------------------------------------------------------------------- /src/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noCheck": true, 5 | "declaration": false, 6 | "isolatedDeclarations": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "declaration": true, 6 | "isolatedDeclarations": true, 7 | "rootDir": "." 8 | }, 9 | "include": [".", ".*", "../typings"] 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/conditional-imports/ts-api-utils.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | 3 | import type * as tsApiUtils from "ts-api-utils"; 4 | 5 | import typescript from "#/conditional-imports/typescript"; 6 | 7 | const require = createRequire(import.meta.url); 8 | 9 | export default (typescript === undefined 10 | ? undefined 11 | : (() => { 12 | try { 13 | return require("ts-api-utils"); 14 | } catch { 15 | return undefined; 16 | } 17 | })()) as typeof tsApiUtils | undefined; 18 | 19 | // export default (await (() => { 20 | // if (ts !== undefined) { 21 | // return import("ts-api-utils").catch(() => undefined); 22 | // } 23 | // return Promise.resolve(undefined); 24 | // })()) as typeof tsApiUtils | undefined; 25 | -------------------------------------------------------------------------------- /src/utils/conditional-imports/typescript.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | 3 | import type typescript from "typescript"; 4 | 5 | const require = createRequire(import.meta.url); 6 | 7 | export default (() => { 8 | try { 9 | return require("typescript"); 10 | } catch { 11 | return undefined; 12 | } 13 | })() as typeof typescript | undefined; 14 | 15 | // export default (await import("typescript") 16 | // .then((r) => r.default) 17 | // .catch(() => undefined)) as typeof typescript | undefined; 18 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line ts/naming-convention -- This is a special var. 2 | export const __VERSION__ = "0.0.0-development"; 3 | -------------------------------------------------------------------------------- /src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils"; 2 | import type { RuleContext } from "@typescript-eslint/utils/ts-eslint"; 3 | 4 | import type { BaseOptions } from "#/utils/rule"; 5 | import { getKeyOfValueInObjectExpression } from "#/utils/tree"; 6 | import { 7 | hasID, 8 | hasKey, 9 | isAssignmentExpression, 10 | isChainExpression, 11 | isDefined, 12 | isIdentifier, 13 | isMemberExpression, 14 | isPrivateIdentifier, 15 | isTSAsExpression, 16 | isTSNonNullExpression, 17 | isTSTypeAnnotation, 18 | isThisExpression, 19 | isUnaryExpression, 20 | isVariableDeclaration, 21 | } from "#/utils/type-guards"; 22 | 23 | export const ruleNameScope = "functional"; 24 | 25 | /** 26 | * Does the given ExpressionStatement specify directive prologues. 27 | */ 28 | export function isDirectivePrologue(node: TSESTree.ExpressionStatement): boolean { 29 | return ( 30 | node.expression.type === AST_NODE_TYPES.Literal && 31 | typeof node.expression.value === "string" && 32 | node.expression.value.startsWith("use ") 33 | ); 34 | } 35 | 36 | /** 37 | * Get the identifier text of the given node. 38 | */ 39 | function getNodeIdentifierText( 40 | node: TSESTree.Node | null | undefined, 41 | context: Readonly>, 42 | ): string | undefined { 43 | if (!isDefined(node)) { 44 | return undefined; 45 | } 46 | 47 | let mut_identifierText: string | undefined | null = null; 48 | 49 | if (isIdentifier(node)) { 50 | mut_identifierText = node.name; 51 | } else if (isPrivateIdentifier(node)) { 52 | mut_identifierText = `#${node.name}`; 53 | } else if (hasID(node) && isDefined(node.id)) { 54 | mut_identifierText = getNodeIdentifierText(node.id, context); 55 | } else if (hasKey(node) && isDefined(node.key)) { 56 | mut_identifierText = getNodeIdentifierText(node.key, context); 57 | } else if (isAssignmentExpression(node)) { 58 | mut_identifierText = getNodeIdentifierText(node.left, context); 59 | } else if (isMemberExpression(node)) { 60 | mut_identifierText = `${getNodeIdentifierText(node.object, context)}.${getNodeIdentifierText( 61 | node.property, 62 | context, 63 | )}`; 64 | } else if (isThisExpression(node)) { 65 | mut_identifierText = "this"; 66 | } else if (isUnaryExpression(node)) { 67 | mut_identifierText = getNodeIdentifierText(node.argument, context); 68 | } else if (isTSTypeAnnotation(node)) { 69 | mut_identifierText = context.sourceCode.getText(node.typeAnnotation as TSESTree.Node).replaceAll(/\s+/gu, ""); 70 | } else if (isTSAsExpression(node) || isTSNonNullExpression(node) || isChainExpression(node)) { 71 | mut_identifierText = getNodeIdentifierText(node.expression, context); 72 | } 73 | 74 | if (mut_identifierText !== null) { 75 | return mut_identifierText; 76 | } 77 | 78 | const keyInObjectExpression = getKeyOfValueInObjectExpression(node); 79 | if (keyInObjectExpression !== null) { 80 | return keyInObjectExpression; 81 | } 82 | 83 | return undefined; 84 | } 85 | 86 | /** 87 | * Get the code of the given node. 88 | */ 89 | export function getNodeCode(node: TSESTree.Node, context: Readonly>): string { 90 | return context.sourceCode.getText(node); 91 | } 92 | 93 | /** 94 | * Get all the identifier texts of the given node. 95 | */ 96 | export function getNodeIdentifierTexts( 97 | node: TSESTree.Node, 98 | context: Readonly>, 99 | ): string[] { 100 | return ( 101 | isVariableDeclaration(node) 102 | ? node.declarations.flatMap((declarator) => getNodeIdentifierText(declarator, context)) 103 | : [getNodeIdentifierText(node, context)] 104 | ).filter((text): text is string => text !== undefined); 105 | } 106 | -------------------------------------------------------------------------------- /src/utils/node-types.ts: -------------------------------------------------------------------------------- 1 | import type { TSESTree } from "@typescript-eslint/utils"; 2 | 3 | export type ESFunction = TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression; 4 | 5 | export type ESFunctionType = 6 | | ESFunction 7 | | TSESTree.TSCallSignatureDeclaration 8 | | TSESTree.TSConstructSignatureDeclaration 9 | | TSESTree.TSDeclareFunction 10 | | TSESTree.TSEmptyBodyFunctionExpression 11 | | TSESTree.TSFunctionType 12 | | TSESTree.TSMethodSignature; 13 | 14 | export type ESClass = TSESTree.ClassDeclaration | TSESTree.ClassExpression; 15 | 16 | export type ESLoop = 17 | | TSESTree.DoWhileStatement 18 | | TSESTree.ForInStatement 19 | | TSESTree.ForOfStatement 20 | | TSESTree.ForStatement 21 | | TSESTree.WhileStatement; 22 | 23 | export type ESArrayTupleType = TSESTree.TSArrayType | TSESTree.TSTupleType; 24 | 25 | export type ESProperty = 26 | | TSESTree.PropertyDefinition 27 | | TSESTree.TSIndexSignature 28 | | TSESTree.TSParameterProperty 29 | | TSESTree.TSPropertySignature; 30 | 31 | export type ESTypeDeclaration = TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration; 32 | -------------------------------------------------------------------------------- /src/utils/schemas.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema4, JSONSchema4ObjectSchema } from "@typescript-eslint/utils/json-schema"; 2 | import { deepmerge } from "deepmerge-ts"; 3 | 4 | const typeSpecifierPatternSchemaProperties: JSONSchema4ObjectSchema["properties"] = { 5 | name: schemaInstanceOrInstanceArray({ 6 | type: "string", 7 | }), 8 | pattern: schemaInstanceOrInstanceArray({ 9 | type: "string", 10 | }), 11 | ignoreName: schemaInstanceOrInstanceArray({ 12 | type: "string", 13 | }), 14 | ignorePattern: schemaInstanceOrInstanceArray({ 15 | type: "string", 16 | }), 17 | }; 18 | 19 | const typeSpecifierSchema: JSONSchema4 = { 20 | oneOf: [ 21 | { 22 | type: "object", 23 | properties: { 24 | ...typeSpecifierPatternSchemaProperties, 25 | from: { 26 | type: "string", 27 | enum: ["file"], 28 | }, 29 | path: { 30 | type: "string", 31 | }, 32 | }, 33 | additionalProperties: false, 34 | }, 35 | { 36 | type: "object", 37 | properties: { 38 | ...typeSpecifierPatternSchemaProperties, 39 | from: { 40 | type: "string", 41 | enum: ["lib"], 42 | }, 43 | }, 44 | additionalProperties: false, 45 | }, 46 | { 47 | type: "object", 48 | properties: { 49 | ...typeSpecifierPatternSchemaProperties, 50 | from: { 51 | type: "string", 52 | enum: ["package"], 53 | }, 54 | package: { 55 | type: "string", 56 | }, 57 | }, 58 | additionalProperties: false, 59 | }, 60 | ], 61 | }; 62 | 63 | export function schemaInstanceOrInstanceArray( 64 | items: JSONSchema4, 65 | ): NonNullable[string] { 66 | return { 67 | oneOf: [ 68 | items, 69 | { 70 | type: "array", 71 | items, 72 | }, 73 | ], 74 | }; 75 | } 76 | 77 | export function overridableOptionsSchema( 78 | coreOptionsPropertiesSchema: NonNullable, 79 | ): JSONSchema4 { 80 | return { 81 | type: "object", 82 | properties: deepmerge(coreOptionsPropertiesSchema, { 83 | overrides: { 84 | type: "array", 85 | items: { 86 | type: "object", 87 | properties: { 88 | specifiers: schemaInstanceOrInstanceArray(typeSpecifierSchema), 89 | options: { 90 | type: "object", 91 | properties: coreOptionsPropertiesSchema, 92 | additionalProperties: false, 93 | }, 94 | inherit: { 95 | type: "boolean", 96 | }, 97 | disable: { 98 | type: "boolean", 99 | }, 100 | }, 101 | additionalProperties: false, 102 | }, 103 | }, 104 | } satisfies JSONSchema4ObjectSchema["properties"]), 105 | additionalProperties: false, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /tests/configs.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import all from "#/configs/all"; 4 | import currying from "#/configs/currying"; 5 | import lite from "#/configs/lite"; 6 | import noExceptions from "#/configs/no-exceptions"; 7 | import noMutations from "#/configs/no-mutations"; 8 | import noOtherParadigms from "#/configs/no-other-paradigms"; 9 | import noStatements from "#/configs/no-statements"; 10 | import off from "#/configs/off"; 11 | import recommended from "#/configs/recommended"; 12 | import strict from "#/configs/strict"; 13 | import stylistic from "#/configs/stylistic"; 14 | import { rules } from "#/rules"; 15 | 16 | describe("configs", () => { 17 | const allRules = Object.values(rules); 18 | const allConfigRules = Object.keys(all); 19 | const offConfigRules = Object.entries(off); 20 | const allNonDeprecatedRules = allRules.filter((rule) => rule.meta.deprecated !== true); 21 | 22 | it('"All" - should have the right number of rules', () => { 23 | expect(allConfigRules.length).to.equal(allNonDeprecatedRules.length); 24 | }); 25 | 26 | it.each(allConfigRules)('"All" - should have not have deprecated rules', (name) => { 27 | expect(rules[name.slice("functional/".length) as keyof typeof rules].meta.deprecated).to.not.equal( 28 | true, 29 | `All Config contains deprecated rule "${name}".`, 30 | ); 31 | }); 32 | 33 | it('"Off" - should have the right number of rules', () => { 34 | expect(offConfigRules.length).to.equal(allRules.length, "should have every rule"); 35 | }); 36 | 37 | it.each(offConfigRules)('"Off" - should turn off all rules', (name, value) => { 38 | const severity = Array.isArray(value) ? value[0] : value; 39 | expect(severity).to.equal("off", `Rule "${name}" should be turned off in the off config.`); 40 | }); 41 | 42 | /** 43 | * A map of each config (except the special ones) to it's name. 44 | */ 45 | const configs = [ 46 | ["Currying", currying], 47 | ["Recommended", recommended], 48 | ["Lite", lite], 49 | ["Functional Strict", strict], 50 | ["No Exceptions", noExceptions], 51 | ["No Mutations", noMutations], 52 | ["No Other Paradigms", noOtherParadigms], 53 | ["No Statements", noStatements], 54 | ["Stylistic", stylistic], 55 | ] as const; 56 | 57 | describe.each(configs)('"%s" Config rules are in the "All" Config', (name, config) => { 58 | const ruleNames = Object.keys(config); 59 | 60 | it.each(ruleNames)(`%s`, (rule) => { 61 | expect(all[rule]).toBeDefined(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/fixture/file.ts: -------------------------------------------------------------------------------- 1 | // File needs to exist. 2 | -------------------------------------------------------------------------------- /tests/fixture/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tests the index file. 3 | */ 4 | 5 | import { readdirSync } from "node:fs"; 6 | 7 | import { describe, expect, it } from "vitest"; 8 | 9 | import functional from "#"; 10 | 11 | describe("index", () => { 12 | it("should have all the rules", () => { 13 | const ruleFiles: string[] = readdirSync("./src/rules").filter( 14 | (file) => file !== "index.ts" && file.endsWith(".ts"), 15 | ); 16 | 17 | expect(Object.hasOwn(functional, "rules"), 'The plugin\'s config object should have a "rules" property.'); 18 | expect(ruleFiles.length).to.equal(Object.keys(functional.rules ?? {}).length); 19 | }); 20 | 21 | it("should have all the configs", () => { 22 | const configFiles: string[] = readdirSync("./src/configs").filter( 23 | (file) => file !== "index.ts" && file.endsWith(".ts"), 24 | ); 25 | 26 | expect(Object.hasOwn(functional, "configs"), 'The plugin\'s config object should have a "configs" property.'); 27 | expect(configFiles.length).to.equal( 28 | Object.keys(functional.configs ?? {}).length, 29 | "should have all the configs except deprecated", 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/functional-parameters.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`functional-parameters > javascript - es latest > options > enforceParameterCount > atLeastOne 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 2, 8 | "endLine": 3, 9 | "line": 1, 10 | "message": "Functions must have at least one parameter.", 11 | "messageId": "paramCountAtLeastOne", 12 | "nodeType": "FunctionDeclaration", 13 | "ruleId": "functional-parameters", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`functional-parameters > javascript - es latest > options > enforceParameterCount > exactlyOne 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 2, 24 | "endLine": 3, 25 | "line": 1, 26 | "message": "Functions must have exactly one parameter.", 27 | "messageId": "paramCountExactlyOne", 28 | "nodeType": "FunctionDeclaration", 29 | "ruleId": "functional-parameters", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`functional-parameters > javascript - es latest > options > enforceParameterCount > exactlyOne 2`] = ` 36 | [ 37 | { 38 | "column": 1, 39 | "endColumn": 2, 40 | "endLine": 3, 41 | "line": 1, 42 | "message": "Functions must have exactly one parameter.", 43 | "messageId": "paramCountExactlyOne", 44 | "nodeType": "FunctionDeclaration", 45 | "ruleId": "functional-parameters", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`functional-parameters > javascript - es latest > options > enforceParameterCount > ignoreIIFE 1`] = ` 52 | [ 53 | { 54 | "column": 2, 55 | "endColumn": 2, 56 | "endLine": 3, 57 | "line": 1, 58 | "message": "Functions must have at least one parameter.", 59 | "messageId": "paramCountAtLeastOne", 60 | "nodeType": "ArrowFunctionExpression", 61 | "ruleId": "functional-parameters", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | 67 | exports[`functional-parameters > javascript - es latest > options > enforceParameterCount > ignoreLambdaExpression 1`] = ` 68 | [ 69 | { 70 | "column": 1, 71 | "endColumn": 18, 72 | "endLine": 1, 73 | "line": 1, 74 | "message": "Functions must have at least one parameter.", 75 | "messageId": "paramCountAtLeastOne", 76 | "nodeType": "FunctionDeclaration", 77 | "ruleId": "functional-parameters", 78 | "severity": 2, 79 | }, 80 | ] 81 | `; 82 | 83 | exports[`functional-parameters > javascript - es latest > options > ignoreIdentifierPattern 1`] = ` 84 | [ 85 | { 86 | "column": 14, 87 | "endColumn": 20, 88 | "endLine": 1, 89 | "line": 1, 90 | "message": "Unexpected rest parameter. Use a regular parameter of type array instead.", 91 | "messageId": "restParam", 92 | "nodeType": "RestElement", 93 | "ruleId": "functional-parameters", 94 | "severity": 2, 95 | }, 96 | ] 97 | `; 98 | 99 | exports[`functional-parameters > javascript - es latest > reports arguments keyword violations 1`] = ` 100 | [ 101 | { 102 | "column": 15, 103 | "endColumn": 24, 104 | "endLine": 2, 105 | "line": 2, 106 | "message": "Unexpected use of \`arguments\`. Use regular function arguments instead.", 107 | "messageId": "arguments", 108 | "nodeType": "Identifier", 109 | "ruleId": "functional-parameters", 110 | "severity": 2, 111 | }, 112 | ] 113 | `; 114 | 115 | exports[`functional-parameters > javascript - es latest > reports rest parameter violations 1`] = ` 116 | [ 117 | { 118 | "column": 14, 119 | "endColumn": 20, 120 | "endLine": 1, 121 | "line": 1, 122 | "message": "Unexpected rest parameter. Use a regular parameter of type array instead.", 123 | "messageId": "restParam", 124 | "nodeType": "RestElement", 125 | "ruleId": "functional-parameters", 126 | "severity": 2, 127 | }, 128 | ] 129 | `; 130 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-class-inheritance.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-class-inheritance > javascript - es latest > ignoreCodePattern > should report class inheritance with non-matching identifiers 1`] = ` 4 | [ 5 | { 6 | "column": 11, 7 | "endColumn": 22, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Unexpected inheritance, use composition instead.", 11 | "messageId": "extends", 12 | "nodeType": "ClassDeclaration", 13 | "ruleId": "no-class-inheritance", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-class-inheritance > javascript - es latest > options > ignoreIdentifierPattern > should report class inheritance with non-matching identifiers 1`] = ` 20 | [ 21 | { 22 | "column": 11, 23 | "endColumn": 22, 24 | "endLine": 1, 25 | "line": 1, 26 | "message": "Unexpected inheritance, use composition instead.", 27 | "messageId": "extends", 28 | "nodeType": "ClassDeclaration", 29 | "ruleId": "no-class-inheritance", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-class-inheritance > javascript - es latest > reports class inheritance 1`] = ` 36 | [ 37 | { 38 | "column": 11, 39 | "endColumn": 22, 40 | "endLine": 1, 41 | "line": 1, 42 | "message": "Unexpected inheritance, use composition instead.", 43 | "messageId": "extends", 44 | "nodeType": "ClassDeclaration", 45 | "ruleId": "no-class-inheritance", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-class-inheritance > typescript > ignoreCodePattern > should report class inheritance with non-matching identifiers 1`] = ` 52 | [ 53 | { 54 | "column": 1, 55 | "endColumn": 9, 56 | "endLine": 1, 57 | "line": 1, 58 | "message": "Unexpected abstract class.", 59 | "messageId": "abstract", 60 | "nodeType": "ClassDeclaration", 61 | "ruleId": "no-class-inheritance", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | 67 | exports[`no-class-inheritance > typescript > options > ignoreIdentifierPattern > should report class inheritance with non-matching identifiers 1`] = ` 68 | [ 69 | { 70 | "column": 1, 71 | "endColumn": 9, 72 | "endLine": 1, 73 | "line": 1, 74 | "message": "Unexpected abstract class.", 75 | "messageId": "abstract", 76 | "nodeType": "ClassDeclaration", 77 | "ruleId": "no-class-inheritance", 78 | "severity": 2, 79 | }, 80 | ] 81 | `; 82 | 83 | exports[`no-class-inheritance > typescript > reports class inheritance 1`] = ` 84 | [ 85 | { 86 | "column": 1, 87 | "endColumn": 9, 88 | "endLine": 1, 89 | "line": 1, 90 | "message": "Unexpected abstract class.", 91 | "messageId": "abstract", 92 | "nodeType": "ClassDeclaration", 93 | "ruleId": "no-class-inheritance", 94 | "severity": 2, 95 | }, 96 | ] 97 | `; 98 | 99 | exports[`no-class-inheritance > typescript > reports class inheritance 2`] = ` 100 | [ 101 | { 102 | "column": 1, 103 | "endColumn": 9, 104 | "endLine": 1, 105 | "line": 1, 106 | "message": "Unexpected abstract class.", 107 | "messageId": "abstract", 108 | "nodeType": "ClassDeclaration", 109 | "ruleId": "no-class-inheritance", 110 | "severity": 2, 111 | }, 112 | { 113 | "column": 20, 114 | "endColumn": 31, 115 | "endLine": 1, 116 | "line": 1, 117 | "message": "Unexpected inheritance, use composition instead.", 118 | "messageId": "extends", 119 | "nodeType": "ClassDeclaration", 120 | "ruleId": "no-class-inheritance", 121 | "severity": 2, 122 | }, 123 | ] 124 | `; 125 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-classes.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-classes > javascript - es latest > ignoreCodePattern > should report classes with non-matching identifiers 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 13, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Unexpected class, use functions not classes.", 11 | "messageId": "generic", 12 | "nodeType": "ClassDeclaration", 13 | "ruleId": "no-classes", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-classes > javascript - es latest > options > ignoreIdentifierPattern > should report classes with non-matching identifiers 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 13, 24 | "endLine": 1, 25 | "line": 1, 26 | "message": "Unexpected class, use functions not classes.", 27 | "messageId": "generic", 28 | "nodeType": "ClassDeclaration", 29 | "ruleId": "no-classes", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-classes > javascript - es latest > reports class declarations 1`] = ` 36 | [ 37 | { 38 | "column": 1, 39 | "endColumn": 13, 40 | "endLine": 1, 41 | "line": 1, 42 | "message": "Unexpected class, use functions not classes.", 43 | "messageId": "generic", 44 | "nodeType": "ClassDeclaration", 45 | "ruleId": "no-classes", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-classes > javascript - es latest > reports class declarations 2`] = ` 52 | [ 53 | { 54 | "column": 15, 55 | "endColumn": 23, 56 | "endLine": 1, 57 | "line": 1, 58 | "message": "Unexpected class, use functions not classes.", 59 | "messageId": "generic", 60 | "nodeType": "ClassExpression", 61 | "ruleId": "no-classes", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-conditional-statements.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-conditional-statements > typescript > if statements > options > allowReturningBranches > ifExhaustive > else required 1`] = ` 4 | [ 5 | { 6 | "column": 3, 7 | "endColumn": 4, 8 | "endLine": 4, 9 | "line": 2, 10 | "message": "Incomplete if, it must have an else statement and every branch must contain a return statement.", 11 | "messageId": "incompleteIf", 12 | "nodeType": "IfStatement", 13 | "ruleId": "no-conditional-statements", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-conditional-statements > typescript > if statements > reports if statements 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 2, 24 | "endLine": 3, 25 | "line": 1, 26 | "message": "Unexpected if, use a conditional expression (ternary operator) instead.", 27 | "messageId": "unexpectedIf", 28 | "nodeType": "IfStatement", 29 | "ruleId": "no-conditional-statements", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-conditional-statements > typescript > switch statements > options > allowReturningBranches > ifExhaustive > requires default case 1`] = ` 36 | [ 37 | { 38 | "column": 3, 39 | "endColumn": 4, 40 | "endLine": 7, 41 | "line": 2, 42 | "message": "Incomplete switch, it must be exhaustive or have an default case and every case must contain a return statement.", 43 | "messageId": "incompleteSwitch", 44 | "nodeType": "SwitchStatement", 45 | "ruleId": "no-conditional-statements", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-conditional-statements > typescript > switch statements > reports switch statements 1`] = ` 52 | [ 53 | { 54 | "column": 1, 55 | "endColumn": 2, 56 | "endLine": 7, 57 | "line": 1, 58 | "message": "Unexpected switch, use a conditional expression (ternary operator) instead.", 59 | "messageId": "unexpectedSwitch", 60 | "nodeType": "SwitchStatement", 61 | "ruleId": "no-conditional-statements", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-expression-statements.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-expression-statements > javascript - es latest > options > ignoreCodePattern 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 17, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Using expressions to cause side-effects not allowed.", 11 | "messageId": "generic", 12 | "nodeType": "ExpressionStatement", 13 | "ruleId": "no-expression-statements", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-expression-statements > javascript - es latest > reports expression statements 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 11, 24 | "endLine": 2, 25 | "line": 2, 26 | "message": "Using expressions to cause side-effects not allowed.", 27 | "messageId": "generic", 28 | "nodeType": "ExpressionStatement", 29 | "ruleId": "no-expression-statements", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-expression-statements > typescript > options > ignoreSelfReturning 1`] = ` 36 | [ 37 | { 38 | "column": 1, 39 | "endColumn": 7, 40 | "endLine": 2, 41 | "line": 2, 42 | "message": "Using expressions to cause side-effects not allowed.", 43 | "messageId": "generic", 44 | "nodeType": "ExpressionStatement", 45 | "ruleId": "no-expression-statements", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-let.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-let > javascript - es latest > options > allowInForLoopInit > should not report let declarations in for loop init 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 7, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Unexpected let, use const instead.", 11 | "messageId": "generic", 12 | "nodeType": "VariableDeclaration", 13 | "ruleId": "no-let", 14 | "severity": 2, 15 | }, 16 | { 17 | "column": 1, 18 | "endColumn": 11, 19 | "endLine": 2, 20 | "line": 2, 21 | "message": "Unexpected let, use const instead.", 22 | "messageId": "generic", 23 | "nodeType": "VariableDeclaration", 24 | "ruleId": "no-let", 25 | "severity": 2, 26 | }, 27 | ] 28 | `; 29 | 30 | exports[`no-let > javascript - es latest > options > allowInFunctions > should not report let declarations in arrow function declarations 1`] = ` 31 | [ 32 | { 33 | "column": 1, 34 | "endColumn": 7, 35 | "endLine": 1, 36 | "line": 1, 37 | "message": "Unexpected let, use const instead.", 38 | "messageId": "generic", 39 | "nodeType": "VariableDeclaration", 40 | "ruleId": "no-let", 41 | "severity": 2, 42 | }, 43 | { 44 | "column": 1, 45 | "endColumn": 11, 46 | "endLine": 2, 47 | "line": 2, 48 | "message": "Unexpected let, use const instead.", 49 | "messageId": "generic", 50 | "nodeType": "VariableDeclaration", 51 | "ruleId": "no-let", 52 | "severity": 2, 53 | }, 54 | ] 55 | `; 56 | 57 | exports[`no-let > javascript - es latest > options > allowInFunctions > should not report let declarations in function declarations 1`] = ` 58 | [ 59 | { 60 | "column": 1, 61 | "endColumn": 7, 62 | "endLine": 1, 63 | "line": 1, 64 | "message": "Unexpected let, use const instead.", 65 | "messageId": "generic", 66 | "nodeType": "VariableDeclaration", 67 | "ruleId": "no-let", 68 | "severity": 2, 69 | }, 70 | { 71 | "column": 1, 72 | "endColumn": 11, 73 | "endLine": 2, 74 | "line": 2, 75 | "message": "Unexpected let, use const instead.", 76 | "messageId": "generic", 77 | "nodeType": "VariableDeclaration", 78 | "ruleId": "no-let", 79 | "severity": 2, 80 | }, 81 | ] 82 | `; 83 | 84 | exports[`no-let > javascript - es latest > options > allowInFunctions > should not report let declarations in method declarations 1`] = ` 85 | [ 86 | { 87 | "column": 1, 88 | "endColumn": 7, 89 | "endLine": 1, 90 | "line": 1, 91 | "message": "Unexpected let, use const instead.", 92 | "messageId": "generic", 93 | "nodeType": "VariableDeclaration", 94 | "ruleId": "no-let", 95 | "severity": 2, 96 | }, 97 | { 98 | "column": 1, 99 | "endColumn": 11, 100 | "endLine": 2, 101 | "line": 2, 102 | "message": "Unexpected let, use const instead.", 103 | "messageId": "generic", 104 | "nodeType": "VariableDeclaration", 105 | "ruleId": "no-let", 106 | "severity": 2, 107 | }, 108 | ] 109 | `; 110 | 111 | exports[`no-let > javascript - es latest > should report let declarations 1`] = ` 112 | [ 113 | { 114 | "column": 1, 115 | "endColumn": 7, 116 | "endLine": 1, 117 | "line": 1, 118 | "message": "Unexpected let, use const instead.", 119 | "messageId": "generic", 120 | "nodeType": "VariableDeclaration", 121 | "ruleId": "no-let", 122 | "severity": 2, 123 | }, 124 | { 125 | "column": 3, 126 | "endColumn": 9, 127 | "endLine": 4, 128 | "line": 4, 129 | "message": "Unexpected let, use const instead.", 130 | "messageId": "generic", 131 | "nodeType": "VariableDeclaration", 132 | "ruleId": "no-let", 133 | "severity": 2, 134 | }, 135 | { 136 | "column": 3, 137 | "endColumn": 13, 138 | "endLine": 5, 139 | "line": 5, 140 | "message": "Unexpected let, use const instead.", 141 | "messageId": "generic", 142 | "nodeType": "VariableDeclaration", 143 | "ruleId": "no-let", 144 | "severity": 2, 145 | }, 146 | ] 147 | `; 148 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-loop-statements.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-loop-statements > javascript - es latest > reports do while loop statements 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 16, 8 | "endLine": 3, 9 | "line": 1, 10 | "message": "Unexpected loop, use map or reduce instead.", 11 | "messageId": "generic", 12 | "nodeType": "DoWhileStatement", 13 | "ruleId": "no-loop-statements", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-loop-statements > javascript - es latest > reports for await loop statements 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 2, 24 | "endLine": 3, 25 | "line": 1, 26 | "message": "Unexpected loop, use map or reduce instead.", 27 | "messageId": "generic", 28 | "nodeType": "ForOfStatement", 29 | "ruleId": "no-loop-statements", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-loop-statements > javascript - es latest > reports for in loop statements 1`] = ` 36 | [ 37 | { 38 | "column": 1, 39 | "endColumn": 2, 40 | "endLine": 3, 41 | "line": 1, 42 | "message": "Unexpected loop, use map or reduce instead.", 43 | "messageId": "generic", 44 | "nodeType": "ForInStatement", 45 | "ruleId": "no-loop-statements", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-loop-statements > javascript - es latest > reports for loop statements 1`] = ` 52 | [ 53 | { 54 | "column": 1, 55 | "endColumn": 2, 56 | "endLine": 3, 57 | "line": 1, 58 | "message": "Unexpected loop, use map or reduce instead.", 59 | "messageId": "generic", 60 | "nodeType": "ForStatement", 61 | "ruleId": "no-loop-statements", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | 67 | exports[`no-loop-statements > javascript - es latest > reports for of loop statements 1`] = ` 68 | [ 69 | { 70 | "column": 1, 71 | "endColumn": 2, 72 | "endLine": 3, 73 | "line": 1, 74 | "message": "Unexpected loop, use map or reduce instead.", 75 | "messageId": "generic", 76 | "nodeType": "ForOfStatement", 77 | "ruleId": "no-loop-statements", 78 | "severity": 2, 79 | }, 80 | ] 81 | `; 82 | 83 | exports[`no-loop-statements > javascript - es latest > reports while loop statements 1`] = ` 84 | [ 85 | { 86 | "column": 1, 87 | "endColumn": 2, 88 | "endLine": 3, 89 | "line": 1, 90 | "message": "Unexpected loop, use map or reduce instead.", 91 | "messageId": "generic", 92 | "nodeType": "WhileStatement", 93 | "ruleId": "no-loop-statements", 94 | "severity": 2, 95 | }, 96 | ] 97 | `; 98 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-mixed-types.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-mixed-types > typescript > options > checkInterfaces > should report mixed types in interfaces when enabled 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 46, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Only the same kind of members allowed in types.", 11 | "messageId": "generic", 12 | "nodeType": "TSInterfaceDeclaration", 13 | "ruleId": "no-mixed-types", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-mixed-types > typescript > options > checkTypeLiterals > should report mixed types in type literals when enabled 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 3, 24 | "endLine": 4, 25 | "line": 1, 26 | "message": "Only the same kind of members allowed in types.", 27 | "messageId": "generic", 28 | "nodeType": "TSTypeAliasDeclaration", 29 | "ruleId": "no-mixed-types", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-mixed-types > typescript > reports mixed types in interfaces 1`] = ` 36 | [ 37 | { 38 | "column": 1, 39 | "endColumn": 2, 40 | "endLine": 4, 41 | "line": 1, 42 | "message": "Only the same kind of members allowed in types.", 43 | "messageId": "generic", 44 | "nodeType": "TSInterfaceDeclaration", 45 | "ruleId": "no-mixed-types", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-mixed-types > typescript > reports mixed types in type literals 1`] = ` 52 | [ 53 | { 54 | "column": 1, 55 | "endColumn": 3, 56 | "endLine": 4, 57 | "line": 1, 58 | "message": "Only the same kind of members allowed in types.", 59 | "messageId": "generic", 60 | "nodeType": "TSTypeAliasDeclaration", 61 | "ruleId": "no-mixed-types", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-promise-reject.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-promise-reject > javascript - es latest > reports Promise.reject 1`] = ` 4 | [ 5 | { 6 | "column": 10, 7 | "endColumn": 39, 8 | "endLine": 2, 9 | "line": 2, 10 | "message": "Unexpected rejection, resolve an error instead.", 11 | "messageId": "generic", 12 | "nodeType": "CallExpression", 13 | "ruleId": "no-promise-reject", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-promise-reject > javascript - es latest > reports new Promise(reject) 1`] = ` 20 | [ 21 | { 22 | "column": 32, 23 | "endColumn": 38, 24 | "endLine": 2, 25 | "line": 2, 26 | "message": "Unexpected rejection, resolve an error instead.", 27 | "messageId": "generic", 28 | "nodeType": "Identifier", 29 | "ruleId": "no-promise-reject", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-promise-reject > javascript - es latest > reports throw in async functions 1`] = ` 36 | [ 37 | { 38 | "column": 3, 39 | "endColumn": 34, 40 | "endLine": 2, 41 | "line": 2, 42 | "message": "Unexpected rejection, resolve an error instead.", 43 | "messageId": "generic", 44 | "nodeType": "ThrowStatement", 45 | "ruleId": "no-promise-reject", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-promise-reject > javascript - es latest > reports throw in try without catch in async functions 1`] = ` 52 | [ 53 | { 54 | "column": 5, 55 | "endColumn": 30, 56 | "endLine": 3, 57 | "line": 3, 58 | "message": "Unexpected rejection, resolve an error instead.", 59 | "messageId": "generic", 60 | "nodeType": "ThrowStatement", 61 | "ruleId": "no-promise-reject", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-return-void.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-return-void > typescript > options > allowNull > reports null returning functions when disallowed 1`] = ` 4 | [ 5 | { 6 | "column": 26, 7 | "endColumn": 32, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Function must return a value.", 11 | "messageId": "generic", 12 | "nodeType": "TSTypeAnnotation", 13 | "ruleId": "no-return-void", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-return-void > typescript > options > allowNull > reports null returning functions with inferred return type when disallowed 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 2, 24 | "endLine": 3, 25 | "line": 1, 26 | "message": "Function must return a value.", 27 | "messageId": "generic", 28 | "nodeType": "FunctionDeclaration", 29 | "ruleId": "no-return-void", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-return-void > typescript > options > allowUndefined > reports undefined returning functions when disallowed 1`] = ` 36 | [ 37 | { 38 | "column": 26, 39 | "endColumn": 37, 40 | "endLine": 1, 41 | "line": 1, 42 | "message": "Function must return a value.", 43 | "messageId": "generic", 44 | "nodeType": "TSTypeAnnotation", 45 | "ruleId": "no-return-void", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-return-void > typescript > options > allowUndefined > reports undefined returning functions with inferred return type when disallowed 1`] = ` 52 | [ 53 | { 54 | "column": 1, 55 | "endColumn": 2, 56 | "endLine": 3, 57 | "line": 1, 58 | "message": "Function must return a value.", 59 | "messageId": "generic", 60 | "nodeType": "FunctionDeclaration", 61 | "ruleId": "no-return-void", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | 67 | exports[`no-return-void > typescript > reports void returning functions 1`] = ` 68 | [ 69 | { 70 | "column": 26, 71 | "endColumn": 32, 72 | "endLine": 1, 73 | "line": 1, 74 | "message": "Function must return a value.", 75 | "messageId": "generic", 76 | "nodeType": "TSTypeAnnotation", 77 | "ruleId": "no-return-void", 78 | "severity": 2, 79 | }, 80 | ] 81 | `; 82 | 83 | exports[`no-return-void > typescript > reports void returning functions with inferred return type 1`] = ` 84 | [ 85 | { 86 | "column": 1, 87 | "endColumn": 2, 88 | "endLine": 3, 89 | "line": 1, 90 | "message": "Function must return a value.", 91 | "messageId": "generic", 92 | "nodeType": "FunctionDeclaration", 93 | "ruleId": "no-return-void", 94 | "severity": 2, 95 | }, 96 | ] 97 | `; 98 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-this-expressions.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-this-expressions > javascript - es latest > reports this expressions 1`] = ` 4 | { 5 | "fixed": false, 6 | "messages": [ 7 | { 8 | "column": 3, 9 | "endColumn": 7, 10 | "endLine": 2, 11 | "line": 2, 12 | "message": "Unexpected this, use functions not classes.", 13 | "messageId": "generic", 14 | "nodeType": "ThisExpression", 15 | "ruleId": "no-this-expressions", 16 | "severity": 2, 17 | }, 18 | ], 19 | "output": "function foo() { 20 | this.bar(); 21 | }", 22 | "steps": [ 23 | { 24 | "fixed": false, 25 | "messages": [ 26 | { 27 | "column": 3, 28 | "endColumn": 7, 29 | "endLine": 2, 30 | "line": 2, 31 | "message": "Unexpected this, use functions not classes.", 32 | "messageId": "generic", 33 | "nodeType": "ThisExpression", 34 | "ruleId": "rule-to-test/no-this-expressions", 35 | "severity": 2, 36 | }, 37 | ], 38 | "output": "function foo() { 39 | this.bar(); 40 | }", 41 | }, 42 | ], 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-throw-statements.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-throw-statements > javascript - es latest > options > allowToRejectPromises > reports throw statements in functions nested in async functions 1`] = ` 4 | [ 5 | { 6 | "column": 5, 7 | "endColumn": 23, 8 | "endLine": 3, 9 | "line": 3, 10 | "message": "Unexpected throw, throwing exceptions is not functional.", 11 | "messageId": "generic", 12 | "nodeType": "ThrowStatement", 13 | "ruleId": "no-throw-statements", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-throw-statements > javascript - es latest > options > allowToRejectPromises > reports throw statements in try with catch in async functions 1`] = ` 20 | [ 21 | { 22 | "column": 5, 23 | "endColumn": 36, 24 | "endLine": 3, 25 | "line": 3, 26 | "message": "Unexpected throw, throwing exceptions is not functional.", 27 | "messageId": "generic", 28 | "nodeType": "ThrowStatement", 29 | "ruleId": "no-throw-statements", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-throw-statements > javascript - es latest > reports throw statements in async functions 1`] = ` 36 | [ 37 | { 38 | "column": 3, 39 | "endColumn": 21, 40 | "endLine": 2, 41 | "line": 2, 42 | "message": "Unexpected throw, throwing exceptions is not functional.", 43 | "messageId": "generic", 44 | "nodeType": "ThrowStatement", 45 | "ruleId": "no-throw-statements", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | 51 | exports[`no-throw-statements > javascript - es latest > reports throw statements of Errors 1`] = ` 52 | [ 53 | { 54 | "column": 3, 55 | "endColumn": 21, 56 | "endLine": 2, 57 | "line": 2, 58 | "message": "Unexpected throw, throwing exceptions is not functional.", 59 | "messageId": "generic", 60 | "nodeType": "ThrowStatement", 61 | "ruleId": "no-throw-statements", 62 | "severity": 2, 63 | }, 64 | ] 65 | `; 66 | 67 | exports[`no-throw-statements > javascript - es latest > reports throw statements of strings 1`] = ` 68 | [ 69 | { 70 | "column": 3, 71 | "endColumn": 17, 72 | "endLine": 2, 73 | "line": 2, 74 | "message": "Unexpected throw, throwing exceptions is not functional.", 75 | "messageId": "generic", 76 | "nodeType": "ThrowStatement", 77 | "ruleId": "no-throw-statements", 78 | "severity": 2, 79 | }, 80 | ] 81 | `; 82 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/no-try-statements.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`no-try-statements > javascript - es latest > options > allowCatch > reports try statements with catch and finally 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 2, 8 | "endLine": 7, 9 | "line": 1, 10 | "message": "Unexpected try-finally, this pattern is not functional.", 11 | "messageId": "finally", 12 | "nodeType": "TryStatement", 13 | "ruleId": "no-try-statements", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`no-try-statements > javascript - es latest > options > allowFinally > reports try statements with catch and finally 1`] = ` 20 | [ 21 | { 22 | "column": 1, 23 | "endColumn": 2, 24 | "endLine": 7, 25 | "line": 1, 26 | "message": "Unexpected try-catch, this pattern is not functional.", 27 | "messageId": "catch", 28 | "nodeType": "TryStatement", 29 | "ruleId": "no-try-statements", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | 35 | exports[`no-try-statements > javascript - es latest > reports try statements 1`] = ` 36 | [ 37 | { 38 | "column": 1, 39 | "endColumn": 2, 40 | "endLine": 5, 41 | "line": 1, 42 | "message": "Unexpected try-catch, this pattern is not functional.", 43 | "messageId": "catch", 44 | "nodeType": "TryStatement", 45 | "ruleId": "no-try-statements", 46 | "severity": 2, 47 | }, 48 | ] 49 | `; 50 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/prefer-property-signatures.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`prefer-property-signatures > typescript > reports method signatures in interfaces 1`] = ` 4 | [ 5 | { 6 | "column": 3, 7 | "endColumn": 37, 8 | "endLine": 2, 9 | "line": 2, 10 | "message": "Use a property signature instead of a method signature", 11 | "messageId": "generic", 12 | "nodeType": "TSMethodSignature", 13 | "ruleId": "prefer-property-signatures", 14 | "severity": 2, 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`prefer-property-signatures > typescript > reports method signatures in type literals 1`] = ` 20 | [ 21 | { 22 | "column": 3, 23 | "endColumn": 37, 24 | "endLine": 2, 25 | "line": 2, 26 | "message": "Use a property signature instead of a method signature", 27 | "messageId": "generic", 28 | "nodeType": "TSMethodSignature", 29 | "ruleId": "prefer-property-signatures", 30 | "severity": 2, 31 | }, 32 | ] 33 | `; 34 | -------------------------------------------------------------------------------- /tests/rules/__snapshots__/prefer-tacit.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`prefer-tacit > typescript > options > checkMemberExpressions > report member expressions when enabled 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 35, 8 | "endLine": 2, 9 | "line": 2, 10 | "message": "Potentially unnecessary function wrapper.", 11 | "messageId": "generic", 12 | "nodeType": "FunctionDeclaration", 13 | "ruleId": "prefer-tacit", 14 | "severity": 2, 15 | "suggestions": [ 16 | { 17 | "desc": "Remove unnecessary function wrapper.", 18 | "fix": { 19 | "range": [ 20 | 46, 21 | 80, 22 | ], 23 | "text": "const foo = a.b.bind(a);", 24 | }, 25 | "messageId": "genericSuggestion", 26 | }, 27 | ], 28 | }, 29 | ] 30 | `; 31 | 32 | exports[`prefer-tacit > typescript > options > checkMemberExpressions > report member expressions when enabled 2`] = ` 33 | [ 34 | { 35 | "column": 13, 36 | "endColumn": 33, 37 | "endLine": 1, 38 | "line": 1, 39 | "message": "Potentially unnecessary function wrapper.", 40 | "messageId": "generic", 41 | "nodeType": "ArrowFunctionExpression", 42 | "ruleId": "prefer-tacit", 43 | "severity": 2, 44 | "suggestions": [ 45 | { 46 | "desc": "Remove unnecessary function wrapper.", 47 | "fix": { 48 | "range": [ 49 | 12, 50 | 32, 51 | ], 52 | "text": "/a/.test.bind(/a/)", 53 | }, 54 | "messageId": "genericSuggestion", 55 | }, 56 | ], 57 | }, 58 | ] 59 | `; 60 | 61 | exports[`prefer-tacit > typescript > reports functions that are just instantiations 1`] = ` 62 | [ 63 | { 64 | "column": 1, 65 | "endColumn": 41, 66 | "endLine": 2, 67 | "line": 2, 68 | "message": "Potentially unnecessary function wrapper.", 69 | "messageId": "generic", 70 | "nodeType": "FunctionDeclaration", 71 | "ruleId": "prefer-tacit", 72 | "severity": 2, 73 | "suggestions": [ 74 | { 75 | "desc": "Remove unnecessary function wrapper.", 76 | "fix": { 77 | "range": [ 78 | 26, 79 | 66, 80 | ], 81 | "text": "const foo = f;", 82 | }, 83 | "messageId": "genericSuggestion", 84 | }, 85 | ], 86 | }, 87 | ] 88 | `; 89 | 90 | exports[`prefer-tacit > typescript > reports functions that can "safely" be changed 1`] = ` 91 | [ 92 | { 93 | "column": 13, 94 | "endColumn": 22, 95 | "endLine": 2, 96 | "line": 2, 97 | "message": "Potentially unnecessary function wrapper.", 98 | "messageId": "generic", 99 | "nodeType": "ArrowFunctionExpression", 100 | "ruleId": "prefer-tacit", 101 | "severity": 2, 102 | "suggestions": [ 103 | { 104 | "desc": "Remove unnecessary function wrapper.", 105 | "fix": { 106 | "range": [ 107 | 29, 108 | 38, 109 | ], 110 | "text": "f", 111 | }, 112 | "messageId": "genericSuggestion", 113 | }, 114 | ], 115 | }, 116 | ] 117 | `; 118 | 119 | exports[`prefer-tacit > typescript > reports functions that can "safely" be changed 2`] = ` 120 | [ 121 | { 122 | "column": 27, 123 | "endColumn": 42, 124 | "endLine": 1, 125 | "line": 1, 126 | "message": "Potentially unnecessary function wrapper.", 127 | "messageId": "generic", 128 | "nodeType": "ArrowFunctionExpression", 129 | "ruleId": "prefer-tacit", 130 | "severity": 2, 131 | "suggestions": [ 132 | { 133 | "desc": "Remove unnecessary function wrapper.", 134 | "fix": { 135 | "range": [ 136 | 26, 137 | 41, 138 | ], 139 | "text": "Boolean", 140 | }, 141 | "messageId": "genericSuggestion", 142 | }, 143 | ], 144 | }, 145 | ] 146 | `; 147 | -------------------------------------------------------------------------------- /tests/rules/immutable-data/__snapshots__/map.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`immutable-data > typescript > options > ignoreImmediateMutation > reports immediately mutation when disabled 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 28, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Modifying a map is not allowed.", 11 | "messageId": "map", 12 | "nodeType": "CallExpression", 13 | "ruleId": "immutable-data", 14 | "severity": 2, 15 | }, 16 | { 17 | "column": 1, 18 | "endColumn": 28, 19 | "endLine": 2, 20 | "line": 2, 21 | "message": "Modifying a map is not allowed.", 22 | "messageId": "map", 23 | "nodeType": "CallExpression", 24 | "ruleId": "immutable-data", 25 | "severity": 2, 26 | }, 27 | { 28 | "column": 1, 29 | "endColumn": 26, 30 | "endLine": 3, 31 | "line": 3, 32 | "message": "Modifying a map is not allowed.", 33 | "messageId": "map", 34 | "nodeType": "CallExpression", 35 | "ruleId": "immutable-data", 36 | "severity": 2, 37 | }, 38 | ] 39 | `; 40 | 41 | exports[`immutable-data > typescript > options > ignoreNonConstDeclarations > reports variables declared as const 1`] = ` 42 | [ 43 | { 44 | "column": 1, 45 | "endColumn": 12, 46 | "endLine": 2, 47 | "line": 2, 48 | "message": "Modifying a map is not allowed.", 49 | "messageId": "map", 50 | "nodeType": "CallExpression", 51 | "ruleId": "immutable-data", 52 | "severity": 2, 53 | }, 54 | { 55 | "column": 1, 56 | "endColumn": 12, 57 | "endLine": 3, 58 | "line": 3, 59 | "message": "Modifying a map is not allowed.", 60 | "messageId": "map", 61 | "nodeType": "CallExpression", 62 | "ruleId": "immutable-data", 63 | "severity": 2, 64 | }, 65 | { 66 | "column": 1, 67 | "endColumn": 10, 68 | "endLine": 4, 69 | "line": 4, 70 | "message": "Modifying a map is not allowed.", 71 | "messageId": "map", 72 | "nodeType": "CallExpression", 73 | "ruleId": "immutable-data", 74 | "severity": 2, 75 | }, 76 | ] 77 | `; 78 | 79 | exports[`immutable-data > typescript > report mutating map methods 1`] = ` 80 | [ 81 | { 82 | "column": 1, 83 | "endColumn": 12, 84 | "endLine": 2, 85 | "line": 2, 86 | "message": "Modifying a map is not allowed.", 87 | "messageId": "map", 88 | "nodeType": "CallExpression", 89 | "ruleId": "immutable-data", 90 | "severity": 2, 91 | }, 92 | { 93 | "column": 1, 94 | "endColumn": 12, 95 | "endLine": 3, 96 | "line": 3, 97 | "message": "Modifying a map is not allowed.", 98 | "messageId": "map", 99 | "nodeType": "CallExpression", 100 | "ruleId": "immutable-data", 101 | "severity": 2, 102 | }, 103 | { 104 | "column": 1, 105 | "endColumn": 10, 106 | "endLine": 4, 107 | "line": 4, 108 | "message": "Modifying a map is not allowed.", 109 | "messageId": "map", 110 | "nodeType": "CallExpression", 111 | "ruleId": "immutable-data", 112 | "severity": 2, 113 | }, 114 | ] 115 | `; 116 | -------------------------------------------------------------------------------- /tests/rules/immutable-data/__snapshots__/set.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`immutable-data > typescript > options > ignoreImmediateMutation > reports immediately mutation when disabled 1`] = ` 4 | [ 5 | { 6 | "column": 1, 7 | "endColumn": 23, 8 | "endLine": 1, 9 | "line": 1, 10 | "message": "Modifying a set is not allowed.", 11 | "messageId": "set", 12 | "nodeType": "CallExpression", 13 | "ruleId": "immutable-data", 14 | "severity": 2, 15 | }, 16 | { 17 | "column": 1, 18 | "endColumn": 26, 19 | "endLine": 2, 20 | "line": 2, 21 | "message": "Modifying a set is not allowed.", 22 | "messageId": "set", 23 | "nodeType": "CallExpression", 24 | "ruleId": "immutable-data", 25 | "severity": 2, 26 | }, 27 | { 28 | "column": 1, 29 | "endColumn": 24, 30 | "endLine": 3, 31 | "line": 3, 32 | "message": "Modifying a set is not allowed.", 33 | "messageId": "set", 34 | "nodeType": "CallExpression", 35 | "ruleId": "immutable-data", 36 | "severity": 2, 37 | }, 38 | ] 39 | `; 40 | 41 | exports[`immutable-data > typescript > options > ignoreNonConstDeclarations > reports variables declared as const 1`] = ` 42 | [ 43 | { 44 | "column": 1, 45 | "endColumn": 9, 46 | "endLine": 2, 47 | "line": 2, 48 | "message": "Modifying a set is not allowed.", 49 | "messageId": "set", 50 | "nodeType": "CallExpression", 51 | "ruleId": "immutable-data", 52 | "severity": 2, 53 | }, 54 | { 55 | "column": 1, 56 | "endColumn": 12, 57 | "endLine": 3, 58 | "line": 3, 59 | "message": "Modifying a set is not allowed.", 60 | "messageId": "set", 61 | "nodeType": "CallExpression", 62 | "ruleId": "immutable-data", 63 | "severity": 2, 64 | }, 65 | { 66 | "column": 1, 67 | "endColumn": 10, 68 | "endLine": 4, 69 | "line": 4, 70 | "message": "Modifying a set is not allowed.", 71 | "messageId": "set", 72 | "nodeType": "CallExpression", 73 | "ruleId": "immutable-data", 74 | "severity": 2, 75 | }, 76 | ] 77 | `; 78 | 79 | exports[`immutable-data > typescript > report mutating set methods 1`] = ` 80 | [ 81 | { 82 | "column": 1, 83 | "endColumn": 9, 84 | "endLine": 2, 85 | "line": 2, 86 | "message": "Modifying a set is not allowed.", 87 | "messageId": "set", 88 | "nodeType": "CallExpression", 89 | "ruleId": "immutable-data", 90 | "severity": 2, 91 | }, 92 | { 93 | "column": 1, 94 | "endColumn": 12, 95 | "endLine": 3, 96 | "line": 3, 97 | "message": "Modifying a set is not allowed.", 98 | "messageId": "set", 99 | "nodeType": "CallExpression", 100 | "ruleId": "immutable-data", 101 | "severity": 2, 102 | }, 103 | { 104 | "column": 1, 105 | "endColumn": 10, 106 | "endLine": 4, 107 | "line": 4, 108 | "message": "Modifying a set is not allowed.", 109 | "messageId": "set", 110 | "nodeType": "CallExpression", 111 | "ruleId": "immutable-data", 112 | "severity": 2, 113 | }, 114 | ] 115 | `; 116 | -------------------------------------------------------------------------------- /tests/rules/immutable-data/map.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/immutable-data"; 6 | 7 | import { typescriptConfig } from "../../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("typescript", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: typescriptConfig, 15 | }); 16 | 17 | it("report mutating map methods", () => { 18 | const invalidResult = invalid({ 19 | code: dedent` 20 | const x = new Map([[5, 6]]); 21 | x.set(4, 8); 22 | x.delete(4); 23 | x.clear(); 24 | `, 25 | options: [], 26 | errors: ["map", "map", "map"], 27 | }); 28 | expect(invalidResult.messages).toMatchSnapshot(); 29 | }); 30 | 31 | it("doesn't report mutating map methods when ignoring maps and sets", () => { 32 | valid({ 33 | code: dedent` 34 | const x = new Map([[5, 6]]); 35 | x.set(4, 8); 36 | x.delete(4); 37 | x.clear(); 38 | `, 39 | options: [ 40 | { 41 | ignoreMapsAndSets: true, 42 | }, 43 | ], 44 | }); 45 | }); 46 | 47 | it("doesn't report non-mutating map methods", () => { 48 | valid(dedent` 49 | const x = new Map([[5, 6]]); 50 | x.size; 51 | x.has(4); 52 | x.values(); 53 | x.entries(); 54 | x.keys(); 55 | `); 56 | }); 57 | 58 | it("doesn't report mutating map methods on non-map objects", () => { 59 | valid(dedent` 60 | const z = { 61 | set: function () {}, 62 | delete: function () {}, 63 | clear: function () {}, 64 | }; 65 | 66 | z.set(); 67 | z.delete(); 68 | z.clear(); 69 | `); 70 | }); 71 | 72 | describe("options", () => { 73 | describe("ignoreNonConstDeclarations", () => { 74 | it("reports variables declared as const", () => { 75 | const invalidResult = invalid({ 76 | code: dedent` 77 | const x = new Map([[5, 6]]); 78 | x.set(4, 8); 79 | x.delete(4); 80 | x.clear(); 81 | `, 82 | options: [ 83 | { 84 | ignoreNonConstDeclarations: true, 85 | }, 86 | ], 87 | errors: ["map", "map", "map"], 88 | }); 89 | expect(invalidResult.messages).toMatchSnapshot(); 90 | }); 91 | 92 | it("doesn't report variables not declared as const", () => { 93 | valid({ 94 | code: dedent` 95 | let x = new Map([[5, 6]]); 96 | x.set(4, 8); 97 | x.delete(4); 98 | x.clear(); 99 | `, 100 | options: [ 101 | { 102 | ignoreNonConstDeclarations: true, 103 | }, 104 | ], 105 | }); 106 | }); 107 | }); 108 | 109 | describe("ignoreImmediateMutation", () => { 110 | it("doesn't report immediately mutation when enabled", () => { 111 | valid({ 112 | code: dedent` 113 | new Map([[5, 6]]).set(4, 8); 114 | new Map([[5, 6]]).delete(4); 115 | new Map([[5, 6]]).clear(); 116 | `, 117 | options: [ 118 | { 119 | ignoreImmediateMutation: true, 120 | }, 121 | ], 122 | }); 123 | }); 124 | 125 | it("reports immediately mutation when disabled", () => { 126 | const invalidResult = invalid({ 127 | code: dedent` 128 | new Map([[5, 6]]).set(4, 8); 129 | new Map([[5, 6]]).delete(4); 130 | new Map([[5, 6]]).clear(); 131 | `, 132 | options: [ 133 | { 134 | ignoreImmediateMutation: false, 135 | }, 136 | ], 137 | errors: ["map", "map", "map"], 138 | }); 139 | expect(invalidResult.messages).toMatchSnapshot(); 140 | }); 141 | }); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /tests/rules/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "node:fs"; 2 | 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { rules } from "#/rules"; 6 | 7 | describe("rules index", () => { 8 | it("import all the rule files", () => { 9 | const rulesNames: string[] = Object.keys(rules); 10 | const files: string[] = fs.readdirSync("./src/rules").filter((file) => file !== "index.ts" && file.endsWith(".ts")); 11 | 12 | expect(rulesNames.length).to.equal(files.length); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/rules/no-classes.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/no-classes"; 6 | 7 | import { esLatestConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("javascript - es latest", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: esLatestConfig, 15 | }); 16 | 17 | it("doesn't report non-issues", () => { 18 | valid("function Foo() {}"); 19 | }); 20 | 21 | it("reports class declarations", () => { 22 | const invalidResult1 = invalid({ 23 | code: "class Foo {}", 24 | errors: ["generic"], 25 | }); 26 | expect(invalidResult1.messages).toMatchSnapshot(); 27 | 28 | const invalidResult2 = invalid({ 29 | code: "const klass = class {}", 30 | errors: ["generic"], 31 | }); 32 | expect(invalidResult2.messages).toMatchSnapshot(); 33 | }); 34 | 35 | describe("options", () => { 36 | describe("ignoreIdentifierPattern", () => { 37 | it("should not report classes with matching identifiers", () => { 38 | valid({ 39 | code: dedent` 40 | class Foo {} 41 | `, 42 | options: [{ ignoreIdentifierPattern: "^Foo$" }], 43 | }); 44 | }); 45 | 46 | it("should report classes with non-matching identifiers", () => { 47 | const invalidResult = invalid({ 48 | code: dedent` 49 | class Bar {} 50 | `, 51 | options: [{ ignoreIdentifierPattern: "^Foo$" }], 52 | errors: ["generic"], 53 | }); 54 | expect(invalidResult.messages).toMatchSnapshot(); 55 | }); 56 | }); 57 | }); 58 | 59 | describe("ignoreCodePattern", () => { 60 | it("should not report classes with matching identifiers", () => { 61 | valid({ 62 | code: dedent` 63 | class Foo {} 64 | `, 65 | options: [{ ignoreCodePattern: "class Foo" }], 66 | }); 67 | }); 68 | 69 | it("should report classes with non-matching identifiers", () => { 70 | const invalidResult = invalid({ 71 | code: dedent` 72 | class Bar {} 73 | `, 74 | options: [{ ignoreCodePattern: "class Foo" }], 75 | errors: ["generic"], 76 | }); 77 | expect(invalidResult.messages).toMatchSnapshot(); 78 | }); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /tests/rules/no-expression-statements.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/no-expression-statements"; 6 | 7 | import { esLatestConfig, typescriptConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("javascript - es latest", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: esLatestConfig, 15 | }); 16 | 17 | it("reports expression statements", () => { 18 | const invalidResult = invalid({ 19 | code: dedent` 20 | var x = []; 21 | x.push(1); 22 | `, 23 | errors: ["generic"], 24 | }); 25 | expect(invalidResult.messages).toMatchSnapshot(); 26 | }); 27 | 28 | it("doesn't report variable declarations", () => { 29 | valid(dedent` 30 | var x = []; 31 | let y = []; 32 | const z = []; 33 | `); 34 | }); 35 | 36 | it("doesn't report directive prologues", () => { 37 | valid(dedent` 38 | "use strict"; 39 | "use server"; 40 | "use client"; 41 | `); 42 | }); 43 | 44 | it("doesn't report yield", () => { 45 | valid(dedent` 46 | export function* foo() { 47 | yield "hello"; 48 | return "world"; 49 | } 50 | `); 51 | }); 52 | 53 | describe("options", () => { 54 | it("ignoreCodePattern", () => { 55 | valid({ 56 | code: dedent` 57 | console.log("yo"); 58 | console.error("yo"); 59 | `, 60 | options: [{ ignoreCodePattern: "^console\\." }], 61 | }); 62 | 63 | valid({ 64 | code: dedent` 65 | assert(1 !== 2); 66 | `, 67 | options: [{ ignoreCodePattern: "^assert" }], 68 | }); 69 | 70 | const invalidResult = invalid({ 71 | code: `console.trace();`, 72 | options: [{ ignoreCodePattern: "^console\\.log" }], 73 | errors: ["generic"], 74 | }); 75 | expect(invalidResult.messages).toMatchSnapshot(); 76 | }); 77 | }); 78 | }); 79 | 80 | describe("typescript", () => { 81 | const { valid, invalid } = createRuleTester({ 82 | name, 83 | rule, 84 | configs: typescriptConfig, 85 | }); 86 | 87 | describe("options", () => { 88 | it("ignoreVoid", () => { 89 | valid({ 90 | code: dedent` 91 | console.log("yo"); 92 | console.error("yo"); 93 | `, 94 | options: [{ ignoreVoid: true }], 95 | }); 96 | 97 | valid({ 98 | code: dedent` 99 | function foo() { return Promise.resolve(); } 100 | foo(); 101 | `, 102 | options: [{ ignoreVoid: true }], 103 | }); 104 | }); 105 | 106 | it("ignoreSelfReturning", () => { 107 | valid({ 108 | code: dedent` 109 | function foo() { return this; } 110 | foo(); 111 | `, 112 | options: [{ ignoreSelfReturning: true }], 113 | }); 114 | 115 | valid({ 116 | code: dedent` 117 | const foo = { bar() { return this; }}; 118 | foo.bar(); 119 | `, 120 | options: [{ ignoreSelfReturning: true }], 121 | }); 122 | 123 | valid({ 124 | code: dedent` 125 | class Foo { bar() { return this; }}; 126 | const foo = new Foo(); 127 | foo.bar(); 128 | `, 129 | options: [{ ignoreSelfReturning: true }], 130 | }); 131 | 132 | const invalidResult = invalid({ 133 | code: dedent` 134 | const foo = () => { return this; }; 135 | foo(); 136 | `, 137 | options: [{ ignoreSelfReturning: true }], 138 | errors: ["generic"], 139 | }); 140 | expect(invalidResult.messages).toMatchSnapshot(); 141 | }); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /tests/rules/no-loop-statements.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/no-loop-statements"; 6 | 7 | import { esLatestConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("javascript - es latest", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: esLatestConfig, 15 | }); 16 | 17 | it("doesn't report non-issues", () => { 18 | valid({ 19 | code: dedent` 20 | if (true) { 21 | console.log("hello world"); 22 | } 23 | `, 24 | }); 25 | }); 26 | 27 | it("reports while loop statements", () => { 28 | const invalidResult = invalid({ 29 | code: dedent` 30 | while (true) { 31 | console.log("hello world"); 32 | } 33 | `, 34 | errors: ["generic"], 35 | }); 36 | expect(invalidResult.messages).toMatchSnapshot(); 37 | }); 38 | 39 | it("reports do while loop statements", () => { 40 | const invalidResult = invalid({ 41 | code: dedent` 42 | do { 43 | console.log("hello world"); 44 | } while (true); 45 | `, 46 | errors: ["generic"], 47 | }); 48 | expect(invalidResult.messages).toMatchSnapshot(); 49 | }); 50 | 51 | it("reports for loop statements", () => { 52 | const invalidResult = invalid({ 53 | code: dedent` 54 | for (let i = 0; i < 10; i++) { 55 | console.log("hello world"); 56 | } 57 | `, 58 | errors: ["generic"], 59 | }); 60 | expect(invalidResult.messages).toMatchSnapshot(); 61 | }); 62 | 63 | it("reports for in loop statements", () => { 64 | const invalidResult = invalid({ 65 | code: dedent` 66 | for (let i in []) { 67 | console.log("hello world"); 68 | } 69 | `, 70 | errors: ["generic"], 71 | }); 72 | expect(invalidResult.messages).toMatchSnapshot(); 73 | }); 74 | 75 | it("reports for of loop statements", () => { 76 | const invalidResult = invalid({ 77 | code: dedent` 78 | for (let i of []) { 79 | console.log("hello world"); 80 | } 81 | `, 82 | errors: ["generic"], 83 | }); 84 | expect(invalidResult.messages).toMatchSnapshot(); 85 | }); 86 | 87 | it("reports for await loop statements", () => { 88 | const invalidResult = invalid({ 89 | code: dedent` 90 | for await (let i of []) { 91 | console.log("hello world"); 92 | } 93 | `, 94 | errors: ["generic"], 95 | }); 96 | expect(invalidResult.messages).toMatchSnapshot(); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /tests/rules/no-mixed-types.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/no-mixed-types"; 6 | 7 | import { typescriptConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("typescript", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: typescriptConfig, 15 | }); 16 | 17 | it("doesn't report non-issues", () => { 18 | valid({ 19 | code: dedent` 20 | type Foo = { 21 | bar: string; 22 | baz: number; 23 | }; 24 | `, 25 | }); 26 | 27 | valid({ 28 | code: dedent` 29 | interface Foo { 30 | bar: string; 31 | baz: number; 32 | } 33 | `, 34 | }); 35 | 36 | valid({ 37 | code: dedent` 38 | type Foo = { 39 | bar: () =>string; 40 | baz(): number; 41 | }; 42 | `, 43 | }); 44 | 45 | valid({ 46 | code: dedent` 47 | interface Foo { 48 | bar: () => string; 49 | baz(): number; 50 | } 51 | `, 52 | }); 53 | }); 54 | 55 | it("reports mixed types in interfaces", () => { 56 | const invalidResult = invalid({ 57 | code: dedent` 58 | interface Foo { 59 | bar: string; 60 | baz(): number; 61 | } 62 | `, 63 | errors: ["generic"], 64 | }); 65 | expect(invalidResult.messages).toMatchSnapshot(); 66 | }); 67 | 68 | it("reports mixed types in type literals", () => { 69 | const invalidResult = invalid({ 70 | code: dedent` 71 | type Foo = { 72 | bar: string; 73 | baz(): number; 74 | }; 75 | `, 76 | errors: ["generic"], 77 | }); 78 | expect(invalidResult.messages).toMatchSnapshot(); 79 | }); 80 | 81 | describe("options", () => { 82 | describe("checkTypeLiterals", () => { 83 | it("should report mixed types in type literals when enabled", () => { 84 | const invalidResult = invalid({ 85 | code: dedent` 86 | type Foo = { 87 | bar: string; 88 | baz(): number; 89 | }; 90 | `, 91 | options: [{ checkTypeLiterals: true }], 92 | errors: ["generic"], 93 | }); 94 | expect(invalidResult.messages).toMatchSnapshot(); 95 | }); 96 | 97 | it("should not report mixed types in type literals when disabled", () => { 98 | valid({ 99 | code: dedent` 100 | type Foo = { 101 | bar: string; 102 | baz(): number; 103 | }; 104 | `, 105 | options: [{ checkTypeLiterals: false }], 106 | }); 107 | }); 108 | }); 109 | 110 | describe("checkInterfaces", () => { 111 | it("should report mixed types in interfaces when enabled", () => { 112 | const invalidResult = invalid({ 113 | code: dedent` 114 | interface Foo { bar: string; baz(): number; } 115 | `, 116 | options: [{ checkInterfaces: true }], 117 | errors: ["generic"], 118 | }); 119 | expect(invalidResult.messages).toMatchSnapshot(); 120 | }); 121 | 122 | it("should not report mixed types in interfaces when disabled", () => { 123 | valid({ 124 | code: dedent` 125 | interface Foo { 126 | bar: string; 127 | baz(): number; 128 | } 129 | `, 130 | options: [{ checkInterfaces: false }], 131 | }); 132 | }); 133 | }); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /tests/rules/no-promise-reject.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/no-promise-reject"; 6 | 7 | import { esLatestConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("javascript - es latest", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: esLatestConfig, 15 | }); 16 | 17 | it("reports Promise.reject", () => { 18 | const invalidResult = invalid({ 19 | code: dedent` 20 | function foo() { 21 | return Promise.reject("hello world"); 22 | } 23 | `, 24 | errors: ["generic"], 25 | }); 26 | expect(invalidResult.messages).toMatchSnapshot(); 27 | }); 28 | 29 | it("reports new Promise(reject)", () => { 30 | const invalidResult = invalid({ 31 | code: dedent` 32 | function foo() { 33 | return new Promise((resolve, reject) => { 34 | reject("hello world"); 35 | }); 36 | } 37 | `, 38 | errors: ["generic"], 39 | }); 40 | expect(invalidResult.messages).toMatchSnapshot(); 41 | }); 42 | 43 | it("reports throw in async functions", () => { 44 | const invalidResult = invalid({ 45 | code: dedent` 46 | async function foo() { 47 | throw new Error("hello world"); 48 | } 49 | `, 50 | errors: ["generic"], 51 | }); 52 | expect(invalidResult.messages).toMatchSnapshot(); 53 | }); 54 | 55 | it("reports throw in try without catch in async functions", () => { 56 | const invalidResult = invalid({ 57 | code: dedent` 58 | async function foo() { 59 | try { 60 | throw new Error("hello"); 61 | } finally { 62 | console.log("world"); 63 | } 64 | } 65 | `, 66 | errors: ["generic"], 67 | }); 68 | expect(invalidResult.messages).toMatchSnapshot(); 69 | }); 70 | 71 | it("doesn't report Promise.resolve", () => { 72 | valid({ 73 | code: dedent` 74 | function foo() { 75 | return Promise.resolve("hello world"); 76 | } 77 | `, 78 | }); 79 | }); 80 | 81 | it("doesn't report new Promise(resolve)", () => { 82 | valid({ 83 | code: dedent` 84 | function foo() { 85 | return new Promise((resolve) => { 86 | resolve("hello world"); 87 | }); 88 | } 89 | `, 90 | }); 91 | }); 92 | 93 | it("doesn't report throw in try in async functions", () => { 94 | valid({ 95 | code: dedent` 96 | async function foo() { 97 | try { 98 | throw new Error("hello world"); 99 | } catch (e) { 100 | console.log(e); 101 | } 102 | } 103 | `, 104 | }); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /tests/rules/no-this-expressions.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/no-this-expressions"; 6 | 7 | import { esLatestConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("javascript - es latest", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: esLatestConfig, 15 | }); 16 | 17 | it("reports this expressions", () => { 18 | const code = dedent` 19 | function foo() { 20 | this.bar(); 21 | } 22 | `; 23 | 24 | expect(invalid(code)).toMatchSnapshot(); 25 | }); 26 | 27 | it("doesn't report non-issues", () => { 28 | valid(dedent` 29 | function foo() { 30 | bar(); 31 | } 32 | `); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/rules/no-try-statements.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/no-try-statements"; 6 | 7 | import { esLatestConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("javascript - es latest", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: esLatestConfig, 15 | }); 16 | 17 | it("doesn't report non-issues", () => { 18 | valid(dedent` 19 | foo(); 20 | `); 21 | }); 22 | 23 | it("reports try statements", () => { 24 | const invalidResult = invalid({ 25 | code: dedent` 26 | try { 27 | foo(); 28 | } catch (e) { 29 | console.log(e); 30 | } 31 | `, 32 | errors: ["catch"], 33 | }); 34 | expect(invalidResult.messages).toMatchSnapshot(); 35 | }); 36 | 37 | describe("options", () => { 38 | describe("allowCatch", () => { 39 | it("doesn't report try statements with catch", () => { 40 | valid({ 41 | code: dedent` 42 | try { 43 | foo(); 44 | } catch (e) { 45 | console.log(e); 46 | } 47 | `, 48 | options: [{ allowCatch: true }], 49 | }); 50 | }); 51 | 52 | it("reports try statements with catch and finally", () => { 53 | const invalidResult = invalid({ 54 | code: dedent` 55 | try { 56 | foo(); 57 | } catch (e) { 58 | console.log(e); 59 | } finally { 60 | console.log("world"); 61 | } 62 | `, 63 | errors: ["finally"], 64 | options: [{ allowCatch: true, allowFinally: false }], 65 | }); 66 | expect(invalidResult.messages).toMatchSnapshot(); 67 | }); 68 | }); 69 | 70 | describe("allowFinally", () => { 71 | it("doesn't report try statements with finally", () => { 72 | valid({ 73 | code: dedent` 74 | try { 75 | foo(); 76 | } finally { 77 | console.log("world"); 78 | } 79 | `, 80 | options: [{ allowFinally: true }], 81 | }); 82 | }); 83 | 84 | it("reports try statements with catch and finally", () => { 85 | const invalidResult = invalid({ 86 | code: dedent` 87 | try { 88 | foo(); 89 | } catch (e) { 90 | console.log(e); 91 | } finally { 92 | console.log("world"); 93 | } 94 | `, 95 | errors: ["catch"], 96 | options: [{ allowCatch: false, allowFinally: true }], 97 | }); 98 | expect(invalidResult.messages).toMatchSnapshot(); 99 | }); 100 | }); 101 | 102 | it("doesn't report try statements with catch and finally if both are allowed", () => { 103 | valid({ 104 | code: dedent` 105 | try { 106 | foo(); 107 | } catch (e) { 108 | console.log(e); 109 | } finally { 110 | console.log("world"); 111 | } 112 | `, 113 | options: [{ allowCatch: true, allowFinally: true }], 114 | }); 115 | }); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /tests/rules/prefer-property-signatures.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/prefer-property-signatures"; 6 | 7 | import { typescriptConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("typescript", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: typescriptConfig, 15 | }); 16 | 17 | it("doesn't report property signatures in interfaces", () => { 18 | valid(dedent` 19 | interface Foo { 20 | bar: (a: number, b: string) => number; 21 | } 22 | `); 23 | }); 24 | 25 | it("doesn't report property signatures in type literals", () => { 26 | valid(dedent` 27 | type Foo = { 28 | bar: (a: number, b: string) => number; 29 | } 30 | `); 31 | }); 32 | 33 | it("reports method signatures in interfaces", () => { 34 | const invalidResult = invalid({ 35 | code: dedent` 36 | interface Foo { 37 | bar(a: number, b: string): number; 38 | } 39 | `, 40 | errors: ["generic"], 41 | }); 42 | expect(invalidResult.messages).toMatchSnapshot(); 43 | }); 44 | 45 | it("reports method signatures in type literals", () => { 46 | const invalidResult = invalid({ 47 | code: dedent` 48 | type Foo = { 49 | bar(a: number, b: string): number; 50 | } 51 | `, 52 | errors: ["generic"], 53 | }); 54 | expect(invalidResult.messages).toMatchSnapshot(); 55 | }); 56 | 57 | describe("options", () => { 58 | describe("ignoreIfReadonlyWrapped", () => { 59 | it("doesn't report method signatures wrapped in Readonly in interfaces", () => { 60 | valid({ 61 | code: dedent` 62 | interface Foo extends Readonly<{ 63 | methodSignature(): void 64 | }>{} 65 | `, 66 | options: [{ ignoreIfReadonlyWrapped: true }], 67 | }); 68 | }); 69 | 70 | it("doesn't report method signatures wrapped in Readonly in type literals", () => { 71 | valid({ 72 | code: dedent` 73 | type Foo = Readonly<{ 74 | methodSignature(): void 75 | }> 76 | `, 77 | options: [{ ignoreIfReadonlyWrapped: true }], 78 | }); 79 | }); 80 | 81 | it("doesn't report method signatures wrapped in Readonly that are intersepted", () => { 82 | valid({ 83 | code: dedent` 84 | type Foo = Bar & Readonly 87 | `, 88 | options: [{ ignoreIfReadonlyWrapped: true }], 89 | }); 90 | }); 91 | 92 | it("doesn't report method signatures wrapped in Readonly that are intersepted and nested", () => { 93 | valid({ 94 | code: dedent` 95 | type Foo = Bar & Readonly 99 | }> 100 | `, 101 | options: [{ ignoreIfReadonlyWrapped: true }], 102 | }); 103 | }); 104 | 105 | it("doesn't report method signatures wrapped in Readonly that are intersepted and deeply nested", () => { 106 | valid({ 107 | code: dedent` 108 | interface Foo extends Bar, Readonly 113 | } 114 | }>{} 115 | `, 116 | options: [{ ignoreIfReadonlyWrapped: true }], 117 | }); 118 | }); 119 | }); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /tests/rules/prefer-tacit.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import { createRuleTester } from "eslint-vitest-rule-tester"; 3 | import { describe, expect, it } from "vitest"; 4 | 5 | import { name, rule } from "#/rules/prefer-tacit"; 6 | 7 | import { typescriptConfig } from "../utils/configs"; 8 | 9 | describe(name, () => { 10 | describe("typescript", () => { 11 | const { valid, invalid } = createRuleTester({ 12 | name, 13 | rule, 14 | configs: typescriptConfig, 15 | }); 16 | 17 | it('reports functions that can "safely" be changed', () => { 18 | const invalidResult1 = invalid({ 19 | code: dedent` 20 | function f(x) {} 21 | const foo = x => f(x); 22 | `, 23 | errors: ["generic"], 24 | }); 25 | expect(invalidResult1.messages).toMatchSnapshot(); 26 | 27 | const invalidResult2 = invalid({ 28 | code: dedent` 29 | const foo = [1, 2, 3].map(x => Boolean(x)); 30 | `, 31 | errors: ["generic"], 32 | }); 33 | expect(invalidResult2.messages).toMatchSnapshot(); 34 | }); 35 | 36 | it("reports functions that are just instantiations", () => { 37 | const invalidResult = invalid({ 38 | code: dedent` 39 | function f(x: T): T {} 40 | function foo(x) { return f(x); } 41 | `, 42 | errors: ["generic"], 43 | }); 44 | expect(invalidResult.messages).toMatchSnapshot(); 45 | }); 46 | 47 | it("doesn't report functions without type defs", () => { 48 | valid(dedent` 49 | function foo(x) { 50 | f(x); 51 | } 52 | `); 53 | 54 | valid(dedent` 55 | const foo = function (x) { 56 | f(x); 57 | }; 58 | `); 59 | 60 | valid(dedent` 61 | const foo = (x) => { 62 | f(x); 63 | }; 64 | `); 65 | }); 66 | 67 | it("doesn't report functions using a different number of parameters", () => { 68 | valid(dedent` 69 | function f(x, y) {} 70 | const foo = x => f(x); 71 | `); 72 | 73 | valid(dedent` 74 | const a = ['1', '2']; 75 | a.map((x) => Number.parseInt(x)); 76 | `); 77 | }); 78 | 79 | it("doesn't report functions using default parameters", () => { 80 | valid(dedent` 81 | function f(x, y = 1) {} 82 | const foo = x => f(x); 83 | `); 84 | }); 85 | 86 | it("doesn't report functions with optional parameters", () => { 87 | valid(dedent` 88 | function f(x: number, y?: nunber) {} 89 | const foo = x => f(x); 90 | `); 91 | }); 92 | 93 | it("doesn't report instantiation expressions", () => { 94 | valid(dedent` 95 | function f(x) {} 96 | const foo = f; 97 | `); 98 | }); 99 | 100 | describe("options", () => { 101 | describe("checkMemberExpressions", () => { 102 | it("doesn't report member expressions when disabled", () => { 103 | valid({ 104 | code: dedent` 105 | declare const a: { b(arg: string): string; }; 106 | function foo(x) { return a.b(x); } 107 | `, 108 | options: [{ checkMemberExpressions: false }], 109 | }); 110 | 111 | valid({ 112 | code: dedent` 113 | [''].filter(str => /a/.test(str)) 114 | `, 115 | options: [{ checkMemberExpressions: false }], 116 | }); 117 | }); 118 | 119 | it("report member expressions when enabled", () => { 120 | const invalidResult1 = invalid({ 121 | code: dedent` 122 | declare const a: { b(arg: string): string; }; 123 | function foo(x) { return a.b(x); } 124 | `, 125 | options: [{ checkMemberExpressions: true }], 126 | errors: ["generic"], 127 | }); 128 | expect(invalidResult1.messages).toMatchSnapshot(); 129 | 130 | const invalidResult2 = invalid({ 131 | code: dedent` 132 | [''].filter(str => /a/.test(str)) 133 | `, 134 | options: [{ checkMemberExpressions: true }], 135 | errors: ["generic"], 136 | }); 137 | expect(invalidResult2.messages).toMatchSnapshot(); 138 | }); 139 | }); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /tests/utils/configs.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import * as babelParser from "@babel/eslint-parser"; 4 | import * as typescriptParser from "@typescript-eslint/parser"; 5 | import type { ParserOptions } from "@typescript-eslint/parser"; 6 | import type { Linter } from "eslint"; 7 | 8 | export const typescriptConfig = { 9 | languageOptions: { 10 | parser: typescriptParser, 11 | parserOptions: { 12 | tsconfigRootDir: path.join(import.meta.dirname, "../fixture"), 13 | projectService: { 14 | // Ensure we're not using the default project 15 | maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 0, 16 | }, 17 | }, 18 | }, 19 | } satisfies Linter.Config & { 20 | languageOptions: { parserOptions: ParserOptions }; 21 | }; 22 | 23 | export const esLatestConfig = { 24 | languageOptions: { 25 | parser: babelParser as NonNullable["parser"], 26 | parserOptions: { 27 | ecmaVersion: "latest", 28 | requireConfigFile: false, 29 | babelOptions: { 30 | babelrc: false, 31 | configFile: false, 32 | }, 33 | }, 34 | }, 35 | } satisfies Linter.Config; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "allowUnreachableCode": false, 7 | "allowUnusedLabels": false, 8 | "alwaysStrict": true, 9 | "esModuleInterop": true, 10 | // "erasableSyntaxOnly": true, 11 | "exactOptionalPropertyTypes": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "importHelpers": false, 14 | "lib": ["ESNext"], 15 | "libReplacement": false, 16 | "module": "ESNext", 17 | "moduleResolution": "Bundler", 18 | "newLine": "LF", 19 | "noEmit": true, 20 | "noEmitOnError": true, 21 | "noErrorTruncation": true, 22 | "noImplicitReturns": true, 23 | "noPropertyAccessFromIndexSignature": true, 24 | "noUncheckedIndexedAccess": true, 25 | "noUnusedLocals": false, 26 | "noUnusedParameters": false, 27 | "preserveConstEnums": true, 28 | "pretty": true, 29 | "resolveJsonModule": true, 30 | "rewriteRelativeImportExtensions": true, 31 | "rootDir": ".", 32 | "skipLibCheck": true, 33 | "sourceMap": false, 34 | "strict": true, 35 | "target": "ESNext", 36 | "useUnknownInCatchVariables": true, 37 | "baseUrl": ".", 38 | "paths": { 39 | "#": ["src/index.ts"], 40 | "#/configs/*": ["src/configs/*"], 41 | "#/options": ["src/options"], 42 | "#/rules": ["src/rules"], 43 | "#/rules/*": ["src/rules/*"], 44 | "#/settings": ["src/settings"], 45 | "#/utils/*": ["src/utils/*"], 46 | "#/conditional-imports/*": ["src/utils/conditional-imports/*"], 47 | "#/tests/*": ["tests/*"] 48 | } 49 | }, 50 | "include": [".", ".*"] 51 | } 52 | -------------------------------------------------------------------------------- /typings/es.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface ObjectConstructor { 3 | hasOwn( 4 | object: ObjectType, 5 | key: Key, 6 | ): object is ObjectType & Record; 7 | } 8 | 9 | interface ArrayConstructor { 10 | isArray(arg: unknown): arg is T[] | ReadonlyArray; 11 | } 12 | 13 | interface ReadonlyArray { 14 | at(index: U): this[U]; 15 | at(this: readonly [...any[], R], index: -1): R; 16 | } 17 | 18 | interface Array { 19 | at(index: U): this[U]; 20 | at(this: readonly [...any[], R], index: -1): R; 21 | } 22 | } 23 | 24 | export {}; 25 | -------------------------------------------------------------------------------- /typings/node.d.ts: -------------------------------------------------------------------------------- 1 | // Needed for node < 22 support. 2 | 3 | declare global { 4 | interface ImportMeta { 5 | /** 6 | * The directory name of the current module. This is the same as the `path.dirname()` of the `import.meta.filename`. 7 | * **Caveat:** only present on `file:` modules. 8 | */ 9 | dirname: string; 10 | 11 | /** 12 | * The full absolute path and filename of the current module, with symlinks resolved. 13 | * This is the same as the `url.fileURLToPath()` of the `import.meta.url`. 14 | * **Caveat:** only local modules support this property. Modules not using the `file:` protocol will not provide it. 15 | */ 16 | filename: string; 17 | 18 | /** 19 | * The absolute `file:` URL of the module. 20 | */ 21 | url: string; 22 | } 23 | } 24 | 25 | export {}; 26 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from "vite-tsconfig-paths"; 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | 7 | test: { 8 | include: ["./**/*.test.ts"], 9 | exclude: ["lib", "node_modules"], 10 | testTimeout: 10_000, 11 | coverage: { 12 | all: true, 13 | include: ["src"], 14 | exclude: ["lib"], 15 | reporter: ["lcov", "text"], 16 | watermarks: { 17 | lines: [80, 95], 18 | functions: [80, 95], 19 | branches: [80, 95], 20 | statements: [80, 95], 21 | }, 22 | }, 23 | }, 24 | }); 25 | --------------------------------------------------------------------------------