git.iko.soy/rowtype-yoga/purslint
on main help sign in
164 files changed+6821−2551
since splitsnapshots
unifiedsplit
.github/ISSUE_TEMPLATE/bug_report.yml+53 .github/workflows/publish-npm.yml+27 .github/workflows/publish-openvsx.yml+29 .github/workflows/publish-vscode.yml+26 .gitignore+1 CONTRIBUTING.md+6 LICENSE+21 README.md+100−67 bin/purelint-cli/src/Main.purs−345 bin/purelint-lsp.js−3 bin/purelint.js−3 bin/purelint-cli/spago.yaml → bin/purslint-cli/spago.yaml+3−2 bin/purslint-cli/src/Main.purs+419 bin/purelint-cli/src/Node/CLI.js → bin/purslint-cli/src/Node/CLI.js bin/purelint-cli/src/Node/CLI.purs → bin/purslint-cli/src/Node/CLI.purs bin/purelint-cli/src/Node/FS/Simple.js → bin/purslint-cli/src/Node/FS/Simple.js bin/purelint-cli/src/Node/FS/Simple.purs → bin/purslint-cli/src/Node/FS/Simple.purs bin/purslint-lsp.js+2 bin/purelint-lsp/spago.yaml → bin/purslint-lsp/spago.yaml+2−2 bin/purelint-lsp/src/LSP.js → bin/purslint-lsp/src/LSP.js+1−1 bin/purelint-lsp/src/LSP.purs → bin/purslint-lsp/src/LSP.purs+234−126 bin/purelint-lsp/src/LSP/Stdio.js → bin/purslint-lsp/src/LSP/Stdio.js+9−9 bin/purelint-lsp/src/LSP/Stdio.purs → bin/purslint-lsp/src/LSP/Stdio.purs bin/purelint-lsp/src/Main.purs → bin/purslint-lsp/src/Main.purs+1−1 bin/purslint.js+2 demo.gif docs/IMPORT_CHECKING_PLAN.md+8−8 examples/BadCode.purs+1−1 examples/TestFix.purs+5−5 lib/purelint/src/Purelint/Fix.purs−107 lib/purelint/src/Purelint/Print.purs−28 lib/purelint/src/Purelint/Rules/FmapId.purs−105 lib/purelint/src/Purelint/Rules/LetToWhere.purs−72 lib/purelint/src/Purelint/Rules/RedundantReverse.purs−63 lib/purelint/src/Purelint/Rules/UseAny.purs−68 lib/purelint/src/Purelint/Rules/UseApplicative.purs−66 lib/purelint/src/Purelint/Rules/UseBimap.purs−205 lib/purelint/src/Purelint/Rules/UseBindFlip.purs−73 lib/purelint/src/Purelint/Rules/UseBreak.purs−95 lib/purelint/src/Purelint/Rules/UseConst.purs−67 lib/purelint/src/Purelint/Rules/UseEitherMap.purs−112 lib/purelint/src/Purelint/Tidy.js−3 lib/purelint/spago.yaml → lib/purslint/spago.yaml+1−1 lib/purelint/src/Purelint/Config.purs → lib/purslint/src/Purslint/Config.purs+14−15 lib/purslint/src/Purslint/Fix.purs+129 lib/purslint/src/Purslint/ImportFix.purs+17 lib/purelint/src/Purelint/Imports.purs → lib/purslint/src/Purslint/Imports.purs+5−5 lib/purslint/src/Purslint/Print.purs+115 lib/purelint/src/Purelint/Rule.purs → lib/purslint/src/Purslint/Rule.purs+3−3 lib/purslint/src/Purslint/Rules/AlternativeLaw.purs+77 lib/purelint/src/Purelint/Rules/BooleanSimplify.purs → lib/purslint/src/Purslint/Rules/BooleanSimplify.purs+40−24 lib/purelint/src/Purelint/Rules/CollapseLambdas.purs → lib/purslint/src/Purslint/Rules/CollapseLambdas.purs+19−19 lib/purelint/src/Purelint/Rules/ConcatMap.purs → lib/purslint/src/Purslint/Rules/ConcatMap.purs+19−22 lib/purelint/src/Purelint/Rules/EtaReduce.purs → lib/purslint/src/Purslint/Rules/EtaReduce.purs+31−20 lib/purelint/src/Purelint/Rules/EtaReduceDecl.purs → lib/purslint/src/Purslint/Rules/EtaReduceDecl.purs+33−44 lib/purslint/src/Purslint/Rules/EvaluateBool.purs+90 lib/purelint/src/Purelint/Rules/EvaluateConst.purs → lib/purslint/src/Purslint/Rules/EvaluateConst.purs+7−5 lib/purslint/src/Purslint/Rules/EvaluateEither.purs+122 lib/purelint/src/Purelint/Rules/EvaluateFst.purs → lib/purslint/src/Purslint/Rules/EvaluateFst.purs+8−5 lib/purslint/src/Purslint/Rules/FlattenCase.purs+145 lib/purelint/src/Purelint/Rules/FunctorLaw.purs → lib/purslint/src/Purslint/Rules/FunctorLaw.purs+33−28 lib/purslint/src/Purslint/Rules/LetToDo.purs+160 lib/purslint/src/Purslint/Rules/LetToWhere.purs+122 lib/purelint/src/Purelint/Rules/MapFusion.purs → lib/purslint/src/Purslint/Rules/MapFusion.purs+57−55 lib/purslint/src/Purslint/Rules/MapIdentity.purs+100 lib/purslint/src/Purslint/Rules/MonadLaw.purs+100 lib/purelint/src/Purelint/Rules/MonoidIdentity.purs → lib/purslint/src/Purslint/Rules/MonoidIdentity.purs+8−5 lib/purelint/src/Purelint/Rules/NotEqual.purs → lib/purslint/src/Purslint/Rules/NotEqual.purs+7−5 lib/purelint/src/Purelint/Rules/NothingBind.purs → lib/purslint/src/Purslint/Rules/NothingBind.purs+7−4 lib/purelint/src/Purelint/Rules/RedundantBind.purs → lib/purslint/src/Purslint/Rules/RedundantBind.purs+8−5 lib/purelint/src/Purelint/Rules/RedundantFlip.purs → lib/purslint/src/Purslint/Rules/RedundantFlip.purs+7−5 lib/purelint/src/Purelint/Rules/RedundantGuard.purs → lib/purslint/src/Purslint/Rules/RedundantGuard.purs+6−4 lib/purelint/src/Purelint/Rules/RedundantId.purs → lib/purslint/src/Purslint/Rules/RedundantId.purs+11−5 lib/purelint/src/Purelint/Rules/RedundantIf.purs → lib/purslint/src/Purslint/Rules/RedundantIf.purs+8−5 lib/purelint/src/Purelint/Rules/RedundantNegate.purs → lib/purslint/src/Purslint/Rules/RedundantNegate.purs+7−5 lib/purelint/src/Purelint/Rules/RedundantNot.purs → lib/purslint/src/Purslint/Rules/RedundantNot.purs+7−5 lib/purslint/src/Purslint/Rules/RedundantParens.purs+423 lib/purslint/src/Purslint/Rules/RedundantReverse.purs+60 lib/purelint/src/Purelint/Rules/UnlessNot.purs → lib/purslint/src/Purslint/Rules/UnlessNot.purs+8−5 lib/purelint/src/Purelint/Rules/UseAll.purs → lib/purslint/src/Purslint/Rules/UseAll.purs+8−6 lib/purelint/src/Purelint/Rules/UseAnd.purs → lib/purslint/src/Purslint/Rules/UseAnd.purs+8−5 lib/purslint/src/Purslint/Rules/UseAny.purs+63 lib/purslint/src/Purslint/Rules/UseApplicative.purs+60 lib/purslint/src/Purslint/Rules/UseApplyFlipped.purs+57 lib/purslint/src/Purslint/Rules/UseBimap.purs+212 lib/purslint/src/Purslint/Rules/UseBindFlip.purs+70 lib/purslint/src/Purslint/Rules/UseBreak.purs+99 lib/purslint/src/Purslint/Rules/UseCaseOf.purs+239 lib/purelint/src/Purelint/Rules/UseComparing.purs → lib/purslint/src/Purslint/Rules/UseComparing.purs+31−32 lib/purslint/src/Purslint/Rules/UseConst.purs+93 lib/purslint/src/Purslint/Rules/UseDollar.purs+97 lib/purslint/src/Purslint/Rules/UseEitherMap.purs+117 lib/purelint/src/Purelint/Rules/UseElemIndex.purs → lib/purslint/src/Purslint/Rules/UseElemIndex.purs+8−5 lib/purelint/src/Purelint/Rules/UseFindMap.purs → lib/purslint/src/Purslint/Rules/UseFindMap.purs+13−5 lib/purelint/src/Purelint/Rules/UseFold.purs → lib/purslint/src/Purslint/Rules/UseFold.purs+6−4 lib/purslint/src/Purslint/Rules/UseFoldBool.purs+90 lib/purslint/src/Purslint/Rules/UseFoldMap.purs+167 lib/purelint/src/Purelint/Rules/UseFoldMapId.purs → lib/purslint/src/Purslint/Rules/UseFoldMapId.purs+8−5 lib/purelint/src/Purelint/Rules/UseFor.purs → lib/purslint/src/Purslint/Rules/UseFor.purs+8−5 lib/purelint/src/Purelint/Rules/UseFromJust.purs → lib/purslint/src/Purslint/Rules/UseFromJust.purs+9−5 lib/purelint/src/Purelint/Rules/UseFromMaybe.purs → lib/purslint/src/Purslint/Rules/UseFromMaybe.purs+7−5 lib/purelint/src/Purelint/Rules/UseFstSnd.purs → lib/purslint/src/Purslint/Rules/UseFstSnd.purs+6−3 lib/purelint/src/Purelint/Rules/UseGuard.purs → lib/purslint/src/Purslint/Rules/UseGuard.purs+8−5 lib/purelint/src/Purelint/Rules/UseGuardMaybe.purs → lib/purslint/src/Purslint/Rules/UseGuardMaybe.purs+7−5 lib/purelint/src/Purelint/Rules/UseHead.purs → lib/purslint/src/Purslint/Rules/UseHead.purs+6−4 lib/purelint/src/Purelint/Rules/UseIsJust.purs → lib/purslint/src/Purslint/Rules/UseIsJust.purs+6−4 lib/purelint/src/Purelint/Rules/UseIsNothing.purs → lib/purslint/src/Purslint/Rules/UseIsNothing.purs+6−4 lib/purelint/src/Purelint/Rules/UseJoin.purs → lib/purslint/src/Purslint/Rules/UseJoin.purs+8−5 lib/purelint/src/Purelint/Rules/UseLastReverse.purs → lib/purslint/src/Purslint/Rules/UseLastReverse.purs+7−5 lib/purelint/src/Purelint/Rules/UseMapMaybe.purs → lib/purslint/src/Purslint/Rules/UseMapMaybe.purs+13−5 lib/purelint/src/Purelint/Rules/UseMaybeMap.purs → lib/purslint/src/Purslint/Rules/UseMaybeMap.purs+5−5 lib/purelint/src/Purelint/Rules/UseMinMax.purs → lib/purslint/src/Purslint/Rules/UseMinMax.purs+7−5 lib/purelint/src/Purelint/Rules/UseMinimumSort.purs → lib/purslint/src/Purslint/Rules/UseMinimumSort.purs+7−5 lib/purelint/src/Purelint/Rules/UseNotElem.purs → lib/purslint/src/Purslint/Rules/UseNotElem.purs+7−5 lib/purelint/src/Purelint/Rules/UseNull.purs → lib/purslint/src/Purslint/Rules/UseNull.purs+7−5 lib/purelint/src/Purelint/Rules/UseOn.purs → lib/purslint/src/Purslint/Rules/UseOn.purs+7−5 lib/purelint/src/Purelint/Rules/UseOr.purs → lib/purslint/src/Purslint/Rules/UseOr.purs+8−5 lib/purslint/src/Purslint/Rules/UsePatternGuards.purs+231 lib/purelint/src/Purelint/Rules/UseReplicate.purs → lib/purslint/src/Purslint/Rules/UseReplicate.purs+13−5 lib/purelint/src/Purelint/Rules/UseSequence.purs → lib/purslint/src/Purslint/Rules/UseSequence.purs+12−4 lib/purelint/src/Purelint/Rules/UseSpan.purs → lib/purslint/src/Purslint/Rules/UseSpan.purs+8−5 lib/purelint/src/Purelint/Rules/UseTraverse.purs → lib/purslint/src/Purslint/Rules/UseTraverse.purs+36−23 lib/purelint/src/Purelint/Rules/UseTraverseSequence.purs → lib/purslint/src/Purslint/Rules/UseTraverseSequence.purs+13−5 lib/purelint/src/Purelint/Rules/UseUncurry.purs → lib/purslint/src/Purslint/Rules/UseUncurry.purs+7−5 lib/purelint/src/Purelint/Rules/UseUnless.purs → lib/purslint/src/Purslint/Rules/UseUnless.purs+7−5 lib/purslint/src/Purslint/Rules/UseUnwrap.purs+70 lib/purelint/src/Purelint/Rules/UseVoid.purs → lib/purslint/src/Purslint/Rules/UseVoid.purs+7−5 lib/purelint/src/Purelint/Rules/UseWhen.purs → lib/purslint/src/Purslint/Rules/UseWhen.purs+7−5 lib/purelint/src/Purelint/Rules/UseZip.purs → lib/purslint/src/Purslint/Rules/UseZip.purs+9−5 lib/purelint/src/Purelint/Rules/WhenNot.purs → lib/purslint/src/Purslint/Rules/WhenNot.purs+8−5 lib/purelint/src/Purelint/Runner.purs → lib/purslint/src/Purslint/Runner.purs+26−25 lib/purslint/src/Purslint/Tidy.js+66 lib/purelint/src/Purelint/Tidy.purs → lib/purslint/src/Purslint/Tidy.purs+5−1 lib/purelint/src/Purelint/Types.purs → lib/purslint/src/Purslint/Types.purs+16−2 package.json+25−9 purslint-lsp+2 spago.lock+94−13 spago.yaml+3−2 test/LintExamples.purs+10−10 test/RedundantParens.purs+134 test/Test/Main.purs+934−317 vscode-extension/.gitignore+1 vscode-extension/out/extension.js+77 vscode-extension/out/extension.js.map+1 vscode-extension/out/test/runTest.js+55 vscode-extension/out/test/runTest.js.map+1 vscode-extension/out/test/suite/extension.test.js+75 vscode-extension/out/test/suite/extension.test.js.map+1 vscode-extension/out/test/suite/index.js+63 vscode-extension/out/test/suite/index.js.map+1 vscode-extension/package.json+55 vscode-extension/purslint-0.1.1.vsix vscode-extension/purslint.png vscode-extension/src/extension.ts+59 vscode-extension/src/test/runTest.ts+21 vscode-extension/src/test/suite/extension.test.ts+54 vscode-extension/src/test/suite/index.ts+26 vscode-extension/test-workspace/Test.purs+13 vscode-extension/tsconfig.json+14 zed-extension/Cargo.lock+3−3 zed-extension/Cargo.toml+1−1 zed-extension/extension.toml+3−3 zed-extension/extension.wasm zed-extension/src/lib.rs+5−5
.github/ISSUE_TEMPLATE/bug_report.ymlnew file+53
@@ -0,0 +1,53 @@
1+name: Bug report
2+description: Report incorrect lint results with input and output.
3+title: "[Bug]: "
4+labels:
5+ - bug
6+body:
7+ - type: markdown
8+ attributes:
9+ value: |
10+ Thanks for taking the time to report a bug.
11+ If possible, include a golden file with input, actual output, and expected output.
12+ - type: textarea
13+ id: input
14+ attributes:
15+ label: Input
16+ description: |
17+ Paste the input here (or attach a golden file and mention the filename).
18+ Include any relevant configuration.
19+ placeholder: |
20+ ```purs
21+ -- Your input code here
22+ ```
23+ validations:
24+ required: true
25+ - type: textarea
26+ id: actual
27+ attributes:
28+ label: Actual output
29+ description: Paste the current output from purslint.
30+ placeholder: |
31+ ```
32+ -- Warnings, fixes, or logs
33+ ```
34+ validations:
35+ required: true
36+ - type: textarea
37+ id: expected
38+ attributes:
39+ label: Expected output
40+ description: Describe the expected output or behaviour.
41+ placeholder: |
42+ ```
43+ -- Expected warnings, fixes, or logs
44+ ```
45+ validations:
46+ required: true
47+ - type: textarea
48+ id: extra
49+ attributes:
50+ label: Additional context
51+ description: Links, screenshots, or anything else helpful.
52+ validations:
53+ required: false
.github/workflows/publish-npm.ymlnew file+27
@@ -0,0 +1,27 @@
1+name: Publish npm package
2+
3+on:
4+ release:
5+ types: [published]
6+ workflow_dispatch:
7+
8+permissions:
9+ contents: read
10+
11+jobs:
12+ publish:
13+ runs-on: ubuntu-latest
14+ steps:
15+ - uses: actions/checkout@v4
16+
17+ - uses: actions/setup-node@v4
18+ with:
19+ node-version: "20"
20+ registry-url: "https://registry.npmjs.org"
21+
22+ - run: npm install -g purescript@0.15.15
23+ - run: npm install --no-audit --no-fund
24+ - run: npm run dist
25+ - run: npm publish --access public
26+ env:
27+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
.github/workflows/publish-openvsx.ymlnew file+29
@@ -0,0 +1,29 @@
1+name: Publish Open VSX
2+
3+on:
4+ workflow_dispatch:
5+ release:
6+ types: [published]
7+
8+permissions:
9+ contents: read
10+
11+jobs:
12+ publish:
13+ runs-on: ubuntu-latest
14+ steps:
15+ - uses: actions/checkout@v4
16+
17+ - uses: actions/setup-node@v4
18+ with:
19+ node-version: "20"
20+
21+ - run: npm install -g purescript@0.15.15
22+ - run: npm install
23+ working-directory: vscode-extension
24+ - run: npx vsce package
25+ working-directory: vscode-extension
26+ - run: npx ovsx publish -p "${OPENVSX_TOKEN}"
27+ working-directory: vscode-extension
28+ env:
29+ OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }}
.github/workflows/publish-vscode.ymlnew file+26
@@ -0,0 +1,26 @@
1+name: Publish VS Code Marketplace
2+
3+on:
4+ workflow_dispatch:
5+ release:
6+ types: [published]
7+
8+permissions:
9+ contents: read
10+
11+jobs:
12+ publish:
13+ runs-on: ubuntu-latest
14+ steps:
15+ - uses: actions/checkout@v4
16+
17+ - uses: actions/setup-node@v4
18+ with:
19+ node-version: "20"
20+
21+ - run: npm install
22+ working-directory: vscode-extension
23+ - run: npx vsce publish --pat "${VSCE_TOKEN}"
24+ working-directory: vscode-extension
25+ env:
26+ VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }}
.gitignore+1
@@ -15,3 +15,4 @@ package-lock.json
1515
1616# Zed extension build
1717zed-extension/target/
18+test-lsp/
CONTRIBUTING.mdnew file+6
@@ -0,0 +1,6 @@
1+# Contributing
2+
3+## Publishing (Marketplace)
4+
5+Publisher management and VSIX uploads live here:
6+https://marketplace.visualstudio.com/manage/publishers/RowtypeYoga
LICENSEnew file+21
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 Rowtype Yoga
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.
README.md+100−67
@@ -1,92 +1,96 @@
1−# PureLint
1+# Purslint
22
3−A PureScript linter inspired by [hlint](https://github.com/ndmitchell/hlint), providing suggestions for cleaner, more idiomatic PureScript code.
3+<p align="center">
4+ <img src="vscode-extension/purslint.png" alt="Purslint logo" width="180" />
5+</p>
46
5−## Features
7+<p align="center">
8+ <a href="https://github.com/user-attachments/assets/6d855014-18f5-4729-be28-e91ec2d19c8f">
9+ <img src="demo.gif" alt="Purslint demo" width="480" />
10+ </a>
11+</p>
612
7−PureLint includes 10 lint rules:
13+A PureScript linter.
814
9−| Rule | Description | Example |
10−|------|-------------|---------|
11−| `FmapId` | `map identity x` → `x` | Redundant identity mapping |
12−| `ConcatMap` | `join <<< map f` → `f =<< _` | Use bind instead of join+map |
13−| `MapFusion` | `map f <<< map g` → `map (f <<< g)` | Fuse consecutive maps |
14−| `UseTraverse` | `sequence <<< map f` → `traverse f` | Use traverse |
15−| `NotEqual` | `not (x == y)` → `x /= y` | Use not-equal operator |
16−| `UseGuard` | `if c then Just x else Nothing` → `guard` | Use guard from Control.MonadZero |
17−| `EtaReduce` | `\x -> f x` → `f` | Eta reduce lambdas |
18−| `EtaReduceDecl` | `f x = g x` → `f = g` | Eta reduce declarations |
19−| `RedundantBind` | `x >>= pure` → `x` | Remove redundant bind |
20−| `LetToWhere` | `let x = y in z` → `z where x = y` | Prefer where clauses |
15+Purslint is very new and may have bugs, so please use an editor with undo functionality. :D
2116
22−## Installation
17+## Install
2318
24−### Prerequisites
19+### VS Code (Marketplace)
2520
26−- [PureScript](https://www.purescript.org/) compiler (`purs`)
27−- [Spago](https://github.com/purescript/spago) package manager
28−- [Go](https://golang.org/) 1.19+
29−- [psgo](https://github.com/puregocore/psgo) - PureScript to Go compiler
21+Install from the Visual Studio Marketplace:
22+[Purslint](https://marketplace.visualstudio.com/items?itemName=RowtypeYoga.purslint).
3023
31−### Building
24+### CLI (npm)
3225
3326```bash
34−# Install dependencies
35−spago install
36−
37−# Build CLI
38−purs compile --codegen corefn 'lib/**/*.purs' 'bin/purelint-cli/**/*.purs' '.spago/**/*.purs'
39−psgo output/*/corefn.json
40−cp output/Main/Main.go main_cli.go
41−go build -o purelint main_cli.go
42−
43−# Build LSP
44−purs compile --codegen corefn 'lib/**/*.purs' 'bin/purelint-lsp/**/*.purs' '.spago/**/*.purs'
45−psgo output/*/corefn.json
46−cp output/Main/Main.go main_lsp.go
47−go build -o purelint-lsp main_lsp.go
48−
49−# Build and run tests
50−purs compile --codegen corefn 'lib/**/*.purs' 'test/**/*.purs' '.spago/**/*.purs'
51−psgo output/*/corefn.json
52−cp output/Test.Main/Test_Main.go test_main.go
53−go build -o test-runner test_main.go
54−./test-runner
27+npm install -g purslint
28+
29+# CLI
30+purslint --help
31+```
32+
33+### Neovim (nvim-lspconfig)
34+
35+After building `purslint-lsp` (see build steps below), you can configure Neovim like this:
36+
37+```lua
38+require("lspconfig").purslint = {
39+ default_config = {
40+ cmd = { "/path/to/purslint-lsp" },
41+ filetypes = { "purescript" },
42+ root_dir = require("lspconfig.util").root_pattern(".git", "spago.yaml", "package.json"),
43+ },
44+}
45+
46+require("lspconfig").purslint.setup({})
47+```
48+
49+If you prefer to run the bundled JS directly:
50+
51+```lua
52+cmd = { "node", "/path/to/dist/purslint-lsp.js" }
5553```
5654
55+### Zed Editor
56+
57+A Zed extension is available in the `zed-extension/` directory. To install:
58+
59+1. Open Zed
60+2. Go to Extensions (Cmd+Shift+X)
61+3. Click "Install Dev Extension"
62+4. Select the `zed-extension/` directory
63+
64+The extension will automatically use the `purslint-lsp` binary.
65+
5766## Usage
5867
5968### CLI
6069
6170```bash
6271# Lint a single file
63−./purelint src/Main.purs
72+./purslint src/Main.purs
6473
6574# Lint multiple files
66−./purelint src/*.purs
75+./purslint src/*.purs
6776
6877# Lint with config file (optional)
69−./purelint --config purelint.json src/*.purs
78+./purslint --config purslint.json src/*.purs
7079```
7180
7281### LSP (Language Server Protocol)
7382
74−The LSP binary (`purelint-lsp`) provides IDE integration. It communicates via stdio using the standard LSP protocol.
75−
76−#### Zed Editor
83+The LSP binary (`purslint-lsp`) provides IDE integration. It communicates via stdio using the standard LSP protocol.
7784
78−A Zed extension is available in the `zed-extension/` directory. To install:
85+## Features
7986
80−1. Open Zed
81−2. Go to Extensions (Cmd+Shift+X)
82−3. Click "Install Dev Extension"
83−4. Select the `zed-extension/` directory
87+Purslint currently ships with **81 lint rules**, covering common PureScript refactors and simplifications.
8488
85−The extension will automatically use the `purelint-lsp` binary.
89+For a complete list, see `lib/purslint/src/Purslint/Rules/`.
8690
8791## Configuration
8892
89−Create a `purelint.json` file in your project root:
93+Create a `purslint.json` file in your project root:
9094
9195```json
9296{
@@ -96,36 +100,63 @@ Create a `purelint.json` file in your project root:
96100
97101## Architecture
98102
99−PureLint is written in PureScript and compiled to Go using `psgo` (purescript-native). This allows:
103+Purslint is written in PureScript and compiled to JavaScript via
104+`purs-backend-es`, then bundled for Node.js. This allows:
100105
101106- Full access to the PureScript CST parser (`purescript-language-cst-parser`)
102−- Native binary performance
103−- Easy distribution without runtime dependencies
107+- Straightforward distribution via npm
108+- A single runtime across CLI and LSP
104109
105110### Project Structure
106111
107112```
108−purelint/
109−├── lib/purelint/src/ # Core library
110−│ └── Purelint/
113+purslint/
114+├── lib/purslint/src/ # Core library
115+│ └── Purslint/
111116│ ├── Rules/ # Lint rules
112117│ ├── Runner.purs # Rule runner
113118│ ├── Types.purs # Core types
114119│ └── Imports.purs # Import checking
115120├── bin/
116−│ ├── purelint-cli/ # CLI application
117−│ └── purelint-lsp/ # LSP server
121+│ ├── purslint-cli/ # CLI application
122+│ └── purslint-lsp/ # LSP server
118123├── test/ # Test suite
119124└── zed-extension/ # Zed editor extension
120125```
121126
122127## Development
123128
129+### Prerequisites
130+
131+- [PureScript](https://www.purescript.org/) compiler (`purs`)
132+- [Spago](https://github.com/purescript/spago) package manager
133+- [Node.js](https://nodejs.org/) 18+
134+
135+### Building
136+
137+```bash
138+# Install dependencies
139+npm install
140+
141+# Build and bundle CLI + LSP
142+npm run dist
143+
144+# Run tests
145+npm test
146+```
147+
148+### Publishing to npm
149+
150+Publishing is handled by GitHub Actions. Add an `NPM_TOKEN` secret with an
151+automation or granular token that has publish rights for `purslint`, then
152+create a GitHub Release to trigger the workflow. You can also run it manually
153+via the workflow dispatch.
154+
124155### Adding a New Rule
125156
126−1. Create a new file in `lib/purelint/src/Purelint/Rules/`
157+1. Create a new file in `lib/purslint/src/Purslint/Rules/`
1271582. Implement the rule using the CST traversal API
128−3. Export the rule from `Purelint.Rules`
159+3. Export the rule from `Purslint.Rules`
1291604. Add tests in `test/Test/Main.purs`
1301615. Rebuild and test
131162
@@ -133,6 +164,8 @@ purelint/
133164
134165- `purescript-language-cst-parser` - PureScript CST parser
135166- `purescript-lsp` - LSP protocol implementation (Go FFI)
167+- `purs-backend-es` - PureScript to JavaScript backend
168+- `esbuild` - Bundler for CLI and LSP
136169
137170## License
138171
bin/purelint-cli/src/Main.pursdeleted−345
@@ -1,345 +0,0 @@
1−module Main where
2−
3−import Prelude
4−
5−import Data.Array as Array
6−import Data.Either (Either(..))
7−import Data.Foldable (for_)
8−import Data.Maybe (Maybe(..))
9−import Data.Newtype (unwrap)
10−import Data.String.CodeUnits as String
11−import Effect (Effect)
12−import Effect.Console (log)
13−import Node.FS.Simple as FS
14−import Node.CLI as Process
15−import Purelint.Config (defaultConfig, filterRules, parseConfig)
16−import Purelint.Fix (applyAllFixes)
17−import Purelint.Rule (Rule)
18−import Purelint.Rules.BooleanSimplify (booleanSimplifyRule)
19−import Purelint.Rules.CollapseLambdas (collapseLambdasRule)
20−import Purelint.Rules.ConcatMap (concatMapRule)
21−import Purelint.Rules.EtaReduce (etaReduceRule)
22−import Purelint.Rules.EtaReduceDecl (etaReduceDeclRule)
23−import Purelint.Rules.EvaluateConst (evaluateConstRule)
24−import Purelint.Rules.EvaluateFst (evaluateFstRule)
25−import Purelint.Rules.FmapId (fmapIdRule)
26−import Purelint.Rules.FunctorLaw (functorLawRule)
27−import Purelint.Rules.LetToWhere (letToWhereRule)
28−import Purelint.Rules.MapFusion (mapFusionRule)
29−import Purelint.Rules.MonoidIdentity (monoidIdentityRule)
30−import Purelint.Rules.NotEqual (notEqualRule)
31−import Purelint.Rules.NothingBind (nothingBindRule)
32−import Purelint.Rules.RedundantBind (redundantBindRule)
33−import Purelint.Rules.RedundantFlip (redundantFlipRule)
34−import Purelint.Rules.RedundantGuard (redundantGuardRule)
35−import Purelint.Rules.RedundantId (redundantIdRule)
36−import Purelint.Rules.RedundantIf (redundantIfRule)
37−import Purelint.Rules.RedundantNegate (redundantNegateRule)
38−import Purelint.Rules.RedundantNot (redundantNotRule)
39−import Purelint.Rules.RedundantReverse (redundantReverseRule)
40−import Purelint.Rules.UnlessNot (unlessNotRule)
41−import Purelint.Rules.UseAll (useAllRule)
42−import Purelint.Rules.UseAnd (useAndRule)
43−import Purelint.Rules.UseAny (useAnyRule)
44−import Purelint.Rules.UseApplicative (useApplicativeRule)
45−import Purelint.Rules.UseBimap (useBimapRule)
46−import Purelint.Rules.UseBindFlip (useBindFlipRule)
47−import Purelint.Rules.UseBreak (useBreakRule)
48−import Purelint.Rules.UseComparing (useComparingRule)
49−import Purelint.Rules.UseConst (useConstRule)
50−import Purelint.Rules.UseEitherMap (useEitherMapRule)
51−import Purelint.Rules.UseElemIndex (useElemIndexRule)
52−import Purelint.Rules.UseFindMap (useFindMapRule)
53−import Purelint.Rules.UseFold (useFoldRule)
54−import Purelint.Rules.UseFoldMapId (useFoldMapIdRule)
55−import Purelint.Rules.UseFor (useForRule)
56−import Purelint.Rules.UseFromJust (useFromJustRule)
57−import Purelint.Rules.UseFromMaybe (useFromMaybeRule)
58−import Purelint.Rules.UseFstSnd (useFstSndRule)
59−import Purelint.Rules.UseGuard (useGuardRule)
60−import Purelint.Rules.UseGuardMaybe (useGuardMaybeRule)
61−import Purelint.Rules.UseHead (useHeadRule)
62−import Purelint.Rules.UseIsJust (useIsJustRule)
63−import Purelint.Rules.UseIsNothing (useIsNothingRule)
64−import Purelint.Rules.UseJoin (useJoinRule)
65−import Purelint.Rules.UseLastReverse (useLastReverseRule)
66−import Purelint.Rules.UseMapMaybe (useMapMaybeRule)
67−import Purelint.Rules.UseMaybeMap (useMaybeMapRule)
68−import Purelint.Rules.UseMinMax (useMinMaxRule)
69−import Purelint.Rules.UseMinimumSort (useMinimumSortRule)
70−import Purelint.Rules.UseNotElem (useNotElemRule)
71−import Purelint.Rules.UseNull (useNullRule)
72−import Purelint.Rules.UseOn (useOnRule)
73−import Purelint.Rules.UseOr (useOrRule)
74−import Purelint.Rules.UseReplicate (useReplicateRule)
75−import Purelint.Rules.UseSequence (useSequenceRule)
76−import Purelint.Rules.UseSpan (useSpanRule)
77−import Purelint.Rules.UseTraverse (useTraverseRule)
78−import Purelint.Rules.UseTraverseSequence (useTraverseSequenceRule)
79−import Purelint.Rules.UseUncurry (useUncurryRule)
80−import Purelint.Rules.UseUnless (useUnlessRule)
81−import Purelint.Rules.UseVoid (useVoidRule)
82−import Purelint.Rules.UseWhen (useWhenRule)
83−import Purelint.Rules.UseZip (useZipRule)
84−import Purelint.Rules.WhenNot (whenNotRule)
85−import Purelint.Runner (runRules)
86−import Purelint.Types (LintResult(..), LintWarning(..), Severity(..), SourceCode(..), Suggestion(..))
87−
88−-- | All available rules (70 total)
89−allRules :: Array Rule
90−allRules =
91− [ booleanSimplifyRule
92− , collapseLambdasRule
93− , concatMapRule
94− , etaReduceRule
95− , etaReduceDeclRule
96− , evaluateConstRule
97− , evaluateFstRule
98− , fmapIdRule
99− , functorLawRule
100− , letToWhereRule
101− , mapFusionRule
102− , monoidIdentityRule
103− , notEqualRule
104− , nothingBindRule
105− , redundantBindRule
106− , redundantFlipRule
107− , redundantGuardRule
108− , redundantIdRule
109− , redundantIfRule
110− , redundantNegateRule
111− , redundantNotRule
112− , redundantReverseRule
113− , unlessNotRule
114− , useAllRule
115− , useAndRule
116− , useAnyRule
117− , useApplicativeRule
118− , useBimapRule
119− , useBindFlipRule
120− , useBreakRule
121− , useComparingRule
122− , useConstRule
123− , useEitherMapRule
124− , useElemIndexRule
125− , useFindMapRule
126− , useFoldRule
127− , useFoldMapIdRule
128− , useForRule
129− , useFromJustRule
130− , useFromMaybeRule
131− , useFstSndRule
132− , useGuardRule
133− , useGuardMaybeRule
134− , useHeadRule
135− , useIsJustRule
136− , useIsNothingRule
137− , useJoinRule
138− , useLastReverseRule
139− , useMapMaybeRule
140− , useMaybeMapRule
141− , useMinMaxRule
142− , useMinimumSortRule
143− , useNotElemRule
144− , useNullRule
145− , useOnRule
146− , useOrRule
147− , useReplicateRule
148− , useSequenceRule
149− , useSpanRule
150− , useTraverseRule
151− , useTraverseSequenceRule
152− , useUncurryRule
153− , useUnlessRule
154− , useVoidRule
155− , useWhenRule
156− , useZipRule
157− , whenNotRule
158− ]
159−
160−-- | Load config from .purelintrc file
161−loadConfig :: Effect _
162−loadConfig = do
163− hasConfig <- FS.exists ".purelintrc"
164− if hasConfig then do
165− content <- FS.readTextFile ".purelintrc"
166− pure $ parseConfig content
167− else
168− pure defaultConfig
169−
170−-- | Format a source range for display
171−formatRange :: { start :: { line :: Int, column :: Int }, end :: { line :: Int, column :: Int } } -> String
172−formatRange r = show (r.start.line + 1) <> ":" <> show (r.start.column + 1) <> "-" <> show (r.end.line + 1) <> ":" <> show (r.end.column + 1)
173−
174−-- | Format severity for display
175−formatSeverity :: Severity -> String
176−formatSeverity Hint = "[hint]"
177−formatSeverity Warning = "[warning]"
178−formatSeverity Error = "[error]"
179−
180−-- | Format a single warning for display
181−formatWarning :: String -> LintWarning -> String
182−formatWarning filePath (LintWarning w) =
183− filePath <> ":" <> formatRange w.range <> " " <> formatSeverity w.severity <> " " <> unwrap w.ruleId <> ": " <> unwrap w.message <>
184− case w.suggestion of
185− Nothing -> ""
186− Just (Suggestion s) -> "\n Replace with: " <> unwrap s.replacement
187−
188−-- | Lint a single file with config
189−lintFileWithConfig :: Array Rule -> String -> Effect Int
190−lintFileWithConfig rules filePath = do
191− content <- FS.readTextFile filePath
192− case runRules rules (SourceCode content) of
193− Left err -> do
194− log $ filePath <> ": " <> err
195− pure 0
196− Right (LintResult result) -> do
197− for_ result.warnings \w -> do
198− log $ formatWarning filePath w
199− pure (Array.length result.warnings)
200−
201−-- | Fix a single file
202−fixFile :: Array Rule -> String -> Effect Int
203−fixFile rules filePath = do
204− content <- FS.readTextFile filePath
205− case runRules rules (SourceCode content) of
206− Left err -> do
207− log $ filePath <> ": " <> err
208− pure 0
209− Right result@(LintResult r) -> do
210− let fixedContent = applyAllFixes content result
211− when (fixedContent /= content) do
212− FS.writeTextFile filePath fixedContent
213− log $ filePath <> ": fixed " <> show (Array.length r.warnings) <> " issue(s)"
214− pure (Array.length r.warnings)
215−
216−-- | Recursively find all .purs files in a directory
217−findPursFiles :: String -> Effect (Array String)
218−findPursFiles dir = do
219− entries <- FS.readdir dir
220− files <- Array.foldM (collectFiles dir) [] entries
221− pure files
222− where
223− collectFiles :: String -> Array String -> String -> Effect (Array String)
224− collectFiles parentDir acc entry = do
225− let path = parentDir <> "/" <> entry
226− -- Skip hidden directories and node_modules
227− if String.take 1 entry == "." || entry == "node_modules" || entry == ".spago" then
228− pure acc
229− else do
230− stats <- FS.stat path
231− if stats.isDirectory then do
232− subFiles <- findPursFiles path
233− pure (acc <> subFiles)
234− else if String.takeRight 5 entry == ".purs" then
235− pure (Array.snoc acc path)
236− else
237− pure acc
238−
239−-- | Lint a file or directory with config
240−lintPathWithConfig :: Array Rule -> String -> Effect Int
241−lintPathWithConfig rules path = do
242− stats <- FS.stat path
243− if stats.isDirectory then do
244− files <- findPursFiles path
245− counts <- Array.foldM (\acc file -> do
246− count <- lintFileWithConfig rules file
247− pure (acc + count)) 0 files
248− pure counts
249− else
250− lintFileWithConfig rules path
251−
252−-- | Fix a file or directory
253−fixPath :: Array Rule -> String -> Effect Int
254−fixPath rules path = do
255− stats <- FS.stat path
256− if stats.isDirectory then do
257− files <- findPursFiles path
258− counts <- Array.foldM (\acc file -> do
259− count <- fixFile rules file
260− pure (acc + count)) 0 files
261− pure counts
262− else
263− fixFile rules path
264−
265−-- | Show usage
266−showUsage :: Effect Unit
267−showUsage = do
268− log "purelint - a linter for PureScript (70 rules)"
269− log ""
270− log "Usage: purelint [OPTIONS] [FILES/DIRECTORIES...]"
271− log ""
272− log "Options:"
273− log " --help Show this help"
274− log " --fix Auto-fix issues (where possible)"
275− log " --init Create a sample .purelintrc"
276− log " --list List all available rules"
277− log ""
278− log "Configuration:"
279− log " Create .purelintrc in your project root to disable rules."
280− log " Add one rule name per line (lines starting with # are comments)."
281− log ""
282− log "Examples:"
283− log " purelint src/ Lint all .purs files in src/"
284− log " purelint --fix src/ Auto-fix issues in src/"
285− log " purelint src/Main.purs Lint a single file"
286−
287−-- | List all available rules
288−listRules :: Effect Unit
289−listRules = do
290− log $ "Available rules (" <> show (Array.length allRules) <> " total):"
291− log ""
292− for_ allRules \rule -> do
293− log $ " " <> unwrap rule.ruleId
294−
295−-- | Create sample config file
296−initConfig :: Effect Unit
297−initConfig = do
298− hasConfig <- FS.exists ".purelintrc"
299− if hasConfig then
300− log ".purelintrc already exists"
301− else do
302− let sampleConfig = """# purelint configuration
303−# Add rule names (one per line) to disable them
304−# Example:
305−# EtaReduce
306−# LetToWhere
307−"""
308− FS.writeTextFile ".purelintrc" sampleConfig
309− log "Created .purelintrc"
310−
311−main :: Effect Unit
312−main = do
313− args <- Process.argv
314− -- args[0] is node, args[1] is the script, args[2..] are the actual arguments
315− let userArgs = Array.drop 1 args
316− case userArgs of
317− [] -> showUsage
318− ["--help"] -> showUsage
319− ["-h"] -> showUsage
320− ["--list"] -> listRules
321− ["--init"] -> initConfig
322− _ -> do
323− -- Check for --fix flag
324− let hasFix = Array.elem "--fix" userArgs
325− let paths = Array.filter (\a -> a /= "--fix") userArgs
326−
327− -- Load config and filter rules
328− config <- loadConfig
329− let enabledRules = filterRules allRules config
330−
331− if hasFix then do
332− -- Fix mode
333− _ <- Array.foldM (\acc path -> do
334− count <- fixPath enabledRules path
335− pure (acc + count)) 0 paths
336− pure unit
337− else do
338− -- Lint mode
339− totalWarnings <- Array.foldM (\acc path -> do
340− count <- lintPathWithConfig enabledRules path
341− pure (acc + count)) 0 paths
342− when (totalWarnings > 0) do
343− log ""
344− log $ "Total: " <> show totalWarnings <> " warning" <> if totalWarnings == 1 then "" else "s"
345− Process.exit 1
bin/purelint-lsp.jsdeleted−3
@@ -1,3 +0,0 @@
1−#!/usr/bin/env node
2−import { main } from "../output-lsp/Main.LSP/index.js";
3−main();
bin/purelint.jsdeleted−3
@@ -1,3 +0,0 @@
1−#!/usr/bin/env node
2−import { main } from "../output-cli/Main/index.js";
3−main();
bin/purelint-cli/spago.yaml → bin/purslint-cli/spago.yamlrenamed+3−2
@@ -1,5 +1,5 @@
11package:
2− name: purelint-cli
2+ name: purslint-cli
33 dependencies:
44 - arrays
55 - console
@@ -8,8 +8,9 @@ package:
88 - foldable-traversable
99 - maybe
1010 - newtype
11+ - optparse
1112 - prelude
12− - purelint
13+ - purslint
1314 - strings
1415 - node-buffer
1516 - node-fs
bin/purslint-cli/src/Main.pursnew file+419
@@ -0,0 +1,419 @@
1+module Main where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Either (Either(..))
7+import Data.Enum (fromEnum)
8+import Data.Foldable (for_)
9+import Data.Maybe (Maybe(..))
10+import Data.Newtype (unwrap)
11+import Data.String.CodeUnits as String
12+import Data.Tuple.Nested ((/\))
13+import Effect (Effect)
14+import Effect.Console (log)
15+import Node.FS.Simple as FS
16+import Node.CLI as Process
17+import Options.Applicative (Parser, argument, execParserPure, fullDesc, header, help, helper, info, long, many, metavar, progDesc, str, switch, (<**>))
18+import Options.Applicative.Builder (defaultPrefs)
19+import Options.Applicative.Types (CompletionResult(..), ParserFailure(ParserFailure), ParserResult(..))
20+import Options.Applicative.Help.Types (ParserHelp(ParserHelp), renderHelp)
21+import Purslint.Config (defaultConfig, filterRules, parseConfig)
22+import Purslint.Fix (applyAllFixes)
23+import Purslint.Rule (Rule)
24+import Purslint.Rules.BooleanSimplify (booleanSimplifyRule)
25+import Purslint.Rules.CollapseLambdas (collapseLambdasRule)
26+import Purslint.Rules.ConcatMap (concatMapRule)
27+import Purslint.Rules.EtaReduce (etaReduceRule)
28+import Purslint.Rules.EtaReduceDecl (etaReduceDeclRule)
29+import Purslint.Rules.AlternativeLaw (alternativeLawRule)
30+import Purslint.Rules.EvaluateBool (evaluateBoolRule)
31+import Purslint.Rules.EvaluateConst (evaluateConstRule)
32+import Purslint.Rules.EvaluateEither (evaluateEitherRule)
33+import Purslint.Rules.EvaluateFst (evaluateFstRule)
34+import Purslint.Rules.MapIdentity (mapIdentityRule)
35+import Purslint.Rules.FunctorLaw (functorLawRule)
36+import Purslint.Rules.LetToDo (letToDoRule)
37+import Purslint.Rules.LetToWhere (letToWhereRule)
38+import Purslint.Rules.MapFusion (mapFusionRule)
39+import Purslint.Rules.MonadLaw (monadLawRule)
40+import Purslint.Rules.MonoidIdentity (monoidIdentityRule)
41+import Purslint.Rules.NotEqual (notEqualRule)
42+import Purslint.Rules.NothingBind (nothingBindRule)
43+import Purslint.Rules.RedundantBind (redundantBindRule)
44+import Purslint.Rules.RedundantFlip (redundantFlipRule)
45+import Purslint.Rules.RedundantGuard (redundantGuardRule)
46+import Purslint.Rules.RedundantId (redundantIdRule)
47+import Purslint.Rules.RedundantIf (redundantIfRule)
48+import Purslint.Rules.RedundantNegate (redundantNegateRule)
49+import Purslint.Rules.RedundantNot (redundantNotRule)
50+import Purslint.Rules.RedundantReverse (redundantReverseRule)
51+import Purslint.Rules.UnlessNot (unlessNotRule)
52+import Purslint.Rules.UseAll (useAllRule)
53+import Purslint.Rules.UseAnd (useAndRule)
54+import Purslint.Rules.UseAny (useAnyRule)
55+import Purslint.Rules.UseApplicative (useApplicativeRule)
56+import Purslint.Rules.UseApplyFlipped (useApplyFlippedRule)
57+import Purslint.Rules.UseBimap (useBimapRule)
58+import Purslint.Rules.UseBindFlip (useBindFlipRule)
59+import Purslint.Rules.UseBreak (useBreakRule)
60+import Purslint.Rules.UseComparing (useComparingRule)
61+import Purslint.Rules.UseConst (useConstRule)
62+import Purslint.Rules.UseDollar (useDollarRule)
63+import Purslint.Rules.UseEitherMap (useEitherMapRule)
64+import Purslint.Rules.UseElemIndex (useElemIndexRule)
65+import Purslint.Rules.UseFindMap (useFindMapRule)
66+import Purslint.Rules.UseFold (useFoldRule)
67+import Purslint.Rules.UseFoldBool (useFoldBoolRule)
68+import Purslint.Rules.UseFoldMap (useFoldMapRule)
69+import Purslint.Rules.UseFoldMapId (useFoldMapIdRule)
70+import Purslint.Rules.UseFor (useForRule)
71+import Purslint.Rules.UseFromJust (useFromJustRule)
72+import Purslint.Rules.UseFromMaybe (useFromMaybeRule)
73+import Purslint.Rules.UseFstSnd (useFstSndRule)
74+import Purslint.Rules.UseGuard (useGuardRule)
75+import Purslint.Rules.UseGuardMaybe (useGuardMaybeRule)
76+import Purslint.Rules.UseHead (useHeadRule)
77+import Purslint.Rules.UseIsJust (useIsJustRule)
78+import Purslint.Rules.UseIsNothing (useIsNothingRule)
79+import Purslint.Rules.UseJoin (useJoinRule)
80+import Purslint.Rules.UseLastReverse (useLastReverseRule)
81+import Purslint.Rules.UseMapMaybe (useMapMaybeRule)
82+import Purslint.Rules.UseMaybeMap (useMaybeMapRule)
83+import Purslint.Rules.UseMinMax (useMinMaxRule)
84+import Purslint.Rules.UseMinimumSort (useMinimumSortRule)
85+import Purslint.Rules.UseNotElem (useNotElemRule)
86+import Purslint.Rules.UseNull (useNullRule)
87+import Purslint.Rules.UseCaseOf (useCaseOfRule)
88+import Purslint.Rules.FlattenCase (flattenCaseRule)
89+import Purslint.Rules.UseOn (useOnRule)
90+import Purslint.Rules.UseOr (useOrRule)
91+import Purslint.Rules.UsePatternGuards (usePatternGuardsRule)
92+import Purslint.Rules.UseReplicate (useReplicateRule)
93+import Purslint.Rules.UseSequence (useSequenceRule)
94+import Purslint.Rules.UseSpan (useSpanRule)
95+import Purslint.Rules.UseTraverse (useTraverseRule)
96+import Purslint.Rules.UseTraverseSequence (useTraverseSequenceRule)
97+import Purslint.Rules.UseUncurry (useUncurryRule)
98+import Purslint.Rules.UseUnless (useUnlessRule)
99+import Purslint.Rules.UseUnwrap (useUnwrapRule)
100+import Purslint.Rules.UseVoid (useVoidRule)
101+import Purslint.Rules.UseWhen (useWhenRule)
102+import Purslint.Rules.UseZip (useZipRule)
103+import Purslint.Rules.WhenNot (whenNotRule)
104+import Purslint.Rules.RedundantParens (redundantParensRule)
105+import Purslint.Runner (runRules)
106+import Purslint.Types (LintResult(..), LintWarning(..), Severity(..), SourceCode(..), Suggestion(..))
107+
108+type CliOptions =
109+ { fix :: Boolean
110+ , listRules :: Boolean
111+ , initConfig :: Boolean
112+ , paths :: Array String
113+ }
114+
115+-- | All available rules (81 total)
116+allRules :: Array Rule
117+allRules =
118+ [ alternativeLawRule
119+ , booleanSimplifyRule
120+ , collapseLambdasRule
121+ , concatMapRule
122+ , etaReduceRule
123+ , etaReduceDeclRule
124+ , evaluateBoolRule
125+ , evaluateConstRule
126+ , evaluateEitherRule
127+ , evaluateFstRule
128+ , mapIdentityRule
129+ , functorLawRule
130+ , letToDoRule
131+ , letToWhereRule
132+ , mapFusionRule
133+ , monadLawRule
134+ , monoidIdentityRule
135+ , notEqualRule
136+ , nothingBindRule
137+ , redundantBindRule
138+ , redundantFlipRule
139+ , redundantGuardRule
140+ , redundantIdRule
141+ , redundantIfRule
142+ , redundantNegateRule
143+ , redundantNotRule
144+ , redundantParensRule
145+ , redundantReverseRule
146+ , unlessNotRule
147+ , useAllRule
148+ , useAndRule
149+ , useAnyRule
150+ , useApplicativeRule
151+ , useApplyFlippedRule
152+ , useBimapRule
153+ , useBindFlipRule
154+ , useBreakRule
155+ , useComparingRule
156+ , useConstRule
157+ , useDollarRule
158+ , useEitherMapRule
159+ , useElemIndexRule
160+ , useFindMapRule
161+ , useFoldRule
162+ , useFoldBoolRule
163+ , useFoldMapRule
164+ , useFoldMapIdRule
165+ , useForRule
166+ , useFromJustRule
167+ , useFromMaybeRule
168+ , useFstSndRule
169+ , useGuardRule
170+ , useGuardMaybeRule
171+ , useHeadRule
172+ , useIsJustRule
173+ , useIsNothingRule
174+ , useJoinRule
175+ , useLastReverseRule
176+ , useMapMaybeRule
177+ , useMaybeMapRule
178+ , useMinMaxRule
179+ , useMinimumSortRule
180+ , useNotElemRule
181+ , useNullRule
182+ , useOnRule
183+ , useOrRule
184+ , usePatternGuardsRule
185+ , useCaseOfRule
186+ , flattenCaseRule
187+ , useReplicateRule
188+ , useSequenceRule
189+ , useSpanRule
190+ , useTraverseRule
191+ , useTraverseSequenceRule
192+ , useUncurryRule
193+ , useUnlessRule
194+ , useUnwrapRule
195+ , useVoidRule
196+ , useWhenRule
197+ , useZipRule
198+ , whenNotRule
199+ ]
200+
201+-- | Load config from .purslintrc file
202+loadConfig :: Effect _
203+loadConfig = do
204+ hasConfig <- FS.exists ".purslintrc"
205+ if hasConfig then do
206+ content <- FS.readTextFile ".purslintrc"
207+ pure $ parseConfig content
208+ else
209+ pure defaultConfig
210+
211+-- | Format a source range for display
212+formatRange :: { start :: { line :: Int, column :: Int }, end :: { line :: Int, column :: Int } } -> String
213+formatRange r = show (r.start.line + 1) <> ":" <> show (r.start.column + 1) <> "-" <> show (r.end.line + 1) <> ":" <> show (r.end.column + 1)
214+
215+-- | Format severity for display
216+formatSeverity :: Severity -> String
217+formatSeverity Refactor = "[refactor]"
218+formatSeverity Hint = "[hint]"
219+formatSeverity Warning = "[warning]"
220+formatSeverity Error = "[error]"
221+
222+-- | Format a single warning for display
223+formatWarning :: String -> LintWarning -> String
224+formatWarning filePath (LintWarning w) =
225+ filePath <> ":" <> formatRange w.range <> " " <> formatSeverity w.severity <> " " <> unwrap w.ruleId <> ": " <> unwrap w.message <>
226+ case w.suggestion of
227+ Nothing -> ""
228+ Just (Suggestion s) -> "\n Replace with: " <> unwrap s.replacement
229+
230+-- | Lint a single file with config
231+lintFileWithConfig :: Array Rule -> String -> Effect Int
232+lintFileWithConfig rules filePath = do
233+ content <- FS.readTextFile filePath
234+ case runRules rules (SourceCode content) of
235+ Left err -> do
236+ log $ filePath <> ": " <> err
237+ pure 0
238+ Right (LintResult result) -> do
239+ for_ result.warnings \w -> do
240+ log $ formatWarning filePath w
241+ pure (Array.length result.warnings)
242+
243+-- | Fix a single file
244+fixFile :: Array Rule -> String -> Effect Int
245+fixFile rules filePath = do
246+ content <- FS.readTextFile filePath
247+ case runRules rules (SourceCode content) of
248+ Left err -> do
249+ log $ filePath <> ": " <> err
250+ pure 0
251+ Right result@(LintResult r) -> do
252+ let fixedContent = applyAllFixes content result
253+ when (fixedContent /= content) do
254+ FS.writeTextFile filePath fixedContent
255+ log $ filePath <> ": fixed " <> show (Array.length r.warnings) <> " issue(s)"
256+ pure (Array.length r.warnings)
257+
258+-- | Recursively find all .purs files in a directory
259+findPursFiles :: String -> Effect (Array String)
260+findPursFiles dir = do
261+ entries <- FS.readdir dir
262+ files <- Array.foldM (collectFiles dir) [] entries
263+ pure files
264+ where
265+ collectFiles :: String -> Array String -> String -> Effect (Array String)
266+ collectFiles parentDir acc entry = do
267+ let path = parentDir <> "/" <> entry
268+ -- Skip hidden directories and node_modules
269+ if String.take 1 entry == "." || entry == "node_modules" || entry == ".spago" then
270+ pure acc
271+ else do
272+ stats <- FS.stat path
273+ if stats.isDirectory then do
274+ subFiles <- findPursFiles path
275+ pure (acc <> subFiles)
276+ else if String.takeRight 5 entry == ".purs" then
277+ pure (Array.snoc acc path)
278+ else
279+ pure acc
280+
281+-- | Lint a file or directory with config
282+lintPathWithConfig :: Array Rule -> String -> Effect Int
283+lintPathWithConfig rules path = do
284+ stats <- FS.stat path
285+ if stats.isDirectory then do
286+ files <- findPursFiles path
287+ counts <- Array.foldM (\acc file -> do
288+ count <- lintFileWithConfig rules file
289+ pure (acc + count)) 0 files
290+ pure counts
291+ else
292+ lintFileWithConfig rules path
293+
294+-- | Fix a file or directory
295+fixPath :: Array Rule -> String -> Effect Int
296+fixPath rules path = do
297+ stats <- FS.stat path
298+ if stats.isDirectory then do
299+ files <- findPursFiles path
300+ counts <- Array.foldM (\acc file -> do
301+ count <- fixFile rules file
302+ pure (acc + count)) 0 files
303+ pure counts
304+ else
305+ fixFile rules path
306+
307+-- | Show usage
308+showUsage :: Effect Unit
309+showUsage = do
310+ log "purslint - a linter for PureScript (81 rules)"
311+ log ""
312+ log "Usage: purslint [OPTIONS] [FILES/DIRECTORIES...]"
313+ log ""
314+ log "Options:"
315+ log " --help Show this help"
316+ log " --fix Auto-fix issues (where possible)"
317+ log " --init Create a sample .purslintrc"
318+ log " --list List all available rules"
319+ log ""
320+ log "Configuration:"
321+ log " Create .purslintrc in your project root to disable rules."
322+ log " Add one rule name per line (lines starting with # are comments)."
323+ log ""
324+ log "Examples:"
325+ log " purslint src/ Lint all .purs files in src/"
326+ log " purslint --fix src/ Auto-fix issues in src/"
327+ log " purslint src/Main.purs Lint a single file"
328+
329+-- | List all available rules
330+listRules :: Effect Unit
331+listRules = do
332+ log $ "Available rules (" <> show (Array.length allRules) <> " total):"
333+ log ""
334+ for_ allRules \rule -> do
335+ log $ " " <> unwrap rule.ruleId
336+
337+-- | Create sample config file
338+initConfig :: Effect Unit
339+initConfig = do
340+ hasConfig <- FS.exists ".purslintrc"
341+ if hasConfig then
342+ log ".purslintrc already exists"
343+ else do
344+ let sampleConfig = """# purslint configuration
345+# Add rule names (one per line) to disable them
346+# Example:
347+# EtaReduce
348+# LetToWhere
349+"""
350+ FS.writeTextFile ".purslintrc" sampleConfig
351+ log "Created .purslintrc"
352+
353+parseFlags :: Array String -> Effect (Maybe CliOptions)
354+parseFlags args = case parseFlags' (Array.drop 2 args) of
355+ Success cli -> pure (Just cli)
356+ Failure (ParserFailure pf) -> case pf programName of
357+ ph@(ParserHelp _) /\ exitCode /\ i /\ _unit -> do
358+ log $ renderHelp i ph
359+ Process.exit (fromEnum exitCode)
360+ pure Nothing
361+ CompletionInvoked (CompletionResult { execCompletion }) -> do
362+ executedCompletion <- execCompletion programName
363+ log executedCompletion
364+ Process.exit 0
365+ pure Nothing
366+ where
367+ programName = "purslint"
368+
369+parseFlags' :: Array String -> ParserResult CliOptions
370+parseFlags' = execParserPure defaultPrefs opts
371+ where
372+ opts =
373+ info (flagsParser <**> helper) $
374+ fullDesc
375+ <> progDesc "Lint PureScript source files"
376+ <> header "purslint - a linter for PureScript"
377+
378+flagsParser :: Parser CliOptions
379+flagsParser = ado
380+ fix <- switch $ long "fix" <> help "Auto-fix issues (where possible)"
381+ listRules <- switch $ long "list" <> help "List all available rules"
382+ initConfig <- switch $ long "init" <> help "Create a sample .purslintrc"
383+ pathsList <- many $ argument str $ metavar "PATH..."
384+ let paths = Array.fromFoldable pathsList
385+ in { fix, listRules, initConfig, paths }
386+
387+main :: Effect Unit
388+main = do
389+ args <- Process.argv
390+ maybeCli <- parseFlags args
391+ case maybeCli of
392+ Nothing -> pure unit
393+ Just cli ->
394+ if cli.listRules then
395+ listRules
396+ else if cli.initConfig then
397+ initConfig
398+ else if Array.null cli.paths then
399+ showUsage
400+ else do
401+ -- Load config and filter rules
402+ config <- loadConfig
403+ let enabledRules = filterRules allRules config
404+
405+ if cli.fix then do
406+ -- Fix mode
407+ _ <- Array.foldM (\acc path -> do
408+ count <- fixPath enabledRules path
409+ pure (acc + count)) 0 cli.paths
410+ pure unit
411+ else do
412+ -- Lint mode
413+ totalWarnings <- Array.foldM (\acc path -> do
414+ count <- lintPathWithConfig enabledRules path
415+ pure (acc + count)) 0 cli.paths
416+ when (totalWarnings > 0) do
417+ log ""
418+ log $ "Total: " <> show totalWarnings <> " warning" <> if totalWarnings == 1 then "" else "s"
419+ Process.exit 1
bin/purelint-cli/src/Node/CLI.js → bin/purslint-cli/src/Node/CLI.jsrenamed

The content is the same.

bin/purelint-cli/src/Node/CLI.purs → bin/purslint-cli/src/Node/CLI.pursrenamed

The content is the same.

bin/purelint-cli/src/Node/FS/Simple.js → bin/purslint-cli/src/Node/FS/Simple.jsrenamed

The content is the same.

bin/purelint-cli/src/Node/FS/Simple.purs → bin/purslint-cli/src/Node/FS/Simple.pursrenamed

The content is the same.

bin/purslint-lsp.jsnew file+2
@@ -0,0 +1,2 @@
1+#!/usr/bin/env node
2+import "../dist/purslint-lsp.js";
bin/purelint-lsp/spago.yaml → bin/purslint-lsp/spago.yamlrenamed+2−2
@@ -1,5 +1,5 @@
11package:
2− name: purelint-lsp
2+ name: purslint-lsp
33 dependencies:
44 - arrays
55 - effect
@@ -8,5 +8,5 @@ package:
88 - maybe
99 - newtype
1010 - prelude
11− - purelint
11+ - purslint
1212 - unsafe-coerce
bin/purelint-lsp/src/LSP.js → bin/purslint-lsp/src/LSP.jsrenamed+1−1
@@ -1,4 +1,4 @@
1−// Purelint.LSP FFI for JavaScript
1+// Purslint.LSP FFI for JavaScript
22
33// Create a null value
44export const mkNull = (_) => null;
bin/purelint-lsp/src/LSP.purs → bin/purslint-lsp/src/LSP.pursrenamed+234−126
@@ -1,9 +1,11 @@
1−module Purelint.LSP where
1+module Purslint.LSP where
22
33import Prelude
44
55import Data.Array as Array
66import Data.Either (Either(..))
7+import Data.Map (Map)
8+import Data.Map as Map
79import Data.Maybe (Maybe(..), fromMaybe)
810import Data.Newtype (unwrap)
911import Effect (Effect)
@@ -11,77 +13,91 @@ import Effect.Ref (Ref)
1113import Effect.Ref as Ref
1214import Foreign (Foreign)
1315import LSP.Stdio as Stdio
14−import Purelint.Config (Config, defaultConfig, filterRules)
15−import Purelint.Rule (Rule)
16−import Purelint.Rules.BooleanSimplify (booleanSimplifyRule)
17−import Purelint.Rules.CollapseLambdas (collapseLambdasRule)
18−import Purelint.Rules.ConcatMap (concatMapRule)
19−import Purelint.Rules.EtaReduce (etaReduceRule)
20−import Purelint.Rules.EtaReduceDecl (etaReduceDeclRule)
21−import Purelint.Rules.FmapId (fmapIdRule)
22−import Purelint.Rules.FunctorLaw (functorLawRule)
23−import Purelint.Rules.LetToWhere (letToWhereRule)
24−import Purelint.Rules.MapFusion (mapFusionRule)
25−import Purelint.Rules.MonoidIdentity (monoidIdentityRule)
26−import Purelint.Rules.NotEqual (notEqualRule)
27−import Purelint.Rules.RedundantBind (redundantBindRule)
28−import Purelint.Rules.RedundantFlip (redundantFlipRule)
29−import Purelint.Rules.RedundantIf (redundantIfRule)
30−import Purelint.Rules.RedundantNot (redundantNotRule)
31−import Purelint.Rules.UseAll (useAllRule)
32−import Purelint.Rules.UseAny (useAnyRule)
33−import Purelint.Rules.UseConst (useConstRule)
34−import Purelint.Rules.UseFold (useFoldRule)
35−import Purelint.Rules.UseFromMaybe (useFromMaybeRule)
36−import Purelint.Rules.UseFstSnd (useFstSndRule)
37−import Purelint.Rules.UseGuard (useGuardRule)
38−import Purelint.Rules.UseIsJust (useIsJustRule)
39−import Purelint.Rules.UseIsNothing (useIsNothingRule)
40−import Purelint.Rules.UseJoin (useJoinRule)
41−import Purelint.Rules.UseMinMax (useMinMaxRule)
42−import Purelint.Rules.UseNotElem (useNotElemRule)
43−import Purelint.Rules.UseNull (useNullRule)
44−import Purelint.Rules.UseSequence (useSequenceRule)
45−import Purelint.Rules.UseTraverse (useTraverseRule)
46−import Purelint.Rules.UseTraverseSequence (useTraverseSequenceRule)
47−import Purelint.Rules.UseUnless (useUnlessRule)
48−import Purelint.Rules.UseVoid (useVoidRule)
49−import Purelint.Rules.UseWhen (useWhenRule)
50−import Purelint.Rules.UseMapMaybe (useMapMaybeRule)
51−import Purelint.Rules.UseGuardMaybe (useGuardMaybeRule)
52−import Purelint.Rules.UseMaybeMap (useMaybeMapRule)
53−import Purelint.Rules.UseApplicative (useApplicativeRule)
54−import Purelint.Rules.UseBindFlip (useBindFlipRule)
55−import Purelint.Rules.UseFor (useForRule)
56−import Purelint.Rules.RedundantGuard (redundantGuardRule)
57−import Purelint.Rules.UseComparing (useComparingRule)
58−import Purelint.Rules.UseOn (useOnRule)
59−import Purelint.Rules.UseFindMap (useFindMapRule)
60−import Purelint.Rules.UseLastReverse (useLastReverseRule)
61−import Purelint.Rules.RedundantReverse (redundantReverseRule)
62−import Purelint.Rules.UseBreak (useBreakRule)
63−import Purelint.Rules.UseSpan (useSpanRule)
64−import Purelint.Rules.UseMinimumSort (useMinimumSortRule)
65−import Purelint.Rules.UseBimap (useBimapRule)
66−import Purelint.Rules.UseEitherMap (useEitherMapRule)
67−import Purelint.Rules.EvaluateFst (evaluateFstRule)
68−import Purelint.Rules.EvaluateConst (evaluateConstRule)
69−import Purelint.Rules.UseOr (useOrRule)
70−import Purelint.Rules.UseAnd (useAndRule)
71−import Purelint.Rules.UseFoldMapId (useFoldMapIdRule)
72−import Purelint.Rules.WhenNot (whenNotRule)
73−import Purelint.Rules.UnlessNot (unlessNotRule)
74−import Purelint.Rules.UseZip (useZipRule)
75−import Purelint.Rules.UseReplicate (useReplicateRule)
76−import Purelint.Rules.UseUncurry (useUncurryRule)
77−import Purelint.Rules.RedundantNegate (redundantNegateRule)
78−import Purelint.Rules.UseFromJust (useFromJustRule)
79−import Purelint.Rules.UseHead (useHeadRule)
80−import Purelint.Rules.RedundantId (redundantIdRule)
81−import Purelint.Rules.NothingBind (nothingBindRule)
82−import Purelint.Rules.UseElemIndex (useElemIndexRule)
83−import Purelint.Runner (runRules)
84−import Purelint.Types (LintResult(..), LintWarning(..), RuleId(..), Severity(..), SourceCode(..), Suggestion(..))
16+import Purslint.Config (Config, defaultConfig, filterRules)
17+import Purslint.Rule (Rule)
18+import Purslint.Rules.BooleanSimplify (booleanSimplifyRule)
19+import Purslint.Rules.CollapseLambdas (collapseLambdasRule)
20+import Purslint.Rules.ConcatMap (concatMapRule)
21+import Purslint.Rules.EtaReduce (etaReduceRule)
22+import Purslint.Rules.EtaReduceDecl (etaReduceDeclRule)
23+import Purslint.Rules.MapIdentity (mapIdentityRule)
24+import Purslint.Rules.FunctorLaw (functorLawRule)
25+import Purslint.Rules.LetToDo (letToDoRule)
26+import Purslint.Rules.LetToWhere (letToWhereRule)
27+import Purslint.Rules.MapFusion (mapFusionRule)
28+import Purslint.Rules.MonadLaw (monadLawRule)
29+import Purslint.Rules.MonoidIdentity (monoidIdentityRule)
30+import Purslint.Rules.NotEqual (notEqualRule)
31+import Purslint.Rules.RedundantBind (redundantBindRule)
32+import Purslint.Rules.RedundantFlip (redundantFlipRule)
33+import Purslint.Rules.RedundantIf (redundantIfRule)
34+import Purslint.Rules.RedundantNot (redundantNotRule)
35+import Purslint.Rules.UseAll (useAllRule)
36+import Purslint.Rules.UseAny (useAnyRule)
37+import Purslint.Rules.UseConst (useConstRule)
38+import Purslint.Rules.UseFold (useFoldRule)
39+import Purslint.Rules.UseFromMaybe (useFromMaybeRule)
40+import Purslint.Rules.UseFstSnd (useFstSndRule)
41+import Purslint.Rules.UseGuard (useGuardRule)
42+import Purslint.Rules.UseIsJust (useIsJustRule)
43+import Purslint.Rules.UseIsNothing (useIsNothingRule)
44+import Purslint.Rules.UseJoin (useJoinRule)
45+import Purslint.Rules.UseMinMax (useMinMaxRule)
46+import Purslint.Rules.UseNotElem (useNotElemRule)
47+import Purslint.Rules.UseNull (useNullRule)
48+import Purslint.Rules.UseSequence (useSequenceRule)
49+import Purslint.Rules.UseTraverse (useTraverseRule)
50+import Purslint.Rules.UseTraverseSequence (useTraverseSequenceRule)
51+import Purslint.Rules.UseUnless (useUnlessRule)
52+import Purslint.Rules.UseUnwrap (useUnwrapRule)
53+import Purslint.Rules.UseVoid (useVoidRule)
54+import Purslint.Rules.UseWhen (useWhenRule)
55+import Purslint.Rules.UseMapMaybe (useMapMaybeRule)
56+import Purslint.Rules.UseGuardMaybe (useGuardMaybeRule)
57+import Purslint.Rules.UseMaybeMap (useMaybeMapRule)
58+import Purslint.Rules.UseApplicative (useApplicativeRule)
59+import Purslint.Rules.UseApplyFlipped (useApplyFlippedRule)
60+import Purslint.Rules.UseBindFlip (useBindFlipRule)
61+import Purslint.Rules.UseFor (useForRule)
62+import Purslint.Rules.RedundantGuard (redundantGuardRule)
63+import Purslint.Rules.UseComparing (useComparingRule)
64+import Purslint.Rules.UseOn (useOnRule)
65+import Purslint.Rules.UseFindMap (useFindMapRule)
66+import Purslint.Rules.UseLastReverse (useLastReverseRule)
67+import Purslint.Rules.RedundantReverse (redundantReverseRule)
68+import Purslint.Rules.UseBreak (useBreakRule)
69+import Purslint.Rules.UseSpan (useSpanRule)
70+import Purslint.Rules.UseMinimumSort (useMinimumSortRule)
71+import Purslint.Rules.UseBimap (useBimapRule)
72+import Purslint.Rules.UseDollar (useDollarRule)
73+import Purslint.Rules.UseEitherMap (useEitherMapRule)
74+import Purslint.Rules.AlternativeLaw (alternativeLawRule)
75+import Purslint.Rules.EvaluateBool (evaluateBoolRule)
76+import Purslint.Rules.EvaluateConst (evaluateConstRule)
77+import Purslint.Rules.EvaluateEither (evaluateEitherRule)
78+import Purslint.Rules.EvaluateFst (evaluateFstRule)
79+import Purslint.Rules.UseOr (useOrRule)
80+import Purslint.Rules.UseAnd (useAndRule)
81+import Purslint.Rules.UseFoldMap (useFoldMapRule)
82+import Purslint.Rules.UseFoldMapId (useFoldMapIdRule)
83+import Purslint.Rules.WhenNot (whenNotRule)
84+import Purslint.Rules.UnlessNot (unlessNotRule)
85+import Purslint.Rules.UseZip (useZipRule)
86+import Purslint.Rules.UseReplicate (useReplicateRule)
87+import Purslint.Rules.UseUncurry (useUncurryRule)
88+import Purslint.Rules.RedundantNegate (redundantNegateRule)
89+import Purslint.Rules.UseFromJust (useFromJustRule)
90+import Purslint.Rules.UseHead (useHeadRule)
91+import Purslint.Rules.RedundantId (redundantIdRule)
92+import Purslint.Rules.NothingBind (nothingBindRule)
93+import Purslint.Rules.UseElemIndex (useElemIndexRule)
94+import Purslint.Rules.UseFoldBool (useFoldBoolRule)
95+import Purslint.Rules.UsePatternGuards (usePatternGuardsRule)
96+import Purslint.Rules.UseCaseOf (useCaseOfRule)
97+import Purslint.Rules.FlattenCase (flattenCaseRule)
98+import Purslint.Rules.RedundantParens (redundantParensRule)
99+import Purslint.Runner (runRules)
100+import Purslint.Types (LintResult(..), LintWarning(..), RuleId(..), Severity(..), SourceCode(..), Suggestion(..))
85101import Unsafe.Coerce (unsafeCoerce)
86102
87103-- | All available rules
@@ -89,16 +105,18 @@ allRules :: Array Rule
89105allRules =
90106 [ useTraverseRule
91107 , mapFusionRule
92− , fmapIdRule
108+ , mapIdentityRule
93109 , notEqualRule
94110 , concatMapRule
95111 , useGuardRule
96112 , etaReduceRule
97113 , etaReduceDeclRule
98114 , redundantBindRule
115+ , letToDoRule
99116 , letToWhereRule
100117 , redundantIfRule
101118 , functorLawRule
119+ , useUnwrapRule
102120 , useVoidRule
103121 , useJoinRule
104122 , useAnyRule
@@ -125,6 +143,7 @@ allRules =
125143 , useGuardMaybeRule
126144 , useMaybeMapRule
127145 , useApplicativeRule
146+ , useApplyFlippedRule
128147 , useBindFlipRule
129148 , useForRule
130149 , redundantGuardRule
@@ -140,10 +159,16 @@ allRules =
140159 , useBimapRule
141160 , useEitherMapRule
142161 , evaluateFstRule
162+ , evaluateBoolRule
163+ , evaluateEitherRule
143164 , evaluateConstRule
144165 , useOrRule
145166 , useAndRule
167+ , useFoldMapRule
146168 , useFoldMapIdRule
169+ , useFoldBoolRule
170+ , monadLawRule
171+ , alternativeLawRule
147172 , whenNotRule
148173 , unlessNotRule
149174 , useZipRule
@@ -155,12 +180,23 @@ allRules =
155180 , redundantIdRule
156181 , nothingBindRule
157182 , useElemIndexRule
183+ , usePatternGuardsRule
184+ , useCaseOfRule
185+ , flattenCaseRule
186+ , redundantParensRule
187+ , useDollarRule
158188 ]
159189
160190-- | Get enabled rules based on config
161191getEnabledRules :: Config -> Array Rule
162192getEnabledRules config = filterRules allRules config
163193
194+-- | LSP server state
195+type LSPState =
196+ { config :: Config
197+ , documents :: Map String String -- URI -> content
198+ }
199+
164200-- | Convert lint warning to LSP diagnostic (with fix data embedded)
165201warningToDiagnostic :: LintWarning -> Foreign
166202warningToDiagnostic (LintWarning w) =
@@ -172,7 +208,7 @@ warningToDiagnostic (LintWarning w) =
172208 base =
173209 { range: range
174210 , severity: severityToLSP w.severity
175− , source: "purelint"
211+ , source: "purslint"
176212 , message: unwrap w.ruleId <> ": " <> unwrap w.message
177213 }
178214 in case w.suggestion of
@@ -180,7 +216,7 @@ warningToDiagnostic (LintWarning w) =
180216 Just (Suggestion sug) -> unsafeCoerce
181217 { range: range
182218 , severity: severityToLSP w.severity
183− , source: "purelint"
219+ , source: "purslint"
184220 , message: unwrap w.ruleId <> ": " <> unwrap w.message
185221 , data:
186222 { hasFix: true
@@ -194,14 +230,18 @@ severityToLSP :: Severity -> Int
194230severityToLSP Error = 1
195231severityToLSP Warning = 2
196232severityToLSP Hint = 4
233+severityToLSP Refactor = 4 -- Same as Hint, but filtered out
197234
198−-- | Lint a document and return diagnostics
235+-- | Lint a document and return diagnostics (excludes Refactor severity)
199236lintDocument :: Config -> String -> Array Foreign
200237lintDocument config content =
201238 let rules = getEnabledRules config
202239 in case runRules rules (SourceCode content) of
203240 Left _ -> []
204− Right (LintResult result) -> map warningToDiagnostic result.warnings
241+ Right (LintResult result) ->
242+ map warningToDiagnostic $ Array.filter (not <<< isRefactor) result.warnings
243+ where
244+ isRefactor (LintWarning w) = w.severity == Refactor
205245
206246-- | Create a publishDiagnostics notification
207247mkPublishDiagnostics :: String -> Array Foreign -> String
@@ -233,7 +273,7 @@ mkInitializeResponse reqId =
233273 }
234274 }
235275 , serverInfo:
236− { name: "purelint-lsp"
276+ { name: "purslint-lsp"
237277 , version: "0.1.0"
238278 }
239279 }
@@ -294,32 +334,32 @@ foreign import hasKey :: String -> Foreign -> Boolean
294334foreign import getKey :: String -> Foreign -> Foreign
295335
296336-- | Parse settings to get disabled rules
297−-- | Expected format: { "purelint": { "disabledRules": ["RuleId1", "RuleId2"] } }
298−-- | Or: { "purelint.disabledRules": ["RuleId1", "RuleId2"] }
299−-- | Or: { "purelint.enabled": false } to disable all rules
337+-- | Expected format: { "purslint": { "disabledRules": ["RuleId1", "RuleId2"] } }
338+-- | Or: { "purslint.disabledRules": ["RuleId1", "RuleId2"] }
339+-- | Or: { "purslint.enabled": false } to disable all rules
300340parseSettings :: Foreign -> Config
301341parseSettings settings =
302− -- Try to get purelint.enabled first
303− case getFieldObj "purelint" settings of
304− Just purelintObj ->
342+ -- Try to get purslint.enabled first
343+ case getFieldObj "purslint" settings of
344+ Just purslintObj ->
305345 -- Check if all rules are disabled
306− case getFieldBool "enabled" purelintObj of
346+ case getFieldBool "enabled" purslintObj of
307347 Just false ->
308348 { disabledRules: map (\r -> r.ruleId) allRules } -- Disable all rules
309349 _ ->
310350 -- Get disabled rules list
311− case getFieldArr "disabledRules" purelintObj of
351+ case getFieldArr "disabledRules" purslintObj of
312352 Just arr ->
313353 { disabledRules: Array.mapMaybe parseRuleId arr }
314354 Nothing -> defaultConfig
315355 Nothing ->
316− -- Try flat format: purelint.disabledRules
317− case getFieldArr "purelint.disabledRules" settings of
356+ -- Try flat format: purslint.disabledRules
357+ case getFieldArr "purslint.disabledRules" settings of
318358 Just arr ->
319359 { disabledRules: Array.mapMaybe parseRuleId arr }
320360 Nothing ->
321− -- Try purelint.enabled
322− case getFieldBool "purelint.enabled" settings of
361+ -- Try purslint.enabled
362+ case getFieldBool "purslint.enabled" settings of
323363 Just false ->
324364 { disabledRules: map (\r -> r.ruleId) allRules }
325365 _ -> defaultConfig
@@ -332,8 +372,8 @@ parseSettings settings =
332372 in if s == "" then Nothing else Just (RuleId s)
333373
334374-- | Handle a single message
335−handleMessage :: Ref Config -> Foreign -> Effect Unit
336−handleMessage configRef msg = do
375+handleMessage :: Ref LSPState -> Foreign -> Effect Unit
376+handleMessage stateRef msg = do
337377 let method = fromMaybe "" (getField "method" msg)
338378 let reqId = fromMaybe 0 (getFieldInt "id" msg)
339379
@@ -347,7 +387,7 @@ handleMessage configRef msg = do
347387 case getFieldObj "initializationOptions" params of
348388 Just options -> do
349389 let config = parseSettings options
350− Ref.write config configRef
390+ Ref.modify_ (_ { config = config }) stateRef
351391 Stdio.logMessage $ "Initial config loaded, disabled: " <> show (Array.length config.disabledRules)
352392 Nothing -> pure unit
353393 Nothing -> pure unit
@@ -355,15 +395,15 @@ handleMessage configRef msg = do
355395 else if method == "initialized" then do
356396 Stdio.logMessage "Client initialized"
357397 else if method == "workspace/didChangeConfiguration" then do
358− handleDidChangeConfiguration configRef msg
398+ handleDidChangeConfiguration stateRef msg
359399 else if method == "textDocument/didOpen" then do
360− handleDidOpen configRef msg
400+ handleDidOpen stateRef msg
361401 else if method == "textDocument/didChange" then do
362− handleDidChange configRef msg
402+ handleDidChange stateRef msg
363403 else if method == "textDocument/didClose" then do
364− handleDidClose msg
404+ handleDidClose stateRef msg
365405 else if method == "textDocument/codeAction" then do
366− handleCodeAction msg reqId
406+ handleCodeAction stateRef msg reqId
367407 else if method == "shutdown" then do
368408 Stdio.logMessage "Shutdown"
369409 Stdio.writeMessage $ mkResponse reqId
@@ -372,8 +412,8 @@ handleMessage configRef msg = do
372412 else do
373413 Stdio.logMessage $ "Unknown method: " <> method
374414
375−handleDidChangeConfiguration :: Ref Config -> Foreign -> Effect Unit
376−handleDidChangeConfiguration configRef msg = do
415+handleDidChangeConfiguration :: Ref LSPState -> Foreign -> Effect Unit
416+handleDidChangeConfiguration stateRef msg = do
377417 Stdio.logMessage "Configuration changed"
378418 case getFieldObj "params" msg of
379419 Nothing -> pure unit
@@ -382,13 +422,12 @@ handleDidChangeConfiguration configRef msg = do
382422 Nothing -> pure unit
383423 Just settings -> do
384424 let config = parseSettings settings
385− Ref.write config configRef
425+ Ref.modify_ (_ { config = config }) stateRef
386426 Stdio.logMessage $ "Config updated, disabled rules: " <> show (Array.length config.disabledRules)
387427
388−handleDidOpen :: Ref Config -> Foreign -> Effect Unit
389−handleDidOpen configRef msg = do
428+handleDidOpen :: Ref LSPState -> Foreign -> Effect Unit
429+handleDidOpen stateRef msg = do
390430 Stdio.logMessage "Document opened"
391− config <- Ref.read configRef
392431 case getFieldObj "params" msg of
393432 Nothing -> pure unit
394433 Just params ->
@@ -397,13 +436,15 @@ handleDidOpen configRef msg = do
397436 Just textDoc -> do
398437 let uri = fromMaybe "" (getField "uri" textDoc)
399438 let text = fromMaybe "" (getField "text" textDoc)
400− let diagnostics = lintDocument config text
439+ -- Store document content
440+ Ref.modify_ (\s -> s { documents = Map.insert uri text s.documents }) stateRef
441+ state <- Ref.read stateRef
442+ let diagnostics = lintDocument state.config text
401443 Stdio.writeMessage $ mkPublishDiagnostics uri diagnostics
402444
403−handleDidChange :: Ref Config -> Foreign -> Effect Unit
404−handleDidChange configRef msg = do
445+handleDidChange :: Ref LSPState -> Foreign -> Effect Unit
446+handleDidChange stateRef msg = do
405447 Stdio.logMessage "Document changed"
406− config <- Ref.read configRef
407448 case getFieldObj "params" msg of
408449 Nothing -> pure unit
409450 Just params -> do
@@ -417,11 +458,14 @@ handleDidChange configRef msg = do
417458 Nothing -> pure unit
418459 Just change -> do
419460 let text = fromMaybe "" (getField "text" change)
420− let diagnostics = lintDocument config text
461+ -- Update stored document content
462+ Ref.modify_ (\s -> s { documents = Map.insert uri text s.documents }) stateRef
463+ state <- Ref.read stateRef
464+ let diagnostics = lintDocument state.config text
421465 Stdio.writeMessage $ mkPublishDiagnostics uri diagnostics
422466
423−handleDidClose :: Foreign -> Effect Unit
424−handleDidClose msg = do
467+handleDidClose :: Ref LSPState -> Foreign -> Effect Unit
468+handleDidClose stateRef msg = do
425469 Stdio.logMessage "Document closed"
426470 case getFieldObj "params" msg of
427471 Nothing -> pure unit
@@ -430,11 +474,14 @@ handleDidClose msg = do
430474 Nothing -> pure unit
431475 Just textDoc -> do
432476 let uri = fromMaybe "" (getField "uri" textDoc)
477+ -- Remove document from store
478+ Ref.modify_ (\s -> s { documents = Map.delete uri s.documents }) stateRef
433479 Stdio.writeMessage $ mkPublishDiagnostics uri []
434480
435−handleCodeAction :: Foreign -> Int -> Effect Unit
436−handleCodeAction msg reqId = do
481+handleCodeAction :: Ref LSPState -> Foreign -> Int -> Effect Unit
482+handleCodeAction stateRef msg reqId = do
437483 Stdio.logMessage "Code action requested"
484+ state <- Ref.read stateRef
438485 case getFieldObj "params" msg of
439486 Nothing -> Stdio.writeMessage $ mkCodeActionResponse reqId []
440487 Just params -> do
@@ -443,17 +490,78 @@ handleCodeAction msg reqId = do
443490 textDoc <- getFieldObj "textDocument" params
444491 getField "uri" textDoc
445492
446− -- Get diagnostics from the request context
493+ -- Get the requested range
494+ let requestedRange = getFieldObj "range" params
495+
496+ -- Get diagnostics from the request context (for regular rules)
447497 let diagnostics = fromMaybe [] $ do
448498 context <- getFieldObj "context" params
449499 getFieldArr "diagnostics" context
450500
451− -- Convert diagnostics to code actions (each diagnostic may produce multiple actions)
452− let actions = Array.concat $ map (diagnosticToCodeActions uri) diagnostics
501+ -- Convert diagnostics to code actions (quickfix + disable rule)
502+ let diagnosticActions = Array.concat $ map (diagnosticToCodeActions uri) diagnostics
503+
504+ -- Get refactor actions from Refactor-severity rules
505+ let refactorActions = case Map.lookup uri state.documents of
506+ Nothing -> []
507+ Just content -> getRefactorActions state.config uri requestedRange content
508+
509+ let actions = diagnosticActions <> refactorActions
453510
454511 Stdio.logMessage $ "Returning " <> show (Array.length actions) <> " code actions"
455512 Stdio.writeMessage $ mkCodeActionResponse reqId actions
456513
514+-- | Get code actions from Refactor-severity rules
515+getRefactorActions :: Config -> String -> Maybe Foreign -> String -> Array Foreign
516+getRefactorActions config uri maybeRange content =
517+ let rules = getEnabledRules config
518+ in case runRules rules (SourceCode content) of
519+ Left _ -> []
520+ Right (LintResult result) ->
521+ let refactorWarnings = Array.filter isRefactor result.warnings
522+ inRangeWarnings = case maybeRange of
523+ Nothing -> refactorWarnings
524+ Just range -> Array.filter (warningInRange range) refactorWarnings
525+ in Array.mapMaybe (warningToRefactorAction uri) inRangeWarnings
526+ where
527+ isRefactor (LintWarning w) = w.severity == Refactor
528+
529+ warningInRange :: Foreign -> LintWarning -> Boolean
530+ warningInRange range (LintWarning w) =
531+ let
532+ startLine = fromMaybe 0 $ getFieldObj "start" range >>= getFieldInt "line"
533+ endLine = fromMaybe 0 $ getFieldObj "end" range >>= getFieldInt "line"
534+ in
535+ w.range.start.line <= endLine && w.range.end.line >= startLine
536+
537+ warningToRefactorAction :: String -> LintWarning -> Maybe Foreign
538+ warningToRefactorAction docUri (LintWarning w) = do
539+ Suggestion sug <- w.suggestion
540+ let range =
541+ { start: { line: w.range.start.line, character: w.range.start.column }
542+ , end: { line: w.range.end.line, character: w.range.end.column }
543+ }
544+ pure $ mkRefactorAction (unwrap sug.description) docUri range (unwrap sug.replacement)
545+
546+-- | Create a refactor code action (no associated diagnostic)
547+mkRefactorAction :: String -> String -> { start :: { line :: Int, character :: Int }, end :: { line :: Int, character :: Int } } -> String -> Foreign
548+mkRefactorAction title uri range newText =
549+ unsafeCoerce
550+ { title: title
551+ , kind: "refactor.rewrite"
552+ , isPreferred: false
553+ , edit:
554+ { documentChanges:
555+ [ { textDocument:
556+ { uri: uri
557+ , version: mkNull unit
558+ }
559+ , edits: [{ range: range, newText: newText }]
560+ }
561+ ]
562+ }
563+ }
564+
457565-- | Convert a diagnostic to code actions (quickfix + disable rule)
458566diagnosticToCodeActions :: String -> Foreign -> Array Foreign
459567diagnosticToCodeActions uri diag =
@@ -508,14 +616,14 @@ mkDisableRuleAction ruleId diagnostic =
508616 , isPreferred: false
509617 , command:
510618 { title: "Disable rule " <> ruleId
511− , command: "purelint.disableRule"
619+ , command: "purslint.disableRule"
512620 , arguments: [ruleId]
513621 }
514622 }
515623
516624-- | Main loop
517−mainLoop :: Ref Config -> Effect Unit
518−mainLoop configRef = do
625+mainLoop :: Ref LSPState -> Effect Unit
626+mainLoop stateRef = do
519627 msgStr <- Stdio.readMessage
520628 case msgStr of
521629 Nothing -> do
@@ -525,18 +633,18 @@ mainLoop configRef = do
525633 case Stdio.parseJSON str of
526634 Nothing -> do
527635 Stdio.logMessage "Failed to parse message"
528− mainLoop configRef
636+ mainLoop stateRef
529637 Just msg -> do
530− handleMessage configRef msg
638+ handleMessage stateRef msg
531639 -- Check if it was exit
532640 let method = fromMaybe "" (getField "method" msg)
533641 if method == "exit"
534642 then pure unit
535− else mainLoop configRef
643+ else mainLoop stateRef
536644
537645main :: Effect Unit
538646main = do
539− Stdio.logMessage "purelint-lsp starting..."
540− configRef <- Ref.new defaultConfig
541− mainLoop configRef
542− Stdio.logMessage "purelint-lsp exiting"
647+ Stdio.logMessage "purslint-lsp starting..."
648+ stateRef <- Ref.new { config: defaultConfig, documents: Map.empty }
649+ mainLoop stateRef
650+ Stdio.logMessage "purslint-lsp exiting"
bin/purelint-lsp/src/LSP/Stdio.js → bin/purslint-lsp/src/LSP/Stdio.jsrenamed+9−9
@@ -2,8 +2,6 @@
22import * as fs from 'fs';
33
44// PureScript Maybe representation that works for both standard and optimized backends
5−// Standard: { value0: x } for Just, Nothing.value for Nothing
6−// Optimized: { tag: 1, _1: x } for Just, { tag: 0 } for Nothing
75const mkJust = (x) => ({ tag: 1, _1: x, value0: x });
86const mkNothing = { tag: 0 };
97
@@ -38,13 +36,17 @@ export const readMessage = () => {
3836 return mkJust(msg);
3937 }
4038
41− // Need to read more data synchronously from stdin
42− const fd = fs.openSync('/dev/stdin', 'r');
43−
39+ // Read synchronously from stdin fd (0)
4440 try {
4541 while (true) {
4642 const chunk = Buffer.alloc(4096);
47− const bytesRead = fs.readSync(fd, chunk, 0, 4096);
43+ let bytesRead;
44+ try {
45+ bytesRead = fs.readSync(0, chunk, 0, 4096);
46+ } catch (e) {
47+ // EAGAIN or other read error
48+ return mkNothing;
49+ }
4850
4951 if (bytesRead === 0) {
5052 // EOF
@@ -60,8 +62,6 @@ export const readMessage = () => {
6062 }
6163 } catch (e) {
6264 return mkNothing;
63− } finally {
64− fs.closeSync(fd);
6565 }
6666};
6767
@@ -74,7 +74,7 @@ export const writeMessage = (msg) => () => {
7474
7575// Log a message to stderr (for debugging)
7676export const logMessage = (msg) => () => {
77− process.stderr.write('[purelint-lsp] ' + msg + '\n');
77+ process.stderr.write('[purslint-lsp] ' + msg + '\n');
7878};
7979
8080// Parse JSON string to Foreign (returns Maybe Foreign)
bin/purelint-lsp/src/LSP/Stdio.purs → bin/purslint-lsp/src/LSP/Stdio.pursrenamed

The content is the same.

bin/purelint-lsp/src/Main.purs → bin/purslint-lsp/src/Main.pursrenamed+1−1
@@ -2,7 +2,7 @@ module Main.LSP where
22
33import Prelude
44import Effect (Effect)
5−import Purelint.LSP as LSP
5+import Purslint.LSP as LSP
66
77main :: Effect Unit
88main = LSP.main
bin/purslint.jsnew file+2
@@ -0,0 +1,2 @@
1+#!/usr/bin/env node
2+import "../dist/purslint.js";
demo.gifnew file

Binary file not shown.

docs/IMPORT_CHECKING_PLAN.md+8−8
@@ -6,7 +6,7 @@ Make lint rules safer by verifying that identifiers like `map`, `identity`, `<$>
66
77## Current State
88
9−- `Purelint.Imports` module exists with:
9+- `Purslint.Imports` module exists with:
1010 - `getImportInfo :: Module Void -> ImportInfo` - extracts import info from a module
1111 - `hasValue :: ImportInfo -> String -> Boolean` - check if a value is imported
1212 - `hasOp :: ImportInfo -> String -> Boolean` - check if an operator is imported
@@ -18,7 +18,7 @@ Make lint rules safer by verifying that identifiers like `map`, `identity`, `<$>
1818### Step 1: Update Rule type to receive ImportInfo
1919
2020```purescript
21−-- In Purelint/Rule.purs
21+-- In Purslint/Rule.purs
2222type RuleContext =
2323 { imports :: ImportInfo
2424 }
@@ -34,7 +34,7 @@ mkRule :: RuleId -> (RuleContext -> Module Void -> Array LintWarning) -> Rule
3434### Step 2: Update Runner to pass context
3535
3636```purescript
37−-- In Purelint/Runner.purs
37+-- In Purslint/Runner.purs
3838runRules :: Array Rule -> SourceCode -> Either String LintResult
3939runRules rules (SourceCode source) = do
4040 mod <- parseModule source
@@ -96,11 +96,11 @@ All rules that check for specific identifiers:
9696
9797## Files to Modify
9898
99−1. `lib/purelint/src/Purelint/Rule.purs` - Add RuleContext
100−2. `lib/purelint/src/Purelint/Runner.purs` - Pass context to rules
101−3. `lib/purelint/src/Purelint/Rules/*.purs` - All 9 rule files
102−4. `bin/purelint-cli/src/Main.purs` - No changes needed (uses Runner)
103−5. `bin/purelint-lsp/src/LSP.purs` - No changes needed (uses Runner)
99+1. `lib/purslint/src/Purslint/Rule.purs` - Add RuleContext
100+2. `lib/purslint/src/Purslint/Runner.purs` - Pass context to rules
101+3. `lib/purslint/src/Purslint/Rules/*.purs` - All 9 rule files
102+4. `bin/purslint-cli/src/Main.purs` - No changes needed (uses Runner)
103+5. `bin/purslint-lsp/src/LSP.purs` - No changes needed (uses Runner)
104104
105105## Testing
106106
examples/BadCode.purs+1−1
@@ -10,7 +10,7 @@ test1 xs = sequenceA (map pure xs)
1010-- MapFusion: map f (map g x) -> map (f <<< g) x
1111test2 xs = map show (map (_ + 1) xs)
1212
13−-- FmapId: map identity x -> x
13+-- MapIdentity: map identity x -> x
1414test3 xs = map identity xs
1515
1616-- NotEqual: not (a == b) -> a /= b
examples/TestFix.purs+5−5
@@ -5,16 +5,16 @@ import Data.Traversable (sequenceA)
55import Data.Array (concat)
66
77-- UseTraverse: sequenceA (map f x) -> traverse f x
8−test1 xs = traverse pure xs
8+test1 xs = traverse pure xs
99
1010-- MapFusion: map f (map g x) -> map (f <<< g) x
1111test2 xs = map (f <<< g) x
1212
13−-- FmapId: map identity x -> x
13+-- MapIdentity: map identity x -> x
1414test3 xs = xs
1515
1616-- NotEqual: not (a == b) -> a /= b
17−test4 a b = a /= b
17+test4 a b = a /= b
1818
1919-- ConcatMap: concat (map f x) -> concatMap f x
2020test5 xs = concatMap f x
@@ -23,7 +23,7 @@ test5 xs = concatMap f x
2323test6 cond x = when cond action
2424
2525-- EtaReduce: \x -> f x -> f
26−test7 = show
26+test7 = show
2727
2828-- RedundantBind: x >>= pure -> x
29−test8 mx = mx
29+test8 mx = mx
lib/purelint/src/Purelint/Fix.pursdeleted−107
@@ -1,107 +0,0 @@
1−module Purelint.Fix where
2−
3−import Prelude
4−
5−import Data.Array as Array
6−import Data.Maybe (Maybe(..))
7−import Data.Newtype (unwrap)
8−import Data.String as String
9−import Data.String.CodeUnits as StringCU
10−import Purelint.Types (LintResult(..), LintWarning(..), Suggestion(..))
11−import PureScript.CST.Types (SourceRange)
12−
13−-- | Extract text at a source range from source code
14−extractRange :: String -> SourceRange -> String
15−extractRange source range =
16− let
17− lines = String.split (String.Pattern "\n") source
18− startLine = range.start.line
19− endLine = range.end.line
20− startCol = range.start.column
21− endCol = range.end.column
22− in
23− if startLine == endLine then
24− -- Single line extraction
25− case Array.index lines startLine of
26− Nothing -> ""
27− Just line -> StringCU.slice startCol endCol line
28− else
29− -- Multi-line extraction
30− let
31− firstLine = case Array.index lines startLine of
32− Nothing -> ""
33− Just line -> StringCU.drop startCol line
34− middleLines = Array.slice (startLine + 1) endLine lines
35− lastLine = case Array.index lines endLine of
36− Nothing -> ""
37− Just line -> StringCU.take endCol line
38− in
39− String.joinWith "\n" ([firstLine] <> middleLines <> [lastLine])
40−
41−-- | Replace text at a source range with new text
42−replaceRange :: String -> SourceRange -> String -> String
43−replaceRange source range replacement =
44− let
45− lines = String.split (String.Pattern "\n") source
46− startLine = range.start.line
47− endLine = range.end.line
48− startCol = range.start.column
49− endCol = range.end.column
50− in
51− if startLine == endLine then
52− -- Single line replacement
53− case Array.index lines startLine of
54− Nothing -> source
55− Just line ->
56− let
57− before = StringCU.take startCol line
58− after = StringCU.drop endCol line
59− newLine = before <> replacement <> after
60− newLines = Array.updateAt startLine newLine lines
61− in
62− case newLines of
63− Nothing -> source
64− Just ls -> String.joinWith "\n" ls
65− else
66− -- Multi-line replacement
67− let
68− beforeLines = Array.take startLine lines
69− afterLines = Array.drop (endLine + 1) lines
70− firstLinePart = case Array.index lines startLine of
71− Nothing -> ""
72− Just line -> StringCU.take startCol line
73− lastLinePart = case Array.index lines endLine of
74− Nothing -> ""
75− Just line -> StringCU.drop endCol line
76− newLine = firstLinePart <> replacement <> lastLinePart
77− in
78− String.joinWith "\n" (beforeLines <> [newLine] <> afterLines)
79−
80−-- | Apply a single fix to source code
81−applyFix :: String -> LintWarning -> Maybe String
82−applyFix source (LintWarning w) = do
83− Suggestion s <- w.suggestion
84− let replacement = unwrap s.replacement
85− Just $ replaceRange source w.range replacement
86−
87−-- | Apply all fixes to source code (in reverse order to preserve ranges)
88−applyAllFixes :: String -> LintResult -> String
89−applyAllFixes source (LintResult result) =
90− -- Sort warnings by range in reverse order (end of file first)
91− let
92− sortedWarnings = Array.sortBy compareRangesReverse result.warnings
93− in
94− Array.foldl applyFixIfPossible source sortedWarnings
95− where
96− compareRangesReverse :: LintWarning -> LintWarning -> Ordering
97− compareRangesReverse (LintWarning a) (LintWarning b) =
98− -- Compare in reverse: later positions first
99− case compare b.range.start.line a.range.start.line of
100− EQ -> compare b.range.start.column a.range.start.column
101− other -> other
102−
103− applyFixIfPossible :: String -> LintWarning -> String
104− applyFixIfPossible src warning =
105− case applyFix src warning of
106− Nothing -> src
107− Just fixed -> fixed
lib/purelint/src/Purelint/Print.pursdeleted−28
@@ -1,28 +0,0 @@
1−module Purelint.Print where
2−
3−import Prelude
4−
5−import Data.Foldable (foldMap)
6−import Data.String as String
7−import Data.String.Regex (regex, replace)
8−import Data.String.Regex.Flags (global)
9−import Data.Either (either)
10−import Data.Void (Void)
11−import PureScript.CST.Print (printSourceToken)
12−import PureScript.CST.Range (class TokensOf, tokensOf)
13−import PureScript.CST.Range.TokenList as TokenList
14−import PureScript.CST.Types (Expr)
15−
16−-- | Print an expression back to source code, normalizing whitespace
17−printExpr :: Expr Void -> String
18−printExpr expr =
19− let raw = foldMap printSourceToken (TokenList.toArray (tokensOf expr))
20− in normalizeWhitespace raw
21−
22−-- | Collapse multiple spaces into single space and trim
23−normalizeWhitespace :: String -> String
24−normalizeWhitespace s =
25− let
26− -- Replace multiple spaces/newlines with single space
27− collapsed = either (const s) (\r -> replace r " " s) (regex "\\s+" global)
28− in String.trim collapsed
lib/purelint/src/Purelint/Rules/FmapId.pursdeleted−105
@@ -1,105 +0,0 @@
1−module Purelint.Rules.FmapId where
2−
3−import Prelude
4−
5−import Data.Array as Array
6−import Data.Array.NonEmpty as NEA
7−import Data.Maybe (Maybe(..))
8−import Data.Tuple (Tuple(..))
9−import Data.Void (Void)
10−import Purelint.Imports (ImportInfo, hasValue, hasOp)
11−import Purelint.Print (printExpr)
12−import Purelint.Rule (Rule, RuleContext, mkRule)
13−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
14−import PureScript.CST.Range (rangeOf)
15−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
16−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
17−
18−-- | Rule: map identity x -> x
19−-- | Also: fmap identity x -> x, identity <$> x -> x, x <#> identity -> x
20−fmapIdRule :: Rule
21−fmapIdRule = mkRule (RuleId "FmapId") run
22− where
23− run :: RuleContext -> Module Void -> Array LintWarning
24− run ctx = foldMapModule visitor
25− where
26− imports = ctx.imports
27− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
28−
29− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
30− checkExpr imports expr = case expr of
31− -- Match: map identity x or fmap identity x
32− ExprApp fnExpr args ->
33− case fnExpr of
34− ExprIdent qn | isMapLike imports qn ->
35− case NEA.toArray args of
36− [AppTerm idExpr, AppTerm xExpr] | isIdentity imports idExpr ->
37− let xText = printExpr xExpr
38− in
39− [ LintWarning
40− { ruleId: RuleId "FmapId"
41− , message: WarningMessage "map identity is redundant"
42− , range: rangeOf expr
43− , severity: Warning
44− , suggestion: Just $ Suggestion
45− { replacement: ReplacementText xText
46− , description: SuggestionDescription "map identity x can be simplified to x"
47− }
48− }
49− ]
50− _ -> []
51− _ -> []
52− -- Match: identity <$> x
53− ExprOp lhs ops | isIdentity imports lhs ->
54− case NEA.toArray ops of
55− [Tuple qn xExpr] | isMapOp imports qn ->
56− let xText = printExpr xExpr
57− in
58− [ LintWarning
59− { ruleId: RuleId "FmapId"
60− , message: WarningMessage "identity <$> is redundant"
61− , range: rangeOf expr
62− , severity: Warning
63− , suggestion: Just $ Suggestion
64− { replacement: ReplacementText xText
65− , description: SuggestionDescription "identity <$> x can be simplified to x"
66− }
67− }
68− ]
69− _ -> []
70− -- Match: x <#> identity
71− ExprOp lhs ops ->
72− case NEA.toArray ops of
73− [Tuple qn idExpr] | isFlippedMapOp imports qn && isIdentity imports idExpr ->
74− let xText = printExpr lhs
75− in
76− [ LintWarning
77− { ruleId: RuleId "FmapId"
78− , message: WarningMessage "<#> identity is redundant"
79− , range: rangeOf expr
80− , severity: Warning
81− , suggestion: Just $ Suggestion
82− { replacement: ReplacementText xText
83− , description: SuggestionDescription "x <#> identity can be simplified to x"
84− }
85− }
86− ]
87− _ -> []
88− _ -> []
89−
90− isMapLike :: ImportInfo -> QualifiedName Ident -> Boolean
91− isMapLike imports (QualifiedName { name: Ident name }) =
92− (name == "map" && hasValue imports "map") || (name == "fmap" && hasValue imports "fmap")
93−
94− isMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
95− isMapOp imports (QualifiedName { name: Operator op }) =
96− (op == "<$>" || op == "<$$>") && hasOp imports "<$>"
97−
98− isFlippedMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
99− isFlippedMapOp imports (QualifiedName { name: Operator op }) =
100− (op == "<#>" || op == "<##>") && hasOp imports "<#>"
101−
102− isIdentity :: ImportInfo -> Expr Void -> Boolean
103− isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
104− (name == "identity" && hasValue imports "identity") || (name == "id" && hasValue imports "id")
105− isIdentity _ _ = false
lib/purelint/src/Purelint/Rules/LetToWhere.pursdeleted−72
@@ -1,72 +0,0 @@
1−module Purelint.Rules.LetToWhere where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Foldable (foldMap)
7−import Data.Maybe (Maybe(..))
8−import Data.Monoid (power)
9−import Data.String (joinWith) as String
10−import Data.String as Str
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
13−import PureScript.CST.Print (printToken)
14−import PureScript.CST.Range (rangeOf, tokensOf)
15−import PureScript.CST.Range.TokenList as TokenList
16−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
17−import PureScript.CST.Types (Comment(..), Expr(..), LetBinding, Module, SourceToken)
18−
19−-- | Rule: let x = y in z -> z where x = y
20−-- | Suggests replacing let...in expressions with where clauses
21−letToWhereRule :: Rule
22−letToWhereRule = mkRule (RuleId "LetToWhere") run
23− where
24− run :: RuleContext -> Module Void -> Array LintWarning
25− run _ctx = foldMapModule visitor
26− where
27− visitor = defaultMonoidalVisitor { onExpr = checkExpr }
28−
29− checkExpr :: Expr Void -> Array LintWarning
30− checkExpr expr = case expr of
31− ExprLet letIn ->
32− let
33− bodyText = printTokens letIn.body
34− bindings = NEA.toArray letIn.bindings # map printBindingTokens
35− bindingsText = String.joinWith "\n " bindings
36− replacement = bodyText <> "\n where\n " <> bindingsText
37− in
38− [ LintWarning
39− { ruleId: RuleId "LetToWhere"
40− , message: WarningMessage "Use where clause instead of let...in"
41− , range: rangeOf expr
42− , severity: Hint
43− , suggestion: Just $ Suggestion
44− { replacement: ReplacementText replacement
45− , description: SuggestionDescription "let x = y in z can be rewritten as z where x = y"
46− }
47− }
48− ]
49− _ -> []
50−
51− -- | Print tokens preserving spaces but collapsing newlines to single space
52− printTokens :: Expr Void -> String
53− printTokens e =
54− Str.trim $ foldMap printSourceTokenWithSpaces (TokenList.toArray (tokensOf e))
55−
56− printBindingTokens :: LetBinding Void -> String
57− printBindingTokens b =
58− Str.trim $ foldMap printSourceTokenWithSpaces (TokenList.toArray (tokensOf b))
59−
60− -- | Print a source token, keeping spaces but converting newlines to spaces
61− printSourceTokenWithSpaces :: SourceToken -> String
62− printSourceTokenWithSpaces tok =
63− foldMap printCommentSpacesOnly tok.leadingComments
64− <> printToken tok.value
65− <> foldMap printCommentSpacesOnly tok.trailingComments
66−
67− -- | Print comment, keeping spaces but skipping newlines
68− printCommentSpacesOnly :: forall l. Comment l -> String
69− printCommentSpacesOnly = case _ of
70− Comment str -> str
71− Space n -> power " " n
72− Line _ _ -> " "
lib/purelint/src/Purelint/Rules/RedundantReverse.pursdeleted−63
@@ -1,63 +0,0 @@
1−module Purelint.Rules.RedundantReverse where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Maybe (Maybe(..))
7−import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12−import PureScript.CST.Range (rangeOf)
13−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15−
16−-- | Rule: reverse (reverse x) -> x
17−redundantReverseRule :: Rule
18−redundantReverseRule = mkRule (RuleId "RedundantReverse") run
19− where
20− run :: RuleContext -> Module Void -> Array LintWarning
21− run ctx = foldMapModule visitor
22− where
23− imports = ctx.imports
24− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
25−
26− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
27− checkExpr imports expr = case expr of
28− -- Match: reverse (reverse x)
29− ExprApp fn args
30− | isReverse imports fn ->
31− case NEA.toArray args of
32− [AppTerm arg] ->
33− case unwrapParens arg of
34− ExprApp innerFn innerArgs
35− | isReverse imports innerFn ->
36− case NEA.toArray innerArgs of
37− [AppTerm innerArg] ->
38− let x = printExpr innerArg
39− in
40− [ LintWarning
41− { ruleId: RuleId "RedundantReverse"
42− , message: WarningMessage "reverse (reverse x) is redundant"
43− , range: rangeOf expr
44− , severity: Warning
45− , suggestion: Just $ Suggestion
46− { replacement: ReplacementText x
47− , description: SuggestionDescription "Remove redundant double reverse"
48− }
49− }
50− ]
51− _ -> []
52− _ -> []
53− _ -> []
54− _ -> []
55−
56− unwrapParens :: Expr Void -> Expr Void
57− unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
58− unwrapParens e = e
59−
60− isReverse :: ImportInfo -> Expr Void -> Boolean
61− isReverse imports (ExprIdent (QualifiedName { name: Ident name })) =
62− name == "reverse" && hasValue imports "reverse"
63− isReverse _ _ = false
lib/purelint/src/Purelint/Rules/UseAny.pursdeleted−68
@@ -1,68 +0,0 @@
1−module Purelint.Rules.UseAny where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Maybe (Maybe(..))
7−import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12−import PureScript.CST.Range (rangeOf)
13−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15−
16−-- | Rule: or (map f x) -> any f x
17−useAnyRule :: Rule
18−useAnyRule = mkRule (RuleId "UseAny") run
19− where
20− run :: RuleContext -> Module Void -> Array LintWarning
21− run ctx = foldMapModule visitor
22− where
23− imports = ctx.imports
24− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
25−
26− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
27− checkExpr imports expr = case expr of
28− -- Match: or (map f x)
29− ExprApp fnExpr args | isOr imports fnExpr ->
30− case NEA.toArray args of
31− [AppTerm innerExpr] ->
32− case unwrapParens innerExpr of
33− ExprApp mapFn mapArgs | isMapLike imports mapFn ->
34− case NEA.toArray mapArgs of
35− [AppTerm fExpr, AppTerm xExpr] ->
36− let
37− fText = printExpr fExpr
38− xText = printExpr xExpr
39− in
40− [ LintWarning
41− { ruleId: RuleId "UseAny"
42− , message: WarningMessage "Use any instead of or (map f x)"
43− , range: rangeOf expr
44− , severity: Warning
45− , suggestion: Just $ Suggestion
46− { replacement: ReplacementText ("any " <> fText <> " " <> xText)
47− , description: SuggestionDescription "or (map f x) can be simplified to any f x"
48− }
49− }
50− ]
51− _ -> []
52− _ -> []
53− _ -> []
54− _ -> []
55−
56− unwrapParens :: Expr Void -> Expr Void
57− unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
58− unwrapParens e = e
59−
60− isOr :: ImportInfo -> Expr Void -> Boolean
61− isOr imports (ExprIdent (QualifiedName { name: Ident name })) =
62− name == "or" && hasValue imports "or"
63− isOr _ _ = false
64−
65− isMapLike :: ImportInfo -> Expr Void -> Boolean
66− isMapLike imports (ExprIdent (QualifiedName { name: Ident name })) =
67− (name == "map" && hasValue imports "map") || (name == "fmap" && hasValue imports "fmap")
68− isMapLike _ _ = false
lib/purelint/src/Purelint/Rules/UseApplicative.pursdeleted−66
@@ -1,66 +0,0 @@
1−module Purelint.Rules.UseApplicative where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Maybe (Maybe(..))
7−import Data.Tuple (Tuple(..))
8−import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
13−import PureScript.CST.Range (rangeOf)
14−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
15−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
16−
17−-- | Rule: pure f <*> x -> f <$> x
18−useApplicativeRule :: Rule
19−useApplicativeRule = mkRule (RuleId "UseApplicative") run
20− where
21− run :: RuleContext -> Module Void -> Array LintWarning
22− run ctx = foldMapModule visitor
23− where
24− imports = ctx.imports
25− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
26−
27− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
28− checkExpr imports expr = case expr of
29− -- Match: pure f <*> x
30− ExprOp lhs ops
31− | hasOp imports "<*>" ->
32− case unwrapParens lhs of
33− ExprApp pureExpr args
34− | isPure imports pureExpr ->
35− case NEA.toArray args of
36− [AppTerm fExpr] ->
37− case NEA.toArray ops of
38− [Tuple (QualifiedName { name: Operator "<*>" }) xExpr] ->
39− let
40− f = printExpr fExpr
41− x = printExpr xExpr
42− in
43− [ LintWarning
44− { ruleId: RuleId "UseApplicative"
45− , message: WarningMessage "pure f <*> x can be simplified to f <$> x"
46− , range: rangeOf expr
47− , severity: Hint
48− , suggestion: Just $ Suggestion
49− { replacement: ReplacementText (f <> " <$> " <> x)
50− , description: SuggestionDescription "Use <$> instead of pure f <*> x"
51− }
52− }
53− ]
54− _ -> []
55− _ -> []
56− _ -> []
57− _ -> []
58−
59− unwrapParens :: Expr Void -> Expr Void
60− unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
61− unwrapParens e = e
62−
63− isPure :: ImportInfo -> Expr Void -> Boolean
64− isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
65− name == "pure" && hasValue imports "pure"
66− isPure _ _ = false
lib/purelint/src/Purelint/Rules/UseBimap.pursdeleted−205
@@ -1,205 +0,0 @@
1−module Purelint.Rules.UseBimap where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Maybe (Maybe(..))
7−import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12−import PureScript.CST.Range (rangeOf)
13−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15−
16−-- | Rule: bimap id g -> second g
17−-- | Also: bimap f id -> first f
18−-- | Also: first f (second g x) -> bimap f g x
19−useBimapRule :: Rule
20−useBimapRule = mkRule (RuleId "UseBimap") run
21− where
22− run :: RuleContext -> Module Void -> Array LintWarning
23− run ctx = foldMapModule visitor
24− where
25− imports = ctx.imports
26− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
27−
28− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
29− checkExpr imports expr = case expr of
30− -- Match: bimap id g or bimap f id
31− ExprApp fn args
32− | isBimap imports fn ->
33− case NEA.toArray args of
34− [AppTerm fArg, AppTerm gArg] ->
35− let fIsId = isIdentity imports fArg
36− gIsId = isIdentity imports gArg
37− in
38− if fIsId && not gIsId then
39− let g = printExpr gArg
40− in
41− [ LintWarning
42− { ruleId: RuleId "UseBimap"
43− , message: WarningMessage "bimap id g can be simplified to second g"
44− , range: rangeOf expr
45− , severity: Warning
46− , suggestion: Just $ Suggestion
47− { replacement: ReplacementText ("second " <> g)
48− , description: SuggestionDescription "Use second instead of bimap id"
49− }
50− }
51− ]
52− else if gIsId && not fIsId then
53− let f = printExpr fArg
54− in
55− [ LintWarning
56− { ruleId: RuleId "UseBimap"
57− , message: WarningMessage "bimap f id can be simplified to first f"
58− , range: rangeOf expr
59− , severity: Warning
60− , suggestion: Just $ Suggestion
61− { replacement: ReplacementText ("first " <> f)
62− , description: SuggestionDescription "Use first instead of bimap f id"
63− }
64− }
65− ]
66− else if fIsId && gIsId then
67− [ LintWarning
68− { ruleId: RuleId "UseBimap"
69− , message: WarningMessage "bimap id id is redundant"
70− , range: rangeOf expr
71− , severity: Warning
72− , suggestion: Just $ Suggestion
73− { replacement: ReplacementText "identity"
74− , description: SuggestionDescription "bimap id id is just identity"
75− }
76− }
77− ]
78− else []
79− [AppTerm fArg, AppTerm gArg, AppTerm xArg] ->
80− let fIsId = isIdentity imports fArg
81− gIsId = isIdentity imports gArg
82− x = printExpr xArg
83− in
84− if fIsId && not gIsId then
85− let g = printExpr gArg
86− in
87− [ LintWarning
88− { ruleId: RuleId "UseBimap"
89− , message: WarningMessage "bimap id g x can be simplified to second g x"
90− , range: rangeOf expr
91− , severity: Warning
92− , suggestion: Just $ Suggestion
93− { replacement: ReplacementText ("second " <> g <> " " <> x)
94− , description: SuggestionDescription "Use second instead of bimap id"
95− }
96− }
97− ]
98− else if gIsId && not fIsId then
99− let f = printExpr fArg
100− in
101− [ LintWarning
102− { ruleId: RuleId "UseBimap"
103− , message: WarningMessage "bimap f id x can be simplified to first f x"
104− , range: rangeOf expr
105− , severity: Warning
106− , suggestion: Just $ Suggestion
107− { replacement: ReplacementText ("first " <> f <> " " <> x)
108− , description: SuggestionDescription "Use first instead of bimap f id"
109− }
110− }
111− ]
112− else if fIsId && gIsId then
113− [ LintWarning
114− { ruleId: RuleId "UseBimap"
115− , message: WarningMessage "bimap id id x is redundant"
116− , range: rangeOf expr
117− , severity: Warning
118− , suggestion: Just $ Suggestion
119− { replacement: ReplacementText x
120− , description: SuggestionDescription "bimap id id x is just x"
121− }
122− }
123− ]
124− else []
125− _ -> []
126− -- Match: first f (second g x) or second g (first f x)
127− | isFirst imports fn ->
128− case NEA.toArray args of
129− [AppTerm fArg, AppTerm innerArg] ->
130− case unwrapParens innerArg of
131− ExprApp secondFn secondArgs
132− | isSecond imports secondFn ->
133− case NEA.toArray secondArgs of
134− [AppTerm gArg, AppTerm xArg] ->
135− let
136− f = printExpr fArg
137− g = printExpr gArg
138− x = printExpr xArg
139− in
140− [ LintWarning
141− { ruleId: RuleId "UseBimap"
142− , message: WarningMessage "first f (second g x) can be simplified to bimap f g x"
143− , range: rangeOf expr
144− , severity: Hint
145− , suggestion: Just $ Suggestion
146− { replacement: ReplacementText ("bimap " <> f <> " " <> g <> " " <> x)
147− , description: SuggestionDescription "Use bimap instead of first and second"
148− }
149− }
150− ]
151− _ -> []
152− _ -> []
153− _ -> []
154− | isSecond imports fn ->
155− case NEA.toArray args of
156− [AppTerm gArg, AppTerm innerArg] ->
157− case unwrapParens innerArg of
158− ExprApp firstFn firstArgs
159− | isFirst imports firstFn ->
160− case NEA.toArray firstArgs of
161− [AppTerm fArg, AppTerm xArg] ->
162− let
163− f = printExpr fArg
164− g = printExpr gArg
165− x = printExpr xArg
166− in
167− [ LintWarning
168− { ruleId: RuleId "UseBimap"
169− , message: WarningMessage "second g (first f x) can be simplified to bimap f g x"
170− , range: rangeOf expr
171− , severity: Hint
172− , suggestion: Just $ Suggestion
173− { replacement: ReplacementText ("bimap " <> f <> " " <> g <> " " <> x)
174− , description: SuggestionDescription "Use bimap instead of first and second"
175− }
176− }
177− ]
178− _ -> []
179− _ -> []
180− _ -> []
181− _ -> []
182−
183− unwrapParens :: Expr Void -> Expr Void
184− unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
185− unwrapParens e = e
186−
187− isBimap :: ImportInfo -> Expr Void -> Boolean
188− isBimap imports (ExprIdent (QualifiedName { name: Ident name })) =
189− name == "bimap" && hasValue imports "bimap"
190− isBimap _ _ = false
191−
192− isFirst :: ImportInfo -> Expr Void -> Boolean
193− isFirst imports (ExprIdent (QualifiedName { name: Ident name })) =
194− name == "first" && hasValue imports "first"
195− isFirst _ _ = false
196−
197− isSecond :: ImportInfo -> Expr Void -> Boolean
198− isSecond imports (ExprIdent (QualifiedName { name: Ident name })) =
199− name == "second" && hasValue imports "second"
200− isSecond _ _ = false
201−
202− isIdentity :: ImportInfo -> Expr Void -> Boolean
203− isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
204− (name == "identity" && hasValue imports "identity") || (name == "id" && hasValue imports "identity")
205− isIdentity _ _ = false
lib/purelint/src/Purelint/Rules/UseBindFlip.pursdeleted−73
@@ -1,73 +0,0 @@
1−module Purelint.Rules.UseBindFlip where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Maybe (Maybe(..))
7−import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12−import PureScript.CST.Range (rangeOf)
13−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15−
16−-- | Rule: join (map f x) -> x >>= f
17−useBindFlipRule :: Rule
18−useBindFlipRule = mkRule (RuleId "UseBindFlip") run
19− where
20− run :: RuleContext -> Module Void -> Array LintWarning
21− run ctx = foldMapModule visitor
22− where
23− imports = ctx.imports
24− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
25−
26− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
27− checkExpr imports expr = case expr of
28− -- Match: join (map f x)
29− ExprApp fnExpr args
30− | isJoin imports fnExpr ->
31− case NEA.toArray args of
32− [AppTerm innerExpr] ->
33− checkInnerMap imports expr (unwrapParens innerExpr)
34− _ -> []
35− _ -> []
36−
37− unwrapParens :: Expr Void -> Expr Void
38− unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
39− unwrapParens e = e
40−
41− checkInnerMap :: ImportInfo -> Expr Void -> Expr Void -> Array LintWarning
42− checkInnerMap imports fullExpr innerExpr =
43− case innerExpr of
44− ExprApp mapFn mapArgs | isMap imports mapFn ->
45− case NEA.toArray mapArgs of
46− [AppTerm fExpr, AppTerm xExpr] ->
47− let
48− f = printExpr fExpr
49− x = printExpr xExpr
50− in
51− [ LintWarning
52− { ruleId: RuleId "UseBindFlip"
53− , message: WarningMessage "join (map f x) can be simplified to x >>= f"
54− , range: rangeOf fullExpr
55− , severity: Warning
56− , suggestion: Just $ Suggestion
57− { replacement: ReplacementText (x <> " >>= " <> f)
58− , description: SuggestionDescription "Use >>= instead of join composed with map"
59− }
60− }
61− ]
62− _ -> []
63− _ -> []
64−
65− isJoin :: ImportInfo -> Expr Void -> Boolean
66− isJoin imports (ExprIdent (QualifiedName { name: Ident name })) =
67− name == "join" && hasValue imports "join"
68− isJoin _ _ = false
69−
70− isMap :: ImportInfo -> Expr Void -> Boolean
71− isMap imports (ExprIdent (QualifiedName { name: Ident name })) =
72− name == "map" && hasValue imports "map"
73− isMap _ _ = false
lib/purelint/src/Purelint/Rules/UseBreak.pursdeleted−95
@@ -1,95 +0,0 @@
1−module Purelint.Rules.UseBreak where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Maybe (Maybe(..))
7−import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12−import PureScript.CST.Range (rangeOf)
13−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15−
16−-- | Rule: span (not <<< p) -> break p (composition form)
17−-- | Also: span (\x -> not (p x)) -> break p (lambda form)
18−useBreakRule :: Rule
19−useBreakRule = mkRule (RuleId "UseBreak") run
20− where
21− run :: RuleContext -> Module Void -> Array LintWarning
22− run ctx = foldMapModule visitor
23− where
24− imports = ctx.imports
25− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
26−
27− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
28− checkExpr imports expr = case expr of
29− -- Match: span (not <<< p) x
30− ExprApp fn args
31− | isSpan imports fn ->
32− case NEA.toArray args of
33− [AppTerm predArg, AppTerm xArg] ->
34− case getNotComposedPred imports predArg of
35− Just p ->
36− let
37− pText = printExpr p
38− x = printExpr xArg
39− in
40− [ LintWarning
41− { ruleId: RuleId "UseBreak"
42− , message: WarningMessage "span (not <<< p) can be simplified to break p"
43− , range: rangeOf expr
44− , severity: Hint
45− , suggestion: Just $ Suggestion
46− { replacement: ReplacementText ("break " <> pText <> " " <> x)
47− , description: SuggestionDescription "Use break instead of span with negated predicate"
48− }
49− }
50− ]
51− Nothing -> []
52− [AppTerm predArg] ->
53− case getNotComposedPred imports predArg of
54− Just p ->
55− let pText = printExpr p
56− in
57− [ LintWarning
58− { ruleId: RuleId "UseBreak"
59− , message: WarningMessage "span (not <<< p) can be simplified to break p"
60− , range: rangeOf expr
61− , severity: Hint
62− , suggestion: Just $ Suggestion
63− { replacement: ReplacementText ("break " <> pText)
64− , description: SuggestionDescription "Use break instead of span with negated predicate"
65− }
66− }
67− ]
68− Nothing -> []
69− _ -> []
70− _ -> []
71−
72− unwrapParens :: Expr Void -> Expr Void
73− unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
74− unwrapParens e = e
75−
76− isSpan :: ImportInfo -> Expr Void -> Boolean
77− isSpan imports (ExprIdent (QualifiedName { name: Ident name })) =
78− name == "span" && hasValue imports "span"
79− isSpan _ _ = false
80−
81− isNot :: ImportInfo -> Expr Void -> Boolean
82− isNot imports (ExprIdent (QualifiedName { name: Ident name })) =
83− name == "not" && hasValue imports "not"
84− isNot _ _ = false
85−
86− -- Check for (not <<< p) pattern
87− getNotComposedPred :: ImportInfo -> Expr Void -> Maybe (Expr Void)
88− getNotComposedPred imports predExpr =
89− case unwrapParens predExpr of
90− ExprApp notFn args
91− | isNot imports notFn ->
92− case NEA.toArray args of
93− [AppTerm p] -> Just p
94− _ -> Nothing
95− _ -> Nothing
lib/purelint/src/Purelint/Rules/UseConst.pursdeleted−67
@@ -1,67 +0,0 @@
1−module Purelint.Rules.UseConst where
2−
3−import Prelude
4−
5−import Data.Array as Array
6−import Data.Array.NonEmpty as NEA
7−import Data.Maybe (Maybe(..))
8−import Data.Tuple (Tuple(..))
9−import Data.Void (Void)
10−import Purelint.Imports (ImportInfo, hasValue)
11−import Purelint.Print (printExpr)
12−import Purelint.Rule (Rule, RuleContext, mkRule)
13−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
14−import PureScript.CST.Range (rangeOf)
15−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
16−import PureScript.CST.Types (AppSpine(..), Binder(..), Expr(..), Ident(..), Module, Name(..), QualifiedName(..), Wrapped(..))
17−
18−-- | Rule: \x -> y (where y doesn't use x) -> const y
19−-- | Only triggers when the binder is a simple unused variable
20−useConstRule :: Rule
21−useConstRule = mkRule (RuleId "UseConst") run
22− where
23− run :: RuleContext -> Module Void -> Array LintWarning
24− run ctx = foldMapModule visitor
25− where
26− imports = ctx.imports
27− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
28−
29− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
30− checkExpr imports expr = case expr of
31− ExprLambda lambda | hasValue imports "const" ->
32− case NEA.toArray lambda.binders of
33− [BinderVar (Name { name: Ident paramName })] ->
34− -- Check if body doesn't mention the parameter
35− if not (mentionsVar paramName lambda.body) then
36− let bodyText = printExpr lambda.body
37− in
38− [ LintWarning
39− { ruleId: RuleId "UseConst"
40− , message: WarningMessage "Lambda ignores its argument"
41− , range: rangeOf expr
42− , severity: Hint
43− , suggestion: Just $ Suggestion
44− { replacement: ReplacementText ("const " <> bodyText)
45− , description: SuggestionDescription "\\x -> y (where y doesn't use x) can be simplified to const y"
46− }
47− }
48− ]
49− else []
50− _ -> []
51− _ -> []
52−
53− -- Simple check if variable name appears in expression
54− mentionsVar :: String -> Expr Void -> Boolean
55− mentionsVar name (ExprIdent (QualifiedName { name: Ident n })) = n == name
56− mentionsVar name (ExprApp fn args) =
57− mentionsVar name fn || Array.any (mentionsVarInSpine name) (NEA.toArray args)
58− mentionsVar name (ExprLambda l) = mentionsVar name l.body
59− mentionsVar name (ExprIf i) = mentionsVar name i.cond || mentionsVar name i.true || mentionsVar name i.false
60− mentionsVar name (ExprOp lhs ops) =
61− mentionsVar name lhs || Array.any (\(Tuple _ e) -> mentionsVar name e) (NEA.toArray ops)
62− mentionsVar name (ExprParens (Wrapped { value })) = mentionsVar name value
63− mentionsVar _ _ = false
64−
65− mentionsVarInSpine :: String -> AppSpine Expr Void -> Boolean
66− mentionsVarInSpine name (AppTerm e) = mentionsVar name e
67− mentionsVarInSpine _ _ = false
lib/purelint/src/Purelint/Rules/UseEitherMap.pursdeleted−112
@@ -1,112 +0,0 @@
1−module Purelint.Rules.UseEitherMap where
2−
3−import Prelude
4−
5−import Data.Array.NonEmpty as NEA
6−import Data.Maybe (Maybe(..))
7−import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12−import PureScript.CST.Range (rangeOf)
13−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14−import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
15−import Data.Tuple (Tuple(..))
16−
17−-- | Rule: either Left (Right <<< f) -> map f
18−useEitherMapRule :: Rule
19−useEitherMapRule = mkRule (RuleId "UseEitherMap") run
20− where
21− run :: RuleContext -> Module Void -> Array LintWarning
22− run ctx = foldMapModule visitor
23− where
24− imports = ctx.imports
25− visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
26−
27− checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
28− checkExpr imports expr = case expr of
29− -- Match: either Left (Right <<< f)
30− ExprApp fn args
31− | isEither imports fn ->
32− case NEA.toArray args of
33− [AppTerm firstArg, AppTerm secondArg] ->
34− if isLeft imports firstArg then
35− case getRightComposed imports secondArg of
36− Just f ->
37− let fText = printExpr f
38− in
39− [ LintWarning
40− { ruleId: RuleId "UseEitherMap"
41− , message: WarningMessage "either Left (Right <<< f) can be simplified to map f"
42− , range: rangeOf expr
43− , severity: Warning
44− , suggestion: Just $ Suggestion
45− { replacement: ReplacementText ("map " <> fText)
46− , description: SuggestionDescription "Use map instead of either Left (Right <<< f)"
47− }
48− }
49− ]
50− Nothing -> []
51− else []
52− [AppTerm firstArg, AppTerm secondArg, AppTerm xArg] ->
53− if isLeft imports firstArg then
54− case getRightComposed imports secondArg of
55− Just f ->
56− let
57− fText = printExpr f
58− x = printExpr xArg
59− in
60− [ LintWarning
61− { ruleId: RuleId "UseEitherMap"
62− , message: WarningMessage "either Left (Right <<< f) x can be simplified to map f x"
63− , range: rangeOf expr
64− , severity: Warning
65− , suggestion: Just $ Suggestion
66− { replacement: ReplacementText ("map " <> fText <> " " <> x)
67− , description: SuggestionDescription "Use map instead of either Left (Right <<< f)"
68− }
69− }
70− ]
71− Nothing -> []
72− else []
73− _ -> []
74− _ -> []
75−
76− unwrapParens :: Expr Void -> Expr Void
77− unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
78− unwrapParens e = e
79−
80− isEither :: ImportInfo -> Expr Void -> Boolean
81− isEither imports (ExprIdent (QualifiedName { name: Ident name })) =
82− name == "either" && hasValue imports "either"
83− isEither _ _ = false
84−
85− isLeft :: ImportInfo -> Expr Void -> Boolean
86− isLeft imports e =
87− case unwrapParens e of
88− ExprIdent (QualifiedName { name: Ident name }) ->
89− name == "Left" && hasValue imports "Left"
90− ExprConstructor (QualifiedName { name: _ }) -> true -- Allow constructor syntax
91− _ -> false
92−
93− isRight :: ImportInfo -> Expr Void -> Boolean
94− isRight imports e =
95− case unwrapParens e of
96− ExprIdent (QualifiedName { name: Ident name }) ->
97− name == "Right" && hasValue imports "Right"
98− ExprConstructor (QualifiedName { name: _ }) -> true
99− _ -> false
100−
101− -- Check for (Right <<< f) pattern
102− getRightComposed :: ImportInfo -> Expr Void -> Maybe (Expr Void)
103− getRightComposed imports e =
104− case unwrapParens e of
105− ExprOp lhs ops ->
106− case NEA.toArray ops of
107− [Tuple (QualifiedName { name: Operator "<<<" }) rhs]
108− | isRight imports lhs -> Just rhs
109− [Tuple (QualifiedName { name: Operator ">>>" }) rhs]
110− | isRight imports rhs -> Just lhs
111− _ -> Nothing
112− _ -> Nothing
lib/purelint/src/Purelint/Tidy.jsdeleted−3
@@ -1,3 +0,0 @@
1−// For now, just return the input unchanged
2−// In a full implementation, we could shell out to purs-tidy or use its API
3−export const formatExpr = (str) => () => str;
lib/purelint/spago.yaml → lib/purslint/spago.yamlrenamed+1−1
@@ -1,5 +1,5 @@
11package:
2− name: purelint
2+ name: purslint
33 dependencies:
44 - arrays
55 - effect
lib/purelint/src/Purelint/Config.purs → lib/purslint/src/Purslint/Config.pursrenamed+14−15
@@ -1,10 +1,11 @@
1−module Purelint.Config where
1+module Purslint.Config where
22
33import Prelude
44
5+import Data.Array (notElem)
56import Data.Array as Array
67import Data.String as String
7−import Purelint.Types (RuleId(..))
8+import Purslint.Types (RuleId(..))
89
910-- | Parsed config - just enabled/disabled rules for now
1011type Config =
@@ -18,24 +19,22 @@ defaultConfig =
1819
1920-- | Parse simple line-based config
2021parseConfig :: String -> Config
21−parseConfig str =
22− let
23− lines = String.split (String.Pattern "\n") str
24− nonEmpty = Array.filter (\l -> String.length l > 0 && not (isComment l)) lines
25− rules = map (RuleId <<< String.trim) nonEmpty
26− in
27− { disabledRules: rules }
28−
22+parseConfig str =
23+ { disabledRules: rules }
2924 where
25+ lines = str # String.split (String.Pattern "\n")
26+ nonEmpty = lines # Array.filter \l -> String.length l > 0 && not (isComment l)
27+ rules = (RuleId <<< String.trim) <$> nonEmpty
28+
3029 isComment :: String -> Boolean
31− isComment line =
32− let trimmed = String.trim line
33− in String.take 1 trimmed == "#"
30+ isComment line = (trimmed # String.take 1) == "#"
31+ where
32+ trimmed = String.trim line
3433
3534-- | Check if a rule is enabled given the config
3635isRuleEnabled :: Config -> RuleId -> Boolean
37−isRuleEnabled config ruleId = not (Array.elem ruleId config.disabledRules)
36+isRuleEnabled config ruleId = notElem ruleId config.disabledRules
3837
3938-- | Filter rules by config
4039filterRules :: forall a. Array { ruleId :: RuleId | a } -> Config -> Array { ruleId :: RuleId | a }
41−filterRules rules config = Array.filter (\r -> isRuleEnabled config r.ruleId) rules
40+filterRules rules config = rules # Array.filter \r -> r.ruleId # isRuleEnabled config
lib/purslint/src/Purslint/Fix.pursnew file+129
@@ -0,0 +1,129 @@
1+module Purslint.Fix where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Foldable (foldMap)
7+import Data.Maybe (Maybe(..))
8+import Data.Monoid (power)
9+import Data.Newtype (un, unwrap)
10+import Data.String as String
11+import Data.String.CodeUnits as StringCU
12+import PureScript.CST.Types (SourceRange)
13+import Purslint.Types (LintResult(..), LintWarning(..), ReplacementText(..), Suggestion(..))
14+import Purslint.ImportFix (processImports)
15+
16+-- | Extract text at a source range from source code
17+extractRange :: String -> SourceRange -> String
18+extractRange source range =
19+ if startLine == endLine then
20+ -- Single line extraction
21+ Array.index lines startLine # foldMap \line -> StringCU.slice startCol endCol line
22+ else do
23+ -- Multi-line extraction
24+ let
25+ firstLine = startLine # Array.index lines # foldMap (StringCU.drop startCol)
26+ middleLines = Array.slice (startLine + 1) endLine lines
27+ lastLine = endLine # Array.index lines # foldMap (StringCU.take endCol)
28+ ([ firstLine ] <> middleLines <> [ lastLine ]) # String.joinWith "\n"
29+ where
30+ lines = source # String.split (String.Pattern "\n")
31+ startLine = range.start.line
32+ endLine = range.end.line
33+ startCol = range.start.column
34+ endCol = range.end.column
35+
36+-- | Replace text at a source range with new text
37+replaceRange :: String -> SourceRange -> String -> String
38+replaceRange source range replacement = do
39+ let
40+ lines = source # String.split (String.Pattern "\n")
41+ startLine = range.start.line
42+ endLine = range.end.line
43+ startCol = range.start.column
44+ endCol = range.end.column
45+ if startLine == endLine then
46+ -- Single line replacement
47+ case Array.index lines startLine of
48+ Nothing -> source
49+ Just line ->
50+ let
51+ before = StringCU.take startCol line
52+ after = StringCU.drop endCol line
53+ newLine = before <> replacement <> after
54+ newLines = Array.updateAt startLine newLine lines
55+ in
56+ case newLines of
57+ Nothing -> source
58+ Just ls -> String.joinWith "\n" ls
59+ else do
60+ -- Multi-line replacement
61+ let
62+ beforeLines = lines # Array.take startLine
63+ afterLines = lines # Array.drop (endLine + 1)
64+ firstLinePart = startLine # Array.index lines # foldMap (StringCU.take startCol)
65+ lastLinePart = endLine # Array.index lines # foldMap (StringCU.drop endCol)
66+ newLine = firstLinePart <> replacement <> lastLinePart
67+ (beforeLines <> [ newLine ] <> afterLines) # String.joinWith "\n"
68+
69+-- | Apply a single fix to source code
70+applyFix :: String -> LintWarning -> Maybe String
71+applyFix source (LintWarning w) = do
72+ Suggestion s <- w.suggestion
73+ let replacement = s.replacement # un ReplacementText
74+ let
75+ { source: updatedSource, replacement: updatedReplacement, linesAdded } =
76+ processImports source replacement s.requiredImports
77+ let shiftedRange = shiftRange linesAdded w.range
78+ Just $ replaceRange updatedSource shiftedRange updatedReplacement
79+
80+-- | Apply all fixes to source code (in reverse order to preserve ranges)
81+-- | Skips overlapping fixes to avoid corruption
82+applyAllFixes :: String -> LintResult -> String
83+applyAllFixes source (LintResult result) =
84+ -- Sort warnings by range in reverse order (end of file first)
85+ -- Then fold, tracking applied ranges to skip overlaps
86+ _.source $ Array.foldl applyFixIfPossible { source, appliedRanges: [] } sortedWarnings
87+ where
88+ sortedWarnings = result.warnings # Array.sortBy compareRangesReverse
89+
90+ compareRangesReverse :: LintWarning -> LintWarning -> Ordering
91+ compareRangesReverse (LintWarning a) (LintWarning b) =
92+ -- Compare in reverse: later positions first
93+ case compare b.range.start.line a.range.start.line of
94+ EQ -> compare b.range.start.column a.range.start.column
95+ other -> other
96+
97+ applyFixIfPossible :: { source :: String, appliedRanges :: Array SourceRange } -> LintWarning -> { source :: String, appliedRanges :: Array SourceRange }
98+ applyFixIfPossible acc (LintWarning w) =
99+ -- Skip if this warning overlaps with any already-applied range
100+ if Array.any (overlaps w.range) acc.appliedRanges then
101+ acc
102+ else case w.suggestion of
103+ Nothing -> acc
104+ Just (Suggestion s) ->
105+ let
106+ replacement = s.replacement # un ReplacementText
107+ importResult = processImports acc.source replacement s.requiredImports
108+ shiftedRange = shiftRange importResult.linesAdded w.range
109+ updatedSource = replaceRange importResult.source shiftedRange importResult.replacement
110+ in
111+ { source: updatedSource
112+ , appliedRanges: Array.cons shiftedRange acc.appliedRanges
113+ }
114+
115+ -- Check if two ranges overlap
116+ overlaps :: SourceRange -> SourceRange -> Boolean
117+ overlaps a b =
118+ not (aEndsBeforeB || bEndsBeforeA)
119+ where
120+ aEndsBeforeB = a.end.line < b.start.line || (a.end.line == b.start.line && a.end.column <= b.start.column)
121+ bEndsBeforeA = b.end.line < a.start.line || (b.end.line == a.start.line && b.end.column <= b.start.column)
122+
123+shiftRange :: Int -> SourceRange -> SourceRange
124+shiftRange lineDelta range =
125+ if lineDelta == 0 then range
126+ else
127+ { start: range.start { line = range.start.line + lineDelta }
128+ , end: range.end { line = range.end.line + lineDelta }
129+ }
lib/purslint/src/Purslint/ImportFix.pursnew file+17
@@ -0,0 +1,17 @@
1+module Purslint.ImportFix where
2+
3+import Data.Maybe (Maybe)
4+-- NOTE: Import processing is currently a no-op. We assume required imports
5+-- are already present in the source and keep replacements intact.
6+processImports :: String -> String -> Array
7+ { codeText :: Maybe String
8+ , importItem :: Maybe String
9+ , moduleName :: String
10+ , qualifier :: Maybe String
11+ }
12+ -> { linesAdded :: Int
13+ , replacement :: String
14+ , source :: String
15+ }
16+processImports source replacement _imports =
17+ { linesAdded: 0, replacement, source }
lib/purelint/src/Purelint/Imports.purs → lib/purslint/src/Purslint/Imports.pursrenamed+5−5
@@ -1,4 +1,4 @@
1−module Purelint.Imports where
1+module Purslint.Imports where
22
33import Prelude
44
@@ -53,7 +53,7 @@ preludeModules = Set.fromFoldable
5353-- | Standard Prelude values we care about
5454standardValues :: Set String
5555standardValues = Set.fromFoldable
56− [ "map", "fmap", "identity", "id", "not"
56+ [ "map", "identity", "id", "not"
5757 , "traverse", "traverse_", "sequenceA", "sequence", "for", "for_"
5858 , "concat", "concatMap", "bind", "pure", "join"
5959 , "when", "unless"
@@ -62,11 +62,11 @@ standardValues = Set.fromFoldable
6262 , "const", "void"
6363 , "maybe", "fromMaybe", "isJust", "isNothing", "Nothing", "Just"
6464 , "flip", "elem", "notElem", "elemIndex", "findIndex"
65− , "mempty", "fold", "foldMap"
65+ , "mempty", "fold", "foldMap", "foldr", "foldl"
6666 , "fst", "snd", "uncurry"
6767 , "max", "min", "minimum", "maximum"
6868 , "compare", "comparing", "on"
69− , "guard"
69+ , "guard", "empty"
7070 , "catMaybes", "mapMaybe", "findMap"
7171 , "either", "Left", "Right"
7272 , "bimap", "first", "second"
@@ -80,7 +80,7 @@ standardValues = Set.fromFoldable
8080standardOps :: Set String
8181standardOps = Set.fromFoldable
8282 [ "<$>", "<#>", "<*>", ">>=", "=<<", ">>", "*>", "<<<", ">>>", "==", "/="
83− , "<>", ">", ">=", "<", "<=", "$>", "!!"
83+ , "<>", ">", ">=", "<", "<=", "$>", "!!", "<|>", "&&", "||"
8484 ]
8585
8686-- | Extract import information from a module
lib/purslint/src/Purslint/Print.pursnew file+115
@@ -0,0 +1,115 @@
1+module Purslint.Print where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Foldable (foldl, foldMap)
7+import Data.Maybe (Maybe(..))
8+import Data.String as String
9+import Data.String.Regex (regex, replace)
10+import Data.String.Regex.Flags (global)
11+import Data.Either (either)
12+import Data.Void (Void)
13+import PureScript.CST.Print (printToken, printSourceToken)
14+import PureScript.CST.Range (tokensOf)
15+import PureScript.CST.Range.TokenList as TokenList
16+import PureScript.CST.Types (Binder, Expr, SourceToken, Token(..), Type)
17+
18+-- | Print an expression back to source code with tidy-style formatting
19+printExpr :: Expr Void -> String
20+printExpr expr = do
21+ let tokens = TokenList.toArray (tokensOf expr)
22+ printTokens tokens
23+
24+-- | Print an expression preserving original whitespace/newlines
25+printExprMultiline :: Expr Void -> String
26+printExprMultiline expr = do
27+ let tokens = TokenList.toArray (tokensOf expr)
28+ String.trim $ foldMap printSourceToken tokens
29+
30+-- | Print an expression without comments with tidy-style formatting
31+printExprNoComments :: Expr Void -> String
32+printExprNoComments = printExpr
33+
34+-- | Print a binder back to source code with tidy-style formatting
35+printBinder :: Binder Void -> String
36+printBinder binder = do
37+ let tokens = TokenList.toArray (tokensOf binder)
38+ printTokens tokens
39+
40+-- | Print a type back to source code with tidy-style formatting
41+printType :: Type Void -> String
42+printType typ = do
43+ let tokens = TokenList.toArray (tokensOf typ)
44+ printTokens tokens
45+
46+-- | Print tokens with tidy-style spacing
47+printTokens :: Array SourceToken -> String
48+printTokens tokens =
49+ String.trim $ foldl go { prev: Nothing, result: "" } tokens # _.result
50+ where
51+ go :: { prev :: Maybe Token, result :: String } -> SourceToken -> { prev :: Maybe Token, result :: String }
52+ go acc tok =
53+ let
54+ t = tok.value
55+ printed = printToken t
56+ space = needsSpaceBetween acc.prev t
57+ in
58+ { prev: Just t
59+ , result: acc.result <> space <> printed
60+ }
61+
62+-- | Determine spacing between two tokens (tidy-style)
63+needsSpaceBetween :: Maybe Token -> Token -> String
64+needsSpaceBetween prev curr = case prev of
65+ Nothing -> ""
66+ Just p ->
67+ -- Space after opening brackets: [ x, { a:
68+ if needsSpaceAfter p then " "
69+ -- Space before closing brackets: x ], a }
70+ else if needsSpaceBefore curr then " "
71+ -- No space after certain tokens
72+ else if noSpaceAfter p then ""
73+ -- No space before certain tokens
74+ else if noSpaceBefore curr then ""
75+ -- Default: space between tokens
76+ else " "
77+
78+-- | Tokens that need space after them (tidy-style)
79+needsSpaceAfter :: Token -> Boolean
80+needsSpaceAfter = case _ of
81+ TokLeftSquare -> true -- [ x
82+ TokLeftBrace -> true -- { a
83+ _ -> false
84+
85+-- | Tokens that need space before them (tidy-style)
86+needsSpaceBefore :: Token -> Boolean
87+needsSpaceBefore = case _ of
88+ TokRightSquare -> true -- x ]
89+ TokRightBrace -> true -- a }
90+ _ -> false
91+
92+-- | Tokens that should NOT have space after them
93+noSpaceAfter :: Token -> Boolean
94+noSpaceAfter = case _ of
95+ TokBackslash -> true -- \x
96+ TokLeftParen -> true -- (x
97+ TokDot -> true -- a.b
98+ TokAt -> true -- @x
99+ TokPipe -> true -- |x in guards
100+ _ -> false
101+
102+-- | Tokens that should NOT have space before them
103+noSpaceBefore :: Token -> Boolean
104+noSpaceBefore = case _ of
105+ TokRightParen -> true -- x)
106+ TokComma -> true -- x,
107+ TokDot -> true -- a.b
108+ _ -> false
109+
110+-- | Collapse multiple spaces into single space and trim
111+normalizeWhitespace :: String -> String
112+normalizeWhitespace s = do
113+ let
114+ collapsed = either (const s) (\r -> replace r " " s) (regex "\\s+" global)
115+ String.trim collapsed
lib/purelint/src/Purelint/Rule.purs → lib/purslint/src/Purslint/Rule.pursrenamed+3−3
@@ -1,9 +1,9 @@
1−module Purelint.Rule where
1+module Purslint.Rule where
22
33import Prelude
44
5−import Purelint.Imports (ImportInfo)
6−import Purelint.Types (LintWarning, RuleId)
5+import Purslint.Imports (ImportInfo)
6+import Purslint.Types (LintWarning, RuleId)
77import PureScript.CST.Types (Module)
88import Data.Void (Void)
99
lib/purslint/src/Purslint/Rules/AlternativeLaw.pursnew file+77
@@ -0,0 +1,77 @@
1+module Purslint.Rules.AlternativeLaw where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple (Tuple(..))
8+import Purslint.Imports (ImportInfo, hasValue, hasOp)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Proper(..), Wrapped(..))
15+
16+-- | Alternative law rules:
17+-- | - empty <|> x -> x (left identity)
18+-- | - x <|> empty -> x (right identity)
19+alternativeLawRule :: Rule
20+alternativeLawRule = mkRule (RuleId "AlternativeLaw") run
21+ where
22+ run :: RuleContext -> Module Void -> Array LintWarning
23+ run ctx = foldMapModule visitor
24+ where
25+ imports = ctx.imports
26+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
27+
28+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
29+ checkExpr imports expr = case expr of
30+ ExprOp lhs ops ->
31+ case NEA.toArray ops of
32+ [Tuple (QualifiedName { name: Operator op }) rhs]
33+ | op == "<|>" && hasOp imports "<|>" -> checkAlt imports expr lhs rhs
34+ _ -> []
35+ _ -> []
36+
37+ checkAlt :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
38+ checkAlt imports fullExpr lhs rhs
39+ -- empty <|> x -> x
40+ | isEmpty imports lhs =
41+ [ LintWarning
42+ { ruleId: RuleId "AlternativeLaw"
43+ , message: WarningMessage "Alternative law: empty <|> x simplifies to x"
44+ , range: rangeOf fullExpr
45+ , severity: Warning
46+ , suggestion: Just $ Suggestion
47+ { replacement: ReplacementText (printExpr rhs)
48+ , description: SuggestionDescription "empty is the identity for <|> (left identity)"
49+ , requiredImports: []
50+ }
51+ }
52+ ]
53+ -- x <|> empty -> x
54+ | isEmpty imports rhs =
55+ [ LintWarning
56+ { ruleId: RuleId "AlternativeLaw"
57+ , message: WarningMessage "Alternative law: x <|> empty simplifies to x"
58+ , range: rangeOf fullExpr
59+ , severity: Warning
60+ , suggestion: Just $ Suggestion
61+ { replacement: ReplacementText (printExpr lhs)
62+ , description: SuggestionDescription "empty is the identity for <|> (right identity)"
63+ , requiredImports: []
64+ }
65+ }
66+ ]
67+ | otherwise = []
68+
69+ isEmpty :: ImportInfo -> Expr Void -> Boolean
70+ isEmpty imports (ExprIdent (QualifiedName { name: Ident name })) =
71+ (name == "empty" && hasValue imports "empty") ||
72+ (name == "mempty" && hasValue imports "mempty")
73+ isEmpty imports (ExprConstructor (QualifiedName { name: Proper "Nothing" })) =
74+ hasValue imports "Nothing"
75+ isEmpty _ (ExprArray (Wrapped { value: Nothing })) = true -- []
76+ isEmpty _ _ = false
77+
lib/purelint/src/Purelint/Rules/BooleanSimplify.purs → lib/purslint/src/Purslint/Rules/BooleanSimplify.pursrenamed+40−24
@@ -1,4 +1,4 @@
1−module Purelint.Rules.BooleanSimplify where
1+module Purslint.Rules.BooleanSimplify where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (Expr(..), Module, Operator(..), QualifiedName(..))
@@ -19,7 +19,7 @@ import PureScript.CST.Types (Expr(..), Module, Operator(..), QualifiedName(..))
1919-- | Also: true == x -> x
2020-- | Also: false == x -> not x
2121booleanSimplifyRule :: Rule
22−booleanSimplifyRule = mkRule (RuleId "BooleanSimplify") run
22+booleanSimplifyRule = run # mkRule (RuleId "BooleanSimplify")
2323 where
2424 run :: RuleContext -> Module Void -> Array LintWarning
2525 run ctx = foldMapModule visitor
@@ -29,24 +29,38 @@ booleanSimplifyRule = mkRule (RuleId "BooleanSimplify") run
2929
3030 checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
3131 checkExpr imports expr = case expr of
32− -- Match: x == true, x == false, true == x, false == x
33− ExprOp lhs ops | hasOp imports "==" ->
34− case NEA.toArray ops of
35− [Tuple qn rhs] | isEqOp qn ->
36− -- x == true -> x
37− if isBoolLit true rhs then
38− [ mkWarning expr (printExpr lhs) "x == true" "x" ]
39− -- x == false -> not x
40− else if isBoolLit false rhs then
41− [ mkWarning expr ("not " <> printExpr lhs) "x == false" "not x" ]
42− -- true == x -> x
43− else if isBoolLit true lhs then
44− [ mkWarning expr (printExpr rhs) "true == x" "x" ]
45− -- false == x -> not x
46− else if isBoolLit false lhs then
47− [ mkWarning expr ("not " <> printExpr rhs) "false == x" "not x" ]
48− else []
49− _ -> []
32+
33+ -- x == true -> x
34+ ExprOp lhs ops
35+ | "==" # hasOp imports
36+ , [ Tuple qn rhs ] <- NEA.toArray ops
37+ , isEqOp qn
38+ , rhs # isBoolLit true ->
39+ [ mkWarning expr (printExpr lhs) "x == true" "x" ]
40+
41+ -- x == false -> not x
42+ ExprOp lhs ops
43+ | "==" # hasOp imports
44+ , [ Tuple qn rhs ] <- NEA.toArray ops
45+ , isEqOp qn
46+ , rhs # isBoolLit false ->
47+ [ mkWarning expr ("not " <> printExpr lhs) "x == false" "not x" ]
48+
49+ -- true == x -> x
50+ ExprOp lhs ops
51+ | "==" # hasOp imports
52+ , [ Tuple qn rhs ] <- NEA.toArray ops
53+ , isEqOp qn
54+ , lhs # isBoolLit true ->
55+ [ mkWarning expr (printExpr rhs) "true == x" "x" ]
56+
57+ -- false == x -> not x
58+ ExprOp lhs ops
59+ | "==" # hasOp imports
60+ , [ Tuple qn rhs ] <- NEA.toArray ops
61+ , isEqOp qn
62+ , lhs # isBoolLit false ->
63+ [ mkWarning expr ("not " <> printExpr rhs) "false == x" "not x" ]
5064 _ -> []
5165
5266 mkWarning :: Expr Void -> String -> String -> String -> LintWarning
@@ -59,6 +73,7 @@ booleanSimplifyRule = mkRule (RuleId "BooleanSimplify") run
5973 , suggestion: Just $ Suggestion
6074 { replacement: ReplacementText replacement
6175 , description: SuggestionDescription $ from <> " can be simplified to " <> to
76+ , requiredImports: []
6277 }
6378 }
6479
@@ -68,3 +83,4 @@ booleanSimplifyRule = mkRule (RuleId "BooleanSimplify") run
6883 isBoolLit :: Boolean -> Expr Void -> Boolean
6984 isBoolLit expected (ExprBoolean _ val) = val == expected
7085 isBoolLit _ _ = false
86+
lib/purelint/src/Purelint/Rules/CollapseLambdas.purs → lib/purslint/src/Purslint/Rules/CollapseLambdas.pursrenamed+19−19
@@ -1,4 +1,4 @@
1−module Purelint.Rules.CollapseLambdas where
1+module Purslint.Rules.CollapseLambdas where
22
33import Prelude
44
@@ -6,17 +6,16 @@ import Data.Array as Array
66import Data.Array.NonEmpty as NEA
77import Data.Maybe (Maybe(..))
88import Data.String as String
9−import Data.Void (Void)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1312import PureScript.CST.Range (rangeOf)
1413import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1514import PureScript.CST.Types (Binder(..), Expr(..), Ident(..), Module, Name(..))
1615
1716-- | Rule: \x -> \y -> body -> \x y -> body
1817collapseLambdasRule :: Rule
19−collapseLambdasRule = mkRule (RuleId "CollapseLambdas") run
18+collapseLambdasRule = run # mkRule (RuleId "CollapseLambdas")
2019 where
2120 run :: RuleContext -> Module Void -> Array LintWarning
2221 run _ctx = foldMapModule visitor
@@ -28,7 +27,7 @@ collapseLambdasRule = mkRule (RuleId "CollapseLambdas") run
2827 ExprLambda lambda ->
2928 -- Check if body is another lambda
3029 case lambda.body of
31− ExprLambda innerLambda ->
30+ ExprLambda innerLambda -> do
3231 let
3332 outerBinders = NEA.toArray lambda.binders
3433 innerBinders = NEA.toArray innerLambda.binders
@@ -36,21 +35,22 @@ collapseLambdasRule = mkRule (RuleId "CollapseLambdas") run
3635 binderNames = Array.mapMaybe getBinderName allBinders
3736 bodyText = printExpr innerLambda.body
3837 replacement = "\\" <> String.joinWith " " binderNames <> " -> " <> bodyText
39− in
40− [ LintWarning
41− { ruleId: RuleId "CollapseLambdas"
42− , message: WarningMessage "Collapse nested lambdas"
43− , range: rangeOf expr
44− , severity: Hint
45− , suggestion: Just $ Suggestion
46− { replacement: ReplacementText replacement
47− , description: SuggestionDescription "\\x -> \\y -> body can be simplified to \\x y -> body"
48− }
49− }
50− ]
38+ [ LintWarning
39+ { ruleId: RuleId "CollapseLambdas"
40+ , message: WarningMessage "Collapse nested lambdas"
41+ , range: rangeOf expr
42+ , severity: Hint
43+ , suggestion: Just $ Suggestion
44+ { replacement: ReplacementText replacement
45+ , description: SuggestionDescription "\\x -> \\y -> body can be simplified to \\x y -> body"
46+ , requiredImports: []
47+ }
48+ }
49+ ]
5150 _ -> []
5251 _ -> []
5352
5453 getBinderName :: Binder Void -> Maybe String
5554 getBinderName (BinderVar (Name { name: Ident n })) = Just n
5655 getBinderName _ = Nothing
56+
lib/purelint/src/Purelint/Rules/ConcatMap.purs → lib/purslint/src/Purslint/Rules/ConcatMap.pursrenamed+19−22
@@ -1,14 +1,13 @@
1−module Purelint.Rules.ConcatMap where
1+module Purslint.Rules.ConcatMap where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
7−import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1211import PureScript.CST.Range (rangeOf)
1312import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1413import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -27,13 +26,10 @@ concatMapRule = mkRule (RuleId "ConcatMap") run
2726 checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
2827 checkExpr imports expr = case expr of
2928 -- Match: concat (map f x) or join (map f x)
30− ExprApp fnExpr args ->
31− case fnExpr of
32− ExprIdent qn | isConcatOrJoin imports qn ->
33− case NEA.head args of
34− AppTerm innerExpr -> checkInnerMap imports expr qn (unwrapParens innerExpr)
35− _ -> []
36− _ -> []
29+ ExprApp (ExprIdent qn) args
30+ | qn # isConcatOrJoin imports
31+ , AppTerm innerExpr <- NEA.head args ->
32+ checkInnerMap imports expr qn (unwrapParens innerExpr)
3733 _ -> []
3834
3935 unwrapParens :: Expr Void -> Expr Void
@@ -41,12 +37,12 @@ concatMapRule = mkRule (RuleId "ConcatMap") run
4137 unwrapParens e = e
4238
4339 checkInnerMap :: ImportInfo -> Expr Void -> QualifiedName Ident -> Expr Void -> Array LintWarning
44− checkInnerMap imports fullExpr fnQn innerExpr =
40+ checkInnerMap imports fullExpr fnQn innerExpr =
4541 case innerExpr of
46− ExprApp mapFn innerArgs | isMapApp imports mapFn ->
47− case NEA.toArray innerArgs of
48− [AppTerm fExpr, AppTerm xExpr] ->
49− let
42+ ExprApp mapFn innerArgs
43+ | isMapApp imports mapFn
44+ , [ AppTerm fExpr, AppTerm xExpr ] <- NEA.toArray innerArgs ->
45+ let
5046 fnName = getIdentName fnQn
5147 replacementFn = if fnName == "concat" then "concatMap" else "(=<<)"
5248 f = printExpr fExpr
@@ -62,10 +58,10 @@ concatMapRule = mkRule (RuleId "ConcatMap") run
6258 , suggestion: Just $ Suggestion
6359 { replacement: ReplacementText replacement
6460 , description: SuggestionDescription msg
61+ , requiredImports: []
6562 }
6663 }
6764 ]
68− _ -> []
6965 _ -> []
7066
7167 isMapApp :: ImportInfo -> Expr Void -> Boolean
@@ -73,12 +69,13 @@ concatMapRule = mkRule (RuleId "ConcatMap") run
7369 isMapApp _ _ = false
7470
7571 isMap :: ImportInfo -> QualifiedName Ident -> Boolean
76− isMap imports (QualifiedName { name: Ident name }) =
77− (name == "map" && hasValue imports "map") || (name == "fmap" && hasValue imports "fmap")
72+ isMap imports (QualifiedName { name: Ident name }) =
73+ name == "map" && hasValue imports "map"
7874
7975 isConcatOrJoin :: ImportInfo -> QualifiedName Ident -> Boolean
80− isConcatOrJoin imports (QualifiedName { name: Ident name }) =
76+ isConcatOrJoin imports (QualifiedName { name: Ident name }) =
8177 (name == "concat" && hasValue imports "concat") || (name == "join" && hasValue imports "join")
8278
8379 getIdentName :: QualifiedName Ident -> String
8480 getIdentName (QualifiedName { name: Ident name }) = name
81+
lib/purelint/src/Purelint/Rules/EtaReduce.purs → lib/purslint/src/Purslint/Rules/EtaReduce.pursrenamed+31−20
@@ -1,17 +1,16 @@
1−module Purelint.Rules.EtaReduce where
1+module Purslint.Rules.EtaReduce where
22
33import Prelude
44
55import Data.Array as Array
66import Data.Array.NonEmpty as NEA
77import Data.Maybe (Maybe(..))
8−import Purelint.Print (printExpr)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (AppSpine(..), Binder(..), Expr(..), Ident(..), Module, Name(..), QualifiedName(..), Wrapped(..))
14−import Data.Void (Void)
1514
1615-- | Rule: \x -> f x -> f (eta reduction)
1716-- | Detects simple lambda functions that can be eta reduced
@@ -26,15 +25,14 @@ etaReduceRule = mkRule (RuleId "EtaReduce") run
2625 checkExpr :: Expr Void -> Array LintWarning
2726 checkExpr expr = case expr of
2827 -- Match: \x -> f x
29− ExprLambda lambda ->
30− checkLambda expr lambda
28+ ExprLambda lambda -> lambda # checkLambda expr
3129 _ -> []
3230
3331 checkLambda :: Expr Void -> _ -> Array LintWarning
3432 checkLambda fullExpr lambda =
3533 -- Check for single binder that's a simple variable
3634 case NEA.toArray lambda.binders of
37− [BinderVar (Name { name: Ident paramName })] ->
35+ [ BinderVar (Name { name: Ident paramName }) ] ->
3836 -- Check if body is f x where x matches the param
3937 checkBody fullExpr paramName (unwrapParens lambda.body)
4038 _ -> []
@@ -42,15 +40,21 @@ etaReduceRule = mkRule (RuleId "EtaReduce") run
4240 checkBody :: Expr Void -> String -> Expr Void -> Array LintWarning
4341 checkBody fullExpr paramName body =
4442 case body of
45− ExprApp fn args ->
43+ ExprApp fn args -> do
4644 -- Check if the last argument is just our parameter
47− let argsArr = NEA.toArray args in
45+ let argsArr = NEA.toArray args
4846 case Array.last argsArr of
49− Just (AppTerm (ExprIdent (QualifiedName { name: Ident argName })))
50− | argName == paramName && not (mentionsParam paramName fn) && Array.length argsArr == 1 ->
51− -- Single arg case: \x -> f x -> f
52− let fnText = printExpr fn
53− in
47+ Just (AppTerm (ExprIdent (QualifiedName { name: Ident argName })))
48+ | argName == paramName
49+ , not (fn # mentionsParam paramName)
50+ , not ((argsArr # Array.dropEnd 1) # Array.any (checkArg paramName)) -> do
51+ -- Can eta reduce
52+ let
53+ remainingArgs = Array.dropEnd 1 argsArr
54+ -- If there are remaining args, wrap in parens for safety
55+ fnText =
56+ if Array.null remainingArgs then printExpr fn
57+ else "(" <> printExpr fn <> " " <> Array.intercalate " " (map printAppSpine remainingArgs) <> ")"
5458 [ LintWarning
5559 { ruleId: RuleId "EtaReduce"
5660 , message: WarningMessage "Lambda can be eta reduced"
@@ -59,12 +63,17 @@ etaReduceRule = mkRule (RuleId "EtaReduce") run
5963 , suggestion: Just $ Suggestion
6064 { replacement: ReplacementText fnText
6165 , description: SuggestionDescription $ "\\x -> f x can be simplified to f"
66+ , requiredImports: []
6267 }
6368 }
6469 ]
6570 _ -> []
6671 _ -> []
6772
73+ printAppSpine :: AppSpine Expr Void -> String
74+ printAppSpine (AppTerm e) = printExpr e
75+ printAppSpine (AppType _ _) = "@_"
76+
6877 unwrapParens :: Expr Void -> Expr Void
6978 unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
7079 unwrapParens e = e
@@ -72,11 +81,13 @@ etaReduceRule = mkRule (RuleId "EtaReduce") run
7281 -- Check if expression mentions the parameter (crude check)
7382 mentionsParam :: String -> Expr Void -> Boolean
7483 mentionsParam name (ExprIdent (QualifiedName { name: Ident n })) = n == name
75− mentionsParam name (ExprApp fn args) =
76− mentionsParam name fn || Array.any (checkArg name) (NEA.toArray args)
77− mentionsParam name (ExprParens (Wrapped { value })) = mentionsParam name value
84+ mentionsParam name (ExprApp fn args) =
85+ (fn # mentionsParam name) || ((NEA.toArray args) # Array.any (checkArg name))
86+ mentionsParam name (ExprParens (Wrapped { value })) = value # mentionsParam name
7887 mentionsParam _ _ = false
7988
8089 checkArg :: String -> AppSpine Expr Void -> Boolean
81− checkArg name (AppTerm e) = mentionsParam name e
82− checkArg _ _ = false
90+ checkArg = case _, _ of
91+ name, (AppTerm e) -> e # mentionsParam name
92+ _, _ -> false
93+
lib/purelint/src/Purelint/Rules/EtaReduceDecl.purs → lib/purslint/src/Purslint/Rules/EtaReduceDecl.pursrenamed+33−44
@@ -1,4 +1,4 @@
1−module Purelint.Rules.EtaReduceDecl where
1+module Purslint.Rules.EtaReduceDecl where
22
33import Prelude
44
@@ -7,9 +7,9 @@ import Data.Array.NonEmpty as NEA
77import Data.Maybe (Maybe(..))
88import Data.String as String
99import Data.Void (Void)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types as CST
@@ -34,48 +34,36 @@ etaReduceDeclRule = mkRule (RuleId "EtaReduceDecl") run
3434 -- Check for pattern: foo x = expr x (single binder, unconditional)
3535 case vb.guarded of
3636 Unconditional _ (Where { expr, bindings: Nothing }) ->
37− checkEtaReducible vb.binders (unwrapParens expr)
37+ unwrapParens expr # checkEtaReducible vb.binders
3838 _ -> []
3939
4040 checkEtaReducible :: Array (Binder Void) -> Expr Void -> Array LintWarning
41− checkEtaReducible binders body =
42− -- Get the last binder if it's a simple variable
43− case Array.last binders of
44− Just lastBinder@(BinderVar (Name { name: Ident paramName })) ->
45− -- Check if body is `f ... x` where x is our param
46− case body of
47− ExprApp fn args ->
48− let argsArr = NEA.toArray args in
49− case Array.last argsArr of
50− Just (AppTerm (ExprIdent (QualifiedName { name: Ident argName })))
51− | argName == paramName
52− && not (mentionsParam paramName fn)
53− && not (Array.any (checkArgMentions paramName) (Array.dropEnd 1 argsArr)) ->
54− -- Can eta reduce - build the new RHS
55− let
56− remainingArgs = Array.dropEnd 1 argsArr
57− newRhs = if Array.null remainingArgs
58− then printExpr fn
59− else printExpr fn <> " " <> Array.intercalate " " (map printAppSpine remainingArgs)
60− -- Range from last binder to end of body
61− binderRange = rangeOf lastBinder
62− bodyRange = rangeOf body
63− fullRange = { start: binderRange.start, end: bodyRange.end }
64− in
65− [ LintWarning
66− { ruleId: RuleId "EtaReduceDecl"
67− , message: WarningMessage "Declaration can be eta reduced"
68− , range: fullRange
69− , severity: Hint
70− , suggestion: Just $ Suggestion
71− { replacement: ReplacementText ("= " <> newRhs)
72− , description: SuggestionDescription "Remove redundant parameter"
73− }
74− }
75− ]
76− _ -> []
77− _ -> []
78− _ -> []
41+ checkEtaReducible binders body
42+ | Just lastBinder@(BinderVar (Name { name: Ident paramName })) <- Array.last binders
43+ , ExprApp fn args <- body
44+ , argsArr <- NEA.toArray args
45+ , Just (AppTerm (ExprIdent (QualifiedName { name: Ident argName }))) <- Array.last argsArr
46+ , argName == paramName
47+ , not mentionsParam paramName fn
48+ , remainingArgs <- Array.dropEnd 1 argsArr
49+ , not (Array.any (checkArgMentions paramName) remainingArgs)
50+ , newRhs <-
51+ if Array.null remainingArgs then printExpr fn
52+ else printExpr fn <> " " <> Array.intercalate " " (map printAppSpine remainingArgs)
53+ , fullRange <- { start: (rangeOf lastBinder).start, end: (rangeOf body).end } =
54+ [ LintWarning
55+ { ruleId: RuleId "EtaReduceDecl"
56+ , message: WarningMessage "Declaration can be eta reduced"
57+ , range: fullRange
58+ , severity: Hint
59+ , suggestion: Just $ Suggestion
60+ { replacement: ReplacementText ("= " <> newRhs)
61+ , description: SuggestionDescription "Remove redundant parameter"
62+ , requiredImports: []
63+ }
64+ }
65+ ]
66+ | otherwise = []
7967
8068 printAppSpine :: AppSpine Expr Void -> String
8169 printAppSpine (AppTerm e) = printExpr e
@@ -92,7 +80,7 @@ etaReduceDeclRule = mkRule (RuleId "EtaReduceDecl") run
9280 -- Check if expression mentions the parameter
9381 mentionsParam :: String -> Expr Void -> Boolean
9482 mentionsParam name (ExprIdent (QualifiedName { name: Ident n })) = n == name
95− mentionsParam name (ExprApp fn args) =
83+ mentionsParam name (ExprApp fn args) =
9684 mentionsParam name fn || Array.any (checkArg name) (NEA.toArray args)
9785 mentionsParam name (ExprParens (Wrapped { value })) = mentionsParam name value
9886 mentionsParam _ _ = false
@@ -100,3 +88,4 @@ etaReduceDeclRule = mkRule (RuleId "EtaReduceDecl") run
10088 checkArg :: String -> AppSpine Expr Void -> Boolean
10189 checkArg name (AppTerm e) = mentionsParam name e
10290 checkArg _ _ = false
91+
lib/purslint/src/Purslint/Rules/EvaluateBool.pursnew file+90
@@ -0,0 +1,90 @@
1+module Purslint.Rules.EvaluateBool where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple (Tuple(..))
8+import Purslint.Imports (ImportInfo, hasOp)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (Expr(..), Module, Operator(..), QualifiedName(..))
15+
16+-- | Constant folding for boolean expressions:
17+-- | - true && x -> x
18+-- | - false && x -> false
19+-- | - x && true -> x
20+-- | - x && false -> false
21+-- | - true || x -> true
22+-- | - false || x -> x
23+-- | - x || true -> true
24+-- | - x || false -> x
25+evaluateBoolRule :: Rule
26+evaluateBoolRule = run # mkRule (RuleId "EvaluateBool")
27+ where
28+ run :: RuleContext -> Module Void -> Array LintWarning
29+ run ctx = foldMapModule visitor
30+ where
31+ imports = ctx.imports
32+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
33+
34+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
35+ checkExpr imports expr = case expr of
36+ ExprOp lhs ops ->
37+ case NEA.toArray ops of
38+ [ Tuple (QualifiedName { name: Operator op }) rhs ]
39+ | op == "&&" && hasOp imports "&&" -> checkAnd imports expr lhs rhs
40+ | op == "||" && hasOp imports "||" -> checkOr imports expr lhs rhs
41+ _ -> []
42+ _ -> []
43+
44+ checkAnd :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
45+ checkAnd _ fullExpr lhs rhs
46+ -- true && x -> x
47+ | isTrue lhs = mkWarning fullExpr "true && x" (printExpr rhs) "x"
48+ -- false && x -> false
49+ | isFalse lhs = mkWarning fullExpr "false && x" "false" "false"
50+ -- x && true -> x
51+ | isTrue rhs = mkWarning fullExpr "x && true" (printExpr lhs) "x"
52+ -- x && false -> false
53+ | isFalse rhs = mkWarning fullExpr "x && false" "false" "false"
54+ | otherwise = []
55+
56+ checkOr :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
57+ checkOr _ fullExpr lhs rhs
58+ -- true || x -> true
59+ | isTrue lhs = mkWarning fullExpr "true || x" "true" "true"
60+ -- false || x -> x
61+ | isFalse lhs = mkWarning fullExpr "false || x" (printExpr rhs) "x"
62+ -- x || true -> true
63+ | isTrue rhs = mkWarning fullExpr "x || true" "true" "true"
64+ -- x || false -> x
65+ | isFalse rhs = mkWarning fullExpr "x || false" (printExpr lhs) "x"
66+ | otherwise = []
67+
68+ isTrue :: Expr Void -> Boolean
69+ isTrue (ExprBoolean _ b) = b
70+ isTrue _ = false
71+
72+ isFalse :: Expr Void -> Boolean
73+ isFalse (ExprBoolean _ b) = not b
74+ isFalse _ = false
75+
76+ mkWarning :: Expr Void -> String -> String -> String -> Array LintWarning
77+ mkWarning fullExpr pattern replacement desc =
78+ [ LintWarning
79+ { ruleId: RuleId "EvaluateBool"
80+ , message: WarningMessage $ pattern <> " can be simplified"
81+ , range: rangeOf fullExpr
82+ , severity: Warning
83+ , suggestion: Just $ Suggestion
84+ { replacement: ReplacementText replacement
85+ , description: SuggestionDescription $ "Simplifies to " <> desc
86+ , requiredImports: []
87+ }
88+ }
89+ ]
90+
lib/purelint/src/Purelint/Rules/EvaluateConst.purs → lib/purslint/src/Purslint/Rules/EvaluateConst.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.EvaluateConst where
1+module Purslint.Rules.EvaluateConst where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..))
@@ -40,6 +40,7 @@ evaluateConstRule = mkRule (RuleId "EvaluateConst") run
4040 , suggestion: Just $ Suggestion
4141 { replacement: ReplacementText x
4242 , description: SuggestionDescription "Evaluate const"
43+ , requiredImports: []
4344 }
4445 }
4546 ]
@@ -50,3 +51,4 @@ evaluateConstRule = mkRule (RuleId "EvaluateConst") run
5051 isConst imports (ExprIdent (QualifiedName { name: Ident name })) =
5152 name == "const" && hasValue imports "const"
5253 isConst _ _ = false
54+
lib/purslint/src/Purslint/Rules/EvaluateEither.pursnew file+122
@@ -0,0 +1,122 @@
1+module Purslint.Rules.EvaluateEither where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
11+import PureScript.CST.Range (rangeOf)
12+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
13+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Proper(..), Wrapped(..))
14+
15+-- | Either constant folding rules:
16+-- | - either f g (Left x) -> f x
17+-- | - either f g (Right y) -> g y
18+evaluateEitherRule :: Rule
19+evaluateEitherRule = mkRule (RuleId "EvaluateEither") run
20+ where
21+ run :: RuleContext -> Module Void -> Array LintWarning
22+ run ctx = foldMapModule visitor
23+ where
24+ imports = ctx.imports
25+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
26+
27+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
28+ checkExpr imports expr = case expr of
29+ -- Match: either f g (Left x) or either f g (Right y)
30+ ExprApp fn args
31+ | isEither imports fn ->
32+ case NEA.toArray args of
33+ [AppTerm f, AppTerm g, AppTerm eitherArg] ->
34+ checkEitherApp imports expr f g eitherArg
35+ _ -> []
36+ _ -> []
37+
38+ checkEitherApp :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
39+ checkEitherApp imports fullExpr f g eitherArg =
40+ case getLeftArg imports eitherArg of
41+ Just x ->
42+ -- either f g (Left x) -> f x
43+ let replacement = printExpr f <> " " <> wrapIfNeeded x
44+ in
45+ [ LintWarning
46+ { ruleId: RuleId "EvaluateEither"
47+ , message: WarningMessage "either f g (Left x) simplifies to f x"
48+ , range: rangeOf fullExpr
49+ , severity: Warning
50+ , suggestion: Just $ Suggestion
51+ { replacement: ReplacementText replacement
52+ , description: SuggestionDescription "Apply f directly to x"
53+ , requiredImports: []
54+ }
55+ }
56+ ]
57+ Nothing ->
58+ case getRightArg imports eitherArg of
59+ Just y ->
60+ -- either f g (Right y) -> g y
61+ let replacement = printExpr g <> " " <> wrapIfNeeded y
62+ in
63+ [ LintWarning
64+ { ruleId: RuleId "EvaluateEither"
65+ , message: WarningMessage "either f g (Right y) simplifies to g y"
66+ , range: rangeOf fullExpr
67+ , severity: Warning
68+ , suggestion: Just $ Suggestion
69+ { replacement: ReplacementText replacement
70+ , description: SuggestionDescription "Apply g directly to y"
71+ , requiredImports: []
72+ }
73+ }
74+ ]
75+ Nothing -> []
76+
77+ isEither :: ImportInfo -> Expr Void -> Boolean
78+ isEither imports (ExprIdent (QualifiedName { name: Ident "either" })) =
79+ hasValue imports "either"
80+ isEither _ _ = false
81+
82+ getLeftArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
83+ getLeftArg imports (ExprApp ctor args)
84+ | isLeft imports ctor =
85+ case NEA.toArray args of
86+ [AppTerm arg] -> Just arg
87+ _ -> Nothing
88+ getLeftArg imports (ExprParens (Wrapped { value: inner })) = getLeftArg imports inner
89+ getLeftArg _ _ = Nothing
90+
91+ getRightArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
92+ getRightArg imports (ExprApp ctor args)
93+ | isRight imports ctor =
94+ case NEA.toArray args of
95+ [AppTerm arg] -> Just arg
96+ _ -> Nothing
97+ getRightArg imports (ExprParens (Wrapped { value: inner })) = getRightArg imports inner
98+ getRightArg _ _ = Nothing
99+
100+ isLeft :: ImportInfo -> Expr Void -> Boolean
101+ isLeft imports (ExprConstructor (QualifiedName { name: Proper "Left" })) =
102+ hasValue imports "Left"
103+ isLeft _ _ = false
104+
105+ isRight :: ImportInfo -> Expr Void -> Boolean
106+ isRight imports (ExprConstructor (QualifiedName { name: Proper "Right" })) =
107+ hasValue imports "Right"
108+ isRight _ _ = false
109+
110+ wrapIfNeeded :: Expr Void -> String
111+ wrapIfNeeded e = case e of
112+ ExprIdent _ -> printExpr e
113+ ExprConstructor _ -> printExpr e
114+ ExprInt _ _ -> printExpr e
115+ ExprNumber _ _ -> printExpr e
116+ ExprString _ _ -> printExpr e
117+ ExprChar _ _ -> printExpr e
118+ ExprArray _ -> printExpr e
119+ ExprRecord _ -> printExpr e
120+ ExprParens _ -> printExpr e
121+ _ -> "(" <> printExpr e <> ")"
122+
lib/purelint/src/Purelint/Rules/EvaluateFst.purs → lib/purslint/src/Purslint/Rules/EvaluateFst.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.EvaluateFst where
1+module Purslint.Rules.EvaluateFst where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Proper(..), QualifiedName(..), Wrapped(..))
@@ -43,6 +43,7 @@ evaluateFstRule = mkRule (RuleId "EvaluateFst") run
4343 , suggestion: Just $ Suggestion
4444 { replacement: ReplacementText x
4545 , description: SuggestionDescription "Extract first element directly"
46+ , requiredImports: []
4647 }
4748 }
4849 ]
@@ -63,6 +64,7 @@ evaluateFstRule = mkRule (RuleId "EvaluateFst") run
6364 , suggestion: Just $ Suggestion
6465 { replacement: ReplacementText y
6566 , description: SuggestionDescription "Extract second element directly"
67+ , requiredImports: []
6668 }
6769 }
6870 ]
@@ -94,3 +96,4 @@ evaluateFstRule = mkRule (RuleId "EvaluateFst") run
9496 _ -> Nothing
9597 _ -> Nothing
9698 getTupleElements _ = Nothing
99+
lib/purslint/src/Purslint/Rules/FlattenCase.pursnew file+145
@@ -0,0 +1,145 @@
1+module Purslint.Rules.FlattenCase where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Maybe (Maybe(..))
8+import Data.String as String
9+import Data.Tuple (Tuple(..))
10+import Data.Void (Void)
11+import Purslint.Print (printBinder, printExpr)
12+import Purslint.Rule (Rule, RuleContext, mkRule)
13+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
14+import PureScript.CST.Range (rangeOf)
15+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
16+import PureScript.CST.Types (Binder(..), Expr(..), Guarded(..), Module, Separated(..), Where(..), Wrapped(..))
17+
18+-- | Rule: Nested case expressions with same fallback can use tuple patterns
19+-- |
20+-- | case x of
21+-- | A -> case y of
22+-- | B -> result
23+-- | _ -> fallback
24+-- | _ -> fallback
25+-- |
26+-- | becomes:
27+-- |
28+-- | case x, y of
29+-- | A, B -> result
30+-- | _, _ -> fallback
31+flattenCaseRule :: Rule
32+flattenCaseRule = mkRule (RuleId "FlattenCase") run
33+ where
34+ run :: RuleContext -> Module Void -> Array LintWarning
35+ run _ctx = foldMapModule visitor
36+ where
37+ visitor = defaultMonoidalVisitor { onExpr = checkExpr }
38+
39+ checkExpr :: Expr Void -> Array LintWarning
40+ checkExpr expr = case expr of
41+ ExprCase caseOf ->
42+ case analyzeNestedCase caseOf.head (NEA.toArray caseOf.branches) of
43+ Just { depth, fallback, arms, headExprs } | depth >= 2 ->
44+ let
45+ headStr = String.joinWith ", " headExprs
46+ armsStr = arms # map formatArm # String.joinWith "\n "
47+ fallbackPatterns = Array.replicate depth "_" # String.joinWith ", "
48+ fallbackStr = fallbackPatterns <> " -> " <> fallback
49+ replacement = "case " <> headStr <> " of\n " <> armsStr <> "\n " <> fallbackStr
50+ in
51+ [ LintWarning
52+ { ruleId: RuleId "FlattenCase"
53+ , message: WarningMessage "Nested case expressions with same fallback can be flattened"
54+ , range: rangeOf expr
55+ , severity: Hint
56+ , suggestion: Just $ Suggestion
57+ { replacement: ReplacementText replacement
58+ , description: SuggestionDescription $ "Use case with " <> show depth <> " scrutinees instead of nested cases"
59+ , requiredImports: []
60+ }
61+ }
62+ ]
63+ _ -> []
64+ _ -> []
65+
66+ -- Analyze nested case to find depth, common fallback, and arms
67+ analyzeNestedCase
68+ :: Separated (Expr Void)
69+ -> Array (Tuple (Separated (Binder Void)) (Guarded Void))
70+ -> Maybe { depth :: Int, fallback :: String, arms :: Array { patterns :: Array String, body :: String }, headExprs :: Array String }
71+ analyzeNestedCase (Separated { head: scrutinee }) branches = do
72+ -- Need exactly 2 branches: one match and one wildcard fallback
73+ case branches of
74+ [ branch1, branch2 ] -> do
75+ let fb1 = getFallbackBody branch1
76+ let fb2 = getFallbackBody branch2
77+ case fb1, fb2 of
78+ -- branch2 is the fallback (wildcard pattern)
79+ Nothing, Just fallback ->
80+ analyzeMatchBranch (printExpr scrutinee) fallback branch1
81+ -- branch1 is the fallback
82+ Just fallback, Nothing ->
83+ analyzeMatchBranch (printExpr scrutinee) fallback branch2
84+ _, _ -> Nothing
85+ _ -> Nothing
86+
87+ -- Check if a branch is a wildcard fallback, return its body
88+ getFallbackBody :: Tuple (Separated (Binder Void)) (Guarded Void) -> Maybe String
89+ getFallbackBody (Tuple (Separated { head: binder, tail: [] }) guarded) =
90+ if isWildcard binder then
91+ case guarded of
92+ Unconditional _ (Where { expr, bindings: Nothing }) -> Just (printExpr expr)
93+ _ -> Nothing
94+ else Nothing
95+ getFallbackBody _ = Nothing
96+
97+ isWildcard :: Binder Void -> Boolean
98+ isWildcard (BinderWildcard _) = true
99+ isWildcard _ = false
100+
101+ -- Analyze a match branch to find nested structure
102+ analyzeMatchBranch
103+ :: String
104+ -> String
105+ -> Tuple (Separated (Binder Void)) (Guarded Void)
106+ -> Maybe { depth :: Int, fallback :: String, arms :: Array { patterns :: Array String, body :: String }, headExprs :: Array String }
107+ analyzeMatchBranch scrutineeStr fallback (Tuple (Separated { head: pattern1, tail: [] }) guarded) =
108+ case guarded of
109+ Unconditional _ (Where { expr, bindings: Nothing }) ->
110+ let pat1Str = printBinder pattern1
111+ in case expr of
112+ -- Check for nested case with same fallback
113+ ExprCase innerCaseOf ->
114+ case analyzeNestedCase innerCaseOf.head (NEA.toArray innerCaseOf.branches) of
115+ Just nested | nested.fallback == fallback ->
116+ -- Found nested case with same fallback - extend it
117+ Just
118+ { depth: nested.depth + 1
119+ , fallback
120+ , arms: nested.arms # map \arm -> arm { patterns = Array.cons pat1Str arm.patterns }
121+ , headExprs: Array.cons scrutineeStr nested.headExprs
122+ }
123+ _ ->
124+ -- Inner case has different fallback or structure - this is a leaf
125+ Just
126+ { depth: 1
127+ , fallback
128+ , arms: [ { patterns: [pat1Str], body: printExpr expr } ]
129+ , headExprs: [scrutineeStr]
130+ }
131+ -- Not a nested case - this is a leaf arm
132+ _ ->
133+ Just
134+ { depth: 1
135+ , fallback
136+ , arms: [ { patterns: [pat1Str], body: printExpr expr } ]
137+ , headExprs: [scrutineeStr]
138+ }
139+ _ -> Nothing
140+ analyzeMatchBranch _ _ _ = Nothing
141+
142+ formatArm :: { patterns :: Array String, body :: String } -> String
143+ formatArm { patterns, body } =
144+ String.joinWith ", " patterns <> " -> " <> body
145+
lib/purelint/src/Purelint/Rules/FunctorLaw.purs → lib/purslint/src/Purslint/Rules/FunctorLaw.pursrenamed+33−28
@@ -1,4 +1,4 @@
1−module Purelint.Rules.FunctorLaw where
1+module Purslint.Rules.FunctorLaw where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
@@ -28,29 +28,32 @@ functorLawRule = mkRule (RuleId "FunctorLaw") run
2828 checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
2929 checkExpr imports expr = case expr of
3030 -- Match: map f (map g x)
31− ExprApp fnExpr args | isMapLike imports fnExpr ->
32− case NEA.toArray args of
33− [AppTerm fExpr, AppTerm innerExpr] ->
34− case unwrapParens innerExpr of
35− ExprApp innerFn innerArgs | isMapLike imports innerFn ->
36− case NEA.toArray innerArgs of
37− [AppTerm gExpr, AppTerm xExpr] ->
38− mkWarning expr fExpr gExpr xExpr "map"
39− _ -> []
40− _ -> []
41− _ -> []
31+ ExprApp fnExpr args
32+ | isMapLike imports fnExpr
33+ , [AppTerm fExpr, AppTerm innerExpr] <- NEA.toArray args ->
34+ checkInnerMap imports expr fExpr (unwrapParens innerExpr)
4235 -- Match: f <$> (g <$> x)
43− ExprOp fExpr ops | hasOp imports "<$>" ->
44− case NEA.toArray ops of
45− [Tuple qn innerExpr] | isMapOp imports qn ->
46− case unwrapParens innerExpr of
47− ExprOp gExpr innerOps ->
48− case NEA.toArray innerOps of
49− [Tuple innerQn xExpr] | isMapOp imports innerQn ->
50− mkWarning expr fExpr gExpr xExpr "<$>"
51− _ -> []
52− _ -> []
53− _ -> []
36+ ExprOp fExpr ops
37+ | hasOp imports "<$>"
38+ , [Tuple qn innerExpr] <- NEA.toArray ops
39+ , isMapOp imports qn ->
40+ checkInnerOp imports expr fExpr (unwrapParens innerExpr)
41+ _ -> []
42+
43+ checkInnerMap :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
44+ checkInnerMap imports fullExpr fExpr inner = case inner of
45+ ExprApp innerFn innerArgs
46+ | isMapLike imports innerFn
47+ , [AppTerm gExpr, AppTerm xExpr] <- NEA.toArray innerArgs ->
48+ mkWarning fullExpr fExpr gExpr xExpr "map"
49+ _ -> []
50+
51+ checkInnerOp :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
52+ checkInnerOp imports fullExpr fExpr inner = case inner of
53+ ExprOp gExpr innerOps
54+ | [Tuple innerQn xExpr] <- NEA.toArray innerOps
55+ , isMapOp imports innerQn ->
56+ mkWarning fullExpr fExpr gExpr xExpr "<$>"
5457 _ -> []
5558
5659 mkWarning :: Expr Void -> Expr Void -> Expr Void -> Expr Void -> String -> Array LintWarning
@@ -71,6 +74,7 @@ functorLawRule = mkRule (RuleId "FunctorLaw") run
7174 , suggestion: Just $ Suggestion
7275 { replacement: ReplacementText replacement
7376 , description: SuggestionDescription $ style <> " f (" <> style <> " g x) can be fused to " <> style <> " (f <<< g) x"
77+ , requiredImports: []
7478 }
7579 }
7680 ]
@@ -81,9 +85,10 @@ functorLawRule = mkRule (RuleId "FunctorLaw") run
8185
8286 isMapLike :: ImportInfo -> Expr Void -> Boolean
8387 isMapLike imports (ExprIdent (QualifiedName { name: Ident name })) =
84− (name == "map" && hasValue imports "map") || (name == "fmap" && hasValue imports "fmap")
88+ name == "map" && hasValue imports "map"
8589 isMapLike _ _ = false
8690
8791 isMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
8892 isMapOp imports (QualifiedName { name: Operator op }) =
8993 op == "<$>" && hasOp imports "<$>"
94+
lib/purslint/src/Purslint/Rules/LetToDo.pursnew file+160
@@ -0,0 +1,160 @@
1+module Purslint.Rules.LetToDo where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Foldable (foldMap)
8+import Data.Maybe (Maybe(..))
9+import Data.Monoid (power)
10+import Data.String (joinWith) as String
11+import Data.String as Str
12+import Data.Tuple.Nested ((/\))
13+import Data.Void (Void, absurd)
14+import Purslint.Rule (Rule, RuleContext, mkRule)
15+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
16+import PureScript.CST.Print (printToken)
17+import PureScript.CST.Range (rangeOf, tokensOf)
18+import PureScript.CST.Range.TokenList as TokenList
19+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
20+import PureScript.CST.Types (Comment(..), Declaration(..), Expr(..), Guarded(..), LetBinding, LineFeed(..), Module, SourceToken, Where(..))
21+
22+-- | Rule: let x = y in z -> do let x = y; z
23+-- | Suggests replacing let...in expressions with do notation
24+letToDoRule :: Rule
25+letToDoRule = mkRule (RuleId "LetToDo") run
26+ where
27+ run :: RuleContext -> Module Void -> Array LintWarning
28+ run _ctx = foldMapModule visitor
29+ where
30+ visitor = defaultMonoidalVisitor
31+ { onDecl = checkDecl
32+ , onExpr = checkExpr
33+ }
34+
35+ checkDecl :: Declaration Void -> Array LintWarning
36+ checkDecl = case _ of
37+ -- Top-level let...in: can put do on the = line
38+ DeclValue { guarded: Unconditional eqToken (Where { expr: ExprLet letIn, bindings: Nothing }) } ->
39+ mkWarningTopLevel eqToken (ExprLet letIn)
40+ _ -> []
41+
42+ checkExpr :: Expr Void -> Array LintWarning
43+ checkExpr = case _ of
44+ -- Match any nested let...in expression
45+ e@(ExprLet _) -> mkWarningNested e
46+ _ -> []
47+
48+ mkWarningTopLevel :: SourceToken -> Expr Void -> Array LintWarning
49+ mkWarningTopLevel eqToken expr = case expr of
50+ ExprLet letIn ->
51+ let
52+ letCol = (rangeOf expr).start.column
53+ doIndent = power " " letCol
54+ letIndent = doIndent <> " "
55+
56+ bodyText = printTokensPreserveNewlines letCol letIn.body
57+ bindings = NEA.toArray letIn.bindings # map (printBindingTokens (letCol + 2))
58+ bindingsText = String.joinWith ("\n" <> letIndent) bindings
59+
60+ replacement = "= do\n" <> doIndent <> "let\n" <> letIndent <> bindingsText <> "\n" <> doIndent <> bodyText
61+ fullRange = { start: eqToken.range.start, end: (rangeOf expr).end }
62+ in
63+ [ LintWarning
64+ { ruleId: RuleId "LetToDo"
65+ , message: WarningMessage "let...in can be written using do notation"
66+ , range: fullRange
67+ , severity: Refactor
68+ , suggestion: Just $ Suggestion
69+ { replacement: ReplacementText replacement
70+ , description: SuggestionDescription "Convert to do notation"
71+ , requiredImports: []
72+ }
73+ }
74+ ]
75+ _ -> []
76+
77+ mkWarningNested :: Expr Void -> Array LintWarning
78+ mkWarningNested expr = case expr of
79+ ExprLet letIn ->
80+ let
81+ letCol = (rangeOf expr).start.column
82+ doIndent = power " " letCol
83+ letIndent = doIndent <> " "
84+
85+ bodyText = printTokensPreserveNewlines letCol letIn.body
86+ bindings = NEA.toArray letIn.bindings # map (printBindingTokens (letCol + 2))
87+ bindingsText = String.joinWith ("\n" <> letIndent) bindings
88+
89+ -- Check if let is on its own line (has Line in leading comments)
90+ hasNewlineBefore = Array.any isLine letIn.keyword.leadingComments
91+
92+ exprRange = rangeOf expr
93+ letLine = exprRange.start.line
94+
95+ -- If let is on its own line, extend range back to end of previous line
96+ -- and put " do" there, then newline + the do block
97+ range /\ replacement =
98+ if hasNewlineBefore && letLine > 0 then
99+ -- Start range at end of previous line (column 9999 will be clamped to EOL)
100+ { start: { line: letLine - 1, column: 9999 }, end: exprRange.end }
101+ /\ (" do\n" <> doIndent <> "let\n" <> letIndent <> bindingsText <> "\n" <> doIndent <> bodyText)
102+ else
103+ -- let is inline or on first line, just replace with do block
104+ exprRange
105+ /\ ("do\n" <> doIndent <> "let\n" <> letIndent <> bindingsText <> "\n" <> doIndent <> bodyText)
106+ in
107+ [ LintWarning
108+ { ruleId: RuleId "LetToDo"
109+ , message: WarningMessage "let...in can be written using do notation"
110+ , range
111+ , severity: Refactor
112+ , suggestion: Just $ Suggestion
113+ { replacement: ReplacementText replacement
114+ , description: SuggestionDescription "Convert to do notation"
115+ , requiredImports: []
116+ }
117+ }
118+ ]
119+ _ -> []
120+
121+ isLine :: Comment LineFeed -> Boolean
122+ isLine (Line _ _) = true
123+ isLine _ = false
124+
125+ printTokensPreserveNewlines :: Int -> Expr Void -> String
126+ printTokensPreserveNewlines targetCol e =
127+ let bodyCol = (rangeOf e).start.column
128+ shiftAmount = targetCol - bodyCol
129+ in Str.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf e))
130+
131+ printBindingTokens :: Int -> LetBinding Void -> String
132+ printBindingTokens targetCol b =
133+ let bindingCol = (rangeOf b).start.column
134+ shiftAmount = (targetCol + 2) - bindingCol
135+ in Str.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf b))
136+
137+ printSourceToken :: Int -> SourceToken -> String
138+ printSourceToken shiftAmount tok =
139+ foldMap (printLeadingComment shiftAmount) tok.leadingComments
140+ <> printToken tok.value
141+ <> foldMap printTrailingComment tok.trailingComments
142+
143+ printLeadingComment :: Int -> Comment LineFeed -> String
144+ printLeadingComment shiftAmount = case _ of
145+ Comment str -> str
146+ Space n -> power " " (max 0 (n + shiftAmount))
147+ Line lf n -> foldMap printLineFeed (Array.replicate n lf)
148+
149+ printLineFeed :: LineFeed -> String
150+ printLineFeed = case _ of
151+ LF -> "\n"
152+ CRLF -> "\r\n"
153+
154+ printTrailingComment :: Comment Void -> String
155+ printTrailingComment = case _ of
156+ Comment str -> str
157+ Space n -> power " " n
158+ Line v _ -> absurd v
159+
160+
lib/purslint/src/Purslint/Rules/LetToWhere.pursnew file+122
@@ -0,0 +1,122 @@
1+module Purslint.Rules.LetToWhere where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Foldable (foldMap)
8+import Data.Maybe (Maybe(..))
9+import Data.Monoid (power)
10+import Data.String (joinWith) as String
11+import Data.String as Str
12+import Data.Tuple.Nested ((/\))
13+import Purslint.Rule (Rule, RuleContext, mkRule)
14+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
15+import PureScript.CST.Print (printToken)
16+import PureScript.CST.Range (class RangeOf, rangeOf, tokensOf)
17+import PureScript.CST.Range.TokenList as TokenList
18+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
19+import PureScript.CST.Types (Comment(..), Declaration(..), Expr(..), Guarded(..), LetBinding, LineFeed(..), Module, SourceToken, Where(..))
20+
21+-- | Rule: let x = y in z -> z where x = y
22+-- | Suggests replacing let...in expressions with where clauses
23+-- | When there's an existing where clause, appends to it instead of creating a new one
24+letToWhereRule :: Rule
25+letToWhereRule = mkRule (RuleId "LetToWhere") run
26+ where
27+ run :: RuleContext -> Module Void -> Array LintWarning
28+ run _ctx = foldMapModule visitor
29+ where
30+ visitor = defaultMonoidalVisitor { onDecl = checkDecl }
31+
32+ checkDecl :: Declaration Void -> Array LintWarning
33+ checkDecl = case _ of
34+ -- No existing where clause - create one
35+ DeclValue { guarded: Unconditional _ wh@(Where { expr: ExprLet letIn, bindings: Nothing }) } ->
36+ mkWarning wh (ExprLet letIn) Nothing
37+ -- Existing where clause - append to it
38+ DeclValue { guarded: Unconditional _ wh@(Where { expr: ExprLet letIn, bindings: Just existingBindings }) } ->
39+ mkWarning wh (ExprLet letIn) (Just existingBindings)
40+ _ -> []
41+
42+ mkWarning :: Where Void -> Expr Void -> Maybe _ -> Array LintWarning
43+ mkWarning wh expr existingBindings = case expr of
44+ ExprLet letIn ->
45+ let
46+ -- Target indent is where the let expression starts
47+ targetCol = (rangeOf expr).start.column
48+ targetIndent = power " " targetCol
49+ -- Body's current indent (where the body expression starts)
50+ bodyCol = (rangeOf letIn.body).start.column
51+ -- Shift amount: how much to adjust indentation
52+ shiftAmount = targetCol - bodyCol
53+
54+ bodyText = printTokensPreserveNewlines shiftAmount letIn.body
55+ newBindings = NEA.toArray letIn.bindings # map (printBindingTokens targetCol)
56+ newBindingsText = String.joinWith ("\n" <> targetIndent) newBindings
57+
58+ replacement /\ range = case existingBindings of
59+ Nothing ->
60+ -- No existing where - just replace the let expr
61+ (bodyText <> "\n" <> targetIndent <> "where\n" <> targetIndent <> newBindingsText) /\ rangeOf expr
62+ Just (_ /\ existingArr) ->
63+ -- Has existing where - replace from let to end of where bindings
64+ let
65+ existingTexts = NEA.toArray existingArr # map (printBindingTokens targetCol)
66+ allBindingsText = String.joinWith ("\n" <> targetIndent) (newBindings <> existingTexts)
67+ lastBinding = NEA.last existingArr
68+ fullRange = { start: (rangeOf expr).start, end: (rangeOf lastBinding).end }
69+ in
70+ (bodyText <> "\n" <> targetIndent <> "where\n" <> targetIndent <> allBindingsText) /\ fullRange
71+ in
72+ [ LintWarning
73+ { ruleId: RuleId "LetToWhere"
74+ , message: WarningMessage "Use where clause instead of let...in"
75+ , range: range
76+ , severity: Refactor
77+ , suggestion: Just $ Suggestion
78+ { replacement: ReplacementText replacement
79+ , description: SuggestionDescription "Convert to where clause"
80+ , requiredImports: []
81+ }
82+ }
83+ ]
84+ _ -> []
85+
86+ -- | Print tokens preserving newlines and adjusting indentation by shiftAmount
87+ printTokensPreserveNewlines :: Int -> Expr Void -> String
88+ printTokensPreserveNewlines shiftAmount e =
89+ Str.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf e))
90+
91+ printBindingTokens :: Int -> LetBinding Void -> String
92+ printBindingTokens targetCol b =
93+ let bindingCol = (rangeOf b).start.column
94+ shiftAmount = targetCol - bindingCol
95+ in Str.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf b))
96+
97+ -- | Print a source token, preserving newlines and adjusting indentation
98+ printSourceToken :: Int -> SourceToken -> String
99+ printSourceToken shiftAmount tok =
100+ foldMap (printLeadingComment shiftAmount) tok.leadingComments
101+ <> printToken tok.value
102+ <> foldMap printTrailingComment tok.trailingComments
103+
104+ -- | Print leading comment (has LineFeed), preserving newlines and adjusting indentation
105+ printLeadingComment :: Int -> Comment LineFeed -> String
106+ printLeadingComment shiftAmount = case _ of
107+ Comment str -> str
108+ Space n -> power " " (max 0 (n + shiftAmount))
109+ Line lf n -> foldMap printLineFeed (Array.replicate n lf)
110+
111+ printLineFeed :: LineFeed -> String
112+ printLineFeed = case _ of
113+ LF -> "\n"
114+ CRLF -> "\r\n"
115+
116+ -- | Print trailing comment (has Void, so Line is impossible)
117+ printTrailingComment :: Comment Void -> String
118+ printTrailingComment = case _ of
119+ Comment str -> str
120+ Space n -> power " " n
121+ Line v _ -> absurd v
122+
lib/purelint/src/Purelint/Rules/MapFusion.purs → lib/purslint/src/Purslint/Rules/MapFusion.pursrenamed+57−55
@@ -1,4 +1,4 @@
1−module Purelint.Rules.MapFusion where
1+module Purslint.Rules.MapFusion where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
@@ -29,18 +29,14 @@ mapFusionRule = mkRule (RuleId "MapFusion") run
2929 checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
3030 checkExpr imports expr = case expr of
3131 -- Match: map f (map g x)
32− ExprApp fnExpr args ->
33− case fnExpr of
34− ExprIdent qn | isMap imports qn ->
35− -- Check if we have at least 2 args and the second is map g x
36− case NEA.toArray args of
37− [AppTerm fExpr, AppTerm innerExpr] ->
38− checkInnerMap imports expr fExpr (unwrapParens innerExpr)
39− _ -> []
40− _ -> []
32+ ExprApp (ExprIdent qn) args
33+ | isMap imports qn
34+ , [AppTerm fExpr, AppTerm innerExpr] <- NEA.toArray args ->
35+ checkInnerMap imports expr fExpr (unwrapParens innerExpr)
4136 -- Match: f <$> (g <$> x) or f <$> g <$> x
42− ExprOp fExpr ops | NEA.length ops >= 1 ->
43− checkOpChain imports expr fExpr (NEA.toArray ops)
37+ ExprOp fExpr ops
38+ | NEA.length ops >= 1 ->
39+ checkOpChain imports expr fExpr (NEA.toArray ops)
4440 _ -> []
4541
4642 unwrapParens :: Expr Void -> Expr Void
@@ -51,17 +47,15 @@ mapFusionRule = mkRule (RuleId "MapFusion") run
5147 checkInnerMap :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
5248 checkInnerMap imports fullExpr fExpr innerExpr =
5349 case innerExpr of
54− ExprApp mapFn innerArgs | isMapApp imports mapFn ->
55− case NEA.toArray innerArgs of
56− [AppTerm gExpr, AppTerm xExpr] ->
50+ ExprApp mapFn innerArgs
51+ | isMapApp imports mapFn
52+ , [AppTerm gExpr, AppTerm xExpr] <- NEA.toArray innerArgs ->
5753 mkFusionWarning fullExpr fExpr gExpr xExpr "map"
58− _ -> []
5954 -- Also check: map f (g <$> x)
60− ExprOp gExpr innerOps ->
61− case NEA.toArray innerOps of
62− [Tuple qn xExpr] | isMapOp imports qn ->
55+ ExprOp gExpr innerOps
56+ | [Tuple qn xExpr] <- NEA.toArray innerOps
57+ , isMapOp imports qn ->
6358 mkFusionWarning fullExpr fExpr gExpr xExpr "map"
64− _ -> []
6559 _ -> []
6660
6761 -- Check operator form: f <$> (g <$> x) or f <$> g <$> x
@@ -69,40 +63,46 @@ mapFusionRule = mkRule (RuleId "MapFusion") run
6963 checkOpChain imports fullExpr fExpr ops =
7064 case ops of
7165 -- f <$> (g <$> x) - inner is parenthesized
72− [Tuple op1 innerExpr] | isMapOp imports op1 ->
73− case unwrapParens innerExpr of
74− ExprOp gExpr innerOps ->
75− case NEA.toArray innerOps of
76− [Tuple op2 xExpr] | isMapOp imports op2 ->
77− mkFusionWarning fullExpr fExpr gExpr xExpr "<$>"
78− _ -> []
79− ExprApp mapFn innerArgs | isMapApp imports mapFn ->
80− case NEA.toArray innerArgs of
81− [AppTerm gExpr, AppTerm xExpr] ->
82− mkFusionWarning fullExpr fExpr gExpr xExpr "<$>"
83− _ -> []
84− _ -> []
66+ [Tuple op1 innerExpr]
67+ | isMapOp imports op1 ->
68+ checkInnerOpExpr imports fullExpr fExpr (unwrapParens innerExpr)
8569 -- x <#> g <#> f - flipped map chain
86− [Tuple op1 gExpr, Tuple op2 fExpr'] | isFlippedMapOp imports op1 && isFlippedMapOp imports op2 ->
87− let
88− x = printExpr fExpr -- fExpr is actually x in this case
89− g = printExpr gExpr
90− f = printExpr fExpr'
91− replacement = x <> " <#> (" <> g <> " >>> " <> f <> ")"
92− in
93− [ LintWarning
94− { ruleId: RuleId "MapFusion"
95− , message: WarningMessage "Fuse nested maps using composition"
96− , range: rangeOf fullExpr
97− , severity: Hint
98− , suggestion: Just $ Suggestion
99− { replacement: ReplacementText replacement
100− , description: SuggestionDescription "x <#> g <#> f can be replaced with x <#> (g >>> f)"
70+ [Tuple op1 gExpr, Tuple op2 fExpr']
71+ | isFlippedMapOp imports op1
72+ , isFlippedMapOp imports op2 ->
73+ let
74+ x = printExpr fExpr -- fExpr is actually x in this case
75+ g = printExpr gExpr
76+ f = printExpr fExpr'
77+ replacement = x <> " <#> (" <> g <> " >>> " <> f <> ")"
78+ in
79+ [ LintWarning
80+ { ruleId: RuleId "MapFusion"
81+ , message: WarningMessage "Fuse nested maps using composition"
82+ , range: rangeOf fullExpr
83+ , severity: Hint
84+ , suggestion: Just $ Suggestion
85+ { replacement: ReplacementText replacement
86+ , description: SuggestionDescription "x <#> g <#> f can be replaced with x <#> (g >>> f)"
87+ , requiredImports: []
88+ }
10189 }
102− }
103− ]
90+ ]
10491 _ -> []
10592
93+ -- Helper: check the inner expression of f <$> (...)
94+ checkInnerOpExpr :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
95+ checkInnerOpExpr imports fullExpr fExpr inner = case inner of
96+ ExprOp gExpr innerOps
97+ | [Tuple op2 xExpr] <- NEA.toArray innerOps
98+ , isMapOp imports op2 ->
99+ mkFusionWarning fullExpr fExpr gExpr xExpr "<$>"
100+ ExprApp mapFn innerArgs
101+ | isMapApp imports mapFn
102+ , [AppTerm gExpr, AppTerm xExpr] <- NEA.toArray innerArgs ->
103+ mkFusionWarning fullExpr fExpr gExpr xExpr "<$>"
104+ _ -> []
105+
106106 mkFusionWarning :: Expr Void -> Expr Void -> Expr Void -> Expr Void -> String -> Array LintWarning
107107 mkFusionWarning fullExpr fExpr gExpr xExpr style =
108108 let
@@ -121,6 +121,7 @@ mapFusionRule = mkRule (RuleId "MapFusion") run
121121 , suggestion: Just $ Suggestion
122122 { replacement: ReplacementText replacement
123123 , description: SuggestionDescription "Nested maps can be fused with composition"
124+ , requiredImports: []
124125 }
125126 }
126127 ]
@@ -131,7 +132,7 @@ mapFusionRule = mkRule (RuleId "MapFusion") run
131132
132133 isMap :: ImportInfo -> QualifiedName Ident -> Boolean
133134 isMap imports (QualifiedName { name: Ident name }) =
134− (name == "map" && hasValue imports "map") || (name == "fmap" && hasValue imports "fmap")
135+ name == "map" && hasValue imports "map"
135136
136137 isMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
137138 isMapOp imports (QualifiedName { name: Operator op }) =
@@ -140,3 +141,4 @@ mapFusionRule = mkRule (RuleId "MapFusion") run
140141 isFlippedMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
141142 isFlippedMapOp imports (QualifiedName { name: Operator op }) =
142143 (op == "<#>" || op == "<##>") && hasOp imports "<#>"
144+
lib/purslint/src/Purslint/Rules/MapIdentity.pursnew file+100
@@ -0,0 +1,100 @@
1+module Purslint.Rules.MapIdentity where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Maybe (Maybe(..))
8+import Data.Tuple (Tuple(..))
9+import Data.Void (Void)
10+import Purslint.Imports (ImportInfo, hasValue, hasOp)
11+import Purslint.Print (printExpr)
12+import Purslint.Rule (Rule, RuleContext, mkRule)
13+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
14+import PureScript.CST.Range (rangeOf)
15+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
16+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
17+
18+-- | Rule: map identity x -> x
19+-- | Also: identity <$> x -> x, x <#> identity -> x
20+mapIdentityRule :: Rule
21+mapIdentityRule = run # mkRule (RuleId "MapIdentity")
22+ where
23+ run :: RuleContext -> Module Void -> Array LintWarning
24+ run ctx = foldMapModule visitor
25+ where
26+ imports = ctx.imports
27+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
28+
29+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
30+ checkExpr imports expr = case expr of
31+ -- Match: map identity x
32+ ExprApp (ExprIdent qn) args
33+ | qn # isMap imports
34+ , [ AppTerm idExpr, AppTerm xExpr ] <- NEA.toArray args
35+ , idExpr # isIdentity imports ->
36+ [ LintWarning
37+ { ruleId: RuleId "MapIdentity"
38+ , message: WarningMessage "map identity is redundant"
39+ , range: rangeOf expr
40+ , severity: Warning
41+ , suggestion: Just $ Suggestion
42+ { replacement: ReplacementText (printExpr xExpr)
43+ , description: SuggestionDescription "map identity x can be simplified to x"
44+ , requiredImports: []
45+ }
46+ }
47+ ]
48+ -- Match: identity <$> x
49+ ExprOp lhs ops
50+ | isIdentity imports lhs
51+ , [ Tuple qn xExpr ] <- NEA.toArray ops
52+ , isMapOp imports qn ->
53+ [ LintWarning
54+ { ruleId: RuleId "MapIdentity"
55+ , message: WarningMessage "identity <$> is redundant"
56+ , range: rangeOf expr
57+ , severity: Warning
58+ , suggestion: Just $ Suggestion
59+ { replacement: ReplacementText (printExpr xExpr)
60+ , description: SuggestionDescription "identity <$> x can be simplified to x"
61+ , requiredImports: []
62+ }
63+ }
64+ ]
65+ -- Match: x <#> identity
66+ ExprOp lhs ops
67+ | [ Tuple qn idExpr ] <- NEA.toArray ops
68+ , isFlippedMapOp imports qn
69+ , isIdentity imports idExpr ->
70+ [ LintWarning
71+ { ruleId: RuleId "MapIdentity"
72+ , message: WarningMessage "<#> identity is redundant"
73+ , range: rangeOf expr
74+ , severity: Warning
75+ , suggestion: Just $ Suggestion
76+ { replacement: ReplacementText (printExpr lhs)
77+ , description: SuggestionDescription "x <#> identity can be simplified to x"
78+ , requiredImports: []
79+ }
80+ }
81+ ]
82+ _ -> []
83+
84+ isMap :: ImportInfo -> QualifiedName Ident -> Boolean
85+ isMap imports (QualifiedName { name: Ident name }) =
86+ name == "map" && hasValue imports "map"
87+
88+ isMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
89+ isMapOp imports (QualifiedName { name: Operator op }) =
90+ (op == "<$>" || op == "<$$>") && hasOp imports "<$>"
91+
92+ isFlippedMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
93+ isFlippedMapOp imports (QualifiedName { name: Operator op }) =
94+ (op == "<#>" || op == "<##>") && hasOp imports "<#>"
95+
96+ isIdentity :: ImportInfo -> Expr Void -> Boolean
97+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
98+ (name == "identity" && hasValue imports "identity") || (name == "id" && hasValue imports "id")
99+ isIdentity _ _ = false
100+
lib/purslint/src/Purslint/Rules/MonadLaw.pursnew file+100
@@ -0,0 +1,100 @@
1+module Purslint.Rules.MonadLaw where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple (Tuple(..))
8+import Purslint.Imports (ImportInfo, hasValue, hasOp)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
15+
16+-- | Monad law rules:
17+-- | - m >>= pure -> m (right identity, covered by RedundantBind)
18+-- | - pure a >>= f -> f a (left identity, covered by RedundantBind)
19+-- | - f =<< pure a -> f a (left identity, flipped)
20+-- | - pure =<< m -> m (right identity, flipped)
21+monadLawRule :: Rule
22+monadLawRule = mkRule (RuleId "MonadLaw") run
23+ where
24+ run :: RuleContext -> Module Void -> Array LintWarning
25+ run ctx = foldMapModule visitor
26+ where
27+ imports = ctx.imports
28+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
29+
30+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
31+ checkExpr imports expr = case expr of
32+ -- Match: f =<< pure a
33+ ExprOp lhs ops ->
34+ case NEA.toArray ops of
35+ [Tuple (QualifiedName { name: Operator op }) rhs]
36+ | op == "=<<" && hasOp imports ">>=" -> checkFlippedBind imports expr lhs rhs
37+ _ -> []
38+ _ -> []
39+
40+ checkFlippedBind :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
41+ checkFlippedBind imports fullExpr f m =
42+ -- f =<< pure a -> f a
43+ case getPureArg imports m of
44+ Just arg ->
45+ let replacement = printExpr f <> " " <> wrapIfNeeded arg
46+ in
47+ [ LintWarning
48+ { ruleId: RuleId "MonadLaw"
49+ , message: WarningMessage "Monad law: f =<< pure a simplifies to f a"
50+ , range: rangeOf fullExpr
51+ , severity: Warning
52+ , suggestion: Just $ Suggestion
53+ { replacement: ReplacementText replacement
54+ , description: SuggestionDescription "Apply f directly to a (left identity)"
55+ , requiredImports: []
56+ }
57+ }
58+ ]
59+ Nothing ->
60+ -- pure =<< m -> m
61+ if isPure imports f then
62+ [ LintWarning
63+ { ruleId: RuleId "MonadLaw"
64+ , message: WarningMessage "Monad law: pure =<< m simplifies to m"
65+ , range: rangeOf fullExpr
66+ , severity: Warning
67+ , suggestion: Just $ Suggestion
68+ { replacement: ReplacementText (printExpr m)
69+ , description: SuggestionDescription "pure =<< m is just m (right identity)"
70+ , requiredImports: []
71+ }
72+ }
73+ ]
74+ else []
75+
76+ isPure :: ImportInfo -> Expr Void -> Boolean
77+ isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
78+ (name == "pure" && hasValue imports "pure") || (name == "return" && hasValue imports "pure")
79+ isPure _ _ = false
80+
81+ getPureArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
82+ getPureArg imports (ExprApp fn args)
83+ | isPure imports fn =
84+ case NEA.toArray args of
85+ [AppTerm arg] -> Just arg
86+ _ -> Nothing
87+ getPureArg _ _ = Nothing
88+
89+ wrapIfNeeded :: Expr Void -> String
90+ wrapIfNeeded e = case e of
91+ ExprIdent _ -> printExpr e
92+ ExprInt _ _ -> printExpr e
93+ ExprNumber _ _ -> printExpr e
94+ ExprString _ _ -> printExpr e
95+ ExprChar _ _ -> printExpr e
96+ ExprArray _ -> printExpr e
97+ ExprRecord _ -> printExpr e
98+ ExprParens _ -> printExpr e
99+ _ -> "(" <> printExpr e <> ")"
100+
lib/purelint/src/Purelint/Rules/MonoidIdentity.purs → lib/purslint/src/Purslint/Rules/MonoidIdentity.pursrenamed+8−5
@@ -1,4 +1,4 @@
1−module Purelint.Rules.MonoidIdentity where
1+module Purslint.Rules.MonoidIdentity where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
@@ -42,6 +42,7 @@ monoidIdentityRule = mkRule (RuleId "MonoidIdentity") run
4242 , suggestion: Just $ Suggestion
4343 { replacement: ReplacementText rhsText
4444 , description: SuggestionDescription "mempty <> x can be simplified to x (Monoid left identity)"
45+ , requiredImports: []
4546 }
4647 }
4748 ]
@@ -56,6 +57,7 @@ monoidIdentityRule = mkRule (RuleId "MonoidIdentity") run
5657 , suggestion: Just $ Suggestion
5758 { replacement: ReplacementText lhsText
5859 , description: SuggestionDescription "x <> mempty can be simplified to x (Monoid right identity)"
60+ , requiredImports: []
5961 }
6062 }
6163 ]
@@ -67,3 +69,4 @@ monoidIdentityRule = mkRule (RuleId "MonoidIdentity") run
6769 isMempty imports (ExprIdent (QualifiedName { name: Ident name })) =
6870 name == "mempty" && hasValue imports "mempty"
6971 isMempty _ _ = false
72+
lib/purelint/src/Purelint/Rules/NotEqual.purs → lib/purslint/src/Purslint/Rules/NotEqual.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.NotEqual where
1+module Purslint.Rules.NotEqual where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
8−import Purelint.Imports (ImportInfo, hasValue, hasOp)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue, hasOp)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
@@ -64,6 +64,7 @@ notEqualRule = mkRule (RuleId "NotEqual") run
6464 , suggestion: Just $ Suggestion
6565 { replacement: ReplacementText replacement
6666 , description: SuggestionDescription msg
67+ , requiredImports: []
6768 }
6869 }
6970 ]
@@ -80,3 +81,4 @@ notEqualRule = mkRule (RuleId "NotEqual") run
8081
8182 isNot :: ImportInfo -> QualifiedName Ident -> Boolean
8283 isNot imports (QualifiedName { name: Ident name }) = name == "not" && hasValue imports "not"
84+
lib/purelint/src/Purelint/Rules/NothingBind.purs → lib/purslint/src/Purslint/Rules/NothingBind.pursrenamed+7−4
@@ -1,4 +1,4 @@
1−module Purelint.Rules.NothingBind where
1+module Purslint.Rules.NothingBind where
22
33import Prelude
44
@@ -6,9 +6,9 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasOp)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasOp)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (Expr(..), Ident(..), Module, Operator(..), Proper(..), QualifiedName(..), Wrapped(..))
@@ -40,6 +40,7 @@ nothingBindRule = mkRule (RuleId "NothingBind") run
4040 , suggestion: Just $ Suggestion
4141 { replacement: ReplacementText "Nothing"
4242 , description: SuggestionDescription "Simplify Nothing >>= f to Nothing"
43+ , requiredImports: []
4344 }
4445 }
4546 ]
@@ -54,6 +55,7 @@ nothingBindRule = mkRule (RuleId "NothingBind") run
5455 , suggestion: Just $ Suggestion
5556 { replacement: ReplacementText "Nothing"
5657 , description: SuggestionDescription "Simplify f =<< Nothing to Nothing"
58+ , requiredImports: []
5759 }
5860 }
5961 ]
@@ -69,3 +71,4 @@ nothingBindRule = mkRule (RuleId "NothingBind") run
6971 isNothing (ExprIdent (QualifiedName { name: Ident "Nothing" })) = true
7072 isNothing (ExprConstructor (QualifiedName { name: Proper "Nothing" })) = true
7173 isNothing _ = false
74+
lib/purelint/src/Purelint/Rules/RedundantBind.purs → lib/purslint/src/Purslint/Rules/RedundantBind.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.RedundantBind where
1+module Purslint.Rules.RedundantBind where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
8−import Purelint.Imports (ImportInfo, hasValue, hasOp)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue, hasOp)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
@@ -50,6 +50,7 @@ redundantBindRule = mkRule (RuleId "RedundantBind") run
5050 , suggestion: Just $ Suggestion
5151 { replacement: ReplacementText lhsText
5252 , description: SuggestionDescription "x >>= pure can be simplified to x"
53+ , requiredImports: []
5354 }
5455 }
5556 ]
@@ -66,6 +67,7 @@ redundantBindRule = mkRule (RuleId "RedundantBind") run
6667 , suggestion: Just $ Suggestion
6768 { replacement: ReplacementText replacement
6869 , description: SuggestionDescription "Apply f directly to x"
70+ , requiredImports: []
6971 }
7072 }
7173 ]
@@ -83,3 +85,4 @@ redundantBindRule = mkRule (RuleId "RedundantBind") run
8385 [AppTerm arg] -> Just arg
8486 _ -> Nothing
8587 getPureArg _ _ = Nothing
88+
lib/purelint/src/Purelint/Rules/RedundantFlip.purs → lib/purslint/src/Purslint/Rules/RedundantFlip.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.RedundantFlip where
1+module Purslint.Rules.RedundantFlip where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -41,6 +41,7 @@ redundantFlipRule = mkRule (RuleId "RedundantFlip") run
4141 , suggestion: Just $ Suggestion
4242 { replacement: ReplacementText fText
4343 , description: SuggestionDescription "flip (flip f) can be simplified to f"
44+ , requiredImports: []
4445 }
4546 }
4647 ]
@@ -60,3 +61,4 @@ redundantFlipRule = mkRule (RuleId "RedundantFlip") run
6061 _ -> Nothing
6162 getFlipArg imports (ExprParens (Wrapped { value })) = getFlipArg imports value
6263 getFlipArg _ _ = Nothing
64+
lib/purelint/src/Purelint/Rules/RedundantGuard.purs → lib/purslint/src/Purslint/Rules/RedundantGuard.pursrenamed+6−4
@@ -1,13 +1,13 @@
1−module Purelint.Rules.RedundantGuard where
1+module Purslint.Rules.RedundantGuard where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -38,6 +38,7 @@ redundantGuardRule = mkRule (RuleId "RedundantGuard") run
3838 , suggestion: Just $ Suggestion
3939 { replacement: ReplacementText "pure unit"
4040 , description: SuggestionDescription "guard true can be replaced with pure unit"
41+ , requiredImports: []
4142 }
4243 }
4344 ]
@@ -69,3 +70,4 @@ redundantGuardRule = mkRule (RuleId "RedundantGuard") run
6970 isFalse :: Expr Void -> Boolean
7071 isFalse (ExprBoolean _ b) = not b
7172 isFalse _ = false
73+
lib/purelint/src/Purelint/Rules/RedundantId.purs → lib/purslint/src/Purslint/Rules/RedundantId.pursrenamed+11−5
@@ -1,4 +1,4 @@
1−module Purelint.Rules.RedundantId where
1+module Purslint.Rules.RedundantId where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
@@ -43,6 +43,7 @@ redundantIdRule = mkRule (RuleId "RedundantId") run
4343 , suggestion: Just $ Suggestion
4444 { replacement: ReplacementText x
4545 , description: SuggestionDescription "Remove redundant identity application"
46+ , requiredImports: []
4647 }
4748 }
4849 ]
@@ -63,6 +64,7 @@ redundantIdRule = mkRule (RuleId "RedundantId") run
6364 , suggestion: Just $ Suggestion
6465 { replacement: ReplacementText x
6566 , description: SuggestionDescription "Remove redundant identity composition"
67+ , requiredImports: []
6668 }
6769 }
6870 ]
@@ -77,6 +79,7 @@ redundantIdRule = mkRule (RuleId "RedundantId") run
7779 , suggestion: Just $ Suggestion
7880 { replacement: ReplacementText x
7981 , description: SuggestionDescription "Remove redundant identity composition"
82+ , requiredImports: []
8083 }
8184 }
8285 ]
@@ -93,6 +96,7 @@ redundantIdRule = mkRule (RuleId "RedundantId") run
9396 , suggestion: Just $ Suggestion
9497 { replacement: ReplacementText x
9598 , description: SuggestionDescription "Remove redundant identity composition"
99+ , requiredImports: []
96100 }
97101 }
98102 ]
@@ -107,6 +111,7 @@ redundantIdRule = mkRule (RuleId "RedundantId") run
107111 , suggestion: Just $ Suggestion
108112 { replacement: ReplacementText x
109113 , description: SuggestionDescription "Remove redundant identity composition"
114+ , requiredImports: []
110115 }
111116 }
112117 ]
@@ -122,3 +127,4 @@ redundantIdRule = mkRule (RuleId "RedundantId") run
122127 isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
123128 (name == "identity" || name == "id") && hasValue imports "identity"
124129 isIdentity _ _ = false
130+
lib/purelint/src/Purelint/Rules/RedundantIf.purs → lib/purslint/src/Purslint/Rules/RedundantIf.pursrenamed+8−5
@@ -1,13 +1,13 @@
1−module Purelint.Rules.RedundantIf where
1+module Purslint.Rules.RedundantIf where
22
33import Prelude
44
55import Data.Maybe (Maybe(..))
66import Data.Void (Void)
7−import Purelint.Imports (ImportInfo, hasValue)
8−import Purelint.Print (printExpr)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (Expr(..), Module)
@@ -41,6 +41,7 @@ redundantIfRule = mkRule (RuleId "RedundantIf") run
4141 , suggestion: Just $ Suggestion
4242 { replacement: ReplacementText (printExpr condExpr)
4343 , description: SuggestionDescription "if a then true else false can be simplified to a"
44+ , requiredImports: []
4445 }
4546 }
4647 ]
@@ -54,6 +55,7 @@ redundantIfRule = mkRule (RuleId "RedundantIf") run
5455 , suggestion: Just $ Suggestion
5556 { replacement: ReplacementText ("not " <> printExpr condExpr)
5657 , description: SuggestionDescription "if a then false else true can be simplified to not a"
58+ , requiredImports: []
5759 }
5860 }
5961 ]
@@ -63,3 +65,4 @@ redundantIfRule = mkRule (RuleId "RedundantIf") run
6365 isBoolLit :: Boolean -> Expr Void -> Boolean
6466 isBoolLit expected (ExprBoolean _ val) = val == expected
6567 isBoolLit _ _ = false
68+
lib/purelint/src/Purelint/Rules/RedundantNegate.purs → lib/purslint/src/Purslint/Rules/RedundantNegate.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.RedundantNegate where
1+module Purslint.Rules.RedundantNegate where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -45,6 +45,7 @@ redundantNegateRule = mkRule (RuleId "RedundantNegate") run
4545 , suggestion: Just $ Suggestion
4646 { replacement: ReplacementText x
4747 , description: SuggestionDescription "Remove redundant double negation"
48+ , requiredImports: []
4849 }
4950 }
5051 ]
@@ -61,3 +62,4 @@ redundantNegateRule = mkRule (RuleId "RedundantNegate") run
6162 isNegate imports (ExprIdent (QualifiedName { name: Ident name })) =
6263 name == "negate" && hasValue imports "negate"
6364 isNegate _ _ = false
65+
lib/purelint/src/Purelint/Rules/RedundantNot.purs → lib/purslint/src/Purslint/Rules/RedundantNot.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.RedundantNot where
1+module Purslint.Rules.RedundantNot where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -41,6 +41,7 @@ redundantNotRule = mkRule (RuleId "RedundantNot") run
4141 , suggestion: Just $ Suggestion
4242 { replacement: ReplacementText xText
4343 , description: SuggestionDescription "not (not x) can be simplified to x"
44+ , requiredImports: []
4445 }
4546 }
4647 ]
@@ -60,3 +61,4 @@ redundantNotRule = mkRule (RuleId "RedundantNot") run
6061 _ -> Nothing
6162 getNotArg imports (ExprParens (Wrapped { value })) = getNotArg imports value
6263 getNotArg _ _ = Nothing
64+
lib/purslint/src/Purslint/Rules/RedundantParens.pursnew file+423
@@ -0,0 +1,423 @@
1+module Purslint.Rules.RedundantParens where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Maybe (Maybe(..))
8+import Data.Tuple (snd)
9+import Data.Tuple.Nested ((/\))
10+import Purslint.Print (printBinder, printExpr, printType)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
13+import PureScript.CST.Range (rangeOf)
14+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
15+import PureScript.CST.Types (AppSpine(..), Binder(..), Declaration(..), DoStatement(..), Expr(..), Guarded(..), GuardedExpr(..), Labeled(..), LetBinding(..), Module, RecordLabeled(..), Row(..), Separated(..), Type(..), ValueBindingFields, Where(..), Wrapped(..))
16+
17+-- | Rule: Detect redundant parentheses
18+-- |
19+-- | Detects parens that don't affect parsing/semantics in various contexts:
20+-- | - Nested parens: ((x))
21+-- | - Outer parens on RHS: x = (expr)
22+-- | - Atomic expressions in function arguments
23+-- | - Atomic expressions in array/record elements
24+-- | - Atomic expressions in if/case/let branches
25+-- | - Atomic expressions in do statements
26+-- |
27+-- | NOT flagged (parens may be needed):
28+-- | - Operator precedence: (a + b) * c
29+-- | - Lambda as argument: map (\x -> x) xs
30+-- | - Sections: (_ + 1)
31+-- | - Negative as argument: f (-1)
32+-- | - Record updates: (r { a = 1 }).b
33+redundantParensRule :: Rule
34+redundantParensRule = mkRule (RuleId "RedundantParens") run
35+ where
36+ run :: RuleContext -> Module Void -> Array LintWarning
37+ run _ctx = foldMapModule visitor
38+ where
39+ visitor =
40+ { onExpr: checkExpr
41+ , onDecl: checkDecl
42+ , onBinder: checkBinder
43+ , onType: checkType
44+ }
45+
46+ -- Check declarations for outer parens on RHS
47+ checkDecl :: Declaration Void -> Array LintWarning
48+ checkDecl = case _ of
49+ DeclValue vb -> checkValueBinding vb
50+ _ -> []
51+
52+ checkValueBinding :: ValueBindingFields Void -> Array LintWarning
53+ checkValueBinding vb = case vb.guarded of
54+ Unconditional _ (Where { expr, bindings: Nothing }) ->
55+ checkOuterParens expr "Redundant parentheses around expression"
56+ Unconditional _ (Where { expr, bindings: Just _ }) ->
57+ -- Has where bindings, still check the expr
58+ checkOuterParens expr "Redundant parentheses around expression"
59+ Guarded guardedExprs ->
60+ NEA.toArray guardedExprs # Array.concatMap \(GuardedExpr ge) ->
61+ case ge.where of
62+ Where { expr } -> checkOuterParens expr "Redundant parentheses around guarded expression"
63+
64+ -- Check if outer expression is wrapped in redundant parens
65+ -- Only flag when inner is "simple" - atomic or basic application
66+ checkOuterParens :: Expr Void -> String -> Array LintWarning
67+ checkOuterParens expr msg = case expr of
68+ outer@(ExprParens (Wrapped { value: inner })) ->
69+ -- Only flag if inner is simple enough that parens are clearly redundant
70+ if isSimpleExpr inner then [ mkWarning outer inner msg ]
71+ else []
72+ _ -> []
73+
74+ -- An expression is "simple" enough that outer parens are redundant
75+ -- This is more conservative than isAtomicExpr
76+ isSimpleExpr :: Expr Void -> Boolean
77+ isSimpleExpr = case _ of
78+ -- Atomic expressions
79+ ExprIdent _ -> true
80+ ExprConstructor _ -> true
81+ ExprInt _ _ -> true
82+ ExprNumber _ _ -> true
83+ ExprString _ _ -> true
84+ ExprChar _ _ -> true
85+ ExprBoolean _ _ -> true
86+ ExprArray _ -> true
87+ ExprRecord _ -> true
88+ ExprRecordAccessor _ -> true
89+ -- Simple applications (f x y)
90+ ExprApp _ _ -> true
91+ -- Simple operators (a + b) - but NOT sections
92+ ExprOp left _ -> not (isSection left) -- Check if it contains a section
93+ -- Negate
94+ ExprNegate _ _ -> true
95+ -- Already has parens (nested) - will be caught by nested check
96+ ExprParens _ -> false
97+ -- Everything else might need parens for readability
98+ _ -> false
99+
100+ -- Check if expression or its parts contain a section
101+ isSection :: Expr Void -> Boolean
102+ isSection = case _ of
103+ ExprSection _ -> true
104+ ExprParens (Wrapped { value }) -> isSection value
105+ _ -> false
106+
107+ checkExpr :: Expr Void -> Array LintWarning
108+ checkExpr expr = case expr of
109+ -- === NESTED PARENS: Always redundant ===
110+ ExprParens (Wrapped { value: inner@(ExprParens _) }) ->
111+ [ mkWarning expr inner "Redundant nested parentheses" ]
112+
113+ -- === FUNCTION APPLICATION: Check args ===
114+ ExprApp _fn args ->
115+ NEA.toArray args # Array.mapMaybe checkAppArg
116+
117+ -- === ARRAY LITERAL: Check elements ===
118+ ExprArray (Wrapped { value: Just (Separated { head, tail }) }) ->
119+ let
120+ allExprs = Array.cons head (map snd tail)
121+ in
122+ allExprs # Array.mapMaybe (checkAtomicParens "Redundant parentheses in array element")
123+
124+ -- === RECORD LITERAL: Check field values ===
125+ ExprRecord (Wrapped { value: Just (Separated { head, tail }) }) ->
126+ let
127+ allFields = Array.cons head (map snd tail)
128+ in
129+ allFields # Array.mapMaybe checkRecordField
130+
131+ -- === IF EXPRESSION: Check condition and branches ===
132+ ExprIf ifExpr ->
133+ Array.catMaybes
134+ [ checkAtomicParens "Redundant parentheses in if condition" ifExpr.cond
135+ , checkAtomicParens "Redundant parentheses in then branch" ifExpr.true
136+ , checkAtomicParens "Redundant parentheses in else branch" ifExpr.false
137+ ]
138+
139+ -- === CASE EXPRESSION: Check scrutinee and branches ===
140+ ExprCase caseExpr ->
141+ let
142+ scrutinees = separatedToArray caseExpr.head
143+ scrutineeWarnings = scrutinees # Array.mapMaybe (checkAtomicParens "Redundant parentheses in case scrutinee")
144+ branchWarnings = NEA.toArray caseExpr.branches # Array.concatMap checkCaseBranch
145+ in
146+ scrutineeWarnings <> branchWarnings
147+
148+ -- === LET EXPRESSION: Check bindings and body ===
149+ ExprLet letExpr ->
150+ let
151+ bindingWarnings = NEA.toArray letExpr.bindings # Array.concatMap checkLetBinding
152+ bodyWarnings = Array.fromFoldable $ checkAtomicParens "Redundant parentheses in let body" letExpr.body
153+ in
154+ bindingWarnings <> bodyWarnings
155+
156+ -- === DO EXPRESSION: Check statements ===
157+ ExprDo doExpr ->
158+ NEA.toArray doExpr.statements # Array.concatMap checkDoStatement
159+
160+ _ -> []
161+
162+ -- Check a function argument
163+ checkAppArg :: AppSpine Expr Void -> Maybe LintWarning
164+ checkAppArg = case _ of
165+ AppTerm outer@(ExprParens (Wrapped { value: inner }))
166+ | isAtomicExpr inner -> Just $ mkWarning outer inner "Redundant parentheses in argument"
167+ _ -> Nothing
168+
169+ -- Check atomic parens helper
170+ checkAtomicParens :: String -> Expr Void -> Maybe LintWarning
171+ checkAtomicParens msg = case _ of
172+ outer@(ExprParens (Wrapped { value: inner }))
173+ | isAtomicExpr inner -> Just $ mkWarning outer inner msg
174+ _ -> Nothing
175+
176+ -- Check record field value
177+ checkRecordField :: RecordLabeled (Expr Void) -> Maybe LintWarning
178+ checkRecordField = case _ of
179+ RecordField _ _ outer@(ExprParens (Wrapped { value: inner }))
180+ | isAtomicExpr inner -> Just $ mkWarning outer inner "Redundant parentheses in record field"
181+ RecordPun _ -> Nothing
182+ RecordField _ _ _ -> Nothing
183+
184+ -- Check case branch body and patterns
185+ checkCaseBranch :: _ -> Array LintWarning
186+ checkCaseBranch (binders /\ guarded) =
187+ let
188+ -- Check for redundant parens around top-level case patterns
189+ binderWarnings = separatedToArray binders # Array.mapMaybe checkTopLevelBinderParens
190+ exprWarnings = case guarded of
191+ Unconditional _ (Where { expr }) ->
192+ Array.fromFoldable $ checkAtomicParens "Redundant parentheses in case branch" expr
193+ Guarded guardedExprs ->
194+ NEA.toArray guardedExprs # Array.concatMap \(GuardedExpr ge) ->
195+ case ge.where of
196+ Where { expr } -> Array.fromFoldable $ checkAtomicParens "Redundant parentheses in guarded case branch" expr
197+ in binderWarnings <> exprWarnings
198+
199+ -- Check for redundant parens around a top-level case pattern
200+ -- e.g., case x of (Just y) -> ... should be Just y -> ...
201+ checkTopLevelBinderParens :: Binder Void -> Maybe LintWarning
202+ checkTopLevelBinderParens = case _ of
203+ outer@(BinderParens (Wrapped { value: inner })) ->
204+ -- Parens are redundant unless inner is a typed binder or operator pattern
205+ case inner of
206+ BinderTyped _ _ _ -> Nothing -- (x :: Type) needs parens
207+ BinderOp _ _ -> Nothing -- (a : as) needs parens for cons
208+ _ -> Just $ mkBinderWarning outer inner "Redundant parentheses around case pattern"
209+ _ -> Nothing
210+
211+ -- Check let binding RHS
212+ checkLetBinding :: LetBinding Void -> Array LintWarning
213+ checkLetBinding = case _ of
214+ LetBindingName vb -> case vb.guarded of
215+ Unconditional _ (Where { expr }) ->
216+ Array.fromFoldable $ checkAtomicParens "Redundant parentheses in let binding" expr
217+ _ -> []
218+ _ -> []
219+
220+ -- Check do statement
221+ checkDoStatement :: DoStatement Void -> Array LintWarning
222+ checkDoStatement = case _ of
223+ DoLet _ bindings ->
224+ NEA.toArray bindings # Array.concatMap checkLetBinding
225+ DoBind _ _ expr ->
226+ Array.fromFoldable $ checkAtomicParens "Redundant parentheses in do bind" expr
227+ DoDiscard expr ->
228+ Array.fromFoldable $ checkAtomicParens "Redundant parentheses in do statement" expr
229+ DoError _ -> []
230+
231+ -- An expression is "atomic" - never needs parens for grouping
232+ isAtomicExpr :: Expr Void -> Boolean
233+ isAtomicExpr = case _ of
234+ ExprIdent _ -> true
235+ ExprConstructor _ -> true
236+ ExprInt _ _ -> true
237+ ExprNumber _ _ -> true
238+ ExprString _ _ -> true
239+ ExprChar _ _ -> true
240+ ExprBoolean _ _ -> true
241+ ExprArray _ -> true
242+ ExprRecord _ -> true
243+ ExprParens _ -> true
244+ ExprRecordAccessor _ -> true
245+ _ -> false
246+
247+ separatedToArray :: forall a. Separated a -> Array a
248+ separatedToArray (Separated { head, tail }) = Array.cons head (map snd tail)
249+
250+ mkWarning :: Expr Void -> Expr Void -> String -> LintWarning
251+ mkWarning outer inner msg = LintWarning
252+ { ruleId: RuleId "RedundantParens"
253+ , message: WarningMessage msg
254+ , range: rangeOf outer
255+ , severity: Hint
256+ , suggestion: Just $ Suggestion
257+ { replacement: ReplacementText (printExpr inner)
258+ , description: SuggestionDescription "Remove unnecessary parentheses"
259+ , requiredImports: []
260+ }
261+ }
262+
263+ -- Check binders (patterns) for redundant parens
264+ checkBinder :: Binder Void -> Array LintWarning
265+ checkBinder = case _ of
266+ -- Nested parens in patterns: ((x))
267+ outer@(BinderParens (Wrapped { value: inner@(BinderParens _) })) ->
268+ [ mkBinderWarning outer inner "Redundant nested parentheses in pattern" ]
269+ -- Parens around atomic patterns in constructor args
270+ BinderConstructor _ args ->
271+ args # Array.mapMaybe checkAtomicBinderParens
272+ -- Parens around atomic patterns in array patterns
273+ BinderArray (Wrapped { value: Just (Separated { head, tail }) }) ->
274+ let
275+ allBinders = Array.cons head (map snd tail)
276+ in
277+ allBinders # Array.mapMaybe checkAtomicBinderParens
278+ -- Parens around atomic patterns in record patterns
279+ BinderRecord (Wrapped { value: Just (Separated { head, tail }) }) ->
280+ let
281+ allFields = Array.cons head (map snd tail)
282+ in
283+ allFields # Array.mapMaybe checkRecordBinderField
284+ _ -> []
285+
286+ -- Check if a binder is atomic (never needs parens)
287+ isAtomicBinder :: Binder Void -> Boolean
288+ isAtomicBinder = case _ of
289+ BinderWildcard _ -> true
290+ BinderVar _ -> true
291+ BinderBoolean _ _ -> true
292+ BinderChar _ _ -> true
293+ BinderString _ _ -> true
294+ BinderInt Nothing _ _ -> true -- No negation
295+ BinderNumber Nothing _ _ -> true -- No negation
296+ BinderArray _ -> true
297+ BinderRecord _ -> true
298+ BinderParens _ -> true
299+ -- Constructor with args needs parens, constructor without args is atomic
300+ BinderConstructor _ args -> Array.null args
301+ _ -> false
302+
303+ checkAtomicBinderParens :: Binder Void -> Maybe LintWarning
304+ checkAtomicBinderParens = case _ of
305+ outer@(BinderParens (Wrapped { value: inner }))
306+ | isAtomicBinder inner -> Just $ mkBinderWarning outer inner "Redundant parentheses in pattern"
307+ _ -> Nothing
308+
309+ checkRecordBinderField :: RecordLabeled (Binder Void) -> Maybe LintWarning
310+ checkRecordBinderField = case _ of
311+ RecordField _ _ outer@(BinderParens (Wrapped { value: inner }))
312+ | isAtomicBinder inner -> Just $ mkBinderWarning outer inner "Redundant parentheses in record pattern"
313+ RecordPun _ -> Nothing
314+ RecordField _ _ _ -> Nothing
315+
316+ mkBinderWarning :: Binder Void -> Binder Void -> String -> LintWarning
317+ mkBinderWarning outer inner msg = LintWarning
318+ { ruleId: RuleId "RedundantParens"
319+ , message: WarningMessage msg
320+ , range: rangeOf outer
321+ , severity: Hint
322+ , suggestion: Just $ Suggestion
323+ { replacement: ReplacementText (printBinder inner)
324+ , description: SuggestionDescription "Remove unnecessary parentheses"
325+ , requiredImports: []
326+ }
327+ }
328+
329+ -- ============================================================================
330+ -- TYPE-LEVEL CHECKS
331+ -- ============================================================================
332+
333+ -- Check types for redundant parens
334+ checkType :: Type Void -> Array LintWarning
335+ checkType = case _ of
336+ -- Nested parens: ((Int))
337+ outer@(TypeParens (Wrapped { value: inner@(TypeParens _) })) ->
338+ [ mkTypeWarning outer inner "Redundant nested parentheses in type" ]
339+
340+ -- Type application with atomic arg: Maybe (Int) -> Maybe Int
341+ TypeApp _ args ->
342+ NEA.toArray args # Array.mapMaybe checkAtomicTypeParens
343+
344+ -- Arrow left side: (Int) -> String -> Int -> String
345+ -- But NOT (a -> b) -> c (function type needs parens)
346+ TypeArrow left _ _ ->
347+ Array.fromFoldable $ checkSimpleTypeParens "Redundant parentheses in function argument type" left
348+
349+ -- Record row types: { x :: (Int) } -> { x :: Int }
350+ TypeRecord (Wrapped { value: Row { labels: Just (Separated { head, tail }) } }) ->
351+ let allLabels = Array.cons head (map snd tail)
352+ in allLabels # Array.mapMaybe checkLabeledTypeParens
353+
354+ -- Constrained type: (Show a) => ... is actually fine in PureScript
355+ -- The parens are optional but conventional, so don't flag
356+
357+ _ -> []
358+
359+ -- Check if a type is atomic (never needs parens)
360+ isAtomicType :: Type Void -> Boolean
361+ isAtomicType = case _ of
362+ TypeVar _ -> true
363+ TypeConstructor _ -> true
364+ TypeWildcard _ -> true
365+ TypeHole _ -> true
366+ TypeString _ _ -> true
367+ TypeInt Nothing _ _ -> true -- No negation
368+ TypeRow _ -> true
369+ TypeRecord _ -> true
370+ TypeParens _ -> true
371+ TypeOpName _ -> true
372+ TypeArrowName _ -> true
373+ _ -> false
374+
375+ -- Check if a type is simple enough that outer parens are redundant
376+ -- More permissive than atomic - includes applications
377+ isSimpleType :: Type Void -> Boolean
378+ isSimpleType = case _ of
379+ TypeVar _ -> true
380+ TypeConstructor _ -> true
381+ TypeWildcard _ -> true
382+ TypeHole _ -> true
383+ TypeString _ _ -> true
384+ TypeInt Nothing _ _ -> true
385+ TypeRow _ -> true
386+ TypeRecord _ -> true
387+ TypeParens _ -> false -- Will be caught by nested check
388+ TypeOpName _ -> true
389+ TypeArrowName _ -> true
390+ TypeApp _ _ -> true -- f a b is simple
391+ TypeOp _ _ -> true -- a + b is simple (at top level)
392+ -- NOT simple: TypeArrow, TypeForall, TypeConstrained, TypeKinded
393+ _ -> false
394+
395+ checkAtomicTypeParens :: Type Void -> Maybe LintWarning
396+ checkAtomicTypeParens = case _ of
397+ outer@(TypeParens (Wrapped { value: inner }))
398+ | isAtomicType inner -> Just $ mkTypeWarning outer inner "Redundant parentheses in type argument"
399+ _ -> Nothing
400+
401+ checkSimpleTypeParens :: String -> Type Void -> Maybe LintWarning
402+ checkSimpleTypeParens msg = case _ of
403+ outer@(TypeParens (Wrapped { value: inner }))
404+ | isSimpleType inner -> Just $ mkTypeWarning outer inner msg
405+ _ -> Nothing
406+
407+ checkLabeledTypeParens :: Labeled _ (Type Void) -> Maybe LintWarning
408+ checkLabeledTypeParens (Labeled { value: typ }) =
409+ checkAtomicTypeParens typ
410+
411+ mkTypeWarning :: Type Void -> Type Void -> String -> LintWarning
412+ mkTypeWarning outer inner msg = LintWarning
413+ { ruleId: RuleId "RedundantParens"
414+ , message: WarningMessage msg
415+ , range: rangeOf outer
416+ , severity: Hint
417+ , suggestion: Just $ Suggestion
418+ { replacement: ReplacementText (printType inner)
419+ , description: SuggestionDescription "Remove unnecessary parentheses"
420+ , requiredImports: []
421+ }
422+ }
423+
lib/purslint/src/Purslint/Rules/RedundantReverse.pursnew file+60
@@ -0,0 +1,60 @@
1+module Purslint.Rules.RedundantReverse where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Void (Void)
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: reverse (reverse x) -> x
17+redundantReverseRule :: Rule
18+redundantReverseRule = mkRule (RuleId "RedundantReverse") run
19+ where
20+ run :: RuleContext -> Module Void -> Array LintWarning
21+ run ctx = foldMapModule visitor
22+ where
23+ imports = ctx.imports
24+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
25+
26+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
27+ checkExpr imports expr = case expr of
28+ ExprApp fn args
29+ | isReverse imports fn
30+ , [ AppTerm arg ] <- NEA.toArray args
31+ , ExprApp innerFn innerArgs <- unwrapParens arg
32+ , isReverse imports innerFn
33+ , [ AppTerm innerArg ] <- NEA.toArray innerArgs ->
34+ let
35+ x = printExpr innerArg
36+ in
37+ [ LintWarning
38+ { ruleId: RuleId "RedundantReverse"
39+ , message: WarningMessage "reverse (reverse x) is redundant"
40+ , range: rangeOf expr
41+ , severity: Warning
42+ , suggestion: Just $ Suggestion
43+ { replacement: ReplacementText x
44+ , description: SuggestionDescription "Remove redundant double reverse"
45+ , requiredImports: []
46+ }
47+ }
48+ ]
49+ _ -> []
50+
51+ unwrapParens :: Expr Void -> Expr Void
52+ unwrapParens = case _ of
53+ (ExprParens (Wrapped { value })) -> unwrapParens value
54+ e -> e
55+
56+ isReverse :: ImportInfo -> Expr Void -> Boolean
57+ isReverse imports (ExprIdent (QualifiedName { name: Ident name })) =
58+ name == "reverse" && hasValue imports "reverse"
59+ isReverse _ _ = false
60+
lib/purelint/src/Purelint/Rules/UnlessNot.purs → lib/purslint/src/Purslint/Rules/UnlessNot.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UnlessNot where
1+module Purslint.Rules.UnlessNot where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -44,6 +44,7 @@ unlessNotRule = mkRule (RuleId "UnlessNot") run
4444 , suggestion: Just $ Suggestion
4545 { replacement: ReplacementText ("when " <> xText <> " " <> action)
4646 , description: SuggestionDescription "Use when instead of unless (not ...)"
47+ , requiredImports: []
4748 }
4849 }
4950 ]
@@ -61,6 +62,7 @@ unlessNotRule = mkRule (RuleId "UnlessNot") run
6162 , suggestion: Just $ Suggestion
6263 { replacement: ReplacementText ("when " <> xText)
6364 , description: SuggestionDescription "Use when instead of unless (not ...)"
65+ , requiredImports: []
6466 }
6567 }
6668 ]
@@ -91,3 +93,4 @@ unlessNotRule = mkRule (RuleId "UnlessNot") run
9193 [AppTerm arg] -> Just arg
9294 _ -> Nothing
9395 _ -> Nothing
96+
lib/purelint/src/Purelint/Rules/UseAll.purs → lib/purslint/src/Purslint/Rules/UseAll.pursrenamed+8−6
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseAll where
1+module Purslint.Rules.UseAll where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -45,6 +45,7 @@ useAllRule = mkRule (RuleId "UseAll") run
4545 , suggestion: Just $ Suggestion
4646 { replacement: ReplacementText ("all " <> fText <> " " <> xText)
4747 , description: SuggestionDescription "and (map f x) can be simplified to all f x"
48+ , requiredImports: []
4849 }
4950 }
5051 ]
@@ -64,5 +65,6 @@ useAllRule = mkRule (RuleId "UseAll") run
6465
6566 isMapLike :: ImportInfo -> Expr Void -> Boolean
6667 isMapLike imports (ExprIdent (QualifiedName { name: Ident name })) =
67− (name == "map" && hasValue imports "map") || (name == "fmap" && hasValue imports "fmap")
68+ name == "map" && hasValue imports "map"
6869 isMapLike _ _ = false
70+
lib/purelint/src/Purelint/Rules/UseAnd.purs → lib/purslint/src/Purslint/Rules/UseAnd.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseAnd where
1+module Purslint.Rules.UseAnd where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -39,6 +39,7 @@ useAndRule = mkRule (RuleId "UseAnd") run
3939 , suggestion: Just $ Suggestion
4040 { replacement: ReplacementText "and"
4141 , description: SuggestionDescription "Use and instead of all identity"
42+ , requiredImports: []
4243 }
4344 }
4445 ]
@@ -55,6 +56,7 @@ useAndRule = mkRule (RuleId "UseAnd") run
5556 , suggestion: Just $ Suggestion
5657 { replacement: ReplacementText ("and " <> x)
5758 , description: SuggestionDescription "Use and instead of all identity"
59+ , requiredImports: []
5860 }
5961 }
6062 ]
@@ -75,3 +77,4 @@ useAndRule = mkRule (RuleId "UseAnd") run
7577 isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
7678 (name == "identity" || name == "id") && hasValue imports "identity"
7779 isIdentity _ _ = false
80+
lib/purslint/src/Purslint/Rules/UseAny.pursnew file+63
@@ -0,0 +1,63 @@
1+module Purslint.Rules.UseAny where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Void (Void)
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: or (map f x) -> any f x
17+useAnyRule :: Rule
18+useAnyRule = mkRule (RuleId "UseAny") run
19+ where
20+ run :: RuleContext -> Module Void -> Array LintWarning
21+ run ctx = foldMapModule visitor
22+ where
23+ imports = ctx.imports
24+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
25+
26+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
27+ checkExpr imports expr = case expr of
28+ ExprApp fnExpr args
29+ | isOr imports fnExpr
30+ , [ AppTerm innerExpr ] <- NEA.toArray args
31+ , ExprApp mapFn mapArgs <- unwrapParens innerExpr
32+ , isMapLike imports mapFn
33+ , [ AppTerm fExpr, AppTerm xExpr ] <- NEA.toArray mapArgs -> do
34+ let fText = printExpr fExpr
35+ let xText = printExpr xExpr
36+ [ LintWarning
37+ { ruleId: RuleId "UseAny"
38+ , message: WarningMessage "Use any instead of or (map f x)"
39+ , range: rangeOf expr
40+ , severity: Warning
41+ , suggestion: Just $ Suggestion
42+ { replacement: ReplacementText ("any " <> fText <> " " <> xText)
43+ , description: SuggestionDescription "or (map f x) can be simplified to any f x"
44+ , requiredImports: []
45+ }
46+ }
47+ ]
48+ _ -> []
49+
50+ unwrapParens :: Expr Void -> Expr Void
51+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
52+ unwrapParens e = e
53+
54+ isOr :: ImportInfo -> Expr Void -> Boolean
55+ isOr imports (ExprIdent (QualifiedName { name: Ident name })) =
56+ name == "or" && hasValue imports "or"
57+ isOr _ _ = false
58+
59+ isMapLike :: ImportInfo -> Expr Void -> Boolean
60+ isMapLike = case _, _ of
61+ imports, (ExprIdent (QualifiedName { name: Ident name })) -> name == "map" && hasValue imports "map"
62+ _, _ -> false
63+
lib/purslint/src/Purslint/Rules/UseApplicative.pursnew file+60
@@ -0,0 +1,60 @@
1+module Purslint.Rules.UseApplicative where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Tuple (Tuple(..))
8+import Data.Void (Void)
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
13+import PureScript.CST.Range (rangeOf)
14+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
15+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
16+
17+-- | Rule: pure f <*> x -> f <$> x
18+useApplicativeRule :: Rule
19+useApplicativeRule = run # mkRule (RuleId "UseApplicative")
20+ where
21+ run :: RuleContext -> Module Void -> Array LintWarning
22+ run ctx = foldMapModule visitor
23+ where
24+ imports = ctx.imports
25+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
26+
27+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
28+ checkExpr imports expr = case expr of
29+ ExprOp lhs ops
30+ | hasOp imports "<*>"
31+ , ExprApp pureExpr args <- unwrapParens lhs
32+ , isPure imports pureExpr
33+ , [ AppTerm fExpr ] <- NEA.toArray args
34+ , [ Tuple (QualifiedName { name: Operator "<*>" }) xExpr ] <- NEA.toArray ops -> do
35+ let
36+ f = printExpr fExpr
37+ x = printExpr xExpr
38+ [ LintWarning
39+ { ruleId: RuleId "UseApplicative"
40+ , message: WarningMessage "pure f <*> x can be simplified to f <$> x"
41+ , range: rangeOf expr
42+ , severity: Hint
43+ , suggestion: Just $ Suggestion
44+ { replacement: ReplacementText (f <> " <$> " <> x)
45+ , description: SuggestionDescription "Use <$> instead of pure f <*> x"
46+ , requiredImports: []
47+ }
48+ }
49+ ]
50+ _ -> []
51+
52+ unwrapParens :: Expr Void -> Expr Void
53+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
54+ unwrapParens e = e
55+
56+ isPure :: ImportInfo -> Expr Void -> Boolean
57+ isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
58+ name == "pure" && hasValue imports "pure"
59+ isPure _ _ = false
60+
lib/purslint/src/Purslint/Rules/UseApplyFlipped.pursnew file+57
@@ -0,0 +1,57 @@
1+module Purslint.Rules.UseApplyFlipped where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Void (Void)
8+import Purslint.Print (printExprNoComments)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
11+import PureScript.CST.Range (rangeOf)
12+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
13+import PureScript.CST.Types (AppSpine(..), Expr(..), Module, Wrapped(..))
14+
15+-- | Rule: f arg -> arg # f (when f is complex like a lambda or partial application)
16+-- | Suggests using # for better readability when the function is more complex than the argument
17+useApplyFlippedRule :: Rule
18+useApplyFlippedRule = mkRule (RuleId "UseApplyFlipped") run
19+ where
20+ run :: RuleContext -> Module Void -> Array LintWarning
21+ run _ctx = foldMapModule visitor
22+ where
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr }
24+
25+ checkExpr :: Expr Void -> Array LintWarning
26+ checkExpr expr = case expr of
27+ -- Match: f arg1 arg2 --> arg2 # f arg1
28+ -- Triggers for any 2-arg application
29+ ExprApp fn args
30+ | [AppTerm firstArg, AppTerm lastArg] <- NEA.toArray args
31+ -> let
32+ fnText = printExprNoComments fn
33+ -- For lambdas, unwrap parens so we get `\x -> ...` not `(\x -> ...)`
34+ firstArgText = case unwrapParens firstArg of
35+ l@(ExprLambda _) -> printExprNoComments l
36+ _ -> printExprNoComments firstArg
37+ lastArgText = printExprNoComments lastArg
38+ replacement = lastArgText <> " # " <> fnText <> " " <> firstArgText
39+ in
40+ [ LintWarning
41+ { ruleId: RuleId "UseApplyFlipped"
42+ , message: WarningMessage "Consider using # for better readability"
43+ , range: rangeOf expr
44+ , severity: Refactor
45+ , suggestion: Just $ Suggestion
46+ { replacement: ReplacementText replacement
47+ , description: SuggestionDescription ("Rewrite as: " <> replacement)
48+ , requiredImports: []
49+ }
50+ }
51+ ]
52+ _ -> []
53+
54+ unwrapParens :: Expr Void -> Expr Void
55+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
56+ unwrapParens e = e
57+
lib/purslint/src/Purslint/Rules/UseBimap.pursnew file+212
@@ -0,0 +1,212 @@
1+module Purslint.Rules.UseBimap where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Void (Void)
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: bimap id g -> second g
17+-- | Also: bimap f id -> first f
18+-- | Also: first f (second g x) -> bimap f g x
19+useBimapRule :: Rule
20+useBimapRule = mkRule (RuleId "UseBimap") run
21+ where
22+ run :: RuleContext -> Module Void -> Array LintWarning
23+ run ctx = foldMapModule visitor
24+ where
25+ imports = ctx.imports
26+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
27+
28+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
29+ checkExpr imports expr = case expr of
30+ -- Match: bimap id g or bimap f id
31+ ExprApp fn args
32+ | isBimap imports fn ->
33+ case NEA.toArray args of
34+ [ AppTerm fArg, AppTerm gArg ] ->
35+ let
36+ fIsId = isIdentity imports fArg
37+ gIsId = isIdentity imports gArg
38+ in
39+ if fIsId && not gIsId then
40+ let
41+ g = printExpr gArg
42+ in
43+ [ LintWarning
44+ { ruleId: RuleId "UseBimap"
45+ , message: WarningMessage "bimap id g can be simplified to second g"
46+ , range: rangeOf expr
47+ , severity: Warning
48+ , suggestion: Just $ Suggestion
49+ { replacement: ReplacementText ("second " <> g)
50+ , description: SuggestionDescription "Use second instead of bimap id"
51+ , requiredImports: []
52+ }
53+ }
54+ ]
55+ else if gIsId && not fIsId then
56+ let
57+ f = printExpr fArg
58+ in
59+ [ LintWarning
60+ { ruleId: RuleId "UseBimap"
61+ , message: WarningMessage "bimap f id can be simplified to first f"
62+ , range: rangeOf expr
63+ , severity: Warning
64+ , suggestion: Just $ Suggestion
65+ { replacement: ReplacementText ("first " <> f)
66+ , description: SuggestionDescription "Use first instead of bimap f id"
67+ , requiredImports: []
68+ }
69+ }
70+ ]
71+ else if fIsId && gIsId then
72+ [ LintWarning
73+ { ruleId: RuleId "UseBimap"
74+ , message: WarningMessage "bimap id id is redundant"
75+ , range: rangeOf expr
76+ , severity: Warning
77+ , suggestion: Just $ Suggestion
78+ { replacement: ReplacementText "identity"
79+ , description: SuggestionDescription "bimap id id is just identity"
80+ , requiredImports: []
81+ }
82+ }
83+ ]
84+ else []
85+ [ AppTerm fArg, AppTerm gArg, AppTerm xArg ] ->
86+ let
87+ fIsId = isIdentity imports fArg
88+ gIsId = isIdentity imports gArg
89+ x = printExpr xArg
90+ in
91+ if fIsId && not gIsId then
92+ let
93+ g = printExpr gArg
94+ in
95+ [ LintWarning
96+ { ruleId: RuleId "UseBimap"
97+ , message: WarningMessage "bimap id g x can be simplified to second g x"
98+ , range: rangeOf expr
99+ , severity: Warning
100+ , suggestion: Just $ Suggestion
101+ { replacement: ReplacementText ("second " <> g <> " " <> x)
102+ , description: SuggestionDescription "Use second instead of bimap id"
103+ , requiredImports: []
104+ }
105+ }
106+ ]
107+ else if gIsId && not fIsId then
108+ let
109+ f = printExpr fArg
110+ in
111+ [ LintWarning
112+ { ruleId: RuleId "UseBimap"
113+ , message: WarningMessage "bimap f id x can be simplified to first f x"
114+ , range: rangeOf expr
115+ , severity: Warning
116+ , suggestion: Just $ Suggestion
117+ { replacement: ReplacementText ("first " <> f <> " " <> x)
118+ , description: SuggestionDescription "Use first instead of bimap f id"
119+ , requiredImports: []
120+ }
121+ }
122+ ]
123+ else if fIsId && gIsId then
124+ [ LintWarning
125+ { ruleId: RuleId "UseBimap"
126+ , message: WarningMessage "bimap id id x is redundant"
127+ , range: rangeOf expr
128+ , severity: Warning
129+ , suggestion: Just $ Suggestion
130+ { replacement: ReplacementText x
131+ , description: SuggestionDescription "bimap id id x is just x"
132+ , requiredImports: []
133+ }
134+ }
135+ ]
136+ else []
137+ _ -> []
138+ -- Match: first f (second g x) or second g (first f x)
139+ | isFirst imports fn ->
140+ case NEA.toArray args of
141+ [ AppTerm fArg, AppTerm innerArg ]
142+ | ExprApp secondFn secondArgs <- unwrapParens innerArg
143+ , isSecond imports secondFn
144+ , [ AppTerm gArg, AppTerm xArg ] <- NEA.toArray secondArgs ->
145+ let
146+ f = printExpr fArg
147+ g = printExpr gArg
148+ x = printExpr xArg
149+ in
150+ [ LintWarning
151+ { ruleId: RuleId "UseBimap"
152+ , message: WarningMessage "first f (second g x) can be simplified to bimap f g x"
153+ , range: rangeOf expr
154+ , severity: Hint
155+ , suggestion: Just $ Suggestion
156+ { replacement: ReplacementText ("bimap " <> f <> " " <> g <> " " <> x)
157+ , description: SuggestionDescription "Use bimap instead of first and second"
158+ , requiredImports: []
159+ }
160+ }
161+ ]
162+ _ -> []
163+ | isSecond imports fn ->
164+ case NEA.toArray args of
165+ [ AppTerm gArg, AppTerm innerArg ]
166+ | ExprApp firstFn firstArgs <- unwrapParens innerArg
167+ , isFirst imports firstFn
168+ , [ AppTerm fArg, AppTerm xArg ] <- NEA.toArray firstArgs ->
169+ let
170+ f = printExpr fArg
171+ g = printExpr gArg
172+ x = printExpr xArg
173+ in
174+ [ LintWarning
175+ { ruleId: RuleId "UseBimap"
176+ , message: WarningMessage "second g (first f x) can be simplified to bimap f g x"
177+ , range: rangeOf expr
178+ , severity: Hint
179+ , suggestion: Just $ Suggestion
180+ { replacement: ReplacementText ("bimap " <> f <> " " <> g <> " " <> x)
181+ , description: SuggestionDescription "Use bimap instead of first and second"
182+ , requiredImports: []
183+ }
184+ }
185+ ]
186+ _ -> []
187+ _ -> []
188+
189+ unwrapParens :: Expr Void -> Expr Void
190+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
191+ unwrapParens e = e
192+
193+ isBimap :: ImportInfo -> Expr Void -> Boolean
194+ isBimap imports (ExprIdent (QualifiedName { name: Ident name })) =
195+ name == "bimap" && hasValue imports "bimap"
196+ isBimap _ _ = false
197+
198+ isFirst :: ImportInfo -> Expr Void -> Boolean
199+ isFirst imports (ExprIdent (QualifiedName { name: Ident name })) =
200+ name == "first" && hasValue imports "first"
201+ isFirst _ _ = false
202+
203+ isSecond :: ImportInfo -> Expr Void -> Boolean
204+ isSecond imports (ExprIdent (QualifiedName { name: Ident name })) =
205+ name == "second" && hasValue imports "second"
206+ isSecond _ _ = false
207+
208+ isIdentity :: ImportInfo -> Expr Void -> Boolean
209+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
210+ (name == "identity" && hasValue imports "identity") || (name == "id" && hasValue imports "identity")
211+ isIdentity _ _ = false
212+
lib/purslint/src/Purslint/Rules/UseBindFlip.pursnew file+70
@@ -0,0 +1,70 @@
1+module Purslint.Rules.UseBindFlip where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Void (Void)
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: join (map f x) -> x >>= f
17+useBindFlipRule :: Rule
18+useBindFlipRule = mkRule (RuleId "UseBindFlip") run
19+ where
20+ run :: RuleContext -> Module Void -> Array LintWarning
21+ run ctx = foldMapModule visitor
22+ where
23+ imports = ctx.imports
24+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
25+
26+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
27+ checkExpr imports expr = case expr of
28+ ExprApp fnExpr args
29+ | isJoin imports fnExpr
30+ , [ AppTerm innerExpr ] <- NEA.toArray args ->
31+ checkInnerMap imports expr (unwrapParens innerExpr)
32+ _ -> []
33+
34+ unwrapParens :: Expr Void -> Expr Void
35+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
36+ unwrapParens e = e
37+
38+ checkInnerMap :: ImportInfo -> Expr Void -> Expr Void -> Array LintWarning
39+ checkInnerMap imports fullExpr innerExpr =
40+ case innerExpr of
41+ ExprApp mapFn mapArgs
42+ | isMap imports mapFn
43+ , [ AppTerm fExpr, AppTerm xExpr ] <- NEA.toArray mapArgs -> do
44+ let
45+ f = printExpr fExpr
46+ x = printExpr xExpr
47+ [ LintWarning
48+ { ruleId: RuleId "UseBindFlip"
49+ , message: WarningMessage "join (map f x) can be simplified to x >>= f"
50+ , range: rangeOf fullExpr
51+ , severity: Warning
52+ , suggestion: Just $ Suggestion
53+ { replacement: ReplacementText (x <> " >>= " <> f)
54+ , description: SuggestionDescription "Use >>= instead of join composed with map"
55+ , requiredImports: []
56+ }
57+ }
58+ ]
59+ _ -> []
60+
61+ isJoin :: ImportInfo -> Expr Void -> Boolean
62+ isJoin = case _, _ of
63+ imports, ExprIdent (QualifiedName { name: Ident name }) -> name == "join" && hasValue imports "join"
64+ _, _ -> false
65+
66+ isMap :: ImportInfo -> Expr Void -> Boolean
67+ isMap = case _, _ of
68+ imports, ExprIdent (QualifiedName { name: Ident name }) -> name == "map" && hasValue imports "map"
69+ _, _ -> false
70+
lib/purslint/src/Purslint/Rules/UseBreak.pursnew file+99
@@ -0,0 +1,99 @@
1+module Purslint.Rules.UseBreak where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Void (Void)
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
12+import PureScript.CST.Range (rangeOf)
13+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
14+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: span (not <<< p) -> break p (composition form)
17+-- | Also: span (\x -> not (p x)) -> break p (lambda form)
18+useBreakRule :: Rule
19+useBreakRule = mkRule (RuleId "UseBreak") run
20+ where
21+ run :: RuleContext -> Module Void -> Array LintWarning
22+ run ctx = foldMapModule visitor
23+ where
24+ imports = ctx.imports
25+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
26+
27+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
28+ checkExpr imports expr = case expr of
29+ -- Match: span (not <<< p) x
30+ ExprApp fn args
31+ | isSpan imports fn ->
32+ case NEA.toArray args of
33+ [ AppTerm predArg, AppTerm xArg ] ->
34+ case getNotComposedPred imports predArg of
35+ Just p ->
36+ let
37+ pText = printExpr p
38+ x = printExpr xArg
39+ in
40+ [ LintWarning
41+ { ruleId: RuleId "UseBreak"
42+ , message: WarningMessage "span (not <<< p) can be simplified to break p"
43+ , range: rangeOf expr
44+ , severity: Hint
45+ , suggestion: Just $ Suggestion
46+ { replacement: ReplacementText ("break " <> pText <> " " <> x)
47+ , description: SuggestionDescription "Use break instead of span with negated predicate"
48+ , requiredImports: []
49+ }
50+ }
51+ ]
52+ Nothing -> []
53+ [ AppTerm predArg ] ->
54+ case getNotComposedPred imports predArg of
55+ Just p ->
56+ let
57+ pText = printExpr p
58+ in
59+ [ LintWarning
60+ { ruleId: RuleId "UseBreak"
61+ , message: WarningMessage "span (not <<< p) can be simplified to break p"
62+ , range: rangeOf expr
63+ , severity: Hint
64+ , suggestion: Just $ Suggestion
65+ { replacement: ReplacementText ("break " <> pText)
66+ , description: SuggestionDescription "Use break instead of span with negated predicate"
67+ , requiredImports: []
68+ }
69+ }
70+ ]
71+ Nothing -> []
72+ _ -> []
73+ _ -> []
74+
75+ unwrapParens :: Expr Void -> Expr Void
76+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
77+ unwrapParens e = e
78+
79+ isSpan :: ImportInfo -> Expr Void -> Boolean
80+ isSpan imports (ExprIdent (QualifiedName { name: Ident name })) =
81+ name == "span" && hasValue imports "span"
82+ isSpan _ _ = false
83+
84+ isNot :: ImportInfo -> Expr Void -> Boolean
85+ isNot imports (ExprIdent (QualifiedName { name: Ident name })) =
86+ name == "not" && hasValue imports "not"
87+ isNot _ _ = false
88+
89+ -- Check for (not <<< p) pattern
90+ getNotComposedPred :: ImportInfo -> Expr Void -> Maybe (Expr Void)
91+ getNotComposedPred imports predExpr =
92+ case unwrapParens predExpr of
93+ ExprApp notFn args
94+ | isNot imports notFn ->
95+ case NEA.toArray args of
96+ [ AppTerm p ] -> Just p
97+ _ -> Nothing
98+ _ -> Nothing
99+
lib/purslint/src/Purslint/Rules/UseCaseOf.pursnew file+239
@@ -0,0 +1,239 @@
1+module Purslint.Rules.UseCaseOf where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Foldable (foldMap)
8+import Data.Maybe (Maybe(..))
9+import Data.Monoid (power)
10+import Data.String as String
11+import Data.Tuple (Tuple(..))
12+import Data.Tuple.Nested ((/\))
13+import Data.Void (Void, absurd)
14+import Purslint.Print (printBinder, printExpr)
15+import Purslint.Rule (Rule, RuleContext, mkRule)
16+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
17+import PureScript.CST.Print (printToken)
18+import PureScript.CST.Range (rangeOf, tokensOf)
19+import PureScript.CST.Range.TokenList as TokenList
20+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
21+import PureScript.CST.Types (Binder(..), Comment(..), Declaration(..), Expr, Guarded(..), GuardedExpr(..), Ident(..), LetBinding(..), LineFeed(..), Module(..), ModuleBody(..), Name(..), SourceToken, ValueBindingFields, Where(..))
22+
23+-- | Rule: Functions with multiple clauses where the name repeats can use case _ of
24+-- |
25+-- | foo x = expr1
26+-- | foo y = expr2
27+-- |
28+-- | becomes:
29+-- |
30+-- | foo = case _ of
31+-- | x -> expr1
32+-- | y -> expr2
33+useCaseOfRule :: Rule
34+useCaseOfRule = mkRule (RuleId "UseCaseOf") run
35+ where
36+ run :: RuleContext -> Module Void -> Array LintWarning
37+ run _ctx mod@(Module m) =
38+ let
39+ ModuleBody body = m.body
40+ topLevelWarnings = groupByName body.decls # Array.concatMap (checkGroup 0)
41+ nestedWarnings = foldMapModule visitor mod
42+ in
43+ topLevelWarnings <> nestedWarnings
44+ where
45+ visitor = defaultMonoidalVisitor { onDecl = checkDecl }
46+
47+ -- Check where bindings in declarations
48+ checkDecl :: Declaration Void -> Array LintWarning
49+ checkDecl = case _ of
50+ DeclValue { guarded } -> checkGuarded guarded
51+ _ -> []
52+
53+ checkGuarded :: Guarded Void -> Array LintWarning
54+ checkGuarded = case _ of
55+ Unconditional _ (Where { bindings: Just (Tuple _ letBindings) }) ->
56+ checkLetBindings letBindings
57+ Guarded guards -> foldMap checkGuardedExpr (NEA.toArray guards)
58+ _ -> []
59+
60+ checkGuardedExpr :: GuardedExpr Void -> Array LintWarning
61+ checkGuardedExpr (GuardedExpr { where: Where { bindings: Just (Tuple _ letBindings) } }) =
62+ checkLetBindings letBindings
63+ checkGuardedExpr _ = []
64+
65+ checkLetBindings :: NEA.NonEmptyArray (LetBinding Void) -> Array LintWarning
66+ checkLetBindings bindings =
67+ let
68+ vbs = NEA.toArray bindings # Array.mapMaybe case _ of
69+ LetBindingName vb -> Just vb
70+ _ -> Nothing
71+ -- Get the base indentation from first binding
72+ baseCol = case Array.head vbs of
73+ Just vb -> (rangeOf (LetBindingName vb)).start.column
74+ Nothing -> 0
75+ groups = groupByNameVB vbs
76+ in
77+ groups # Array.concatMap (checkGroup baseCol)
78+
79+ -- Group consecutive DeclValue with same name (for top-level)
80+ groupByName :: Array (Declaration Void) -> Array (Array (ValueBindingFields Void))
81+ groupByName decls = go [] Nothing decls
82+ where
83+ go :: Array (Array (ValueBindingFields Void)) -> Maybe { name :: String, clauses :: Array (ValueBindingFields Void) } -> Array (Declaration Void) -> Array (Array (ValueBindingFields Void))
84+ go acc current arr = case Array.uncons arr of
85+ Nothing -> case current of
86+ Nothing -> acc
87+ Just c -> Array.snoc acc c.clauses
88+ Just { head: d, tail: rest } -> case d of
89+ DeclValue vb ->
90+ let
91+ name = getName vb
92+ in
93+ case current of
94+ Nothing -> go acc (Just { name, clauses: [ vb ] }) rest
95+ Just c | c.name == name -> go acc (Just { name, clauses: Array.snoc c.clauses vb }) rest
96+ Just c -> go (Array.snoc acc c.clauses) (Just { name, clauses: [ vb ] }) rest
97+ _ -> case current of
98+ Nothing -> go acc Nothing rest
99+ Just c -> go (Array.snoc acc c.clauses) Nothing rest
100+
101+ -- Group consecutive ValueBindingFields with same name (for where bindings)
102+ groupByNameVB :: Array (ValueBindingFields Void) -> Array (Array (ValueBindingFields Void))
103+ groupByNameVB vbs = go [] Nothing vbs
104+ where
105+ go :: Array (Array (ValueBindingFields Void)) -> Maybe { name :: String, clauses :: Array (ValueBindingFields Void) } -> Array (ValueBindingFields Void) -> Array (Array (ValueBindingFields Void))
106+ go acc current arr = case Array.uncons arr of
107+ Nothing -> case current of
108+ Nothing -> acc
109+ Just c -> Array.snoc acc c.clauses
110+ Just { head: vb, tail: rest } ->
111+ let
112+ name = getName vb
113+ in
114+ case current of
115+ Nothing -> go acc (Just { name, clauses: [ vb ] }) rest
116+ Just c | c.name == name -> go acc (Just { name, clauses: Array.snoc c.clauses vb }) rest
117+ Just c -> go (Array.snoc acc c.clauses) (Just { name, clauses: [ vb ] }) rest
118+
119+ getName :: ValueBindingFields Void -> String
120+ getName vb = case vb.name of
121+ Name { name: Ident n } -> n
122+
123+ checkGroup :: Int -> Array (ValueBindingFields Void) -> Array LintWarning
124+ checkGroup baseCol clauses
125+ | Array.length clauses < 2 = []
126+ | not (Array.all hasBindersAndUnconditional clauses) = []
127+ | otherwise = foldMap
128+ ( \first ->
129+ let
130+ name = getName first
131+ numArgs = Array.length first.binders
132+ -- Check all clauses have same number of binders
133+ sameBinders = Array.all (\vb -> Array.length vb.binders == numArgs) clauses
134+ in
135+ if not sameBinders || numArgs == 0 then []
136+ else
137+ let
138+ -- Build the replacement
139+ indent = power " " baseCol
140+ wildcards = Array.replicate numArgs "_" # String.joinWith ", "
141+ caseHeader = name <> " = case " <> wildcards <> " of"
142+ caseArms = clauses # Array.mapMaybe (formatClause (baseCol + 2))
143+ replacement = caseHeader <> "\n" <> (caseArms # map ((indent <> " ") <> _) # String.joinWith "\n")
144+ -- Range spans from first clause to last
145+ firstRange = rangeOf (LetBindingName first)
146+ lastRange = case Array.last clauses of
147+ Just lst -> rangeOf (LetBindingName lst)
148+ Nothing -> firstRange
149+ fullRange = { start: firstRange.start, end: lastRange.end }
150+ in
151+ [ LintWarning
152+ { ruleId: RuleId "UseCaseOf"
153+ , message: WarningMessage $ "Function '" <> name <> "' has " <> show (Array.length clauses) <> " clauses - consider using case _ of"
154+ , range: fullRange
155+ , severity: Refactor
156+ , suggestion: Just $ Suggestion
157+ { replacement: ReplacementText replacement
158+ , description: SuggestionDescription "Convert to case _ of"
159+ , requiredImports: []
160+ }
161+ }
162+ ]
163+ )
164+ (Array.head clauses)
165+
166+ hasBindersAndUnconditional :: ValueBindingFields Void -> Boolean
167+ hasBindersAndUnconditional vb = case vb.guarded of
168+ Unconditional _ (Where { bindings: Nothing }) -> Array.length vb.binders > 0
169+ Unconditional _ (Where { bindings: Just _ }) -> Array.length vb.binders > 0
170+ _ -> false
171+
172+ formatClause :: Int -> ValueBindingFields Void -> Maybe String
173+ formatClause baseCol vb = case vb.guarded of
174+ Unconditional _ (Where { expr, bindings: Nothing }) ->
175+ let
176+ binders = vb.binders # map printBinder # String.joinWith ", "
177+ exprRange = rangeOf expr
178+ isMultiline = exprRange.start.line /= exprRange.end.line
179+ in
180+ if isMultiline
181+ then
182+ let
183+ -- Target indentation for the expression body
184+ targetCol = baseCol + 2
185+ body = printExprTokens targetCol expr
186+ in
187+ Just $ binders <> " ->\n" <> power " " targetCol <> body
188+ else Just $ binders <> " -> " <> printExpr expr
189+ Unconditional _ (Where { expr, bindings: Just (_ /\ letBindings) }) ->
190+ let
191+ binders = vb.binders # map printBinder # String.joinWith ", "
192+ body = printExpr expr
193+ indent = power " " baseCol
194+ -- Format the where bindings with proper indentation
195+ whereBindings = NEA.toArray letBindings # map (printBindingTokens (baseCol + 4))
196+ whereText = String.joinWith ("\n" <> indent <> " ") whereBindings
197+ in
198+ Just $ binders <> " ->\n" <> indent <> " " <> body <> "\n" <> indent <> " where\n" <> indent <> " " <> whereText
199+ _ -> Nothing
200+
201+ printExprTokens :: Int -> Expr Void -> String
202+ printExprTokens targetCol expr =
203+ let
204+ exprCol = (rangeOf expr).start.column
205+ shiftAmount = targetCol - exprCol
206+ in
207+ String.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf expr))
208+
209+ printBindingTokens :: Int -> LetBinding Void -> String
210+ printBindingTokens targetCol b =
211+ let
212+ bindingCol = (rangeOf b).start.column
213+ shiftAmount = targetCol - bindingCol
214+ in
215+ String.trim $ foldMap (printSourceToken shiftAmount) (TokenList.toArray (tokensOf b))
216+
217+ printSourceToken :: Int -> SourceToken -> String
218+ printSourceToken shiftAmount tok =
219+ foldMap (printLeadingComment shiftAmount) tok.leadingComments
220+ <> printToken tok.value
221+ <> foldMap printTrailingComment tok.trailingComments
222+
223+ printLeadingComment :: Int -> Comment LineFeed -> String
224+ printLeadingComment shiftAmount = case _ of
225+ Comment str -> str
226+ Space n -> power " " (max 0 (n + shiftAmount))
227+ Line lf n -> foldMap printLineFeed (Array.replicate n lf)
228+
229+ printLineFeed :: LineFeed -> String
230+ printLineFeed = case _ of
231+ LF -> "\n"
232+ CRLF -> "\r\n"
233+
234+ printTrailingComment :: Comment Void -> String
235+ printTrailingComment = case _ of
236+ Comment str -> str
237+ Space n -> power " " n
238+ Line v _ -> absurd v
239+
lib/purelint/src/Purelint/Rules/UseComparing.purs → lib/purslint/src/Purslint/Rules/UseComparing.pursrenamed+31−32
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseComparing where
1+module Purslint.Rules.UseComparing where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Binder(..), Expr(..), Ident(..), Module, Name(..), QualifiedName(..), Wrapped(..))
@@ -28,10 +28,10 @@ useComparingRule = mkRule (RuleId "UseComparing") run
2828 -- Match: \x y -> compare (f x) (f y)
2929 ExprLambda { binders, body }
3030 | hasValue imports "compare" ->
31− case NEA.toArray binders of
32− [BinderVar (Name { name: Ident x }), BinderVar (Name { name: Ident y })] ->
33− checkCompareBody imports expr x y (unwrapParens body)
34− _ -> []
31+ case NEA.toArray binders of
32+ [ BinderVar (Name { name: Ident x }), BinderVar (Name { name: Ident y }) ] ->
33+ checkCompareBody imports expr x y (unwrapParens body)
34+ _ -> []
3535 _ -> []
3636
3737 unwrapParens :: Expr Void -> Expr Void
@@ -42,38 +42,37 @@ useComparingRule = mkRule (RuleId "UseComparing") run
4242 checkCompareBody imports fullExpr x y body =
4343 case body of
4444 ExprApp fnExpr args
45− | isCompare imports fnExpr ->
46− case NEA.toArray args of
47− [AppTerm fxExpr, AppTerm fyExpr] ->
48− case extractFunctionCall (unwrapParens fxExpr), extractFunctionCall (unwrapParens fyExpr) of
49− Just { fn: f1, arg: arg1 }, Just { fn: f2, arg: arg2 }
50− | f1 == f2 && arg1 == x && arg2 == y ->
51− [ LintWarning
52− { ruleId: RuleId "UseComparing"
53− , message: WarningMessage "\\x y -> compare (f x) (f y) can be simplified to comparing f"
54− , range: rangeOf fullExpr
55− , severity: Hint
56− , suggestion: Just $ Suggestion
57− { replacement: ReplacementText ("comparing " <> f1)
58− , description: SuggestionDescription "Use comparing instead of lambda with compare"
59− }
60− }
61− ]
62− _, _ -> []
63− _ -> []
45+ | isCompare imports fnExpr
46+ , [ AppTerm fxExpr, AppTerm fyExpr ] <- NEA.toArray args
47+ , Just { fn: f1, arg: arg1 } <- extractFunctionCall (unwrapParens fxExpr)
48+ , Just { fn: f2, arg: arg2 } <- extractFunctionCall (unwrapParens fyExpr)
49+ , f1 == f2 && arg1 == x && arg2 == y ->
50+ [ LintWarning
51+ { ruleId: RuleId "UseComparing"
52+ , message: WarningMessage "\\x y -> compare (f x) (f y) can be simplified to comparing f"
53+ , range: rangeOf fullExpr
54+ , severity: Hint
55+ , suggestion: Just $ Suggestion
56+ { replacement: ReplacementText ("comparing " <> f1)
57+ , description: SuggestionDescription "Use comparing instead of lambda with compare"
58+ , requiredImports: []
59+ }
60+ }
61+ ]
6462 _ -> []
6563
6664 isCompare :: ImportInfo -> Expr Void -> Boolean
67− isCompare imports (ExprIdent (QualifiedName { name: Ident name })) =
68− name == "compare" && hasValue imports "compare"
69− isCompare _ _ = false
65+ isCompare = case _, _ of
66+ imports, ExprIdent (QualifiedName { name: Ident name }) -> name == "compare" && hasValue imports "compare"
67+ _, _ -> false
7068
7169 extractFunctionCall :: Expr Void -> Maybe { fn :: String, arg :: String }
7270 extractFunctionCall (ExprApp fnExpr args) =
7371 case fnExpr, NEA.toArray args of
74− ExprIdent (QualifiedName { name: Ident fn }), [AppTerm argExpr] ->
72+ ExprIdent (QualifiedName { name: Ident fn }), [ AppTerm argExpr ] ->
7573 case argExpr of
7674 ExprIdent (QualifiedName { name: Ident arg }) -> Just { fn, arg }
7775 _ -> Nothing
7876 _, _ -> Nothing
7977 extractFunctionCall _ = Nothing
78+
lib/purslint/src/Purslint/Rules/UseConst.pursnew file+93
@@ -0,0 +1,93 @@
1+module Purslint.Rules.UseConst where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Maybe (Maybe(..))
8+import Data.Monoid (power)
9+import Data.String as String
10+import Data.Void (Void)
11+import Purslint.Imports (ImportInfo, hasValue)
12+import Purslint.Print (printExpr, printExprMultiline)
13+import Purslint.Rule (Rule, RuleContext, mkRule)
14+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
15+import PureScript.CST.Range (rangeOf)
16+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
17+import PureScript.CST.Types (Binder(..), Expr(..), Module)
18+
19+-- | Rule: \_ -> y -> const y
20+-- | Only triggers when the binder is a simple unused variable
21+useConstRule :: Rule
22+useConstRule = mkRule (RuleId "UseConst") run
23+ where
24+ run :: RuleContext -> Module Void -> Array LintWarning
25+ run ctx = foldMapModule visitor
26+ where
27+ imports = ctx.imports
28+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
29+
30+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
31+ checkExpr imports expr = case expr of
32+ ExprLambda lambda | hasValue imports "const" ->
33+ case NEA.toArray lambda.binders of
34+ -- Only match wildcard binder: \_ -> body
35+ -- Named unused params like \f -> body are already warned by the compiler
36+ [BinderWildcard _] ->
37+ let
38+ bodyRange = rangeOf lambda.body
39+ isMultiline = bodyRange.start.line /= bodyRange.end.line
40+ replacement =
41+ if isMultiline then
42+ -- Preserve multiline structure: const\n<original body>
43+ let
44+ bodyStr = printExprMultiline lambda.body
45+ -- Get the column where the body starts for indent reference
46+ bodyCol = bodyRange.start.column
47+ indent n = power " " n
48+ -- The first line has no indentation (due to trim), subsequent lines have original
49+ lines = String.split (String.Pattern "\n") bodyStr
50+ -- Find minimum non-empty line indent from lines AFTER the first
51+ minIndent = case Array.tail lines of
52+ Nothing -> 0
53+ Just rest -> rest
54+ # Array.filter (\l -> String.trim l /= "")
55+ # map getIndent
56+ # Array.foldl min 999
57+ -- Re-indent: first line gets bodyCol indent, rest get normalized
58+ reindented = case Array.uncons lines of
59+ Nothing -> ""
60+ Just { head: first, tail: rest } ->
61+ let
62+ firstReindented = indent bodyCol <> first
63+ restReindented = rest # map (\l ->
64+ if String.trim l == "" then ""
65+ else indent bodyCol <> String.drop minIndent l
66+ )
67+ in String.joinWith "\n" (Array.cons firstReindented restReindented)
68+ in
69+ "const\n" <> reindented
70+ else
71+ "const " <> printExpr lambda.body
72+ in
73+ [ LintWarning
74+ { ruleId: RuleId "UseConst"
75+ , message: WarningMessage "Lambda ignores its argument"
76+ , range: rangeOf expr
77+ , severity: Hint
78+ , suggestion: Just $ Suggestion
79+ { replacement: ReplacementText replacement
80+ , description: SuggestionDescription "\\_ -> y can be simplified to const y"
81+ , requiredImports: []
82+ }
83+ }
84+ ]
85+ _ -> []
86+ _ -> []
87+
88+ -- Get the number of leading spaces in a string
89+ getIndent :: String -> Int
90+ getIndent s =
91+ let loop i = if String.take 1 (String.drop i s) == " " then loop (i + 1) else i
92+ in loop 0
93+
lib/purslint/src/Purslint/Rules/UseDollar.pursnew file+97
@@ -0,0 +1,97 @@
1+module Purslint.Rules.UseDollar where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Maybe (Maybe(..))
8+import Purslint.Print (printExpr, printType)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
11+import PureScript.CST.Range (rangeOf)
12+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
13+import PureScript.CST.Types (AppSpine(..), Expr(..), Module, Wrapped(..))
14+
15+-- | Rule: Replace parens around last function argument with $
16+-- |
17+-- | f (expr) -> f $ expr
18+-- | map f (filter g xs) -> map f $ filter g xs
19+-- |
20+-- | Only applies when the inner expression is non-atomic (otherwise
21+-- | the parens are likely redundant and RedundantParens will catch it).
22+useDollarRule :: Rule
23+useDollarRule = mkRule (RuleId "UseDollar") run
24+ where
25+ run :: RuleContext -> Module Void -> Array LintWarning
26+ run _ctx = foldMapModule visitor
27+ where
28+ visitor = defaultMonoidalVisitor { onExpr = checkExpr }
29+
30+ checkExpr :: Expr Void -> Array LintWarning
31+ checkExpr expr = case expr of
32+ -- Match function application where last arg is parenthesized
33+ -- f a b (expr) -> f a b $ expr
34+ ExprApp fn args ->
35+ case NEA.last args of
36+ AppTerm (ExprParens (Wrapped { value: inner }))
37+ -- Don't suggest $ for atomic expressions - parens are likely redundant anyway
38+ | not (isAtomicExpr inner)
39+ -- Don't suggest $ if inner is a section (would change semantics)
40+ , not (isSection inner) ->
41+ [ mkWarning expr fn args inner ]
42+ _ -> []
43+
44+ _ -> []
45+
46+ -- Check if expression is atomic (never needs parens)
47+ isAtomicExpr :: Expr Void -> Boolean
48+ isAtomicExpr = case _ of
49+ ExprIdent _ -> true
50+ ExprConstructor _ -> true
51+ ExprInt _ _ -> true
52+ ExprNumber _ _ -> true
53+ ExprString _ _ -> true
54+ ExprChar _ _ -> true
55+ ExprBoolean _ _ -> true
56+ ExprArray _ -> true
57+ ExprRecord _ -> true
58+ ExprParens _ -> true
59+ ExprRecordAccessor _ -> true
60+ _ -> false
61+
62+ -- Check if expression is a section (would change semantics with $)
63+ isSection :: Expr Void -> Boolean
64+ isSection = case _ of
65+ ExprSection _ -> true
66+ ExprParens (Wrapped { value }) -> isSection value
67+ _ -> false
68+
69+ mkWarning :: Expr Void -> Expr Void -> NEA.NonEmptyArray (AppSpine Expr Void) -> Expr Void -> LintWarning
70+ mkWarning outer fn args inner =
71+ let
72+ argsArray = NEA.toArray args
73+ initArgs = Array.dropEnd 1 argsArray
74+
75+ prefix = case initArgs of
76+ [] -> printExpr fn
77+ _ -> printExpr fn <> " " <> (initArgs # map printAppSpine # Array.intercalate " ")
78+
79+ replacement = prefix <> " $ " <> printExpr inner
80+ in
81+ LintWarning
82+ { ruleId: RuleId "UseDollar"
83+ , message: WarningMessage "Use $ instead of parentheses"
84+ , range: rangeOf outer
85+ , severity: Hint
86+ , suggestion: Just $ Suggestion
87+ { replacement: ReplacementText replacement
88+ , description: SuggestionDescription "Replace parentheses with $"
89+ , requiredImports: []
90+ }
91+ }
92+
93+ printAppSpine :: AppSpine Expr Void -> String
94+ printAppSpine = case _ of
95+ AppTerm e -> printExpr e
96+ AppType _ t -> "@" <> printType t
97+
lib/purslint/src/Purslint/Rules/UseEitherMap.pursnew file+117
@@ -0,0 +1,117 @@
1+module Purslint.Rules.UseEitherMap where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Foldable (foldMap)
7+import Data.Maybe (Maybe(..))
8+import Data.Tuple (Tuple(..))
9+import Data.Void (Void)
10+import PureScript.CST.Range (rangeOf)
11+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
12+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
13+import Purslint.Imports (ImportInfo, hasValue)
14+import Purslint.Print (printExpr)
15+import Purslint.Rule (Rule, RuleContext, mkRule)
16+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
17+
18+-- | Rule: either Left (Right <<< f) -> map f
19+useEitherMapRule :: Rule
20+useEitherMapRule = mkRule (RuleId "UseEitherMap") run
21+ where
22+ run :: RuleContext -> Module Void -> Array LintWarning
23+ run ctx = foldMapModule visitor
24+ where
25+ imports = ctx.imports
26+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
27+
28+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
29+ checkExpr imports expr = case expr of
30+ -- Match: either Left (Right <<< f)
31+ ExprApp fn args
32+ | isEither imports fn ->
33+ case NEA.toArray args of
34+ [ AppTerm firstArg, AppTerm secondArg ] ->
35+ if isLeft imports firstArg then
36+ foldMap
37+ ( \f -> do
38+ let
39+ fText = printExpr f
40+ [ LintWarning
41+ { ruleId: RuleId "UseEitherMap"
42+ , message: WarningMessage "either Left (Right <<< f) can be simplified to map f"
43+ , range: rangeOf expr
44+ , severity: Warning
45+ , suggestion: Just $ Suggestion
46+ { replacement: ReplacementText ("map " <> fText)
47+ , description: SuggestionDescription "Use map instead of either Left (Right <<< f)"
48+ , requiredImports: []
49+ }
50+ }
51+ ]
52+ )
53+ (getRightComposed imports secondArg)
54+ else []
55+ [ AppTerm firstArg, AppTerm secondArg, AppTerm xArg ] ->
56+ if isLeft imports firstArg then
57+ case getRightComposed imports secondArg of
58+ Just f ->
59+ let
60+ fText = printExpr f
61+ x = printExpr xArg
62+ in
63+ [ LintWarning
64+ { ruleId: RuleId "UseEitherMap"
65+ , message: WarningMessage "either Left (Right <<< f) x can be simplified to map f x"
66+ , range: rangeOf expr
67+ , severity: Warning
68+ , suggestion: Just $ Suggestion
69+ { replacement: ReplacementText ("map " <> fText <> " " <> x)
70+ , description: SuggestionDescription "Use map instead of either Left (Right <<< f)"
71+ , requiredImports: []
72+ }
73+ }
74+ ]
75+ Nothing -> []
76+ else []
77+ _ -> []
78+ _ -> []
79+
80+ unwrapParens :: Expr Void -> Expr Void
81+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
82+ unwrapParens e = e
83+
84+ isEither :: ImportInfo -> Expr Void -> Boolean
85+ isEither imports (ExprIdent (QualifiedName { name: Ident name })) =
86+ name == "either" && hasValue imports "either"
87+ isEither _ _ = false
88+
89+ isLeft :: ImportInfo -> Expr Void -> Boolean
90+ isLeft imports e =
91+ case unwrapParens e of
92+ ExprIdent (QualifiedName { name: Ident name }) ->
93+ name == "Left" && hasValue imports "Left"
94+ ExprConstructor (QualifiedName { name: _ }) -> true -- Allow constructor syntax
95+ _ -> false
96+
97+ isRight :: ImportInfo -> Expr Void -> Boolean
98+ isRight imports e =
99+ case unwrapParens e of
100+ ExprIdent (QualifiedName { name: Ident name }) ->
101+ name == "Right" && hasValue imports "Right"
102+ ExprConstructor (QualifiedName { name: _ }) -> true
103+ _ -> false
104+
105+ -- Check for (Right <<< f) pattern
106+ getRightComposed :: ImportInfo -> Expr Void -> Maybe (Expr Void)
107+ getRightComposed imports e =
108+ case unwrapParens e of
109+ ExprOp lhs ops ->
110+ case NEA.toArray ops of
111+ [ Tuple (QualifiedName { name: Operator "<<<" }) rhs ]
112+ | isRight imports lhs -> Just rhs
113+ [ Tuple (QualifiedName { name: Operator ">>>" }) rhs ]
114+ | isRight imports rhs -> Just lhs
115+ _ -> Nothing
116+ _ -> Nothing
117+
lib/purelint/src/Purelint/Rules/UseElemIndex.purs → lib/purslint/src/Purslint/Rules/UseElemIndex.pursrenamed+8−5
@@ -1,4 +1,4 @@
1−module Purelint.Rules.UseElemIndex where
1+module Purslint.Rules.UseElemIndex where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (AppSpine(..), Binder(..), Expr(..), Ident(..), Module, Name(..), Operator(..), QualifiedName(..), Wrapped(..))
@@ -44,6 +44,7 @@ useElemIndexRule = mkRule (RuleId "UseElemIndex") run
4444 , suggestion: Just $ Suggestion
4545 { replacement: ReplacementText ("elemIndex " <> aText)
4646 , description: SuggestionDescription "Use elemIndex instead of findIndex (== a)"
47+ , requiredImports: []
4748 }
4849 }
4950 ]
@@ -63,6 +64,7 @@ useElemIndexRule = mkRule (RuleId "UseElemIndex") run
6364 , suggestion: Just $ Suggestion
6465 { replacement: ReplacementText ("elemIndex " <> aText <> " " <> x)
6566 , description: SuggestionDescription "Use elemIndex instead of findIndex (== a)"
67+ , requiredImports: []
6668 }
6769 }
6870 ]
@@ -126,3 +128,4 @@ useElemIndexRule = mkRule (RuleId "UseElemIndex") run
126128 getBinderName (BinderVar (Name { name: Ident name })) = Just name
127129 getBinderName (BinderParens (Wrapped { value: inner })) = getBinderName inner
128130 getBinderName _ = Nothing
131+
lib/purelint/src/Purelint/Rules/UseFindMap.purs → lib/purslint/src/Purslint/Rules/UseFindMap.pursrenamed+13−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseFindMap where
1+module Purslint.Rules.UseFindMap where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -56,6 +56,13 @@ useFindMapRule = mkRule (RuleId "UseFindMap") run
5656 , suggestion: Just $ Suggestion
5757 { replacement: ReplacementText ("findMap " <> f <> " " <> x)
5858 , description: SuggestionDescription "Use findMap instead of head composed with mapMaybe"
59+ , requiredImports:
60+ [ { moduleName: "Data.Array"
61+ , importItem: Just "findMap"
62+ , codeText: Just "findMap"
63+ , qualifier: Nothing
64+ }
65+ ]
5966 }
6067 }
6168 ]
@@ -71,3 +78,4 @@ useFindMapRule = mkRule (RuleId "UseFindMap") run
7178 isMapMaybe imports (ExprIdent (QualifiedName { name: Ident name })) =
7279 name == "mapMaybe" && hasValue imports "mapMaybe"
7380 isMapMaybe _ _ = false
81+
lib/purelint/src/Purelint/Rules/UseFold.purs → lib/purslint/src/Purslint/Rules/UseFold.pursrenamed+6−4
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseFold where
1+module Purslint.Rules.UseFold where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..))
@@ -36,6 +36,7 @@ useFoldRule = mkRule (RuleId "UseFold") run
3636 , suggestion: Just $ Suggestion
3737 { replacement: ReplacementText "fold"
3838 , description: SuggestionDescription "Use fold instead of foldMap identity"
39+ , requiredImports: []
3940 }
4041 }
4142 ]
@@ -51,3 +52,4 @@ useFoldRule = mkRule (RuleId "UseFold") run
5152 isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
5253 name == "identity" && hasValue imports "identity"
5354 isIdentity _ _ = false
55+
lib/purslint/src/Purslint/Rules/UseFoldBool.pursnew file+90
@@ -0,0 +1,90 @@
1+module Purslint.Rules.UseFoldBool where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Rule (Rule, RuleContext, mkRule)
9+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
10+import PureScript.CST.Range (rangeOf)
11+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
12+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
13+
14+-- | Fold simplification rules:
15+-- | - foldr (&&) true -> and
16+-- | - foldr (||) false -> or
17+-- | - foldl (&&) true -> and
18+-- | - foldl (||) false -> or
19+useFoldBoolRule :: Rule
20+useFoldBoolRule = mkRule (RuleId "UseFoldBool") run
21+ where
22+ run :: RuleContext -> Module Void -> Array LintWarning
23+ run ctx = foldMapModule visitor
24+ where
25+ imports = ctx.imports
26+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
27+
28+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
29+ checkExpr imports expr = case expr of
30+ -- Match: foldr f z or foldl f z
31+ ExprApp fn args
32+ | isFoldr imports fn || isFoldl imports fn ->
33+ case NEA.toArray args of
34+ [AppTerm opArg, AppTerm initArg] -> checkFoldArgs imports expr fn opArg initArg
35+ [AppTerm opArg, AppTerm initArg, _] -> checkFoldArgs imports expr fn opArg initArg
36+ _ -> []
37+ _ -> []
38+
39+ checkFoldArgs :: ImportInfo -> Expr Void -> Expr Void -> Expr Void -> Expr Void -> Array LintWarning
40+ checkFoldArgs imports fullExpr _fn opArg initArg
41+ -- foldr/foldl (&&) true -> and
42+ | isAndOp opArg && isTrue initArg && hasValue imports "and" =
43+ [ mkWarning fullExpr "foldr/foldl (&&) true" "and" ]
44+ -- foldr/foldl (||) false -> or
45+ | isOrOp opArg && isFalse initArg && hasValue imports "or" =
46+ [ mkWarning fullExpr "foldr/foldl (||) false" "or" ]
47+ | otherwise = []
48+
49+ isFoldr :: ImportInfo -> Expr Void -> Boolean
50+ isFoldr imports (ExprIdent (QualifiedName { name: Ident name })) =
51+ name == "foldr" && hasValue imports "foldr"
52+ isFoldr _ _ = false
53+
54+ isFoldl :: ImportInfo -> Expr Void -> Boolean
55+ isFoldl imports (ExprIdent (QualifiedName { name: Ident name })) =
56+ name == "foldl" && hasValue imports "foldl"
57+ isFoldl _ _ = false
58+
59+ isAndOp :: Expr Void -> Boolean
60+ isAndOp (ExprOpName (QualifiedName { name: Operator "&&" })) = true
61+ isAndOp (ExprIdent (QualifiedName { name: Ident "conj" })) = true
62+ isAndOp _ = false
63+
64+ isOrOp :: Expr Void -> Boolean
65+ isOrOp (ExprOpName (QualifiedName { name: Operator "||" })) = true
66+ isOrOp (ExprIdent (QualifiedName { name: Ident "disj" })) = true
67+ isOrOp _ = false
68+
69+ isTrue :: Expr Void -> Boolean
70+ isTrue (ExprBoolean _ b) = b
71+ isTrue _ = false
72+
73+ isFalse :: Expr Void -> Boolean
74+ isFalse (ExprBoolean _ b) = not b
75+ isFalse _ = false
76+
77+ mkWarning :: Expr Void -> String -> String -> LintWarning
78+ mkWarning fullExpr pattern replacement =
79+ LintWarning
80+ { ruleId: RuleId "UseFoldBool"
81+ , message: WarningMessage $ "Use " <> replacement <> " instead of " <> pattern
82+ , range: rangeOf fullExpr
83+ , severity: Hint
84+ , suggestion: Just $ Suggestion
85+ { replacement: ReplacementText replacement
86+ , description: SuggestionDescription $ "Replace with " <> replacement
87+ , requiredImports: []
88+ }
89+ }
90+
lib/purslint/src/Purslint/Rules/UseFoldMap.pursnew file+167
@@ -0,0 +1,167 @@
1+module Purslint.Rules.UseFoldMap where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Maybe (Maybe(..))
8+import Data.Monoid (power)
9+import Data.Tuple (Tuple, snd)
10+import Data.Tuple.Nested ((/\))
11+import Data.String as String
12+import Purslint.Print (printExpr, printExprMultiline)
13+import Purslint.Rule (Rule, RuleContext, mkRule)
14+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
15+import PureScript.CST.Range (rangeOf)
16+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
17+import PureScript.CST.Types (Binder(..), Expr(..), Guarded(..), Ident(..), Module, Name(..), Proper(..), QualifiedName(..), Separated(..), Where(..), Wrapped(..), AppSpine(..))
18+
19+-- | Rule: case mx of Nothing -> mempty; Just x -> f x --> foldMap f mx
20+-- | Also: case mx of Nothing -> ""; Just x -> f x --> foldMap f mx (String is Monoid)
21+useFoldMapRule :: Rule
22+useFoldMapRule = mkRule (RuleId "UseFoldMap") run
23+ where
24+ run :: RuleContext -> Module Void -> Array LintWarning
25+ run _ctx = foldMapModule visitor
26+ where
27+ visitor = defaultMonoidalVisitor { onExpr = checkExpr }
28+
29+ checkExpr :: Expr Void -> Array LintWarning
30+ checkExpr expr = case expr of
31+ ExprCase caseOf
32+ | [scrutinee] <- separatedToArray caseOf.head
33+ , [branch1, branch2] <- NEA.toArray caseOf.branches
34+ -> checkMaybeCase expr scrutinee branch1 branch2
35+ <> checkMaybeCase expr scrutinee branch2 branch1
36+ _ -> []
37+
38+ checkMaybeCase expr scrutinee nothingBranch justBranch
39+ | isNothingBranch nothingBranch
40+ , Just isDefinitelyMempty <- isMemptyResult nothingBranch
41+ , Just { var, body } <- getJustBranch justBranch
42+ = let
43+ -- Get the column where this expression starts for proper indentation
44+ exprCol = (rangeOf expr).start.column
45+ indent n = power " " n
46+
47+ -- Severity: Hint if definitely mempty, Refactor if [] (needs Monoid constraint)
48+ severity = if isDefinitelyMempty then Hint else Refactor
49+ messageNote = if isDefinitelyMempty then "" else " (requires Monoid on element type)"
50+
51+ -- Check if body is `f ... var` where last arg is our variable
52+ suggestion = case body of
53+ ExprApp fn args
54+ | argsArr <- NEA.toArray args
55+ , Just (AppTerm lastArg) <- Array.last argsArr
56+ , isVar var lastArg
57+ -> let
58+ remainingArgs = Array.dropEnd 1 argsArr
59+ fnPart = if Array.null remainingArgs
60+ then wrapIfNeeded fn
61+ else "(" <> printExpr fn <> " " <> Array.intercalate " " (map printAppSpine remainingArgs) <> ")"
62+ in "foldMap " <> fnPart <> " " <> wrapIfNeeded scrutinee
63+ -- Body uses var in a more complex way, need a lambda
64+ _ ->
65+ let
66+ bodyStr = printExprMultiline body
67+ bodyRange = rangeOf body
68+ isMultiline = bodyRange.start.line /= bodyRange.end.line
69+ in
70+ if isMultiline then
71+ -- Format multiline lambda with absolute indentation based on expression column
72+ let
73+ -- Base indent for continuation lines (where foldMap is)
74+ baseIndent = exprCol
75+ -- Lambda paren indent (2 more than foldMap)
76+ parenIndent = baseIndent + 2
77+ -- Body indent (4 more than paren = 6 more than foldMap)
78+ bodyIndent = baseIndent + 6
79+
80+ -- Normalize indentation: strip original indent, add target indent
81+ lines = String.split (String.Pattern "\n") bodyStr
82+ -- Find minimum non-empty line indent (skip first line since trim removes its indent)
83+ minIndent = case Array.tail lines of
84+ Nothing -> 0
85+ Just rest -> rest
86+ # Array.filter (\l -> String.trim l /= "")
87+ # map getIndent
88+ # Array.foldl min 999
89+ -- Strip minIndent from each line (except first) and add bodyIndent
90+ reindented = case Array.uncons lines of
91+ Nothing -> ""
92+ Just { head: first, tail: rest } ->
93+ let
94+ reindentedRest = rest # map (\l ->
95+ if String.trim l == "" then ""
96+ else indent bodyIndent <> String.drop minIndent l
97+ )
98+ in String.joinWith "\n" (Array.cons (indent bodyIndent <> first) reindentedRest)
99+ in
100+ "foldMap\n" <> indent parenIndent <> "( \\" <> var <> " ->\n" <> reindented <> "\n" <> indent parenIndent <> ")\n" <> indent parenIndent <> wrapIfNeeded scrutinee
101+ else
102+ "foldMap (\\" <> var <> " -> " <> printExpr body <> ") " <> wrapIfNeeded scrutinee
103+ in
104+ [ LintWarning
105+ { ruleId: RuleId "UseFoldMap"
106+ , message: WarningMessage $ "case of Nothing/Just with mempty can be simplified to foldMap" <> messageNote
107+ , range: rangeOf expr
108+ , severity: severity
109+ , suggestion: Just $ Suggestion
110+ { replacement: ReplacementText suggestion
111+ , description: SuggestionDescription "Use foldMap instead of case with Nothing -> mempty"
112+ , requiredImports: []
113+ }
114+ }
115+ ]
116+ | otherwise = []
117+
118+ printAppSpine :: AppSpine Expr Void -> String
119+ printAppSpine (AppTerm e) = printExpr e
120+ printAppSpine (AppType _ _) = "@_"
121+
122+ isNothingBranch branch = case branch of
123+ (Separated { head: BinderConstructor (QualifiedName { name: Proper "Nothing" }) [] }) /\ _ -> true
124+ _ -> false
125+
126+ -- Returns Just true if definitely mempty, Just false if [] (needs Monoid on element), Nothing if not mempty
127+ isMemptyResult :: Tuple (Separated (Binder Void)) (Guarded Void) -> Maybe Boolean
128+ isMemptyResult branch = case branch of
129+ _ /\ Unconditional _ (Where { expr: e, bindings: Nothing }) -> isMemptyCase (unwrapParens e)
130+ _ -> Nothing
131+
132+ -- Returns Just true if definitely mempty, Just false if [] (needs Monoid on element), Nothing if not mempty
133+ isMemptyCase :: Expr Void -> Maybe Boolean
134+ isMemptyCase (ExprIdent (QualifiedName { name: Ident "mempty" })) = Just true
135+ isMemptyCase (ExprString _ "") = Just true -- "" is mempty for String
136+ isMemptyCase (ExprArray (Wrapped { value: Nothing })) = Just false -- [] needs Monoid on element type
137+ isMemptyCase _ = Nothing
138+
139+ getJustBranch branch = case branch of
140+ (Separated { head: BinderConstructor (QualifiedName { name: Proper "Just" }) [BinderVar (Name { name: Ident var })] })
141+ /\ Unconditional _ (Where { expr: body, bindings: Nothing })
142+ -> Just { var, body }
143+ _ -> Nothing
144+
145+ isVar :: String -> Expr Void -> Boolean
146+ isVar name (ExprIdent (QualifiedName { name: Ident n })) = n == name
147+ isVar _ _ = false
148+
149+ unwrapParens :: Expr Void -> Expr Void
150+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
151+ unwrapParens e = e
152+
153+ wrapIfNeeded :: Expr Void -> String
154+ wrapIfNeeded e = case e of
155+ ExprIdent _ -> printExpr e
156+ ExprParens _ -> printExpr e
157+ _ -> "(" <> printExpr e <> ")"
158+
159+ separatedToArray :: forall a. Separated a -> Array a
160+ separatedToArray (Separated { head, tail }) = Array.cons head (map snd tail)
161+
162+ -- Get the number of leading spaces in a string
163+ getIndent :: String -> Int
164+ getIndent s =
165+ let loop i = if String.take 1 (String.drop i s) == " " then loop (i + 1) else i
166+ in loop 0
167+
lib/purelint/src/Purelint/Rules/UseFoldMapId.purs → lib/purslint/src/Purslint/Rules/UseFoldMapId.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseFoldMapId where
1+module Purslint.Rules.UseFoldMapId where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -39,6 +39,7 @@ useFoldMapIdRule = mkRule (RuleId "UseFoldMapId") run
3939 , suggestion: Just $ Suggestion
4040 { replacement: ReplacementText "fold"
4141 , description: SuggestionDescription "Use fold instead of foldMap identity"
42+ , requiredImports: []
4243 }
4344 }
4445 ]
@@ -55,6 +56,7 @@ useFoldMapIdRule = mkRule (RuleId "UseFoldMapId") run
5556 , suggestion: Just $ Suggestion
5657 { replacement: ReplacementText ("fold " <> x)
5758 , description: SuggestionDescription "Use fold instead of foldMap identity"
59+ , requiredImports: []
5860 }
5961 }
6062 ]
@@ -75,3 +77,4 @@ useFoldMapIdRule = mkRule (RuleId "UseFoldMapId") run
7577 isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
7678 (name == "identity" || name == "id") && hasValue imports "identity"
7779 isIdentity _ _ = false
80+
lib/purelint/src/Purelint/Rules/UseFor.purs → lib/purslint/src/Purslint/Rules/UseFor.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseFor where
1+module Purslint.Rules.UseFor where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -51,6 +51,7 @@ useForRule = mkRule (RuleId "UseFor") run
5151 , suggestion: Just $ Suggestion
5252 { replacement: ReplacementText "for"
5353 , description: SuggestionDescription "Use for instead of flip traverse"
54+ , requiredImports: []
5455 }
5556 }
5657 ]
@@ -63,6 +64,7 @@ useForRule = mkRule (RuleId "UseFor") run
6364 , suggestion: Just $ Suggestion
6465 { replacement: ReplacementText "for_"
6566 , description: SuggestionDescription "Use for_ instead of flip traverse_"
67+ , requiredImports: []
6668 }
6769 }
6870 ]
@@ -72,3 +74,4 @@ useForRule = mkRule (RuleId "UseFor") run
7274 isFlip imports (ExprIdent (QualifiedName { name: Ident name })) =
7375 name == "flip" && hasValue imports "flip"
7476 isFlip _ _ = false
77+
lib/purelint/src/Purelint/Rules/UseFromJust.purs → lib/purslint/src/Purslint/Rules/UseFromJust.pursrenamed+9−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseFromJust where
1+module Purslint.Rules.UseFromJust where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Proper(..), QualifiedName(..), Wrapped(..))
@@ -43,6 +43,7 @@ useFromJustRule = mkRule (RuleId "UseFromJust") run
4343 , suggestion: Just $ Suggestion
4444 { replacement: ReplacementText xText
4545 , description: SuggestionDescription "Simplify fromMaybe with Just"
46+ , requiredImports: []
4647 }
4748 }
4849 ]
@@ -59,6 +60,7 @@ useFromJustRule = mkRule (RuleId "UseFromJust") run
5960 , suggestion: Just $ Suggestion
6061 { replacement: ReplacementText d
6162 , description: SuggestionDescription "Simplify fromMaybe with Nothing"
63+ , requiredImports: []
6264 }
6365 }
6466 ]
@@ -73,6 +75,7 @@ useFromJustRule = mkRule (RuleId "UseFromJust") run
7375 , suggestion: Just $ Suggestion
7476 { replacement: ReplacementText d
7577 , description: SuggestionDescription "Simplify fromMaybe with Nothing"
78+ , requiredImports: []
7679 }
7780 }
7881 ]
@@ -102,3 +105,4 @@ useFromJustRule = mkRule (RuleId "UseFromJust") run
102105 _ -> Nothing
103106 _ -> Nothing
104107 getJustArg _ = Nothing
108+
lib/purelint/src/Purelint/Rules/UseFromMaybe.purs → lib/purslint/src/Purslint/Rules/UseFromMaybe.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseFromMaybe where
1+module Purslint.Rules.UseFromMaybe where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..))
@@ -39,6 +39,7 @@ useFromMaybeRule = mkRule (RuleId "UseFromMaybe") run
3939 , suggestion: Just $ Suggestion
4040 { replacement: ReplacementText ("fromMaybe " <> defaultText)
4141 , description: SuggestionDescription "Use fromMaybe instead of maybe x id"
42+ , requiredImports: []
4243 }
4344 }
4445 ]
@@ -54,3 +55,4 @@ useFromMaybeRule = mkRule (RuleId "UseFromMaybe") run
5455 isId imports (ExprIdent (QualifiedName { name: Ident name })) =
5556 name == "identity" && hasValue imports "identity"
5657 isId _ _ = false
58+
lib/purelint/src/Purelint/Rules/UseFstSnd.purs → lib/purslint/src/Purslint/Rules/UseFstSnd.pursrenamed+6−3
@@ -1,12 +1,12 @@
1−module Purelint.Rules.UseFstSnd where
1+module Purslint.Rules.UseFstSnd where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Rule (Rule, RuleContext, mkRule)
9−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Rule (Rule, RuleContext, mkRule)
9+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1010import PureScript.CST.Range (rangeOf)
1111import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1212import PureScript.CST.Types (Binder(..), Expr(..), Ident(..), Module, Name(..), Proper(..), QualifiedName(..), Wrapped(..))
@@ -40,6 +40,7 @@ useFstSndRule = mkRule (RuleId "UseFstSnd") run
4040 , suggestion: Just $ Suggestion
4141 { replacement: ReplacementText "fst"
4242 , description: SuggestionDescription "Use fst instead of lambda extracting first element"
43+ , requiredImports: []
4344 }
4445 }
4546 ]
@@ -52,6 +53,7 @@ useFstSndRule = mkRule (RuleId "UseFstSnd") run
5253 , suggestion: Just $ Suggestion
5354 { replacement: ReplacementText "snd"
5455 , description: SuggestionDescription "Use snd instead of lambda extracting second element"
56+ , requiredImports: []
5557 }
5658 }
5759 ]
@@ -77,3 +79,4 @@ useFstSndRule = mkRule (RuleId "UseFstSnd") run
7779 getBinderName (BinderWildcard _) = Nothing -- wildcards can't be referenced in body
7880 getBinderName (BinderParens (Wrapped { value: inner })) = getBinderName inner
7981 getBinderName _ = Nothing
82+
lib/purelint/src/Purelint/Rules/UseGuard.purs → lib/purslint/src/Purslint/Rules/UseGuard.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseGuard where
1+module Purslint.Rules.UseGuard where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -54,6 +54,7 @@ useGuardRule = mkRule (RuleId "UseGuard") run
5454 , suggestion: Just $ Suggestion
5555 { replacement: ReplacementText replacement
5656 , description: SuggestionDescription "if cond then pure unit else x can be replaced with unless cond x"
57+ , requiredImports: []
5758 }
5859 }
5960 ]
@@ -71,6 +72,7 @@ useGuardRule = mkRule (RuleId "UseGuard") run
7172 , suggestion: Just $ Suggestion
7273 { replacement: ReplacementText replacement
7374 , description: SuggestionDescription "if cond then x else pure unit can be replaced with when cond x"
75+ , requiredImports: []
7476 }
7577 }
7678 ]
@@ -97,3 +99,4 @@ useGuardRule = mkRule (RuleId "UseGuard") run
9799 isUnit :: Expr Void -> Boolean
98100 isUnit (ExprIdent (QualifiedName { name: Ident name })) = name == "unit"
99101 isUnit _ = false
102+
lib/purelint/src/Purelint/Rules/UseGuardMaybe.purs → lib/purslint/src/Purslint/Rules/UseGuardMaybe.pursrenamed+7−5
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseGuardMaybe where
1+module Purslint.Rules.UseGuardMaybe where
22
33import Prelude
44
55import Data.Maybe (Maybe(..))
66import Data.Void (Void)
7−import Purelint.Imports (ImportInfo, hasValue)
8−import Purelint.Print (printExpr)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -42,6 +42,7 @@ useGuardMaybeRule = mkRule (RuleId "UseGuardMaybe") run
4242 , suggestion: Just $ Suggestion
4343 { replacement: ReplacementText ("guard " <> c <> " $> " <> x)
4444 , description: SuggestionDescription "Use guard c $> x instead of if-then-else with Maybe"
45+ , requiredImports: []
4546 }
4647 }
4748 ]
@@ -75,3 +76,4 @@ useGuardMaybeRule = mkRule (RuleId "UseGuardMaybe") run
7576 isNothing imports (ExprConstructor (QualifiedName { name: _ })) =
7677 hasValue imports "Nothing"
7778 isNothing _ _ = false
79+
lib/purelint/src/Purelint/Rules/UseHead.purs → lib/purslint/src/Purslint/Rules/UseHead.pursrenamed+6−4
@@ -1,12 +1,12 @@
1−module Purelint.Rules.UseHead where
1+module Purslint.Rules.UseHead where
22
33import Prelude
44
55import Data.Maybe (Maybe(..))
66import Data.Void (Void)
7−import Purelint.Imports (ImportInfo, hasValue)
8−import Purelint.Rule (Rule, RuleContext, mkRule)
9−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Rule (Rule, RuleContext, mkRule)
9+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1010import PureScript.CST.Range (rangeOf)
1111import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1212import PureScript.CST.Types (Expr(..), Ident(..), IntValue(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
@@ -39,6 +39,7 @@ useHeadRule = mkRule (RuleId "UseHead") run
3939 , suggestion: Just $ Suggestion
4040 { replacement: ReplacementText "head"
4141 , description: SuggestionDescription "Use head instead of !! 0"
42+ , requiredImports: []
4243 }
4344 }
4445 ]
@@ -54,3 +55,4 @@ useHeadRule = mkRule (RuleId "UseHead") run
5455 isZeroLiteral (ExprInt _ (SmallInt 0)) = true
5556 isZeroLiteral (ExprInt _ (BigInt s)) = s == "0"
5657 isZeroLiteral _ = false
58+
lib/purelint/src/Purelint/Rules/UseIsJust.purs → lib/purslint/src/Purslint/Rules/UseIsJust.pursrenamed+6−4
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseIsJust where
1+module Purslint.Rules.UseIsJust where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -37,6 +37,7 @@ useIsJustRule = mkRule (RuleId "UseIsJust") run
3737 , suggestion: Just $ Suggestion
3838 { replacement: ReplacementText "isJust"
3939 , description: SuggestionDescription "Use isJust instead of maybe false (const true)"
40+ , requiredImports: []
4041 }
4142 }
4243 ]
@@ -66,3 +67,4 @@ useIsJustRule = mkRule (RuleId "UseIsJust") run
6667 isTrue :: Expr Void -> Boolean
6768 isTrue (ExprBoolean _ b) = b
6869 isTrue _ = false
70+
lib/purelint/src/Purelint/Rules/UseIsNothing.purs → lib/purslint/src/Purslint/Rules/UseIsNothing.pursrenamed+6−4
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseIsNothing where
1+module Purslint.Rules.UseIsNothing where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -37,6 +37,7 @@ useIsNothingRule = mkRule (RuleId "UseIsNothing") run
3737 , suggestion: Just $ Suggestion
3838 { replacement: ReplacementText "isNothing"
3939 , description: SuggestionDescription "Use isNothing instead of maybe true (const false)"
40+ , requiredImports: []
4041 }
4142 }
4243 ]
@@ -66,3 +67,4 @@ useIsNothingRule = mkRule (RuleId "UseIsNothing") run
6667 isFalse :: Expr Void -> Boolean
6768 isFalse (ExprBoolean _ b) = not b
6869 isFalse _ = false
70+
lib/purelint/src/Purelint/Rules/UseJoin.purs → lib/purslint/src/Purslint/Rules/UseJoin.pursrenamed+8−5
@@ -1,4 +1,4 @@
1−module Purelint.Rules.UseJoin where
1+module Purslint.Rules.UseJoin where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
@@ -40,6 +40,7 @@ useJoinRule = mkRule (RuleId "UseJoin") run
4040 , suggestion: Just $ Suggestion
4141 { replacement: ReplacementText ("join " <> xText)
4242 , description: SuggestionDescription "x >>= identity can be simplified to join x"
43+ , requiredImports: []
4344 }
4445 }
4546 ]
@@ -58,6 +59,7 @@ useJoinRule = mkRule (RuleId "UseJoin") run
5859 , suggestion: Just $ Suggestion
5960 { replacement: ReplacementText ("join " <> xText)
6061 , description: SuggestionDescription "identity =<< x can be simplified to join x"
62+ , requiredImports: []
6163 }
6264 }
6365 ]
@@ -76,3 +78,4 @@ useJoinRule = mkRule (RuleId "UseJoin") run
7678 isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
7779 (name == "identity" && hasValue imports "identity") || (name == "id" && hasValue imports "id")
7880 isIdentity _ _ = false
81+
lib/purelint/src/Purelint/Rules/UseLastReverse.purs → lib/purslint/src/Purslint/Rules/UseLastReverse.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseLastReverse where
1+module Purslint.Rules.UseLastReverse where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -50,6 +50,7 @@ useLastReverseRule = mkRule (RuleId "UseLastReverse") run
5050 , suggestion: Just $ Suggestion
5151 { replacement: ReplacementText (newFn <> " " <> x)
5252 , description: SuggestionDescription $ "Use " <> newFn <> " instead"
53+ , requiredImports: []
5354 }
5455 }
5556 ]
@@ -76,3 +77,4 @@ useLastReverseRule = mkRule (RuleId "UseLastReverse") run
7677 isReverse imports (ExprIdent (QualifiedName { name: Ident name })) =
7778 name == "reverse" && hasValue imports "reverse"
7879 isReverse _ _ = false
80+
lib/purelint/src/Purelint/Rules/UseMapMaybe.purs → lib/purslint/src/Purslint/Rules/UseMapMaybe.pursrenamed+13−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseMapMaybe where
1+module Purslint.Rules.UseMapMaybe where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -56,6 +56,13 @@ useMapMaybeRule = mkRule (RuleId "UseMapMaybe") run
5656 , suggestion: Just $ Suggestion
5757 { replacement: ReplacementText ("mapMaybe " <> f <> " " <> x)
5858 , description: SuggestionDescription "Use mapMaybe instead of catMaybes composed with map"
59+ , requiredImports:
60+ [ { moduleName: "Data.Array"
61+ , importItem: Just "mapMaybe"
62+ , codeText: Just "mapMaybe"
63+ , qualifier: Nothing
64+ }
65+ ]
5966 }
6067 }
6168 ]
@@ -71,3 +78,4 @@ useMapMaybeRule = mkRule (RuleId "UseMapMaybe") run
7178 isMap imports (ExprIdent (QualifiedName { name: Ident name })) =
7279 name == "map" && hasValue imports "map"
7380 isMap _ _ = false
81+
lib/purelint/src/Purelint/Rules/UseMaybeMap.purs → lib/purslint/src/Purslint/Rules/UseMaybeMap.pursrenamed+5−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseMaybeMap where
1+module Purslint.Rules.UseMaybeMap where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
lib/purelint/src/Purelint/Rules/UseMinMax.purs → lib/purslint/src/Purslint/Rules/UseMinMax.pursrenamed+7−5
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseMinMax where
1+module Purslint.Rules.UseMinMax where
22
33import Prelude
44
55import Data.Maybe (Maybe(..))
66import Data.Void (Void)
7−import Purelint.Imports (ImportInfo, hasOp)
8−import Purelint.Print (printExpr)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
7+import Purslint.Imports (ImportInfo, hasOp)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
@@ -64,6 +64,7 @@ useMinMaxRule = mkRule (RuleId "UseMinMax") run
6464 , suggestion: Just $ Suggestion
6565 { replacement: ReplacementText (fn <> " " <> a <> " " <> b)
6666 , description: SuggestionDescription ("Use " <> fn <> " instead of if/else with comparison")
67+ , requiredImports: []
6768 }
6869 }
6970
@@ -76,3 +77,4 @@ useMinMaxRule = mkRule (RuleId "UseMinMax") run
7677 _ -> Nothing
7778 getComparison imports (ExprParens (Wrapped { value })) = getComparison imports value
7879 getComparison _ _ = Nothing
80+
lib/purelint/src/Purelint/Rules/UseMinimumSort.purs → lib/purslint/src/Purslint/Rules/UseMinimumSort.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseMinimumSort where
1+module Purslint.Rules.UseMinimumSort where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -50,6 +50,7 @@ useMinimumSortRule = mkRule (RuleId "UseMinimumSort") run
5050 , suggestion: Just $ Suggestion
5151 { replacement: ReplacementText (newFn <> " " <> x)
5252 , description: SuggestionDescription $ "Use " <> newFn <> " instead of sorting"
53+ , requiredImports: []
5354 }
5455 }
5556 ]
@@ -76,3 +77,4 @@ useMinimumSortRule = mkRule (RuleId "UseMinimumSort") run
7677 isSort imports (ExprIdent (QualifiedName { name: Ident name })) =
7778 name == "sort" && hasValue imports "sort"
7879 isSort _ _ = false
80+
lib/purelint/src/Purelint/Rules/UseNotElem.purs → lib/purslint/src/Purslint/Rules/UseNotElem.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseNotElem where
1+module Purslint.Rules.UseNotElem where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -42,6 +42,7 @@ useNotElemRule = mkRule (RuleId "UseNotElem") run
4242 , suggestion: Just $ Suggestion
4343 { replacement: ReplacementText ("notElem " <> xText <> " " <> yText)
4444 , description: SuggestionDescription "Use notElem instead of not (elem ...)"
45+ , requiredImports: []
4546 }
4647 }
4748 ]
@@ -64,3 +65,4 @@ useNotElemRule = mkRule (RuleId "UseNotElem") run
6465 _ -> Nothing
6566 getElemArgs imports (ExprParens (Wrapped { value })) = getElemArgs imports value
6667 getElemArgs _ _ = Nothing
68+
lib/purelint/src/Purelint/Rules/UseNull.purs → lib/purslint/src/Purslint/Rules/UseNull.pursrenamed+7−5
@@ -1,4 +1,4 @@
1−module Purelint.Rules.UseNull where
1+module Purslint.Rules.UseNull where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), IntValue(..), Module, Operator(..), QualifiedName(..))
@@ -57,6 +57,7 @@ useNullRule = mkRule (RuleId "UseNull") run
5757 , suggestion: Just $ Suggestion
5858 { replacement: ReplacementText ("null " <> xText)
5959 , description: SuggestionDescription $ from <> " can be simplified to null x"
60+ , requiredImports: []
6061 }
6162 }
6263
@@ -77,3 +78,4 @@ useNullRule = mkRule (RuleId "UseNull") run
7778 _ -> Nothing
7879 _ -> Nothing
7980 getLengthArg _ _ = Nothing
81+
lib/purelint/src/Purelint/Rules/UseOn.purs → lib/purslint/src/Purslint/Rules/UseOn.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseOn where
1+module Purslint.Rules.UseOn where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Binder(..), Expr(..), Ident(..), Module, Name(..), QualifiedName(..), Wrapped(..))
@@ -54,6 +54,7 @@ useOnRule = mkRule (RuleId "UseOn") run
5454 , suggestion: Just $ Suggestion
5555 { replacement: ReplacementText ("on " <> f <> " " <> g1)
5656 , description: SuggestionDescription "Use on instead of lambda applying g to both arguments"
57+ , requiredImports: []
5758 }
5859 }
5960 ]
@@ -70,3 +71,4 @@ useOnRule = mkRule (RuleId "UseOn") run
7071 _ -> Nothing
7172 _, _ -> Nothing
7273 extractFunctionCall _ = Nothing
74+
lib/purelint/src/Purelint/Rules/UseOr.purs → lib/purslint/src/Purslint/Rules/UseOr.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseOr where
1+module Purslint.Rules.UseOr where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -40,6 +40,7 @@ useOrRule = mkRule (RuleId "UseOr") run
4040 , suggestion: Just $ Suggestion
4141 { replacement: ReplacementText "or"
4242 , description: SuggestionDescription "Use or instead of any identity"
43+ , requiredImports: []
4344 }
4445 }
4546 ]
@@ -56,6 +57,7 @@ useOrRule = mkRule (RuleId "UseOr") run
5657 , suggestion: Just $ Suggestion
5758 { replacement: ReplacementText ("or " <> x)
5859 , description: SuggestionDescription "Use or instead of any identity"
60+ , requiredImports: []
5961 }
6062 }
6163 ]
@@ -76,3 +78,4 @@ useOrRule = mkRule (RuleId "UseOr") run
7678 isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
7779 (name == "identity" || name == "id") && hasValue imports "identity"
7880 isIdentity _ _ = false
81+
lib/purslint/src/Purslint/Rules/UsePatternGuards.pursnew file+231
@@ -0,0 +1,231 @@
1+module Purslint.Rules.UsePatternGuards where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Array.NonEmpty as NEA
7+import Data.Foldable (foldMap)
8+import Data.Maybe (Maybe(..))
9+import Data.Newtype (unwrap)
10+import Data.String as String
11+import Data.Tuple (Tuple(..))
12+import Data.Void (Void)
13+import PureScript.CST.Print (printSourceToken)
14+import PureScript.CST.Range (rangeOf, tokensOf)
15+import PureScript.CST.Range.TokenList as TokenList
16+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
17+import PureScript.CST.Types (Binder(..), Expr(..), Guarded(..), GuardedExpr(..), Module, PatternGuard(..), Separated(..), Where(..), Wrapped(..))
18+import Purslint.Print (printBinder, printExpr, printExprMultiline)
19+import Purslint.Rule (Rule, RuleContext, mkRule)
20+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
21+
22+-- | Rule: Nested case expressions with single match + fallback can use pattern guards
23+-- |
24+-- | case expr of
25+-- | Pattern1 -> case expr2 of
26+-- | Pattern2 -> result
27+-- | _ -> fallback
28+-- | _ -> fallback
29+-- |
30+-- | becomes:
31+-- |
32+-- | case expr of
33+-- | Pattern1 | Pattern2 <- expr2 -> result
34+-- | _ -> fallback
35+usePatternGuardsRule :: Rule
36+usePatternGuardsRule = mkRule (RuleId "UsePatternGuards") run
37+ where
38+ run :: RuleContext -> Module Void -> Array LintWarning
39+ run _ctx = foldMapModule visitor
40+ where
41+ visitor = defaultMonoidalVisitor { onExpr = checkExpr }
42+
43+ checkExpr :: Expr Void -> Array LintWarning
44+ checkExpr expr = case expr of
45+ ExprCase caseOf ->
46+ case caseOf.head of
47+ Separated { head: scrutineeExpr } ->
48+ case NEA.head caseOf.branches of
49+ Tuple (Separated { head: firstBinder }) _ ->
50+ case analyzeNestedPatternCase (printExpr scrutineeExpr) (NEA.toArray caseOf.branches) of
51+ Just result | result.depth >= 2 ->
52+ let
53+ range = rangeOf expr
54+ -- Get indentation from where the first branch actually starts
55+ branchCol = (rangeOf firstBinder).start.column
56+ indent n = String.joinWith "" (Array.replicate n " ")
57+
58+ -- Use the original branch indentation
59+ allGuards = result.existingGuards <> result.guards
60+ guardStr = allGuards # String.joinWith ("\n" <> indent (branchCol + 2) <> ", ")
61+
62+ replacement = "case " <> result.scrutinee <> " of\n"
63+ <> indent branchCol <> result.topPattern <> "\n"
64+ <> indent (branchCol + 2) <> "| " <> guardStr <> " -> " <> String.trim result.body <> "\n"
65+ <> indent branchCol <> "_ -> " <> result.fallback
66+ in
67+ [ LintWarning
68+ { ruleId: RuleId "UsePatternGuards"
69+ , message: WarningMessage "Nested case expressions can be flattened using pattern guards"
70+ , range: range
71+ , severity: Hint
72+ , suggestion: Just $ Suggestion
73+ { replacement: ReplacementText replacement
74+ , description: SuggestionDescription $ "Use pattern guards instead of " <> show result.depth <> " nested case expressions"
75+ , requiredImports: []
76+ }
77+ }
78+ ]
79+ _ -> []
80+ _ -> []
81+
82+ -- Analyze nested case to find pattern guard opportunities
83+ analyzeNestedPatternCase
84+ :: String -- scrutinee
85+ -> Array (Tuple (Separated (Binder Void)) (Guarded Void))
86+ -> Maybe { depth :: Int, scrutinee :: String, topPattern :: String, existingGuards :: Array String, guards :: Array String, body :: String, fallback :: String }
87+ analyzeNestedPatternCase scrutinee branches = do
88+ -- Need exactly 2 branches: one match and one wildcard fallback
89+ case branches of
90+ [ branch1, branch2 ] -> do
91+ let fb1 = getFallbackBody branch1
92+ let fb2 = getFallbackBody branch2
93+ case fb1, fb2 of
94+ -- branch2 is the fallback (wildcard pattern)
95+ Nothing, Just fallback ->
96+ analyzeMatchBranch scrutinee fallback branch1
97+ -- branch1 is the fallback
98+ Just fallback, Nothing ->
99+ analyzeMatchBranch scrutinee fallback branch2
100+ _, _ -> Nothing
101+ _ -> Nothing
102+
103+ -- Check if a branch is a wildcard fallback, return its body
104+ getFallbackBody :: Tuple (Separated (Binder Void)) (Guarded Void) -> Maybe String
105+ getFallbackBody (Tuple (Separated { head: binder, tail: [] }) guarded) =
106+ if isWildcard binder then
107+ case guarded of
108+ Unconditional _ (Where { expr, bindings: Nothing }) -> Just (printExpr expr)
109+ _ -> Nothing
110+ else Nothing
111+ getFallbackBody _ = Nothing
112+
113+ isWildcard :: Binder Void -> Boolean
114+ isWildcard (BinderWildcard _) = true
115+ isWildcard _ = false
116+
117+ -- Analyze a match branch to find nested pattern structure
118+ analyzeMatchBranch
119+ :: String -- scrutinee
120+ -> String -- fallback
121+ -> Tuple (Separated (Binder Void)) (Guarded Void)
122+ -> Maybe { depth :: Int, scrutinee :: String, topPattern :: String, existingGuards :: Array String, guards :: Array String, body :: String, fallback :: String }
123+ analyzeMatchBranch scrutinee fallback (Tuple (Separated { head: pattern1, tail: [] }) guarded) =
124+ let pat1Str = printBinder pattern1
125+ in case guarded of
126+ -- Unconditional: Pattern -> nested case
127+ Unconditional _ (Where { expr, bindings: Nothing }) ->
128+ analyzeNestedExpr scrutinee fallback pat1Str [] expr
129+ -- Guarded: Pattern | guard -> nested case
130+ Guarded guardedExprs ->
131+ case NEA.toArray guardedExprs of
132+ [ GuardedExpr ge ] ->
133+ case ge.where of
134+ Where { expr, bindings: Nothing } ->
135+ let existingGuards = printPatternGuards ge.patterns
136+ in analyzeNestedExpr scrutinee fallback pat1Str existingGuards expr
137+ _ -> Nothing
138+ _ -> Nothing
139+ _ -> Nothing
140+ analyzeMatchBranch _ _ _ = Nothing
141+
142+ -- Print pattern guards back to source
143+ printPatternGuards :: Separated (PatternGuard Void) -> Array String
144+ printPatternGuards (Separated { head, tail }) =
145+ Array.cons (printPatternGuard head) (map (\(Tuple _ pg) -> printPatternGuard pg) tail)
146+
147+ printPatternGuard :: PatternGuard Void -> String
148+ printPatternGuard (PatternGuard pg) =
149+ let exprStr = foldMap printSourceToken (TokenList.toArray (tokensOf pg.expr))
150+ in case pg.binder of
151+ Nothing -> String.trim exprStr
152+ Just (Tuple binder _) -> printBinder binder <> " <- " <> String.trim exprStr
153+
154+ -- Analyze nested expression (case) to build pattern guards
155+ analyzeNestedExpr
156+ :: String -- outer scrutinee
157+ -> String -- fallback
158+ -> String -- top pattern
159+ -> Array String -- existing guards
160+ -> Expr Void -- expression to analyze
161+ -> Maybe { depth :: Int, scrutinee :: String, topPattern :: String, existingGuards :: Array String, guards :: Array String, body :: String, fallback :: String }
162+ analyzeNestedExpr scrutinee fallback pat1Str existingGuards expr = case expr of
163+ ExprCase innerCaseOf ->
164+ case innerCaseOf.head of
165+ Separated { head: innerScrutineeExpr } ->
166+ let innerScrutinee = printExpr innerScrutineeExpr
167+ in case analyzeNestedPatternCase innerScrutinee (NEA.toArray innerCaseOf.branches) of
168+ Just nested | nested.fallback == fallback ->
169+ -- Found nested case with same fallback - extend guards
170+ Just
171+ { depth: nested.depth + 1
172+ , scrutinee
173+ , topPattern: pat1Str
174+ , existingGuards
175+ , guards: Array.cons (nested.topPattern <> " <- " <> nested.scrutinee) (nested.existingGuards <> nested.guards)
176+ , body: nested.body
177+ , fallback
178+ }
179+ _ ->
180+ -- Inner case has different fallback - check if it's a simple nested case we can still use
181+ case NEA.toArray innerCaseOf.branches of
182+ [ innerBranch1, innerBranch2 ] ->
183+ let innerFb1 = getFallbackBody innerBranch1
184+ innerFb2 = getFallbackBody innerBranch2
185+ in case innerFb1, innerFb2 of
186+ Nothing, Just innerFallback | innerFallback == fallback ->
187+ case getMatchPatternAndBody innerBranch1 of
188+ Just { pattern: innerPat, body: innerBody, innerExistingGuards } ->
189+ Just
190+ { depth: 2
191+ , scrutinee
192+ , topPattern: pat1Str
193+ , existingGuards
194+ , guards: Array.cons (innerPat <> " <- " <> innerScrutinee) innerExistingGuards
195+ , body: innerBody
196+ , fallback
197+ }
198+ Nothing -> Nothing
199+ Just innerFallback, Nothing | innerFallback == fallback ->
200+ case getMatchPatternAndBody innerBranch2 of
201+ Just { pattern: innerPat, body: innerBody, innerExistingGuards } ->
202+ Just
203+ { depth: 2
204+ , scrutinee
205+ , topPattern: pat1Str
206+ , existingGuards
207+ , guards: Array.cons (innerPat <> " <- " <> innerScrutinee) innerExistingGuards
208+ , body: innerBody
209+ , fallback
210+ }
211+ Nothing -> Nothing
212+ _, _ -> Nothing
213+ _ -> Nothing
214+ _ -> Nothing
215+
216+ getMatchPatternAndBody :: Tuple (Separated (Binder Void)) (Guarded Void) -> Maybe { pattern :: String, body :: String, innerExistingGuards :: Array String }
217+ getMatchPatternAndBody (Tuple (Separated { head: binder, tail: [] }) guarded) =
218+ case guarded of
219+ Unconditional _ (Where { expr, bindings: Nothing }) ->
220+ Just { pattern: printBinder binder, body: printExprMultiline expr, innerExistingGuards: [] }
221+ Guarded guardedExprs ->
222+ case NEA.toArray guardedExprs of
223+ [ GuardedExpr ge ] ->
224+ case ge.where of
225+ Where { expr, bindings: Nothing } ->
226+ Just { pattern: printBinder binder, body: printExprMultiline expr, innerExistingGuards: printPatternGuards ge.patterns }
227+ _ -> Nothing
228+ _ -> Nothing
229+ _ -> Nothing
230+ getMatchPatternAndBody _ = Nothing
231+
lib/purelint/src/Purelint/Rules/UseReplicate.purs → lib/purslint/src/Purslint/Rules/UseReplicate.pursrenamed+13−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseReplicate where
1+module Purslint.Rules.UseReplicate where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -44,6 +44,13 @@ useReplicateRule = mkRule (RuleId "UseReplicate") run
4444 , suggestion: Just $ Suggestion
4545 { replacement: ReplacementText ("replicate " <> n <> " " <> xText)
4646 , description: SuggestionDescription "Use replicate instead of take n (repeat x)"
47+ , requiredImports:
48+ [ { moduleName: "Data.Array"
49+ , importItem: Just "replicate"
50+ , codeText: Just "replicate"
51+ , qualifier: Nothing
52+ }
53+ ]
4754 }
4855 }
4956 ]
@@ -74,3 +81,4 @@ useReplicateRule = mkRule (RuleId "UseReplicate") run
7481 [AppTerm arg] -> Just arg
7582 _ -> Nothing
7683 _ -> Nothing
84+
lib/purelint/src/Purelint/Rules/UseSequence.purs → lib/purslint/src/Purslint/Rules/UseSequence.pursrenamed+12−4
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseSequence where
1+module Purslint.Rules.UseSequence where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..))
@@ -37,6 +37,13 @@ useSequenceRule = mkRule (RuleId "UseSequence") run
3737 , suggestion: Just $ Suggestion
3838 { replacement: ReplacementText "sequence"
3939 , description: SuggestionDescription "Use sequence instead of traverse identity"
40+ , requiredImports:
41+ [ { moduleName: "Data.Traversable"
42+ , importItem: Just "sequence"
43+ , codeText: Just "sequence"
44+ , qualifier: Nothing
45+ }
46+ ]
4047 }
4148 }
4249 ]
@@ -52,3 +59,4 @@ useSequenceRule = mkRule (RuleId "UseSequence") run
5259 isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
5360 name == "identity" && hasValue imports "identity"
5461 isIdentity _ _ = false
62+
lib/purelint/src/Purelint/Rules/UseSpan.purs → lib/purslint/src/Purslint/Rules/UseSpan.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseSpan where
1+module Purslint.Rules.UseSpan where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -44,6 +44,7 @@ useSpanRule = mkRule (RuleId "UseSpan") run
4444 , suggestion: Just $ Suggestion
4545 { replacement: ReplacementText ("span " <> pText <> " " <> x)
4646 , description: SuggestionDescription "Use span instead of break with negated predicate"
47+ , requiredImports: []
4748 }
4849 }
4950 ]
@@ -61,6 +62,7 @@ useSpanRule = mkRule (RuleId "UseSpan") run
6162 , suggestion: Just $ Suggestion
6263 { replacement: ReplacementText ("span " <> pText)
6364 , description: SuggestionDescription "Use span instead of break with negated predicate"
65+ , requiredImports: []
6466 }
6567 }
6668 ]
@@ -91,3 +93,4 @@ useSpanRule = mkRule (RuleId "UseSpan") run
9193 [AppTerm p] -> Just p
9294 _ -> Nothing
9395 _ -> Nothing
96+
lib/purelint/src/Purelint/Rules/UseTraverse.purs → lib/purslint/src/Purslint/Rules/UseTraverse.pursrenamed+36−23
@@ -1,4 +1,4 @@
1−module Purelint.Rules.UseTraverse where
1+module Purslint.Rules.UseTraverse where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array as Array
66import Data.Array.NonEmpty as NEA
77import Data.Maybe (Maybe(..))
88import Data.Tuple (Tuple(..))
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
@@ -28,14 +28,11 @@ useTraverseRule = mkRule (RuleId "UseTraverse") run
2828
2929 checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
3030 checkExpr imports expr = case expr of
31− -- Match: sequenceA (map f x) or sequenceA (f <$> x) or sequenceA (fmap f x)
32− ExprApp fnExpr args ->
33− case fnExpr of
34− ExprIdent qn | isSequenceA imports qn || isSequence imports qn ->
35− case NEA.head args of
36− AppTerm innerExpr -> checkInnerArg imports expr qn (unwrapParens innerExpr)
37− _ -> []
38− _ -> []
31+ -- Match: sequenceA (map f x) or sequenceA (f <$> x)
32+ ExprApp (ExprIdent qn) args
33+ | isSequenceA imports qn || isSequence imports qn
34+ , AppTerm innerExpr <- NEA.head args ->
35+ checkInnerArg imports expr qn (unwrapParens innerExpr)
3936 _ -> []
4037
4138 -- | Unwrap parentheses to get the inner expression
@@ -47,17 +44,21 @@ useTraverseRule = mkRule (RuleId "UseTraverse") run
4744 checkInnerArg imports fullExpr seqQn innerExpr =
4845 case innerExpr of
4946 -- Match: map f x
50− ExprApp mapFn mapArgs | isMapLikeApp imports mapFn ->
51− case NEA.toArray mapArgs of
52− [AppTerm fExpr, AppTerm xExpr] ->
47+ ExprApp mapFn mapArgs
48+ | isMapLikeApp imports mapFn
49+ , [AppTerm fExpr, AppTerm xExpr] <- NEA.toArray mapArgs ->
5350 mkWarning fullExpr seqQn (printExpr fExpr) (printExpr xExpr)
54− _ -> mkWarningTemplate fullExpr seqQn
51+ ExprApp mapFn _
52+ | isMapLikeApp imports mapFn ->
53+ mkWarningTemplate fullExpr seqQn
5554 -- Match: f <$> x (operator form)
56− ExprOp lhs ops | hasMapOperator imports ops ->
57− case NEA.toArray ops of
58− [Tuple _ rhs] ->
55+ ExprOp lhs ops
56+ | hasMapOperator imports ops
57+ , [Tuple _ rhs] <- NEA.toArray ops ->
5958 mkWarning fullExpr seqQn (printExpr lhs) (printExpr rhs)
60− _ -> mkWarningTemplate fullExpr seqQn
59+ ExprOp _ ops
60+ | hasMapOperator imports ops ->
61+ mkWarningTemplate fullExpr seqQn
6162 _ -> []
6263
6364 mkWarning :: Expr Void -> QualifiedName Ident -> String -> String -> Array LintWarning
@@ -68,15 +69,26 @@ useTraverseRule = mkRule (RuleId "UseTraverse") run
6869 replacement = if seqName == "sequenceA" || seqName == "sequenceA_" then "traverse" else "mapM"
6970 replacementSuffix = if seqName == "sequenceA_" || seqName == "sequence_" then "_" else ""
7071 concreteReplacement = replacement <> replacementSuffix <> " " <> fText <> " " <> xText
72+ moduleName = if replacement == "mapM" then "Control.Monad" else "Data.Traversable"
73+ importItem = replacement <> replacementSuffix
7174 in
7275 [ LintWarning
7376 { ruleId: RuleId "UseTraverse"
74− , message: WarningMessage $ "Use " <> replacement <> replacementSuffix <> " instead of " <> seqName <> " composed with map/fmap/<$>"
77+ , message: WarningMessage $ "Use " <> replacement <> replacementSuffix <> " instead of " <> seqName <> " composed with map / <$>"
7578 , range
7679 , severity: Warning
7780 , suggestion: Just $ Suggestion
7881 { replacement: ReplacementText concreteReplacement
7982 , description: SuggestionDescription $ seqName <> " (map f x) can be replaced with " <> replacement <> replacementSuffix <> " f x"
83+ , requiredImports:
84+ if replacement == "mapM" then []
85+ else
86+ [ { moduleName
87+ , importItem: Just importItem
88+ , codeText: Just importItem
89+ , qualifier: Nothing
90+ }
91+ ]
8092 }
8193 }
8294 ]
@@ -91,7 +103,7 @@ useTraverseRule = mkRule (RuleId "UseTraverse") run
91103 in
92104 [ LintWarning
93105 { ruleId: RuleId "UseTraverse"
94− , message: WarningMessage $ "Use " <> replacement <> replacementSuffix <> " instead of " <> seqName <> " composed with map/fmap/<$>"
106+ , message: WarningMessage $ "Use " <> replacement <> replacementSuffix <> " instead of " <> seqName <> " composed with map / <$>"
95107 , range
96108 , severity: Warning
97109 , suggestion: Nothing -- No auto-fix for complex patterns
@@ -123,3 +135,4 @@ useTraverseRule = mkRule (RuleId "UseTraverse") run
123135
124136 getIdentName :: QualifiedName Ident -> String
125137 getIdentName (QualifiedName { name: Ident name }) = name
138+
lib/purelint/src/Purelint/Rules/UseTraverseSequence.purs → lib/purslint/src/Purslint/Rules/UseTraverseSequence.pursrenamed+13−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseTraverseSequence where
1+module Purslint.Rules.UseTraverseSequence where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -42,6 +42,13 @@ useTraverseSequenceRule = mkRule (RuleId "UseTraverseSequence") run
4242 , suggestion: Just $ Suggestion
4343 { replacement: ReplacementText ("traverse " <> fText <> " " <> xText)
4444 , description: SuggestionDescription "Use traverse instead of sequence (map ...)"
45+ , requiredImports:
46+ [ { moduleName: "Data.Traversable"
47+ , importItem: Just "traverse"
48+ , codeText: Just "traverse"
49+ , qualifier: Nothing
50+ }
51+ ]
4552 }
4653 }
4754 ]
@@ -64,3 +71,4 @@ useTraverseSequenceRule = mkRule (RuleId "UseTraverseSequence") run
6471 _ -> Nothing
6572 getMapArgs imports (ExprParens (Wrapped { value })) = getMapArgs imports value
6673 getMapArgs _ _ = Nothing
74+
lib/purelint/src/Purelint/Rules/UseUncurry.purs → lib/purslint/src/Purslint/Rules/UseUncurry.pursrenamed+7−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseUncurry where
1+module Purslint.Rules.UseUncurry where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Proper(..), QualifiedName(..), Separated(..), Wrapped(..))
@@ -47,6 +47,7 @@ useUncurryRule = mkRule (RuleId "UseUncurry") run
4747 , suggestion: Just $ Suggestion
4848 { replacement: ReplacementText ("uncurry " <> f <> " " <> p1Text)
4949 , description: SuggestionDescription "Use uncurry instead of fst/snd"
50+ , requiredImports: []
5051 }
5152 }
5253 ]
@@ -88,3 +89,4 @@ useUncurryRule = mkRule (RuleId "UseUncurry") run
8889 [AppTerm arg] -> Just arg
8990 _ -> Nothing
9091 _ -> Nothing
92+
lib/purelint/src/Purelint/Rules/UseUnless.purs → lib/purslint/src/Purslint/Rules/UseUnless.pursrenamed+7−5
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseUnless where
1+module Purslint.Rules.UseUnless where
22
33import Prelude
44
55import Data.Maybe (Maybe(..))
66import Data.Void (Void)
7−import Purelint.Imports (ImportInfo, hasValue)
8−import Purelint.Print (printExpr)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (Expr(..), Ident(..), Module, QualifiedName(..))
@@ -38,6 +38,7 @@ useUnlessRule = mkRule (RuleId "UseUnless") run
3838 , suggestion: Just $ Suggestion
3939 { replacement: ReplacementText ("unless " <> condText <> " " <> actionText)
4040 , description: SuggestionDescription "Use unless instead of if/else with pure unit"
41+ , requiredImports: []
4142 }
4243 }
4344 ]
@@ -53,3 +54,4 @@ useUnlessRule = mkRule (RuleId "UseUnless") run
5354 isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
5455 (name == "pure" && hasValue imports "pure") || (name == "return" && hasValue imports "return")
5556 isPure _ _ = false
57+
lib/purslint/src/Purslint/Rules/UseUnwrap.pursnew file+70
@@ -0,0 +1,70 @@
1+module Purslint.Rules.UseUnwrap where
2+
3+import Prelude
4+
5+import Data.Array.NonEmpty as NEA
6+import Data.Maybe (Maybe(..))
7+import Data.Void (Void)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
11+import PureScript.CST.Range (rangeOf)
12+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
13+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..))
14+
15+-- | Rule: unwrap x -> un ?Constructor x
16+-- | Suggests replacing unwrap with typed hole to trigger type-directed search
17+useUnwrapRule :: Rule
18+useUnwrapRule = mkRule (RuleId "UseUnwrap") run
19+ where
20+ run :: RuleContext -> Module Void -> Array LintWarning
21+ run _ctx = foldMapModule visitor
22+ where
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr }
24+
25+ checkExpr :: Expr Void -> Array LintWarning
26+ checkExpr expr = case expr of
27+ -- Match: unwrap x
28+ ExprApp fn args
29+ | isUnwrap fn
30+ , [AppTerm arg] <- NEA.toArray args
31+ -> let
32+ argText = printExpr arg
33+ replacement = "un ?Constructor " <> argText
34+ in
35+ [ LintWarning
36+ { ruleId: RuleId "UseUnwrap"
37+ , message: WarningMessage "unwrap can be replaced with un ?Constructor for type-directed search"
38+ , range: rangeOf expr
39+ , severity: Hint
40+ , suggestion: Just $ Suggestion
41+ { replacement: ReplacementText replacement
42+ , description: SuggestionDescription "Use un with typed hole to find the right accessor"
43+ , requiredImports: []
44+ }
45+ }
46+ ]
47+ -- Match: unwrap (partial application)
48+ ExprIdent qn
49+ | isUnwrapIdent qn
50+ -> [ LintWarning
51+ { ruleId: RuleId "UseUnwrap"
52+ , message: WarningMessage "unwrap can be replaced with un ?Constructor for type-directed search"
53+ , range: rangeOf expr
54+ , severity: Hint
55+ , suggestion: Just $ Suggestion
56+ { replacement: ReplacementText "un ?Constructor"
57+ , description: SuggestionDescription "Use un with typed hole to find the right accessor"
58+ , requiredImports: []
59+ }
60+ }
61+ ]
62+ _ -> []
63+
64+ isUnwrap :: Expr Void -> Boolean
65+ isUnwrap (ExprIdent qn) = isUnwrapIdent qn
66+ isUnwrap _ = false
67+
68+ isUnwrapIdent :: QualifiedName Ident -> Boolean
69+ isUnwrapIdent (QualifiedName { name: Ident name }) = name == "unwrap"
70+
lib/purelint/src/Purelint/Rules/UseVoid.purs → lib/purslint/src/Purslint/Rules/UseVoid.pursrenamed+7−5
@@ -1,4 +1,4 @@
1−module Purelint.Rules.UseVoid where
1+module Purslint.Rules.UseVoid where
22
33import Prelude
44
@@ -6,10 +6,10 @@ import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Tuple (Tuple(..))
88import Data.Void (Void)
9−import Purelint.Imports (ImportInfo, hasValue, hasOp)
10−import Purelint.Print (printExpr)
11−import Purelint.Rule (Rule, RuleContext, mkRule)
12−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
9+import Purslint.Imports (ImportInfo, hasValue, hasOp)
10+import Purslint.Print (printExpr)
11+import Purslint.Rule (Rule, RuleContext, mkRule)
12+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1313import PureScript.CST.Range (rangeOf)
1414import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1515import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
@@ -41,6 +41,7 @@ useVoidRule = mkRule (RuleId "UseVoid") run
4141 , suggestion: Just $ Suggestion
4242 { replacement: ReplacementText ("void " <> aText)
4343 , description: SuggestionDescription "a *> pure unit can be simplified to void a"
44+ , requiredImports: []
4445 }
4546 }
4647 ]
@@ -64,3 +65,4 @@ useVoidRule = mkRule (RuleId "UseVoid") run
6465 isUnit :: Expr Void -> Boolean
6566 isUnit (ExprIdent (QualifiedName { name: Ident name })) = name == "unit"
6667 isUnit _ = false
68+
lib/purelint/src/Purelint/Rules/UseWhen.purs → lib/purslint/src/Purslint/Rules/UseWhen.pursrenamed+7−5
@@ -1,13 +1,13 @@
1−module Purelint.Rules.UseWhen where
1+module Purslint.Rules.UseWhen where
22
33import Prelude
44
55import Data.Maybe (Maybe(..))
66import Data.Void (Void)
7−import Purelint.Imports (ImportInfo, hasValue)
8−import Purelint.Print (printExpr)
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
7+import Purslint.Imports (ImportInfo, hasValue)
8+import Purslint.Print (printExpr)
9+import Purslint.Rule (Rule, RuleContext, mkRule)
10+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1111import PureScript.CST.Range (rangeOf)
1212import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1313import PureScript.CST.Types (Expr(..), Ident(..), Module, QualifiedName(..))
@@ -38,6 +38,7 @@ useWhenRule = mkRule (RuleId "UseWhen") run
3838 , suggestion: Just $ Suggestion
3939 { replacement: ReplacementText ("when " <> condText <> " " <> actionText)
4040 , description: SuggestionDescription "Use when instead of if/else with pure unit"
41+ , requiredImports: []
4142 }
4243 }
4344 ]
@@ -53,3 +54,4 @@ useWhenRule = mkRule (RuleId "UseWhen") run
5354 isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
5455 (name == "pure" && hasValue imports "pure") || (name == "return" && hasValue imports "return")
5556 isPure _ _ = false
57+
lib/purelint/src/Purelint/Rules/UseZip.purs → lib/purslint/src/Purslint/Rules/UseZip.pursrenamed+9−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.UseZip where
1+module Purslint.Rules.UseZip where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Proper(..), QualifiedName(..), Wrapped(..))
@@ -39,6 +39,7 @@ useZipRule = mkRule (RuleId "UseZip") run
3939 , suggestion: Just $ Suggestion
4040 { replacement: ReplacementText "zip"
4141 , description: SuggestionDescription "Use zip instead of zipWith Tuple"
42+ , requiredImports: []
4243 }
4344 }
4445 ]
@@ -55,6 +56,7 @@ useZipRule = mkRule (RuleId "UseZip") run
5556 , suggestion: Just $ Suggestion
5657 { replacement: ReplacementText ("zip " <> x)
5758 , description: SuggestionDescription "Use zip instead of zipWith Tuple"
59+ , requiredImports: []
5860 }
5961 }
6062 ]
@@ -73,6 +75,7 @@ useZipRule = mkRule (RuleId "UseZip") run
7375 , suggestion: Just $ Suggestion
7476 { replacement: ReplacementText ("zip " <> x <> " " <> y)
7577 , description: SuggestionDescription "Use zip instead of zipWith Tuple"
78+ , requiredImports: []
7679 }
7780 }
7881 ]
@@ -93,3 +96,4 @@ useZipRule = mkRule (RuleId "UseZip") run
9396 isTupleCtor (ExprConstructor (QualifiedName { name: Proper "Tuple" })) = true
9497 isTupleCtor (ExprIdent (QualifiedName { name: Ident "Tuple" })) = true
9598 isTupleCtor _ = false
99+
lib/purelint/src/Purelint/Rules/WhenNot.purs → lib/purslint/src/Purslint/Rules/WhenNot.pursrenamed+8−5
@@ -1,14 +1,14 @@
1−module Purelint.Rules.WhenNot where
1+module Purslint.Rules.WhenNot where
22
33import Prelude
44
55import Data.Array.NonEmpty as NEA
66import Data.Maybe (Maybe(..))
77import Data.Void (Void)
8−import Purelint.Imports (ImportInfo, hasValue)
9−import Purelint.Print (printExpr)
10−import Purelint.Rule (Rule, RuleContext, mkRule)
11−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
8+import Purslint.Imports (ImportInfo, hasValue)
9+import Purslint.Print (printExpr)
10+import Purslint.Rule (Rule, RuleContext, mkRule)
11+import Purslint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
1212import PureScript.CST.Range (rangeOf)
1313import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
1414import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
@@ -44,6 +44,7 @@ whenNotRule = mkRule (RuleId "WhenNot") run
4444 , suggestion: Just $ Suggestion
4545 { replacement: ReplacementText ("unless " <> xText <> " " <> action)
4646 , description: SuggestionDescription "Use unless instead of when (not ...)"
47+ , requiredImports: []
4748 }
4849 }
4950 ]
@@ -61,6 +62,7 @@ whenNotRule = mkRule (RuleId "WhenNot") run
6162 , suggestion: Just $ Suggestion
6263 { replacement: ReplacementText ("unless " <> xText)
6364 , description: SuggestionDescription "Use unless instead of when (not ...)"
65+ , requiredImports: []
6466 }
6567 }
6668 ]
@@ -91,3 +93,4 @@ whenNotRule = mkRule (RuleId "WhenNot") run
9193 [AppTerm arg] -> Just arg
9294 _ -> Nothing
9395 _ -> Nothing
96+
lib/purelint/src/Purelint/Runner.purs → lib/purslint/src/Purslint/Runner.pursrenamed+26−25
@@ -1,21 +1,21 @@
1−module Purelint.Runner where
1+module Purslint.Runner where
22
33import Prelude
44
55import Data.Array as Array
66import Data.Either (Either(..))
7−import Data.Newtype (unwrap)
8−import Purelint.Imports (getImportInfo, ImportInfo)
9−import Purelint.Rule (Rule, RuleContext)
10−import Purelint.Types (LintResult(..), SourceCode(..))
7+import Data.Newtype (un, unwrap)
118import PureScript.CST (RecoveredParserResult(..), parseModule)
129import PureScript.CST.Errors (printParseError)
13−import PureScript.CST.Types (Module, ModuleHeader(..), ModuleName(..), ImportDecl(..), Name(..))
10+import PureScript.CST.Types (ImportDecl(..), Module(..), ModuleHeader(..), ModuleName(..), Name(..))
11+import Purslint.Imports (getImportInfo, ImportInfo)
12+import Purslint.Rule (Rule, RuleContext)
13+import Purslint.Types (LintResult(..), SourceCode(..))
1414import Unsafe.Coerce (unsafeCoerce)
1515
1616-- | Run a set of rules on source code
1717runRules :: Array Rule -> SourceCode -> Either String LintResult
18−runRules rules (SourceCode source) =
18+runRules rules (SourceCode source) =
1919 case parseModule source of
2020 ParseSucceeded m -> Right $ runOnModule rules (toVoidModule m)
2121 ParseSucceededWithErrors m _ -> Right $ runOnModule rules (toVoidModule m)
@@ -23,7 +23,7 @@ runRules rules (SourceCode source) =
2323
2424-- | Get import info from source code (for debugging)
2525getImportsFromSource :: SourceCode -> Either String ImportInfo
26−getImportsFromSource (SourceCode source) =
26+getImportsFromSource (SourceCode source) =
2727 case parseModule source of
2828 ParseSucceeded m -> Right $ getImportInfo (toVoidModule m)
2929 ParseSucceededWithErrors m _ -> Right $ getImportInfo (toVoidModule m)
@@ -31,36 +31,37 @@ getImportsFromSource (SourceCode source) =
3131
3232-- | Get raw module names from imports (for debugging)
3333getModuleNames :: SourceCode -> Either String (Array String)
34−getModuleNames (SourceCode source) =
34+getModuleNames (SourceCode source) =
3535 case parseModule source of
3636 ParseSucceeded m -> Right $ extractModuleNames (toVoidModule m)
3737 ParseSucceededWithErrors m _ -> Right $ extractModuleNames (toVoidModule m)
3838 ParseFailed _ -> Left "Parse error"
3939 where
40− extractModuleNames mod =
41− let (ModuleHeader header) = (unwrap mod).header
42− in map extractName header.imports
43− extractName (ImportDecl imp) =
40+ extractModuleNames (Module mod) = do
41+ let ModuleHeader header = mod.header
42+ header.imports <#> extractName
43+ extractName (ImportDecl imp) =
4444 case imp.module of
4545 Name { name: ModuleName n } -> n
4646
4747-- | Run rules on a successfully parsed module
4848runOnModule :: Array Rule -> Module Void -> LintResult
49−runOnModule rules mod =
50− let
51− imports = getImportInfo mod
52− ctx :: RuleContext
53− ctx = { imports }
54− warnings = Array.concatMap (\rule -> rule.run ctx mod) rules
55− modName = getModuleName mod
56− in
57− LintResult { warnings, moduleName: modName }
49+runOnModule rules mod =
50+ LintResult { warnings, moduleName: modName }
51+ where
52+ imports = getImportInfo mod
53+
54+ ctx :: RuleContext
55+ ctx = { imports }
56+ warnings = rules # Array.concatMap \rule -> rule.run ctx mod
57+ modName = getModuleName mod
5858
5959-- | Get the module name from a parsed module
6060getModuleName :: forall e. Module e -> ModuleName
61−getModuleName mod =
62− let (ModuleHeader header) = (unwrap mod).header
63− in (unwrap header.name).name
61+getModuleName mod =
62+ (header.name # un Name).name
63+ where
64+ ModuleHeader header = (mod # un Module).header
6465
6566-- | Convert a recovered module to a Void module (assuming no errors)
6667-- | This is safe after ParseSucceeded because there are no error nodes
lib/purslint/src/Purslint/Tidy.jsnew file+66
@@ -0,0 +1,66 @@
1+import { execSync } from "child_process";
2+
3+// Format a full PureScript module using purs-tidy CLI
4+// Returns the formatted module string
5+export const formatModule = (str) => () => {
6+ try {
7+ const result = execSync("purs-tidy format", {
8+ input: str,
9+ encoding: "utf8",
10+ stdio: ["pipe", "pipe", "pipe"],
11+ timeout: 5000,
12+ });
13+ return result;
14+ } catch (_e) {
15+ // If purs-tidy fails or isn't installed, return original
16+ return str;
17+ }
18+};
19+
20+// Format a PureScript expression using purs-tidy CLI
21+// Wraps in a module, formats, and extracts the expression back
22+export const formatExpr = (str) => () => {
23+ try {
24+ // Wrap expression in a minimal module
25+ const moduleCode = `module T where\nx = ${str}\n`;
26+
27+ // Shell out to purs-tidy
28+ const result = execSync("purs-tidy format", {
29+ input: moduleCode,
30+ encoding: "utf8",
31+ stdio: ["pipe", "pipe", "pipe"],
32+ timeout: 5000,
33+ });
34+
35+ // Extract the expression after "x = "
36+ const lines = result.split("\n");
37+ const xLine = lines.findIndex((l) => l.startsWith("x = "));
38+ if (xLine === -1) return str;
39+
40+ // Get everything after "x = " including continuation lines
41+ const exprLines = [];
42+ exprLines.push(lines[xLine].slice(4)); // Remove "x = "
43+
44+ // Collect continuation lines until we hit another top-level declaration
45+ // or end of file. Keep relative indentation intact.
46+ for (let i = xLine + 1; i < lines.length; i++) {
47+ const line = lines[i];
48+ // Empty line or indented = still part of expression
49+ if (line === "" || line.startsWith(" ")) {
50+ exprLines.push(line);
51+ } else {
52+ break;
53+ }
54+ }
55+
56+ // Trim trailing empty lines
57+ while (exprLines.length > 0 && exprLines[exprLines.length - 1] === "") {
58+ exprLines.pop();
59+ }
60+
61+ return exprLines.join("\n");
62+ } catch (_e) {
63+ // If purs-tidy fails or isn't installed, return original
64+ return str;
65+ }
66+};
lib/purelint/src/Purelint/Tidy.purs → lib/purslint/src/Purslint/Tidy.pursrenamed+5−1
@@ -1,9 +1,13 @@
1−module Purelint.Tidy where
1+module Purslint.Tidy where
22
33import Prelude
44
55import Effect (Effect)
66
7+-- | Format a full PureScript module string using purs-tidy
8+-- | Returns the formatted string, or the original if formatting fails
9+foreign import formatModule :: String -> Effect String
10+
711-- | Format a PureScript expression string using purs-tidy
812-- | Returns the formatted string, or the original if formatting fails
913foreign import formatExpr :: String -> Effect String
lib/purelint/src/Purelint/Types.purs → lib/purslint/src/Purslint/Types.pursrenamed+16−2
@@ -1,4 +1,4 @@
1−module Purelint.Types where
1+module Purslint.Types where
22
33import Prelude
44
@@ -15,8 +15,10 @@ derive newtype instance Ord RuleId
1515derive newtype instance Show RuleId
1616
1717-- | Severity of a lint warning
18+-- | Refactor is special: it doesn't show diagnostics, only code actions
1819data Severity
19− = Hint
20+ = Refactor
21+ | Hint
2022 | Warning
2123 | Error
2224
@@ -24,14 +26,24 @@ derive instance Eq Severity
2426derive instance Ord Severity
2527
2628instance Show Severity where
29+ show Refactor = "refactor"
2730 show Hint = "hint"
2831 show Warning = "warning"
2932 show Error = "error"
3033
34+-- | Information about imports required for a suggestion
35+type RequiredImport =
36+ { moduleName :: String
37+ , importItem :: Maybe String
38+ , codeText :: Maybe String
39+ , qualifier :: Maybe String
40+ }
41+
3142-- | A suggested replacement for the original code
3243newtype Suggestion = Suggestion
3344 { replacement :: ReplacementText
3445 , description :: SuggestionDescription
46+ , requiredImports :: Array RequiredImport
3547 }
3648
3749derive instance Newtype Suggestion _
@@ -90,3 +102,5 @@ newtype SourceCode = SourceCode String
90102derive instance Newtype SourceCode _
91103derive newtype instance Eq SourceCode
92104derive newtype instance Show SourceCode
105+
106+
package.json+25−9
@@ -1,33 +1,49 @@
11{
2− "name": "purelint",
2+ "name": "purslint",
33 "version": "0.1.0",
44 "description": "A linter for PureScript",
55 "type": "module",
66 "bin": {
7− "purelint": "./dist/purelint.js",
8− "purelint-lsp": "./dist/purelint-lsp.js"
7+ "purslint": "./bin/purslint.js",
8+ "purslint-lsp": "./bin/purslint-lsp.js"
99 },
1010 "scripts": {
11− "build:cli": "spago build -p purelint-cli --output output-cli",
12− "build:lsp": "spago build -p purelint-lsp --output output-lsp",
11+ "build:cli": "spago build -p purslint-cli --output output-cli",
12+ "build:lsp": "spago build -p purslint-lsp --output output-lsp",
1313 "build": "npm run build:cli && npm run build:lsp",
14− "bundle:cli": "purs-backend-es bundle-app --corefn-dir output-cli --main Main --platform node --to dist/purelint.js --int-tags",
15− "bundle:lsp": "purs-backend-es bundle-app --corefn-dir output-lsp --main Main.LSP --platform node --to dist/purelint-lsp.js --int-tags",
14+ "bundle:cli": "purs-backend-es bundle-app --corefn-dir output-cli --main Main --platform node --to dist/purslint.js --int-tags",
15+ "bundle:lsp": "purs-backend-es bundle-app --corefn-dir output-lsp --main Main.LSP --platform node --to dist/purslint-lsp.js --int-tags",
1616 "bundle": "npm run bundle:cli && npm run bundle:lsp",
1717 "dist": "npm run build && npm run bundle",
1818 "test": "spago test",
19− "dev:cli": "node bin/purelint.js",
20− "dev:lsp": "node bin/purelint-lsp.js"
19+ "test:lsp": "node test-lsp/test-lsp.mjs",
20+ "test:vscode": "cd vscode-extension && npm test",
21+ "test:all": "npm test && npm run test:lsp && npm run test:vscode"
2122 },
2223 "keywords": [
2324 "purescript",
2425 "linter"
2526 ],
27+ "repository": {
28+ "type": "git",
29+ "url": "https://github.com/rowtype-yoga/purslint"
30+ },
31+ "homepage": "https://github.com/rowtype-yoga/purslint",
32+ "bugs": {
33+ "url": "https://github.com/rowtype-yoga/purslint/issues"
34+ },
35+ "files": [
36+ "bin",
37+ "dist",
38+ "README.md",
39+ "package.json"
40+ ],
2641 "license": "MIT",
2742 "dependencies": {
2843 "spago": "^0.93.45"
2944 },
3045 "devDependencies": {
46+ "esbuild": "^0.27.2",
3147 "purs-backend-es": "^1.4.3"
3248 }
3349}
purslint-lspnew file+2
@@ -0,0 +1,2 @@
1+#!/bin/bash
2+exec node "$(cd "$(dirname "$0")" && pwd)/dist/purslint-lsp.js" "$@"
spago.lock+94−13
@@ -1,8 +1,8 @@
11{
22 "workspace": {
33 "packages": {
4− "purelint": {
5− "path": "lib/purelint",
4+ "purslint": {
5+ "path": "lib/purslint",
66 "core": {
77 "dependencies": [
88 "arrays",
@@ -69,8 +69,8 @@
6969 "build_plan": []
7070 }
7171 },
72− "purelint-cli": {
73− "path": "bin/purelint-cli",
72+ "purslint-cli": {
73+ "path": "bin/purslint-cli",
7474 "core": {
7575 "dependencies": [
7676 "arrays",
@@ -83,8 +83,9 @@
8383 "node-buffer",
8484 "node-fs",
8585 "node-process",
86+ "optparse",
8687 "prelude",
87− "purelint",
88+ "purslint",
8889 "strings"
8990 ],
9091 "build_plan": [
@@ -104,6 +105,7 @@
104105 "enums",
105106 "exceptions",
106107 "exists",
108+ "exitcodes",
107109 "foldable-traversable",
108110 "foreign",
109111 "foreign-object",
@@ -130,6 +132,8 @@
130132 "now",
131133 "nullable",
132134 "numbers",
135+ "open-memoize",
136+ "optparse",
133137 "ordered-collections",
134138 "orders",
135139 "parallel",
@@ -137,7 +141,7 @@
137141 "posix-types",
138142 "prelude",
139143 "profunctor",
140− "purelint",
144+ "purslint",
141145 "refs",
142146 "safe-coerce",
143147 "st",
@@ -156,8 +160,8 @@
156160 "build_plan": []
157161 }
158162 },
159− "purelint-lsp": {
160− "path": "bin/purelint-lsp",
163+ "purslint-lsp": {
164+ "path": "bin/purslint-lsp",
161165 "core": {
162166 "dependencies": [
163167 "arrays",
@@ -167,7 +171,7 @@
167171 "maybe",
168172 "newtype",
169173 "prelude",
170− "purelint",
174+ "purslint",
171175 "unsafe-coerce"
172176 ],
173177 "build_plan": [
@@ -204,7 +208,7 @@
204208 "partial",
205209 "prelude",
206210 "profunctor",
207− "purelint",
211+ "purslint",
208212 "refs",
209213 "safe-coerce",
210214 "st",
@@ -223,7 +227,7 @@
223227 "build_plan": []
224228 }
225229 },
226− "purelint-tests": {
230+ "purslint-tests": {
227231 "path": "./",
228232 "core": {
229233 "dependencies": [
@@ -233,10 +237,13 @@
233237 "either",
234238 "maybe",
235239 "newtype",
240+ "optparse",
236241 "prelude",
237− "purelint"
242+ "purslint"
238243 ],
239244 "build_plan": [
245+ "aff",
246+ "arraybuffer-types",
240247 "arrays",
241248 "bifunctors",
242249 "catenable-lists",
@@ -244,13 +251,17 @@
244251 "const",
245252 "contravariant",
246253 "control",
254+ "datetime",
247255 "distributive",
248256 "effect",
249257 "either",
250258 "enums",
251259 "exceptions",
252260 "exists",
261+ "exitcodes",
253262 "foldable-traversable",
263+ "foreign",
264+ "foreign-object",
254265 "free",
255266 "functions",
256267 "functors",
@@ -263,14 +274,23 @@
263274 "lists",
264275 "maybe",
265276 "newtype",
277+ "node-buffer",
278+ "node-event-emitter",
279+ "node-process",
280+ "node-streams",
266281 "nonempty",
282+ "nullable",
267283 "numbers",
284+ "open-memoize",
285+ "optparse",
268286 "ordered-collections",
269287 "orders",
288+ "parallel",
270289 "partial",
290+ "posix-types",
271291 "prelude",
272292 "profunctor",
273− "purelint",
293+ "purslint",
274294 "refs",
275295 "safe-coerce",
276296 "st",
@@ -1075,6 +1095,14 @@
10751095 "unsafe-coerce"
10761096 ]
10771097 },
1098+ "exitcodes": {
1099+ "type": "registry",
1100+ "version": "4.0.0",
1101+ "integrity": "sha256-4wxViTbyOoyKJ/WaRGI6+hZmgMKI5Miv16lSwefiLSM=",
1102+ "dependencies": [
1103+ "enums"
1104+ ]
1105+ },
10781106 "foldable-traversable": {
10791107 "type": "registry",
10801108 "version": "6.0.0",
@@ -1458,6 +1486,59 @@
14581486 "maybe"
14591487 ]
14601488 },
1489+ "open-memoize": {
1490+ "type": "registry",
1491+ "version": "6.2.0",
1492+ "integrity": "sha256-p1m7wF3aHQ80yUvqMs20OTMl496WS6YpKlmI2Nkg9j0=",
1493+ "dependencies": [
1494+ "either",
1495+ "integers",
1496+ "lazy",
1497+ "lists",
1498+ "maybe",
1499+ "partial",
1500+ "prelude",
1501+ "strings",
1502+ "tuples"
1503+ ]
1504+ },
1505+ "optparse": {
1506+ "type": "registry",
1507+ "version": "5.0.1",
1508+ "integrity": "sha256-cEzEkNW4q0gZlXl4z0zn+H2vs6l2UAp7NPHCsois73k=",
1509+ "dependencies": [
1510+ "aff",
1511+ "arrays",
1512+ "bifunctors",
1513+ "console",
1514+ "control",
1515+ "effect",
1516+ "either",
1517+ "enums",
1518+ "exists",
1519+ "exitcodes",
1520+ "foldable-traversable",
1521+ "free",
1522+ "gen",
1523+ "integers",
1524+ "lazy",
1525+ "lists",
1526+ "maybe",
1527+ "newtype",
1528+ "node-buffer",
1529+ "node-process",
1530+ "node-streams",
1531+ "nonempty",
1532+ "numbers",
1533+ "open-memoize",
1534+ "partial",
1535+ "prelude",
1536+ "strings",
1537+ "tailrec",
1538+ "transformers",
1539+ "tuples"
1540+ ]
1541+ },
14611542 "ordered-collections": {
14621543 "type": "registry",
14631544 "version": "3.2.0",
spago.yaml+3−2
@@ -4,15 +4,16 @@ workspace:
44 extraPackages: {}
55
66package:
7− name: purelint-tests
7+ name: purslint-tests
88 dependencies:
99 - console
1010 - effect
1111 - either
1212 - maybe
1313 - newtype
14+ - optparse
1415 - prelude
15− - purelint
16+ - purslint
1617 - arrays
1718 test:
1819 main: Test.Main
test/LintExamples.purs+10−10
@@ -10,24 +10,24 @@ import Data.Traversable (class Traversable, sequence, traverse)
1010import Data.Tuple (Tuple(..), fst, snd)
1111
1212-- ============================================================================
13−-- FmapId: map identity x -> x
13+-- MapIdentity: map identity x -> x
1414-- ============================================================================
1515
1616-- | map identity x -> x
17−fmapId1 :: Array Int
18−fmapId1 = map identity [ 1, 2, 3 ]
17+mapIdentity1 :: Array Int
18+mapIdentity1 = map identity [ 1, 2, 3 ]
1919
20−-- | fmap identity x -> x (same as map in PureScript)
21−fmapId2 :: Array Int
22−fmapId2 = map identity [ 1, 2, 3 ]
20+-- | map identity x -> x (same as map in PureScript)
21+mapIdentity2 :: Array Int
22+mapIdentity2 = map identity [ 1, 2, 3 ]
2323
2424-- | identity <$> x -> x
25−fmapId3 :: Array Int
26−fmapId3 = identity <$> [ 1, 2, 3 ]
25+mapIdentity3 :: Array Int
26+mapIdentity3 = identity <$> [ 1, 2, 3 ]
2727
2828-- | x <#> identity -> x
29−fmapId4 :: Array Int
30−fmapId4 = [ 1, 2, 3 ] <#> identity
29+mapIdentity4 :: Array Int
30+mapIdentity4 = [ 1, 2, 3 ] <#> identity
3131
3232-- ============================================================================
3333-- MapFusion: map f (map g x) -> map (f <<< g) x
test/RedundantParens.pursnew file+134
@@ -0,0 +1,134 @@
1+module Test.RedundantParens where
2+
3+import Prelude
4+
5+-- =============================================================================
6+-- REDUNDANT PARENS TEST CASES
7+-- Each pair: xxx_with has parens, xxx_without doesn't
8+-- If JS output is identical, parens are redundant
9+-- =============================================================================
10+
11+-- === NESTED PARENS (always redundant) ===
12+nested_with = ((1))
13+nested_without = (1)
14+
15+nested3_with = (((1)))
16+nested3_without = 1
17+
18+-- === ATOMIC EXPRESSIONS IN FUNCTION ARGS ===
19+arg_int_with = show (42)
20+arg_int_without = show 42
21+
22+arg_string_with = show ("hello")
23+arg_string_without = show "hello"
24+
25+arg_ident_with x = show (x)
26+arg_ident_without x = show x
27+
28+arg_array_with = length ([1, 2, 3])
29+arg_array_without = length [1, 2, 3]
30+
31+arg_record_with = show ({ a: 1 })
32+arg_record_without = show { a: 1 }
33+
34+arg_bool_with = show (true)
35+arg_bool_without = show true
36+
37+-- Multiple args
38+multi_arg_with = add (1) (2)
39+multi_arg_without = add 1 2
40+
41+-- === ARRAY ELEMENTS ===
42+arr_elem_with = [(1), (2), (3)]
43+arr_elem_without = [1, 2, 3]
44+
45+arr_mixed_with = [(1), 2, (3)]
46+arr_mixed_without = [1, 2, 3]
47+
48+-- === NEEDED FOR PRECEDENCE (not redundant) ===
49+prec_mult_with = (1 + 2) * 3
50+-- prec_mult_without would be: 1 + 2 * 3 (different!)
51+
52+prec_sub_with = (1 + 2) - 3
53+-- Note: same precedence, but left-assoc so this is actually redundant
54+prec_sub_without = 1 + 2 - 3
55+
56+-- === NEEDED FOR COMPLEX ARGS (not redundant) ===
57+complex_arg_with = show (1 + 2)
58+-- complex_arg_without would be: show 1 + 2 (different!)
59+
60+-- === NEEDED FOR LAMBDA ARGS ===
61+lambda_arg_with = map (\x -> x + 1) [1]
62+-- lambda_arg_without would extend the lambda body
63+
64+-- === NEEDED FOR NEGATE AS ARG ===
65+negate_arg_with f = f (-1)
66+-- negate_arg_without would be: f - 1 (different!)
67+
68+-- === SECTIONS (parens required by syntax) ===
69+section_with = (_ + 1)
70+-- section_without is not valid syntax
71+
72+-- === OUTER PARENS ON RHS (whole expression wrapped) ===
73+rhs_lit_with = (42)
74+rhs_lit_without = 42
75+
76+rhs_op_with = (1 + 2)
77+rhs_op_without = 1 + 2
78+
79+rhs_app_with = (show 1)
80+rhs_app_without = show 1
81+
82+rhs_lambda_with = (\x -> x)
83+rhs_lambda_without = \x -> x
84+
85+rhs_if_with = (if true then 1 else 2)
86+rhs_if_without = if true then 1 else 2
87+
88+rhs_case_with = (case 1 of x -> x)
89+rhs_case_without = case 1 of x -> x
90+
91+rhs_let_with = (let x = 1 in x)
92+rhs_let_without = let x = 1 in x
93+
94+rhs_do_with = (do pure 1)
95+rhs_do_without = do pure 1
96+
97+-- === PARENS IN IF BRANCHES ===
98+if_cond_with = if (true) then 1 else 2
99+if_cond_without = if true then 1 else 2
100+
101+if_then_with = if true then (1) else 2
102+if_then_without = if true then 1 else 2
103+
104+if_else_with = if true then 1 else (2)
105+if_else_without = if true then 1 else 2
106+
107+-- === PARENS IN CASE ===
108+case_scrut_with = case (1) of x -> x
109+case_scrut_without = case 1 of x -> x
110+
111+case_body_with = case 1 of x -> (x)
112+case_body_without = case 1 of x -> x
113+
114+-- === PARENS IN LET ===
115+let_bind_with = let x = (1) in x
116+let_bind_without = let x = 1 in x
117+
118+let_body_with = let x = 1 in (x)
119+let_body_without = let x = 1 in x
120+
121+-- === PARENS IN DO ===
122+do_expr_with = do (pure 1)
123+do_expr_without = do pure 1
124+
125+-- === PARENS IN RECORD FIELDS ===
126+rec_field_with = { a: (1), b: (2) }
127+rec_field_without = { a: 1, b: 2 }
128+
129+-- === HELPERS ===
130+add :: Int -> Int -> Int
131+add a b = a + b
132+
133+length :: forall a. Array a -> Int
134+length _ = 0
test/Test/Main.purs+934−317
@@ -10,79 +10,93 @@ import Data.Newtype (unwrap)
1010import Data.Traversable (traverse)
1111import Effect (Effect)
1212import Effect.Class.Console (log)
13−import Purelint.Fix (applyAllFixes)
14−import Purelint.Tidy (formatExpr)
15−import Purelint.Rules.FmapId (fmapIdRule)
16−import Purelint.Rules.MapFusion (mapFusionRule)
17−import Purelint.Rules.UseTraverse (useTraverseRule)
18−import Purelint.Rules.ConcatMap (concatMapRule)
19−import Purelint.Rules.NotEqual (notEqualRule)
20−import Purelint.Rules.UseGuard (useGuardRule)
21−import Purelint.Rules.EtaReduce (etaReduceRule)
22−import Purelint.Rules.EtaReduceDecl (etaReduceDeclRule)
23−import Purelint.Rules.RedundantBind (redundantBindRule)
24−import Purelint.Rules.LetToWhere (letToWhereRule)
25−import Purelint.Rules.RedundantIf (redundantIfRule)
26−import Purelint.Rules.FunctorLaw (functorLawRule)
27−import Purelint.Rules.UseVoid (useVoidRule)
28−import Purelint.Rules.UseJoin (useJoinRule)
29−import Purelint.Rules.UseAny (useAnyRule)
30−import Purelint.Rules.UseAll (useAllRule)
31−import Purelint.Rules.BooleanSimplify (booleanSimplifyRule)
32−import Purelint.Rules.CollapseLambdas (collapseLambdasRule)
33−import Purelint.Rules.UseConst (useConstRule)
34−import Purelint.Rules.UseNull (useNullRule)
35−import Purelint.Rules.UseFromMaybe (useFromMaybeRule)
36−import Purelint.Rules.UseIsJust (useIsJustRule)
37−import Purelint.Rules.UseIsNothing (useIsNothingRule)
38−import Purelint.Rules.UseWhen (useWhenRule)
39−import Purelint.Rules.UseUnless (useUnlessRule)
40−import Purelint.Rules.RedundantFlip (redundantFlipRule)
41−import Purelint.Rules.UseNotElem (useNotElemRule)
42−import Purelint.Rules.UseMinMax (useMinMaxRule)
43−import Purelint.Rules.MonoidIdentity (monoidIdentityRule)
44−import Purelint.Rules.UseFold (useFoldRule)
45−import Purelint.Rules.UseSequence (useSequenceRule)
46−import Purelint.Rules.UseTraverseSequence (useTraverseSequenceRule)
47−import Purelint.Rules.RedundantNot (redundantNotRule)
48−import Purelint.Rules.UseFstSnd (useFstSndRule)
49−import Purelint.Rules.UseMapMaybe (useMapMaybeRule)
50−import Purelint.Rules.UseGuardMaybe (useGuardMaybeRule)
51−import Purelint.Rules.UseMaybeMap (useMaybeMapRule)
52−import Purelint.Rules.UseApplicative (useApplicativeRule)
53−import Purelint.Rules.UseBindFlip (useBindFlipRule)
54−import Purelint.Rules.UseFor (useForRule)
55−import Purelint.Rules.RedundantGuard (redundantGuardRule)
56−import Purelint.Rules.UseComparing (useComparingRule)
57−import Purelint.Rules.UseOn (useOnRule)
58−import Purelint.Rules.UseFindMap (useFindMapRule)
59−import Purelint.Rules.UseLastReverse (useLastReverseRule)
60−import Purelint.Rules.RedundantReverse (redundantReverseRule)
61−import Purelint.Rules.UseBreak (useBreakRule)
62−import Purelint.Rules.UseSpan (useSpanRule)
63−import Purelint.Rules.UseMinimumSort (useMinimumSortRule)
64−import Purelint.Rules.UseBimap (useBimapRule)
65−import Purelint.Rules.UseEitherMap (useEitherMapRule)
66−import Purelint.Rules.EvaluateFst (evaluateFstRule)
67−import Purelint.Rules.EvaluateConst (evaluateConstRule)
68−import Purelint.Rules.UseOr (useOrRule)
69−import Purelint.Rules.UseAnd (useAndRule)
70−import Purelint.Rules.UseFoldMapId (useFoldMapIdRule)
71−import Purelint.Rules.WhenNot (whenNotRule)
72−import Purelint.Rules.UnlessNot (unlessNotRule)
73−import Purelint.Rules.UseZip (useZipRule)
74−import Purelint.Rules.UseReplicate (useReplicateRule)
75−import Purelint.Rules.UseUncurry (useUncurryRule)
76−import Purelint.Rules.RedundantNegate (redundantNegateRule)
77−import Purelint.Rules.UseFromJust (useFromJustRule)
78−import Purelint.Rules.UseHead (useHeadRule)
79−import Purelint.Rules.RedundantId (redundantIdRule)
80−import Purelint.Rules.NothingBind (nothingBindRule)
81−import Purelint.Rules.UseElemIndex (useElemIndexRule)
82−import Purelint.Rule (Rule)
83−import Purelint.Runner (runRules, getImportsFromSource, getModuleNames)
84−import Purelint.Imports (hasValue, isPreludeModule)
85−import Purelint.Types (LintResult(..), LintWarning(..), RuleId(..), SourceCode(..), Suggestion(..))
13+import Purslint.Fix (applyAllFixes)
14+import Purslint.Tidy (formatExpr, formatModule)
15+import Purslint.Rules.MapIdentity (mapIdentityRule)
16+import Purslint.Rules.MapFusion (mapFusionRule)
17+import Purslint.Rules.UseTraverse (useTraverseRule)
18+import Purslint.Rules.ConcatMap (concatMapRule)
19+import Purslint.Rules.NotEqual (notEqualRule)
20+import Purslint.Rules.UseGuard (useGuardRule)
21+import Purslint.Rules.EtaReduce (etaReduceRule)
22+import Purslint.Rules.EtaReduceDecl (etaReduceDeclRule)
23+import Purslint.Rules.RedundantBind (redundantBindRule)
24+import Purslint.Rules.LetToWhere (letToWhereRule)
25+import Purslint.Rules.LetToDo (letToDoRule)
26+import Purslint.Rules.RedundantIf (redundantIfRule)
27+import Purslint.Rules.FunctorLaw (functorLawRule)
28+import Purslint.Rules.UseVoid (useVoidRule)
29+import Purslint.Rules.UseJoin (useJoinRule)
30+import Purslint.Rules.UseAny (useAnyRule)
31+import Purslint.Rules.UseAll (useAllRule)
32+import Purslint.Rules.BooleanSimplify (booleanSimplifyRule)
33+import Purslint.Rules.CollapseLambdas (collapseLambdasRule)
34+import Purslint.Rules.UseConst (useConstRule)
35+import Purslint.Rules.UseDollar (useDollarRule)
36+import Purslint.Rules.UseNull (useNullRule)
37+import Purslint.Rules.UseFromMaybe (useFromMaybeRule)
38+import Purslint.Rules.UseIsJust (useIsJustRule)
39+import Purslint.Rules.UseIsNothing (useIsNothingRule)
40+import Purslint.Rules.UseWhen (useWhenRule)
41+import Purslint.Rules.UseUnless (useUnlessRule)
42+import Purslint.Rules.RedundantFlip (redundantFlipRule)
43+import Purslint.Rules.UseNotElem (useNotElemRule)
44+import Purslint.Rules.UseMinMax (useMinMaxRule)
45+import Purslint.Rules.MonoidIdentity (monoidIdentityRule)
46+import Purslint.Rules.UseFold (useFoldRule)
47+import Purslint.Rules.UseSequence (useSequenceRule)
48+import Purslint.Rules.UseTraverseSequence (useTraverseSequenceRule)
49+import Purslint.Rules.RedundantNot (redundantNotRule)
50+import Purslint.Rules.UseFstSnd (useFstSndRule)
51+import Purslint.Rules.UseMapMaybe (useMapMaybeRule)
52+import Purslint.Rules.UseGuardMaybe (useGuardMaybeRule)
53+import Purslint.Rules.UseMaybeMap (useMaybeMapRule)
54+import Purslint.Rules.UseApplicative (useApplicativeRule)
55+import Purslint.Rules.UseBindFlip (useBindFlipRule)
56+import Purslint.Rules.UseFor (useForRule)
57+import Purslint.Rules.UseUnwrap (useUnwrapRule)
58+import Purslint.Rules.RedundantGuard (redundantGuardRule)
59+import Purslint.Rules.UseComparing (useComparingRule)
60+import Purslint.Rules.UseOn (useOnRule)
61+import Purslint.Rules.UseFindMap (useFindMapRule)
62+import Purslint.Rules.UseLastReverse (useLastReverseRule)
63+import Purslint.Rules.RedundantReverse (redundantReverseRule)
64+import Purslint.Rules.UseBreak (useBreakRule)
65+import Purslint.Rules.UseSpan (useSpanRule)
66+import Purslint.Rules.UseMinimumSort (useMinimumSortRule)
67+import Purslint.Rules.UseBimap (useBimapRule)
68+import Purslint.Rules.UseEitherMap (useEitherMapRule)
69+import Purslint.Rules.EvaluateFst (evaluateFstRule)
70+import Purslint.Rules.EvaluateConst (evaluateConstRule)
71+import Purslint.Rules.UseOr (useOrRule)
72+import Purslint.Rules.UseAnd (useAndRule)
73+import Purslint.Rules.UseFoldMap (useFoldMapRule)
74+import Purslint.Rules.UseFoldMapId (useFoldMapIdRule)
75+import Purslint.Rules.UseApplyFlipped (useApplyFlippedRule)
76+import Purslint.Rules.WhenNot (whenNotRule)
77+import Purslint.Rules.UnlessNot (unlessNotRule)
78+import Purslint.Rules.UseZip (useZipRule)
79+import Purslint.Rules.UseReplicate (useReplicateRule)
80+import Purslint.Rules.UseUncurry (useUncurryRule)
81+import Purslint.Rules.RedundantNegate (redundantNegateRule)
82+import Purslint.Rules.UseFromJust (useFromJustRule)
83+import Purslint.Rules.UseHead (useHeadRule)
84+import Purslint.Rules.RedundantId (redundantIdRule)
85+import Purslint.Rules.NothingBind (nothingBindRule)
86+import Purslint.Rules.UseElemIndex (useElemIndexRule)
87+import Purslint.Rules.MonadLaw (monadLawRule)
88+import Purslint.Rules.EvaluateBool (evaluateBoolRule)
89+import Purslint.Rules.UseFoldBool (useFoldBoolRule)
90+import Purslint.Rules.AlternativeLaw (alternativeLawRule)
91+import Purslint.Rules.EvaluateEither (evaluateEitherRule)
92+import Purslint.Rules.UseCaseOf (useCaseOfRule)
93+import Purslint.Rules.FlattenCase (flattenCaseRule)
94+import Purslint.Rules.UsePatternGuards (usePatternGuardsRule)
95+import Purslint.Rules.RedundantParens (redundantParensRule)
96+import Purslint.Rule (Rule)
97+import Purslint.Runner (runRules, getImportsFromSource, getModuleNames)
98+import Purslint.Imports (hasValue, isPreludeModule)
99+import Purslint.Types (LintResult(..), LintWarning(..), RuleId(..), SourceCode(..), Suggestion(..))
86100
87101-- | Simple test framework
88102type TestResult = { passed :: Boolean, name :: String, message :: String }
@@ -143,12 +157,27 @@ assertFix name rules source expectedFixed =
143157 then runTest name true "OK"
144158 else runTest name false ("Expected:\n" <> expectedFixed <> "\nGot:\n" <> fixed)
145159
160+-- | Assert that applying fixes produces expected output AND result is tidy-formatted
161+assertFixFormatted :: String -> Array Rule -> String -> String -> Effect TestResult
162+assertFixFormatted name rules source expectedFixed =
163+ case runRules rules (SourceCode source) of
164+ Left err -> pure $ runTest name false ("Parse error: " <> err)
165+ Right result -> do
166+ let fixed = applyAllFixes source result
167+ -- Check that the fix output is already tidy-formatted
168+ tidied <- formatModule fixed
169+ if tidied /= fixed
170+ then pure $ runTest name false ("Fix output not tidy-formatted.\nGot:\n" <> fixed <> "\nTidy expects:\n" <> tidied)
171+ else if fixed /= expectedFixed
172+ then pure $ runTest name false ("Expected:\n" <> expectedFixed <> "\nGot:\n" <> fixed)
173+ else pure $ runTest name true "OK"
174+
146175-- | Run all tests and report results
147176main :: Effect Unit
148177main = do
149− log "Running purelint tests...\n"
178+ log "Running purslint tests...\n"
150179
151− let results = fmapIdTests
180+ let results = mapIdentityTests
152181 <> mapFusionTests
153182 <> useTraverseTests
154183 <> concatMapTests
@@ -198,20 +227,43 @@ main = do
198227 <> evaluateConstTests
199228 <> useOrTests
200229 <> useAndTests
230+ <> useFoldMapTests
231+ <> useApplyFlippedTests
201232 <> useFoldMapIdTests
202233 <> whenNotTests
203234 <> unlessNotTests
204235 <> useZipTests
236+ <> useReplicateTests
237+ <> useUncurryTests
238+ <> useFromJustTests
239+ <> useHeadTests
240+ <> useElemIndexTests
241+ <> useBreakTests
242+ <> useSpanTests
243+ <> useDollarTests
244+ <> useUnwrapTests
205245 <> redundantNegateTests
206246 <> redundantIdTests
207247 <> nothingBindTests
208− <> goldenTests
248+ <> monadLawTests
249+ <> evaluateBoolTests
250+ <> useFoldBoolTests
251+ <> alternativeLawTests
252+ <> evaluateEitherTests
253+ <> useCaseOfTests
254+ <> flattenCaseTests
255+ <> usePatternGuardsTests
256+ <> redundantParensTests
257+
258+ -- Run effectful golden tests (with purs-tidy check)
259+ goldenResults <- goldenTests
260+ let allResults = results <> goldenResults
209261
210− let passed = Array.length $ Array.filter _.passed results
211− let total = Array.length results
262+ let passed = Array.length $ Array.filter _.passed allResults
263+ let total = Array.length allResults
212264
213265 -- Report failures
214− let failures = Array.filter (not <<< _.passed) results
266+ let failures = Array.filter (not <<< _.passed) allResults
215267 when (Array.length failures > 0) do
216268 log "FAILURES:"
217269 for_ failures (\r -> log (" ✗ " <> r.name <> ": " <> r.message))
@@ -239,42 +291,40 @@ main = do
239291
240292-- | Helper: wrap code in a module with Prelude import
241293withPrelude :: String -> String
242−withPrelude code = "module Test where\nimport Prelude\nimport Data.Maybe\nimport Data.Foldable\nimport Data.Tuple\nimport Data.Monoid\nimport Data.Traversable\nimport Data.Array\nimport Control.Alternative\nimport Data.Ord\nimport Data.Either\nimport Data.Bifunctor\nimport Data.List\n" <> code
294+withPrelude code = "module Test where\n\nimport Prelude\nimport Data.Maybe\nimport Data.Foldable\nimport Data.Tuple\nimport Data.Monoid\nimport Data.Traversable\nimport Data.Array\nimport Control.Alternative\nimport Data.Ord\nimport Data.Either\nimport Data.Bifunctor\nimport Data.List\n\n" <> code
243295
244296-- | Helper: wrap code in module without imports
245297withoutImports :: String -> String
246298withoutImports code = "module Test where\n" <> code
247299
248300-- ============================================================================
249−-- FmapId Tests
301+-- MapIdentity Tests
250302-- ============================================================================
251303
252−fmapIdTests :: Array TestResult
253−fmapIdTests =
304+mapIdentityTests :: Array TestResult
305+mapIdentityTests =
254306 -- Positive cases: should detect
255− [ assertWarningCount "FmapId: map identity x" [fmapIdRule]
307+ [ assertWarningCount "MapIdentity: map identity x" [mapIdentityRule]
256308 (withPrelude "x = map identity [1]") 1
257− , assertRuleId "FmapId: correct rule id" [fmapIdRule]
258− (withPrelude "x = map identity [1]") "FmapId"
259− , assertSuggestion "FmapId: map identity suggestion" [fmapIdRule]
260− (withPrelude "x = map identity [1]") "[1]"
261− , assertWarningCount "FmapId: fmap identity x" [fmapIdRule]
262− (withPrelude "x = fmap identity [1]") 1
263− , assertWarningCount "FmapId: identity <$> x" [fmapIdRule]
309+ , assertRuleId "MapIdentity: correct rule id" [mapIdentityRule]
310+ (withPrelude "x = map identity [1]") "MapIdentity"
311+ , assertSuggestion "MapIdentity: map identity suggestion" [mapIdentityRule]
312+ (withPrelude "x = map identity [1]") "[ 1 ]"
313+ , assertWarningCount "MapIdentity: identity <$> x" [mapIdentityRule]
264314 (withPrelude "x = identity <$> [1]") 1
265− , assertWarningCount "FmapId: x <#> identity" [fmapIdRule]
315+ , assertWarningCount "MapIdentity: x <#> identity" [mapIdentityRule]
266316 (withPrelude "x = [1] <#> identity") 1
267317
268318 -- Negative cases: should NOT detect
269− , assertWarningCount "FmapId: no fire on map show" [fmapIdRule]
319+ , assertWarningCount "MapIdentity: no fire on map show" [mapIdentityRule]
270320 (withPrelude "x = map show [1]") 0
271− , assertWarningCount "FmapId: no fire without import" [fmapIdRule]
321+ , assertWarningCount "MapIdentity: no fire without import" [mapIdentityRule]
272322 (withoutImports "x = map identity [1]") 0
273323
274324 -- Auto-fix test
275− , assertFix "FmapId: fix map identity" [fmapIdRule]
325+ , assertFix "MapIdentity: fix map identity" [mapIdentityRule]
276326 (withPrelude "x = map identity [1]")
277− (withPrelude "x = [1]")
327+ (withPrelude "x = [ 1 ]")
278328 ]
279329
280330-- ============================================================================
@@ -440,7 +490,7 @@ redundantBindTests =
440490 , assertRuleId "RedundantBind: correct rule id" [redundantBindRule]
441491 (withPrelude "x = [1] >>= pure") "RedundantBind"
442492 , assertSuggestion "RedundantBind: x >>= pure suggestion" [redundantBindRule]
443− (withPrelude "x = [1] >>= pure") "[1]"
493+ (withPrelude "x = [1] >>= pure") "[ 1 ]"
444494 , assertWarningCount "RedundantBind: pure x >>= f" [redundantBindRule]
445495 (withPrelude "x = pure 1 >>= show") 1
446496 , assertSuggestion "RedundantBind: pure x >>= f suggestion" [redundantBindRule]
@@ -469,13 +519,16 @@ letToWhereTests =
469519 , assertWarningCount "LetToWhere: multiple bindings" [letToWhereRule]
470520 (withPrelude "x = let a = 1\n b = 2 in a + b") 1
471521
472− -- Suggestion preserves spaces
522+ -- Suggestion preserves spaces based on let column position
473523 , assertSuggestion "LetToWhere: preserves spaces in simple case" [letToWhereRule]
474− (withPrelude "x = let y = 1 in y + 1") "y + 1\n where\n y = 1"
524+ (withPrelude "x = let y = 1 in y + 1") "y + 1\n where\n y = 1"
475525 , assertSuggestion "LetToWhere: preserves spaces in expressions" [letToWhereRule]
476− (withPrelude "f x = let a = x + 1 in a * 2") "a * 2\n where\n a = x + 1"
526+ (withPrelude "f x = let a = x + 1 in a * 2") "a * 2\n where\n a = x + 1"
477527 , assertSuggestion "LetToWhere: preserves spaces with multiple bindings" [letToWhereRule]
478− (withPrelude "f x = let a = x + 1\n b = x * 2 in a + b") "a + b\n where\n a = x + 1\n b = x * 2"
528+ (withPrelude "f x = let a = x + 1\n b = x * 2 in a + b") "a + b\n where\n a = x + 1\n b = x * 2"
529+ -- Nested let should preserve indentation context
530+ , assertSuggestion "LetToWhere: nested let preserves indentation" [letToWhereRule]
531+ (withPrelude "f x =\n let y = 1 in y + 1") "y + 1\n where\n y = 1"
479532 ]
480533
481534-- ============================================================================
@@ -578,7 +631,7 @@ useAnyTests =
578631 , assertRuleId "UseAny: correct rule id" [useAnyRule]
579632 (withPrelude "x = or (map f [1])") "UseAny"
580633 , assertSuggestion "UseAny: suggests any" [useAnyRule]
581− (withPrelude "x = or (map f [1])") "any f [1]"
634+ (withPrelude "x = or (map f [1])") "any f [ 1 ]"
582635
583636 -- Negative cases
584637 , assertWarningCount "UseAny: no fire on or alone" [useAnyRule]
@@ -599,7 +652,7 @@ useAllTests =
599652 , assertRuleId "UseAll: correct rule id" [useAllRule]
600653 (withPrelude "x = and (map f [1])") "UseAll"
601654 , assertSuggestion "UseAll: suggests all" [useAllRule]
602− (withPrelude "x = and (map f [1])") "all f [1]"
655+ (withPrelude "x = and (map f [1])") "all f [ 1 ]"
603656
604657 -- Negative cases
605658 , assertWarningCount "UseAll: no fire on and alone" [useAllRule]
@@ -662,19 +715,21 @@ collapseLambdasTests =
662715
663716useConstTests :: Array TestResult
664717useConstTests =
665− -- Positive cases
666− [ assertWarningCount "UseConst: \\x -> y (unused)" [useConstRule]
667− (withPrelude "x = \\a -> 42") 1
718+ -- Positive cases: only fires on wildcard binder \_
719+ [ assertWarningCount "UseConst: \\_ -> y" [useConstRule]
720+ (withPrelude "x = \\_ -> 42") 1
668721 , assertRuleId "UseConst: correct rule id" [useConstRule]
669− (withPrelude "x = \\a -> 42") "UseConst"
722+ (withPrelude "x = \\_ -> 42") "UseConst"
670723 , assertSuggestion "UseConst: suggests const" [useConstRule]
671− (withPrelude "x = \\a -> 42") "const 42"
724+ (withPrelude "x = \\_ -> 42") "const 42"
672725
673726 -- Negative cases
727+ , assertWarningCount "UseConst: no fire on named param (compiler warns)" [useConstRule]
728+ (withPrelude "x = \\a -> 42") 0
674729 , assertWarningCount "UseConst: no fire when param used" [useConstRule]
675730 (withPrelude "x = \\a -> a + 1") 0
676731 , assertWarningCount "UseConst: no fire without import" [useConstRule]
677− (withoutImports "x = \\a -> 42") 0
732+ (withoutImports "x = \\_ -> 42") 0
678733 ]
679734
680735-- ============================================================================
@@ -689,7 +744,7 @@ useNullTests =
689744 , assertRuleId "UseNull: correct rule id" [useNullRule]
690745 (withPrelude "x = length [1] == 0") "UseNull"
691746 , assertSuggestion "UseNull: suggests null" [useNullRule]
692− (withPrelude "x = length [1] == 0") "null [1]"
747+ (withPrelude "x = length [1] == 0") "null [ 1 ]"
693748 , assertWarningCount "UseNull: 0 == length x" [useNullRule]
694749 (withPrelude "x = 0 == length [1]") 1
695750
@@ -815,7 +870,7 @@ useNotElemTests =
815870 , assertRuleId "UseNotElem: correct rule id" [useNotElemRule]
816871 (withPrelude "x = not (elem 1 [1,2,3])") "UseNotElem"
817872 , assertSuggestion "UseNotElem: suggests notElem" [useNotElemRule]
818− (withPrelude "x = not (elem 1 [1,2,3])") "notElem 1 [1,2,3]"
873+ (withPrelude "x = not (elem 1 [1,2,3])") "notElem 1 [ 1, 2, 3 ]"
819874
820875 -- Negative cases
821876 , assertWarningCount "UseNotElem: no fire on not (foo x)" [useNotElemRule]
@@ -855,7 +910,7 @@ monoidIdentityTests =
855910 , assertRuleId "MonoidIdentity: correct rule id" [monoidIdentityRule]
856911 (withPrelude "x = mempty <> [1,2,3]") "MonoidIdentity"
857912 , assertSuggestion "MonoidIdentity: suggests x" [monoidIdentityRule]
858− (withPrelude "x = mempty <> [1,2,3]") "[1,2,3]"
913+ (withPrelude "x = mempty <> [1,2,3]") "[ 1, 2, 3 ]"
859914 , assertWarningCount "MonoidIdentity: x <> mempty" [monoidIdentityRule]
860915 (withPrelude "x = [1,2,3] <> mempty") 1
861916
@@ -911,7 +966,7 @@ useTraverseSequenceTests =
911966 , assertRuleId "UseTraverseSequence: correct rule id" [useTraverseSequenceRule]
912967 (withPrelude "x = sequence (map show [1,2,3])") "UseTraverseSequence"
913968 , assertSuggestion "UseTraverseSequence: suggests traverse" [useTraverseSequenceRule]
914− (withPrelude "x = sequence (map show [1,2,3])") "traverse show [1,2,3]"
969+ (withPrelude "x = sequence (map show [1,2,3])") "traverse show [ 1, 2, 3 ]"
915970
916971 -- Negative cases
917972 , assertWarningCount "UseTraverseSequence: no fire on sequence xs" [useTraverseSequenceRule]
@@ -969,7 +1024,7 @@ useMapMaybeTests =
9691024 , assertRuleId "UseMapMaybe: correct rule id" [useMapMaybeRule]
9701025 (withPrelude "x = catMaybes (map f [1,2,3])") "UseMapMaybe"
9711026 , assertSuggestion "UseMapMaybe: suggests mapMaybe" [useMapMaybeRule]
972− (withPrelude "x = catMaybes (map f [1,2,3])") "mapMaybe f [1,2,3]"
1027+ (withPrelude "x = catMaybes (map f [1,2,3])") "mapMaybe f [ 1, 2, 3 ]"
9731028 ]
9741029
9751030-- ============================================================================
@@ -1073,7 +1128,7 @@ useFindMapTests =
10731128 , assertRuleId "UseFindMap: correct rule id" [useFindMapRule]
10741129 (withPrelude "x = head (mapMaybe f [1,2,3])") "UseFindMap"
10751130 , assertSuggestion "UseFindMap: suggests findMap" [useFindMapRule]
1076− (withPrelude "x = head (mapMaybe f [1,2,3])") "findMap f [1,2,3]"
1131+ (withPrelude "x = head (mapMaybe f [1,2,3])") "findMap f [ 1, 2, 3 ]"
10771132 ]
10781133
10791134-- ============================================================================
@@ -1087,7 +1142,7 @@ useLastReverseTests =
10871142 , assertRuleId "UseLastReverse: correct rule id" [useLastReverseRule]
10881143 (withPrelude "x = head (reverse [1,2,3])") "UseLastReverse"
10891144 , assertSuggestion "UseLastReverse: suggests last" [useLastReverseRule]
1090− (withPrelude "x = head (reverse [1,2,3])") "last [1,2,3]"
1145+ (withPrelude "x = head (reverse [1,2,3])") "last [ 1, 2, 3 ]"
10911146 ]
10921147
10931148-- ============================================================================
@@ -1115,7 +1170,7 @@ useMinimumSortTests =
11151170 , assertRuleId "UseMinimumSort: correct rule id" [useMinimumSortRule]
11161171 (withPrelude "x = head (sort [1,2,3])") "UseMinimumSort"
11171172 , assertSuggestion "UseMinimumSort: suggests minimum" [useMinimumSortRule]
1118− (withPrelude "x = head (sort [1,2,3])") "minimum [1,2,3]"
1173+ (withPrelude "x = head (sort [1,2,3])") "minimum [ 1, 2, 3 ]"
11191174 ]
11201175
11211176-- ============================================================================
@@ -1175,7 +1230,7 @@ useOrTests =
11751230 , assertRuleId "UseOr: correct rule id" [useOrRule]
11761231 (withPrelude "x = any identity [true, false]") "UseOr"
11771232 , assertSuggestion "UseOr: suggests or" [useOrRule]
1178− (withPrelude "x = any identity [true, false]") "or [true, false]"
1233+ (withPrelude "x = any identity [true, false]") "or [ true, false ]"
11791234 ]
11801235
11811236-- ============================================================================
@@ -1189,7 +1244,53 @@ useAndTests =
11891244 , assertRuleId "UseAnd: correct rule id" [useAndRule]
11901245 (withPrelude "x = all identity [true, false]") "UseAnd"
11911246 , assertSuggestion "UseAnd: suggests and" [useAndRule]
1192− (withPrelude "x = all identity [true, false]") "and [true, false]"
1247+ (withPrelude "x = all identity [true, false]") "and [ true, false ]"
1248+ ]
1249+
1250+-- ============================================================================
1251+-- UseFoldMap Tests
1252+-- ============================================================================
1253+
1254+useFoldMapTests :: Array TestResult
1255+useFoldMapTests =
1256+ [ assertWarningCount "UseFoldMap: case Nothing/Just with empty string" [useFoldMapRule]
1257+ (withPrelude "x mx = case mx of\n Nothing -> \"\"\n Just s -> f s") 1
1258+ , assertRuleId "UseFoldMap: correct rule id" [useFoldMapRule]
1259+ (withPrelude "x mx = case mx of\n Nothing -> \"\"\n Just s -> f s") "UseFoldMap"
1260+ , assertSuggestion "UseFoldMap: suggests foldMap f" [useFoldMapRule]
1261+ (withPrelude "x mx = case mx of\n Nothing -> \"\"\n Just s -> f s") "foldMap f mx"
1262+ , assertSuggestion "UseFoldMap: suggests lambda for complex body" [useFoldMapRule]
1263+ (withPrelude "x mx = case mx of\n Nothing -> \"\"\n Just s -> s <> \"!\"") "foldMap (\\s -> s <> \"!\") mx"
1264+ , assertWarningCount "UseFoldMap: reversed order Just/Nothing" [useFoldMapRule]
1265+ (withPrelude "x mx = case mx of\n Just s -> f s\n Nothing -> \"\"") 1
1266+
1267+ -- Complex scrutinee should be wrapped in parens
1268+ , assertSuggestion "UseFoldMap: complex scrutinee gets parens" [useFoldMapRule]
1269+ (withPrelude "x f mx = case f mx of\n Nothing -> \"\"\n Just s -> g s") "foldMap g (f mx)"
1270+
1271+ -- Negative cases: should NOT detect
1272+ , assertWarningCount "UseFoldMap: no fire on guarded Just branch" [useFoldMapRule]
1273+ (withPrelude "x mx = case mx of\n Just s | s > \"\" -> f s\n _ -> \"\"") 0
1274+ , assertWarningCount "UseFoldMap: no fire on where binding in Just branch" [useFoldMapRule]
1275+ (withPrelude "x mx = case mx of\n Just s -> g s where g = f\n Nothing -> \"\"") 0
1276+
1277+ -- Multiline body inside if-then-else should generate proper suggestion with correct indentation
1278+ , assertSuggestion "UseFoldMap: multiline in if-then-else" [useFoldMapRule]
1279+ (withPrelude "checkExpr args = case args of\n [firstArg, secondArg] ->\n if isLeft firstArg then\n case getRightComposed secondArg of\n Just f ->\n let fText = show f\n in\n [ fText ]\n Nothing -> []\n else []") "foldMap\n ( \\f ->\n let fText = show f\n in\n [ fText ]\n )\n (getRightComposed secondArg)"
1280+ ]
1281+
1282+-- ============================================================================
1283+-- UseApplyFlipped Tests
1284+-- ============================================================================
1285+
1286+useApplyFlippedTests :: Array TestResult
1287+useApplyFlippedTests =
1288+ [ assertWarningCount "UseApplyFlipped: f lambda arg" [useApplyFlippedRule]
1289+ (withPrelude "x = foldMap (\\y -> y <> \"!\") xs") 1
1290+ , assertRuleId "UseApplyFlipped: correct rule id" [useApplyFlippedRule]
1291+ (withPrelude "x = foldMap (\\y -> y <> \"!\") xs") "UseApplyFlipped"
1292+ , assertSuggestion "UseApplyFlipped: suggests # form" [useApplyFlippedRule]
1293+ (withPrelude "x = foldMap (\\y -> y <> \"!\") xs") "xs # foldMap \\y -> y <> \"!\""
11931294 ]
11941295
11951296-- ============================================================================
@@ -1245,7 +1346,163 @@ useZipTests =
12451346 , assertRuleId "UseZip: correct rule id" [useZipRule]
12461347 (withPrelude "x = zipWith Tuple [1] [2]") "UseZip"
12471348 , assertSuggestion "UseZip: suggests zip" [useZipRule]
1248− (withPrelude "x = zipWith Tuple [1] [2]") "zip [1] [2]"
1349+ (withPrelude "x = zipWith Tuple [1] [2]") "zip [ 1 ] [ 2 ]"
1350+ ]
1351+
1352+-- ============================================================================
1353+-- UseReplicate Tests
1354+-- ============================================================================
1355+
1356+useReplicateTests :: Array TestResult
1357+useReplicateTests =
1358+ [ assertWarningCount "UseReplicate: take n (repeat x)" [useReplicateRule]
1359+ (withPrelude "x = take 3 (repeat 1)") 1
1360+ , assertRuleId "UseReplicate: correct rule id" [useReplicateRule]
1361+ (withPrelude "x = take 3 (repeat 1)") "UseReplicate"
1362+ , assertSuggestion "UseReplicate: suggests replicate" [useReplicateRule]
1363+ (withPrelude "x = take 3 (repeat 1)") "replicate 3 1"
1364+ , assertWarningCount "UseReplicate: no fire on take n xs" [useReplicateRule]
1365+ (withPrelude "x = take 3 xs") 0
1366+ , assertWarningCount "UseReplicate: no fire without import" [useReplicateRule]
1367+ (withoutImports "x = take 3 (repeat 1)") 0
1368+ ]
1369+
1370+-- ============================================================================
1371+-- UseUncurry Tests
1372+-- ============================================================================
1373+
1374+useUncurryTests :: Array TestResult
1375+useUncurryTests =
1376+ [ assertWarningCount "UseUncurry: f (fst p) (snd p)" [useUncurryRule]
1377+ (withPrelude "x = f (fst p) (snd p)") 1
1378+ , assertRuleId "UseUncurry: correct rule id" [useUncurryRule]
1379+ (withPrelude "x = f (fst p) (snd p)") "UseUncurry"
1380+ , assertSuggestion "UseUncurry: suggests uncurry" [useUncurryRule]
1381+ (withPrelude "x = f (fst p) (snd p)") "uncurry f p"
1382+ , assertWarningCount "UseUncurry: no fire on different args" [useUncurryRule]
1383+ (withPrelude "x = f (fst p) (snd q)") 0
1384+ , assertWarningCount "UseUncurry: no fire without import" [useUncurryRule]
1385+ (withoutImports "x = f (fst p) (snd p)") 0
1386+ ]
1387+
1388+-- ============================================================================
1389+-- UseFromJust Tests
1390+-- ============================================================================
1391+
1392+useFromJustTests :: Array TestResult
1393+useFromJustTests =
1394+ [ assertWarningCount "UseFromJust: fromMaybe d (Just x)" [useFromJustRule]
1395+ (withPrelude "x = fromMaybe 0 (Just 1)") 1
1396+ , assertRuleId "UseFromJust: correct rule id" [useFromJustRule]
1397+ (withPrelude "x = fromMaybe 0 (Just 1)") "UseFromJust"
1398+ , assertSuggestion "UseFromJust: simplifies Just" [useFromJustRule]
1399+ (withPrelude "x = fromMaybe 0 (Just 1)") "1"
1400+ , assertWarningCount "UseFromJust: fromMaybe d Nothing" [useFromJustRule]
1401+ (withPrelude "x = fromMaybe 0 Nothing") 1
1402+ , assertSuggestion "UseFromJust: simplifies Nothing" [useFromJustRule]
1403+ (withPrelude "x = fromMaybe 0 Nothing") "0"
1404+ , assertWarningCount "UseFromJust: no fire without import" [useFromJustRule]
1405+ (withoutImports "x = fromMaybe 0 (Just 1)") 0
1406+ ]
1407+
1408+-- ============================================================================
1409+-- UseHead Tests
1410+-- ============================================================================
1411+
1412+useHeadTests :: Array TestResult
1413+useHeadTests =
1414+ [ assertWarningCount "UseHead: xs !! 0" [useHeadRule]
1415+ (withPrelude "x = xs !! 0") 1
1416+ , assertRuleId "UseHead: correct rule id" [useHeadRule]
1417+ (withPrelude "x = xs !! 0") "UseHead"
1418+ , assertSuggestion "UseHead: suggests head" [useHeadRule]
1419+ (withPrelude "x = xs !! 0") "head"
1420+ , assertWarningCount "UseHead: no fire on xs !! 1" [useHeadRule]
1421+ (withPrelude "x = xs !! 1") 0
1422+ , assertWarningCount "UseHead: no fire without import" [useHeadRule]
1423+ (withoutImports "x = xs !! 0") 0
1424+ ]
1425+
1426+-- ============================================================================
1427+-- UseElemIndex Tests
1428+-- ============================================================================
1429+
1430+useElemIndexTests :: Array TestResult
1431+useElemIndexTests =
1432+ [ assertWarningCount "UseElemIndex: findIndex (\\x -> x == a) xs" [useElemIndexRule]
1433+ (withPrelude "x = findIndex (\\y -> y == a) xs") 1
1434+ , assertRuleId "UseElemIndex: correct rule id" [useElemIndexRule]
1435+ (withPrelude "x = findIndex (\\y -> y == a) xs") "UseElemIndex"
1436+ , assertSuggestion "UseElemIndex: suggests elemIndex" [useElemIndexRule]
1437+ (withPrelude "x = findIndex (\\y -> y == a) xs") "elemIndex a xs"
1438+ , assertWarningCount "UseElemIndex: no fire on other predicate" [useElemIndexRule]
1439+ (withPrelude "x = findIndex (\\y -> y > a) xs") 0
1440+ , assertWarningCount "UseElemIndex: no fire without import" [useElemIndexRule]
1441+ (withoutImports "x = findIndex (\\y -> y == a) xs") 0
1442+ ]
1443+
1444+-- ============================================================================
1445+-- UseBreak Tests
1446+-- ============================================================================
1447+
1448+useBreakTests :: Array TestResult
1449+useBreakTests =
1450+ [ assertWarningCount "UseBreak: span (not p) xs" [useBreakRule]
1451+ (withPrelude "x = span (not p) xs") 1
1452+ , assertRuleId "UseBreak: correct rule id" [useBreakRule]
1453+ (withPrelude "x = span (not p) xs") "UseBreak"
1454+ , assertSuggestion "UseBreak: suggests break" [useBreakRule]
1455+ (withPrelude "x = span (not p) xs") "break p xs"
1456+ , assertWarningCount "UseBreak: no fire without import" [useBreakRule]
1457+ (withoutImports "x = span (not p) xs") 0
1458+ ]
1459+
1460+-- ============================================================================
1461+-- UseSpan Tests
1462+-- ============================================================================
1463+
1464+useSpanTests :: Array TestResult
1465+useSpanTests =
1466+ [ assertWarningCount "UseSpan: break (not p) xs" [useSpanRule]
1467+ (withPrelude "x = break (not p) xs") 1
1468+ , assertRuleId "UseSpan: correct rule id" [useSpanRule]
1469+ (withPrelude "x = break (not p) xs") "UseSpan"
1470+ , assertSuggestion "UseSpan: suggests span" [useSpanRule]
1471+ (withPrelude "x = break (not p) xs") "span p xs"
1472+ , assertWarningCount "UseSpan: no fire without import" [useSpanRule]
1473+ (withoutImports "x = break (not p) xs") 0
1474+ ]
1475+
1476+-- ============================================================================
1477+-- UseDollar Tests
1478+-- ============================================================================
1479+
1480+useDollarTests :: Array TestResult
1481+useDollarTests =
1482+ [ assertWarningCount "UseDollar: map f (filter g xs)" [useDollarRule]
1483+ (withPrelude "x = map f (filter g xs)") 1
1484+ , assertRuleId "UseDollar: correct rule id" [useDollarRule]
1485+ (withPrelude "x = map f (filter g xs)") "UseDollar"
1486+ , assertSuggestion "UseDollar: suggests $" [useDollarRule]
1487+ (withPrelude "x = map f (filter g xs)") "map f $ filter g xs"
1488+ , assertWarningCount "UseDollar: no fire on atomic arg" [useDollarRule]
1489+ (withPrelude "x = f (1)") 0
1490+ ]
1491+
1492+-- ============================================================================
1493+-- UseUnwrap Tests
1494+-- ============================================================================
1495+
1496+useUnwrapTests :: Array TestResult
1497+useUnwrapTests =
1498+ [ assertRuleId "UseUnwrap: correct rule id" [useUnwrapRule]
1499+ (withPrelude "x = unwrap foo") "UseUnwrap"
1500+ , assertSuggestion "UseUnwrap: suggests un ?Constructor" [useUnwrapRule]
1501+ (withPrelude "x = unwrap foo") "un ?Constructor foo"
1502+ , assertWarningCount "UseUnwrap: unwrap partial" [useUnwrapRule]
1503+ (withPrelude "x = unwrap") 1
1504+ , assertSuggestion "UseUnwrap: unwrap partial suggestion" [useUnwrapRule]
1505+ (withPrelude "x = unwrap") "un ?Constructor"
12491506 ]
12501507
12511508-- ============================================================================
@@ -1291,13 +1548,351 @@ nothingBindTests =
12911548 ]
12921549
12931550-- ============================================================================
1294−-- Golden Tests: LintExamples.purs
1551+-- MonadLaw Tests
1552+-- ============================================================================
1553+
1554+monadLawTests :: Array TestResult
1555+monadLawTests =
1556+ [ assertWarningCount "MonadLaw: f =<< pure a" [monadLawRule]
1557+ (withPrelude "x = show =<< pure 42") 1
1558+ , assertRuleId "MonadLaw: correct rule id" [monadLawRule]
1559+ (withPrelude "x = show =<< pure 42") "MonadLaw"
1560+ , assertSuggestion "MonadLaw: suggests f a" [monadLawRule]
1561+ (withPrelude "x = show =<< pure 42") "show 42"
1562+ , assertWarningCount "MonadLaw: pure =<< m" [monadLawRule]
1563+ (withPrelude "x = pure =<< Just 1") 1
1564+ , assertSuggestion "MonadLaw: pure =<< m suggests m" [monadLawRule]
1565+ (withPrelude "x = pure =<< Just 1") "Just 1"
1566+ ]
1567+
1568+-- ============================================================================
1569+-- EvaluateBool Tests
1570+-- ============================================================================
1571+
1572+evaluateBoolTests :: Array TestResult
1573+evaluateBoolTests =
1574+ [ assertWarningCount "EvaluateBool: true && x" [evaluateBoolRule]
1575+ (withPrelude "x = true && foo") 1
1576+ , assertSuggestion "EvaluateBool: true && x -> x" [evaluateBoolRule]
1577+ (withPrelude "x = true && foo") "foo"
1578+ , assertWarningCount "EvaluateBool: false && x" [evaluateBoolRule]
1579+ (withPrelude "x = false && foo") 1
1580+ , assertSuggestion "EvaluateBool: false && x -> false" [evaluateBoolRule]
1581+ (withPrelude "x = false && foo") "false"
1582+ , assertWarningCount "EvaluateBool: true || x" [evaluateBoolRule]
1583+ (withPrelude "x = true || foo") 1
1584+ , assertSuggestion "EvaluateBool: true || x -> true" [evaluateBoolRule]
1585+ (withPrelude "x = true || foo") "true"
1586+ , assertWarningCount "EvaluateBool: false || x" [evaluateBoolRule]
1587+ (withPrelude "x = false || foo") 1
1588+ , assertSuggestion "EvaluateBool: false || x -> x" [evaluateBoolRule]
1589+ (withPrelude "x = false || foo") "foo"
1590+ , assertWarningCount "EvaluateBool: x && true" [evaluateBoolRule]
1591+ (withPrelude "x = foo && true") 1
1592+ , assertWarningCount "EvaluateBool: x || false" [evaluateBoolRule]
1593+ (withPrelude "x = foo || false") 1
1594+ ]
1595+
1596+-- ============================================================================
1597+-- UseFoldBool Tests
1598+-- ============================================================================
1599+
1600+useFoldBoolTests :: Array TestResult
1601+useFoldBoolTests =
1602+ [ assertWarningCount "UseFoldBool: foldr (&&) true" [useFoldBoolRule]
1603+ (withPrelude "x = foldr (&&) true xs") 1
1604+ , assertSuggestion "UseFoldBool: foldr (&&) true -> and" [useFoldBoolRule]
1605+ (withPrelude "x = foldr (&&) true xs") "and"
1606+ , assertWarningCount "UseFoldBool: foldr (||) false" [useFoldBoolRule]
1607+ (withPrelude "x = foldr (||) false xs") 1
1608+ , assertSuggestion "UseFoldBool: foldr (||) false -> or" [useFoldBoolRule]
1609+ (withPrelude "x = foldr (||) false xs") "or"
1610+ ]
1611+
1612+-- ============================================================================
1613+-- AlternativeLaw Tests
1614+-- ============================================================================
1615+
1616+alternativeLawTests :: Array TestResult
1617+alternativeLawTests =
1618+ [ assertWarningCount "AlternativeLaw: empty <|> x" [alternativeLawRule]
1619+ (withAlt "x = empty <|> foo") 1
1620+ , assertSuggestion "AlternativeLaw: empty <|> x -> x" [alternativeLawRule]
1621+ (withAlt "x = empty <|> foo") "foo"
1622+ , assertWarningCount "AlternativeLaw: x <|> empty" [alternativeLawRule]
1623+ (withAlt "x = foo <|> empty") 1
1624+ , assertSuggestion "AlternativeLaw: x <|> empty -> x" [alternativeLawRule]
1625+ (withAlt "x = foo <|> empty") "foo"
1626+ , assertWarningCount "AlternativeLaw: Nothing <|> x" [alternativeLawRule]
1627+ (withAlt "x = Nothing <|> foo") 1
1628+ ]
1629+
1630+-- ============================================================================
1631+-- EvaluateEither Tests
1632+-- ============================================================================
1633+
1634+evaluateEitherTests :: Array TestResult
1635+evaluateEitherTests =
1636+ [ assertWarningCount "EvaluateEither: either f g (Left x)" [evaluateEitherRule]
1637+ (withPrelude "x = either show identity (Left 42)") 1
1638+ , assertSuggestion "EvaluateEither: either f g (Left x) -> f x" [evaluateEitherRule]
1639+ (withPrelude "x = either show identity (Left 42)") "show 42"
1640+ , assertWarningCount "EvaluateEither: either f g (Right y)" [evaluateEitherRule]
1641+ (withPrelude "x = either show identity (Right \"hi\")") 1
1642+ , assertSuggestion "EvaluateEither: either f g (Right y) -> g y" [evaluateEitherRule]
1643+ (withPrelude "x = either show identity (Right \"hi\")") "identity \"hi\""
1644+ ]
1645+
1646+-- ============================================================================
1647+-- UseCaseOf Tests
1648+-- ============================================================================
1649+
1650+useCaseOfTests :: Array TestResult
1651+useCaseOfTests =
1652+ -- Positive cases: should detect
1653+ [ assertWarningCount "UseCaseOf: two clauses" [useCaseOfRule]
1654+ (withPrelude "foo True = 1\nfoo False = 0") 1
1655+ , assertRuleId "UseCaseOf: correct rule id" [useCaseOfRule]
1656+ (withPrelude "foo True = 1\nfoo False = 0") "UseCaseOf"
1657+ , assertSuggestion "UseCaseOf: suggestion format" [useCaseOfRule]
1658+ (withPrelude "foo True = 1\nfoo False = 0") "foo = case _ of\n True -> 1\n False -> 0"
1659+ , assertWarningCount "UseCaseOf: three clauses" [useCaseOfRule]
1660+ (withPrelude "bar Nothing = 0\nbar (Just 0) = 1\nbar (Just _) = 2") 1
1661+ , assertWarningCount "UseCaseOf: two args" [useCaseOfRule]
1662+ (withPrelude "add True True = 2\nadd True False = 1\nadd False True = 1\nadd False False = 0") 1
1663+ , assertSuggestion "UseCaseOf: two args suggestion" [useCaseOfRule]
1664+ (withPrelude "add True True = 2\nadd True False = 1\nadd False True = 1\nadd False False = 0") "add = case _, _ of\n True, True -> 2\n True, False -> 1\n False, True -> 1\n False, False -> 0"
1665+
1666+ -- Clauses with where bindings
1667+ , assertWarningCount "UseCaseOf: clause with where binding" [useCaseOfRule]
1668+ (withPrelude "foo x = go x\n where\n isJoin True = x\n where x = 1\n isJoin False = 0") 1
1669+ , assertSuggestion "UseCaseOf: clause with where binding suggestion" [useCaseOfRule]
1670+ (withPrelude "foo x = go x\n where\n isJoin True = x\n where x = 1\n isJoin False = 0") "isJoin = case _ of\n True ->\n x\n where\n x = 1\n False -> 0"
1671+
1672+ -- Regression test: nested case expression should preserve multiline formatting
1673+ , assertWarningCount "UseCaseOf: nested case expression" [useCaseOfRule]
1674+ (withPrelude "extractFn (Just x) =\n case x of\n 1 -> \"one\"\n _ -> \"other\"\nextractFn Nothing = \"nothing\"") 1
1675+ , assertSuggestion "UseCaseOf: nested case should preserve newlines" [useCaseOfRule]
1676+ (withPrelude "extractFn (Just x) =\n case x of\n 1 -> \"one\"\n _ -> \"other\"\nextractFn Nothing = \"nothing\"") "extractFn = case _ of\n (Just x) ->\n case x of\n 1 -> \"one\"\n _ -> \"other\"\n Nothing -> \"nothing\""
1677+
1678+ -- Negative cases: should NOT detect
1679+ , assertWarningCount "UseCaseOf: single clause" [useCaseOfRule]
1680+ (withPrelude "foo x = x + 1") 0
1681+ , assertWarningCount "UseCaseOf: no binders" [useCaseOfRule]
1682+ (withPrelude "foo = 42\nbar = 43") 0
1683+ , assertWarningCount "UseCaseOf: guarded" [useCaseOfRule]
1684+ (withPrelude "foo x | x > 0 = 1\nfoo x = 0") 0
1685+ ]
1686+
1687+-- ============================================================================
1688+-- FlattenCase Tests
1689+-- ============================================================================
1690+
1691+flattenCaseTests :: Array TestResult
1692+flattenCaseTests =
1693+ -- Positive cases: should detect
1694+ [ assertWarningCount "FlattenCase: two levels" [flattenCaseRule]
1695+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 0\n _ -> 0") 1
1696+ , assertRuleId "FlattenCase: correct rule id" [flattenCaseRule]
1697+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 0\n _ -> 0") "FlattenCase"
1698+ , assertSuggestion "FlattenCase: suggestion format" [flattenCaseRule]
1699+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 0\n _ -> 0") "case x, y of\n Just a, Just b -> a + b\n _, _ -> 0"
1700+
1701+ -- Negative cases: should NOT detect
1702+ , assertWarningCount "FlattenCase: single level" [flattenCaseRule]
1703+ (withPrelude "foo x = case x of\n Just a -> a\n _ -> 0") 0
1704+ , assertWarningCount "FlattenCase: different fallbacks" [flattenCaseRule]
1705+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 1\n _ -> 0") 0
1706+ ]
1707+
1708+-- ============================================================================
1709+-- UsePatternGuards Tests
1710+-- ============================================================================
1711+
1712+usePatternGuardsTests :: Array TestResult
1713+usePatternGuardsTests =
1714+ -- Positive cases: should detect
1715+ [ assertWarningCount "UsePatternGuards: two levels" [usePatternGuardsRule]
1716+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 0\n _ -> 0") 1
1717+ , assertRuleId "UsePatternGuards: correct rule id" [usePatternGuardsRule]
1718+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 0\n _ -> 0") "UsePatternGuards"
1719+ , assertWarningCount "UsePatternGuards: three levels" [usePatternGuardsRule]
1720+ (withPrelude "foo x y z = case x of\n Just a -> case y of\n Just b -> case z of\n Just c -> a + b + c\n _ -> 0\n _ -> 0\n _ -> 0") 2
1721+
1722+ -- Negative cases: should NOT detect
1723+ , assertWarningCount "UsePatternGuards: single level" [usePatternGuardsRule]
1724+ (withPrelude "foo x = case x of\n Just a -> a\n _ -> 0") 0
1725+ , assertWarningCount "UsePatternGuards: different fallbacks" [usePatternGuardsRule]
1726+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 1\n _ -> 0") 0
1727+
1728+ -- Fix test: verifies correct indentation relative to original branch position
1729+ , assertFix "UsePatternGuards: fix with proper indentation" [usePatternGuardsRule]
1730+ (withPrelude "foo x y = case x of\n Just a -> case y of\n Just b -> a + b\n _ -> 0\n _ -> 0")
1731+ (withPrelude "foo x y = case x of\n Just a\n | Just b <- y -> a + b\n _ -> 0")
1732+ ]
1733+
1734+-- ============================================================================
1735+-- RedundantParens Tests
12951736-- ============================================================================
12961737
1738+redundantParensTests :: Array TestResult
1739+redundantParensTests =
1740+ -- Positive cases: should detect redundant parens
1741+ [ assertWarningCount "RedundantParens: nested parens ((x))" [redundantParensRule]
1742+ (withPrelude "x = ((1))") 1
1743+ , assertRuleId "RedundantParens: correct rule id" [redundantParensRule]
1744+ (withPrelude "x = ((1))") "RedundantParens"
1745+ , assertSuggestion "RedundantParens: nested parens suggestion" [redundantParensRule]
1746+ (withPrelude "x = ((1))") "(1)"
1747+
1748+ -- Function arguments with atomic expressions
1749+ , assertWarningCount "RedundantParens: literal in arg" [redundantParensRule]
1750+ (withPrelude "x = show (42)") 1
1751+ , assertSuggestion "RedundantParens: literal in arg suggestion" [redundantParensRule]
1752+ (withPrelude "x = show (42)") "42"
1753+ , assertWarningCount "RedundantParens: identifier in arg" [redundantParensRule]
1754+ (withPrelude "x y = show (y)") 1
1755+ , assertWarningCount "RedundantParens: string in arg" [redundantParensRule]
1756+ (withPrelude "x = show (\"hi\")") 1
1757+ , assertWarningCount "RedundantParens: array in arg" [redundantParensRule]
1758+ (withPrelude "x = length ([1, 2])") 1
1759+ , assertWarningCount "RedundantParens: record in arg" [redundantParensRule]
1760+ (withPrelude "x = show ({ a: 1 })") 1
1761+
1762+ -- Multiple redundant args
1763+ , assertWarningCount "RedundantParens: two redundant args" [redundantParensRule]
1764+ (withPrelude "x = add (1) (2)") 2
1765+
1766+ -- Array elements
1767+ , assertWarningCount "RedundantParens: array element literal" [redundantParensRule]
1768+ (withPrelude "x = [(1), (2)]") 2
1769+ , assertWarningCount "RedundantParens: array mixed" [redundantParensRule]
1770+ (withPrelude "x = [(1), 2, (3)]") 2
1771+ , assertWarningCount "RedundantParens: no fire on complex array element" [redundantParensRule]
1772+ (withPrelude "x = [(1 + 2)]") 0
1773+
1774+ -- Negative cases: should NOT detect
1775+ , assertWarningCount "RedundantParens: no fire on precedence (1+2)*3" [redundantParensRule]
1776+ (withPrelude "x = (1 + 2) * 3") 0
1777+ , assertWarningCount "RedundantParens: no fire on complex arg" [redundantParensRule]
1778+ (withPrelude "x = show (1 + 2)") 0
1779+ , assertWarningCount "RedundantParens: no fire on lambda arg" [redundantParensRule]
1780+ (withPrelude "x = map (\\y -> y + 1) [1]") 0
1781+ , assertWarningCount "RedundantParens: no fire on negate arg" [redundantParensRule]
1782+ (withPrelude "x f = f (-1)") 0
1783+ , assertWarningCount "RedundantParens: no fire on section" [redundantParensRule]
1784+ (withPrelude "x = (_ + 1)") 0
1785+ , assertWarningCount "RedundantParens: no fire on if" [redundantParensRule]
1786+ (withPrelude "x = (if true then 1 else 2)") 0
1787+ , assertWarningCount "RedundantParens: no fire on let" [redundantParensRule]
1788+ (withPrelude "x = (let y = 1 in y)") 0
1789+
1790+ -- Outer parens on RHS (simple cases)
1791+ , assertWarningCount "RedundantParens: outer parens on literal RHS" [redundantParensRule]
1792+ (withPrelude "x = (42)") 1
1793+ , assertWarningCount "RedundantParens: outer parens on app RHS" [redundantParensRule]
1794+ (withPrelude "x = (show 1)") 1
1795+ , assertWarningCount "RedundantParens: outer parens on op RHS" [redundantParensRule]
1796+ (withPrelude "x = (1 + 2)") 1
1797+
1798+ -- If branches
1799+ , assertWarningCount "RedundantParens: if condition" [redundantParensRule]
1800+ (withPrelude "x = if (true) then 1 else 2") 1
1801+ , assertWarningCount "RedundantParens: if then branch" [redundantParensRule]
1802+ (withPrelude "x = if true then (1) else 2") 1
1803+
1804+ -- Case branches
1805+ , assertWarningCount "RedundantParens: case scrutinee" [redundantParensRule]
1806+ (withPrelude "x = case (1) of y -> y") 1
1807+ , assertWarningCount "RedundantParens: case body" [redundantParensRule]
1808+ (withPrelude "x = case 1 of y -> (y)") 1
1809+
1810+ -- Let expression
1811+ , assertWarningCount "RedundantParens: let binding" [redundantParensRule]
1812+ (withPrelude "x = let y = (1) in y") 1
1813+ , assertWarningCount "RedundantParens: let body" [redundantParensRule]
1814+ (withPrelude "x = let y = 1 in (y)") 1
1815+
1816+ -- Record fields
1817+ , assertWarningCount "RedundantParens: record field" [redundantParensRule]
1818+ (withPrelude "x = { a: (1) }") 1
1819+ , assertWarningCount "RedundantParens: multiple record fields" [redundantParensRule]
1820+ (withPrelude "x = { a: (1), b: (2) }") 2
1821+
1822+ -- Pattern (binder) cases
1823+ , assertWarningCount "RedundantParens: nested parens in pattern" [redundantParensRule]
1824+ (withPrelude "f ((x)) = x") 1
1825+ , assertSuggestion "RedundantParens: nested parens in pattern suggestion" [redundantParensRule]
1826+ (withPrelude "f ((x)) = x") "(x)"
1827+ , assertWarningCount "RedundantParens: atomic pattern in constructor arg" [redundantParensRule]
1828+ (withPrelude "f (Just (x)) = x\nf Nothing = 0") 1
1829+ , assertSuggestion "RedundantParens: constructor arg suggestion" [redundantParensRule]
1830+ (withPrelude "f (Just (x)) = x\nf Nothing = 0") "x"
1831+ , assertWarningCount "RedundantParens: wildcard in constructor" [redundantParensRule]
1832+ (withPrelude "f (Just (_)) = 1\nf Nothing = 0") 1
1833+ , assertWarningCount "RedundantParens: literal in constructor" [redundantParensRule]
1834+ (withPrelude "f (Just (1)) = true\nf _ = false") 1
1835+ , assertWarningCount "RedundantParens: array pattern element" [redundantParensRule]
1836+ (withPrelude "f [(x), (y)] = x + y\nf _ = 0") 2
1837+
1838+ -- Negative pattern cases
1839+ , assertWarningCount "RedundantParens: no fire on constructor with args" [redundantParensRule]
1840+ (withPrelude "f (Just (Right x)) = x\nf _ = 0") 0
1841+ , assertWarningCount "RedundantParens: no fire on typed pattern" [redundantParensRule]
1842+ (withPrelude "f (x :: Int) = x") 0
1843+ , assertWarningCount "RedundantParens: no fire on negated number pattern" [redundantParensRule]
1844+ (withPrelude "f (Just (-1)) = true\nf _ = false") 0
1845+
1846+ -- Top-level case pattern parens
1847+ , assertWarningCount "RedundantParens: top-level case pattern" [redundantParensRule]
1848+ (withPrelude "f = case _ of\n (Just x) -> x\n Nothing -> 0") 1
1849+ , assertSuggestion "RedundantParens: top-level case pattern suggestion" [redundantParensRule]
1850+ (withPrelude "f = case _ of\n (Just x) -> x\n Nothing -> 0") "Just x"
1851+ , assertWarningCount "RedundantParens: top-level constructor pattern" [redundantParensRule]
1852+ (withPrelude "f = case _ of\n (Foo a b) -> a\n _ -> 0") 1
1853+ , assertWarningCount "RedundantParens: no fire on cons pattern" [redundantParensRule]
1854+ (withPrelude "f = case _ of\n (x : xs) -> x\n _ -> 0") 0
1855+ , assertWarningCount "RedundantParens: no fire on typed case pattern" [redundantParensRule]
1856+ (withPrelude "f = case _ of\n (x :: Int) -> x") 0
1857+
1858+ -- Type-level redundant parens
1859+ , assertWarningCount "RedundantParens: nested type parens" [redundantParensRule]
1860+ (withPrelude "x :: ((Int))\nx = 1") 1
1861+ , assertSuggestion "RedundantParens: nested type parens suggestion" [redundantParensRule]
1862+ (withPrelude "x :: ((Int))\nx = 1") "(Int)"
1863+ , assertWarningCount "RedundantParens: type app with atomic arg" [redundantParensRule]
1864+ (withPrelude "x :: Maybe (Int)\nx = Nothing") 1
1865+ , assertSuggestion "RedundantParens: type app atomic arg suggestion" [redundantParensRule]
1866+ (withPrelude "x :: Maybe (Int)\nx = Nothing") "Int"
1867+ , assertWarningCount "RedundantParens: type app with type var" [redundantParensRule]
1868+ (withPrelude "x :: forall a. Maybe (a)\nx = Nothing") 1
1869+ , assertWarningCount "RedundantParens: arrow left simple" [redundantParensRule]
1870+ (withPrelude "x :: (Int) -> String\nx = show") 1
1871+ , assertWarningCount "RedundantParens: record field type" [redundantParensRule]
1872+ (withPrelude "type R = { x :: (Int) }") 1
1873+
1874+ -- Type-level negative cases
1875+ , assertWarningCount "RedundantParens: no fire on function type in arg" [redundantParensRule]
1876+ (withPrelude "x :: (Int -> Int) -> Int\nx f = f 1") 0
1877+ , assertWarningCount "RedundantParens: no fire on forall in arg" [redundantParensRule]
1878+ (withPrelude "x :: (forall a. a -> a) -> Int\nx f = f 1") 0
1879+ , assertWarningCount "RedundantParens: no fire on constrained type" [redundantParensRule]
1880+ (withPrelude "x :: (Show a) => a -> String\nx = show") 0
1881+ ]
1882+
1883+-- | Helper for Alternative imports
1884+withAlt :: String -> String
1885+withAlt code = """
1886+module Test where
1887+import Prelude
1888+import Control.Alternative (empty, (<|>))
1889+import Data.Maybe (Maybe(..), Nothing, Just)
1890+""" <> code
1891+
12971892-- | All rules for golden testing
12981893allRulesForGolden :: Array Rule
12991894allRulesForGolden =
1300− [ fmapIdRule
1895+ [ mapIdentityRule
13011896 , mapFusionRule
13021897 , useTraverseRule
13031898 , concatMapRule
@@ -1343,200 +1938,222 @@ allRulesForGolden =
13431938 , useFindMapRule
13441939 ]
13451940
1346−-- | Assert that a specific rule fires at least once when running all rules
1347−assertRuleFires :: String -> String -> String -> TestResult
1348−assertRuleFires name source ruleId =
1349− case runRules allRulesForGolden (SourceCode source) of
1350− Left err -> runTest name false ("Parse error: " <> err)
1351− Right (LintResult result) ->
1352− let matches = Array.filter (\(LintWarning w) -> unwrap w.ruleId == ruleId) result.warnings
1353− in if Array.length matches > 0
1354− then runTest name true "OK"
1355− else runTest name false ("Rule " <> ruleId <> " did not fire")
1356−
1357−-- | The embedded LintExamples source
1358−lintExamplesSource :: String
1359−lintExamplesSource = """module Test.LintExamples where
1360−
1361−import Prelude
1362−
1363−import Control.Alternative
1364−import Control.Monad
1365−import Data.Array
1366−import Data.Foldable
1367−import Data.Maybe
1368−import Data.Monoid
1369−import Data.Ord
1370−import Data.Traversable
1371−import Data.Tuple
1372−
1373−-- FmapId
1374−fmapId1 = map identity [ 1, 2, 3 ]
1375−
1376−-- MapFusion
1377−mapFusion1 = map show (map (_ + 1) [ 1, 2, 3 ])
1378−
1379−-- UseTraverse
1380−useTraverse1 f xs = sequence (map f xs)
1381−
1382−-- ConcatMap
1383−concatMap1 f xs = join (map f xs)
1384−
1385−-- NotEqual
1386−notEqual1 x y = not (x == y)
1387−
1388−-- UseGuard
1389−useGuard1 cond action = if cond then action else pure unit
1390−
1391−-- EtaReduce
1392−etaReduce1 = \x -> show x
1393−
1394−-- EtaReduceDecl
1395−etaReduceDecl1 x = show x
1396−
1397−-- RedundantBind
1398−redundantBind1 x f = pure x >>= f
1399−
1400−-- LetToWhere
1401−letToWhere1 y z = let x = y in z
1402−
1403−-- RedundantIf
1404−redundantIf1 a = if a then true else false
1405−
1406−-- UseVoid
1407−useVoid1 a = a *> pure unit
1408−
1409−-- UseJoin
1410−useJoin1 x = x >>= identity
1411−
1412−-- UseAny
1413−useAny1 f xs = or (map f xs)
1414−
1415−-- UseAll
1416−useAll1 f xs = and (map f xs)
1417−
1418−-- BooleanSimplify
1419−booleanSimplify1 x = x == true
1420−
1421−-- CollapseLambdas
1422−collapseLambdas1 = \x -> \y -> x + y
1423−
1424−-- UseConst
1425−useConst1 = \x -> 42
1426−
1427−-- UseNull
1428−useNull1 xs = length xs == 0
1429−
1430−-- UseFromMaybe
1431−useFromMaybe1 def = maybe def identity
1432−
1433−-- UseIsJust
1434−useIsJust1 = maybe false (const true)
1435−
1436−-- UseIsNothing
1437−useIsNothing1 = maybe true (const false)
1438−
1439−-- UseWhen
1440−useWhen1 cond action = if cond then action else pure unit
1441−
1442−-- UseUnless
1443−useUnless1 cond action = if cond then pure unit else action
1444−
1445−-- RedundantFlip
1446−redundantFlip1 f = flip (flip f)
1447−
1448−-- UseNotElem
1449−useNotElem1 x xs = not (elem x xs)
1450−
1451−-- UseMinMax
1452−useMax1 a b = if a > b then a else b
1453−
1454−-- MonoidIdentity
1455−monoidIdentity1 x = mempty <> x
1456−
1457−-- UseFold
1458−useFold1 = foldMap identity
1459−
1460−-- UseSequence
1461−useSequence1 = traverse identity
1462−
1463−-- UseTraverseSequence
1464−useTraverseSequence1 f xs = sequence (map f xs)
1465−
1466−-- RedundantNot
1467−redundantNot1 x = not (not x)
1468−
1469−-- UseFstSnd
1470−useFst1 = \(Tuple x y) -> x
1471−
1472−-- UseMapMaybe
1473−useMapMaybe1 = catMaybes (map f [1,2,3])
1474−
1475−-- UseApplicative
1476−useApplicative1 = pure f <*> x
1477−
1478−-- UseBindFlip
1479−useBindFlip1 = join (map f x)
1480−
1481−-- UseFor
1482−useFor1 = flip traverse
1483−
1484−-- RedundantGuard
1485−redundantGuard1 = guard true
1486−
1487−-- UseComparing
1488−useComparing1 = \a b -> compare (f a) (f b)
1489−
1490−-- UseOn
1491−useOn1 = \a b -> f (g a) (g b)
1492−
1493−-- UseFindMap
1494−useFindMap1 = head (mapMaybe f [1,2,3])
1495−"""
1496−
1497−goldenTests :: Array TestResult
1498−goldenTests =
1499− [ assertRuleFires "Golden: FmapId fires" lintExamplesSource "FmapId"
1500− , assertRuleFires "Golden: MapFusion fires" lintExamplesSource "MapFusion"
1501− , assertRuleFires "Golden: UseTraverse fires" lintExamplesSource "UseTraverse"
1502− , assertRuleFires "Golden: ConcatMap fires" lintExamplesSource "ConcatMap"
1503− , assertRuleFires "Golden: NotEqual fires" lintExamplesSource "NotEqual"
1504− , assertRuleFires "Golden: UseGuard fires" lintExamplesSource "UseGuard"
1505− , assertRuleFires "Golden: EtaReduce fires" lintExamplesSource "EtaReduce"
1506− , assertRuleFires "Golden: EtaReduceDecl fires" lintExamplesSource "EtaReduceDecl"
1507− , assertRuleFires "Golden: RedundantBind fires" lintExamplesSource "RedundantBind"
1508− , assertRuleFires "Golden: LetToWhere fires" lintExamplesSource "LetToWhere"
1509− , assertRuleFires "Golden: RedundantIf fires" lintExamplesSource "RedundantIf"
1510− , assertRuleFires "Golden: UseVoid fires" lintExamplesSource "UseVoid"
1511− , assertRuleFires "Golden: UseJoin fires" lintExamplesSource "UseJoin"
1512− , assertRuleFires "Golden: UseAny fires" lintExamplesSource "UseAny"
1513− , assertRuleFires "Golden: UseAll fires" lintExamplesSource "UseAll"
1514− , assertRuleFires "Golden: BooleanSimplify fires" lintExamplesSource "BooleanSimplify"
1515− , assertRuleFires "Golden: CollapseLambdas fires" lintExamplesSource "CollapseLambdas"
1516− , assertRuleFires "Golden: UseConst fires" lintExamplesSource "UseConst"
1517− , assertRuleFires "Golden: UseNull fires" lintExamplesSource "UseNull"
1518− , assertRuleFires "Golden: UseFromMaybe fires" lintExamplesSource "UseFromMaybe"
1519− , assertRuleFires "Golden: UseIsJust fires" lintExamplesSource "UseIsJust"
1520− , assertRuleFires "Golden: UseIsNothing fires" lintExamplesSource "UseIsNothing"
1521− , assertRuleFires "Golden: UseWhen fires" lintExamplesSource "UseWhen"
1522− , assertRuleFires "Golden: UseUnless fires" lintExamplesSource "UseUnless"
1523− , assertRuleFires "Golden: RedundantFlip fires" lintExamplesSource "RedundantFlip"
1524− , assertRuleFires "Golden: UseNotElem fires" lintExamplesSource "UseNotElem"
1525− , assertRuleFires "Golden: UseMinMax fires" lintExamplesSource "UseMinMax"
1526− , assertRuleFires "Golden: MonoidIdentity fires" lintExamplesSource "MonoidIdentity"
1527− , assertRuleFires "Golden: UseFold fires" lintExamplesSource "UseFold"
1528− , assertRuleFires "Golden: UseSequence fires" lintExamplesSource "UseSequence"
1529− , assertRuleFires "Golden: UseTraverseSequence fires" lintExamplesSource "UseTraverseSequence"
1530− , assertRuleFires "Golden: RedundantNot fires" lintExamplesSource "RedundantNot"
1531− , assertRuleFires "Golden: UseFstSnd fires" lintExamplesSource "UseFstSnd"
1532− , assertRuleFires "Golden: UseMapMaybe fires" lintExamplesSource "UseMapMaybe"
1533− , assertRuleFires "Golden: UseApplicative fires" lintExamplesSource "UseApplicative"
1534− , assertRuleFires "Golden: UseBindFlip fires" lintExamplesSource "UseBindFlip"
1535− , assertRuleFires "Golden: UseFor fires" lintExamplesSource "UseFor"
1536− , assertRuleFires "Golden: RedundantGuard fires" lintExamplesSource "RedundantGuard"
1537− , assertRuleFires "Golden: UseComparing fires" lintExamplesSource "UseComparing"
1538− , assertRuleFires "Golden: UseOn fires" lintExamplesSource "UseOn"
1539− , assertRuleFires "Golden: UseFindMap fires" lintExamplesSource "UseFindMap"
1941+goldenTests :: Effect (Array TestResult)
1942+goldenTests = traverse identity
1943+ -- MapIdentity: map identity x -> x
1944+ [ assertFixFormatted "Golden: MapIdentity fix" [mapIdentityRule]
1945+ (withPrelude "x = map identity [ 1, 2, 3 ]")
1946+ (withPrelude "x = [ 1, 2, 3 ]")
1947+
1948+ -- MapFusion: map f (map g x) -> map (f <<< g) x
1949+ , assertFixFormatted "Golden: MapFusion fix" [mapFusionRule]
1950+ (withPrelude "x = map show (map (_ + 1) [ 1, 2, 3 ])")
1951+ (withPrelude "x = map (show <<< (_ + 1)) [ 1, 2, 3 ]")
1952+
1953+ -- UseTraverse: sequence (map f x) -> mapM f x
1954+ , assertFixFormatted "Golden: UseTraverse fix" [useTraverseRule]
1955+ (withPrelude "x f xs = sequence (map f xs)")
1956+ (withPrelude "x f xs = mapM f xs")
1957+
1958+ -- ConcatMap: join (map f x) -> (=<<) f x
1959+ , assertFixFormatted "Golden: ConcatMap fix" [concatMapRule]
1960+ (withPrelude "x f xs = join (map f xs)")
1961+ (withPrelude "x f xs = (=<<) f xs")
1962+
1963+ -- NotEqual: not (a == b) -> a /= b
1964+ , assertFixFormatted "Golden: NotEqual fix" [notEqualRule]
1965+ (withPrelude "x a b = not (a == b)")
1966+ (withPrelude "x a b = a /= b")
1967+
1968+ -- EtaReduce: \x -> f x -> f
1969+ , assertFixFormatted "Golden: EtaReduce fix" [etaReduceRule]
1970+ (withPrelude "x = \\y -> show y")
1971+ (withPrelude "x = show")
1972+
1973+ -- RedundantBind: pure x >>= f -> f x
1974+ , assertFixFormatted "Golden: RedundantBind fix" [redundantBindRule]
1975+ (withPrelude "x y f = pure y >>= f")
1976+ (withPrelude "x y f = f y")
1977+
1978+ -- RedundantIf: if a then true else false -> a
1979+ , assertFixFormatted "Golden: RedundantIf fix" [redundantIfRule]
1980+ (withPrelude "x a = if a then true else false")
1981+ (withPrelude "x a = a")
1982+
1983+ -- UseVoid: a *> pure unit -> void a
1984+ , assertFixFormatted "Golden: UseVoid fix" [useVoidRule]
1985+ (withPrelude "x a = a *> pure unit")
1986+ (withPrelude "x a = void a")
1987+
1988+ -- UseJoin: x >>= identity -> join x
1989+ , assertFixFormatted "Golden: UseJoin fix" [useJoinRule]
1990+ (withPrelude "x m = m >>= identity")
1991+ (withPrelude "x m = join m")
1992+
1993+ -- UseAny: or (map f x) -> any f x
1994+ , assertFixFormatted "Golden: UseAny fix" [useAnyRule]
1995+ (withPrelude "x f xs = or (map f xs)")
1996+ (withPrelude "x f xs = any f xs")
1997+
1998+ -- UseAll: and (map f x) -> all f x
1999+ , assertFixFormatted "Golden: UseAll fix" [useAllRule]
2000+ (withPrelude "x f xs = and (map f xs)")
2001+ (withPrelude "x f xs = all f xs")
2002+
2003+ -- BooleanSimplify: x == true -> x
2004+ , assertFixFormatted "Golden: BooleanSimplify fix" [booleanSimplifyRule]
2005+ (withPrelude "x a = a == true")
2006+ (withPrelude "x a = a")
2007+
2008+ -- CollapseLambdas: \x -> \y -> body -> \x y -> body
2009+ , assertFixFormatted "Golden: CollapseLambdas fix" [collapseLambdasRule]
2010+ (withPrelude "x = \\a -> \\b -> a + b")
2011+ (withPrelude "x = \\a b -> a + b")
2012+
2013+ -- UseConst: \_ -> y -> const y
2014+ , assertFixFormatted "Golden: UseConst fix" [useConstRule]
2015+ (withPrelude "x = \\_ -> 42")
2016+ (withPrelude "x = const 42")
2017+
2018+ -- UseNull: length xs == 0 -> null xs
2019+ , assertFixFormatted "Golden: UseNull fix" [useNullRule]
2020+ (withPrelude "x xs = length xs == 0")
2021+ (withPrelude "x xs = null xs")
2022+
2023+ -- UseFromMaybe: maybe def identity -> fromMaybe def
2024+ , assertFixFormatted "Golden: UseFromMaybe fix" [useFromMaybeRule]
2025+ (withPrelude "x def = maybe def identity")
2026+ (withPrelude "x def = fromMaybe def")
2027+
2028+ -- UseIsJust: maybe false (const true) -> isJust
2029+ , assertFixFormatted "Golden: UseIsJust fix" [useIsJustRule]
2030+ (withPrelude "x = maybe false (const true)")
2031+ (withPrelude "x = isJust")
2032+
2033+ -- UseIsNothing: maybe true (const false) -> isNothing
2034+ , assertFixFormatted "Golden: UseIsNothing fix" [useIsNothingRule]
2035+ (withPrelude "x = maybe true (const false)")
2036+ (withPrelude "x = isNothing")
2037+
2038+ -- UseWhen: if cond then action else pure unit -> when cond action
2039+ , assertFixFormatted "Golden: UseWhen fix" [useWhenRule]
2040+ (withPrelude "x cond action = if cond then action else pure unit")
2041+ (withPrelude "x cond action = when cond action")
2042+
2043+ -- UseUnless: if cond then pure unit else action -> unless cond action
2044+ , assertFixFormatted "Golden: UseUnless fix" [useUnlessRule]
2045+ (withPrelude "x cond action = if cond then pure unit else action")
2046+ (withPrelude "x cond action = unless cond action")
2047+
2048+ -- RedundantFlip: flip (flip f) -> f
2049+ , assertFixFormatted "Golden: RedundantFlip fix" [redundantFlipRule]
2050+ (withPrelude "x f = flip (flip f)")
2051+ (withPrelude "x f = f")
2052+
2053+ -- UseNotElem: not (elem x xs) -> notElem x xs
2054+ , assertFixFormatted "Golden: UseNotElem fix" [useNotElemRule]
2055+ (withPrelude "x a xs = not (elem a xs)")
2056+ (withPrelude "x a xs = notElem a xs")
2057+
2058+ -- UseMinMax: if a > b then a else b -> max a b
2059+ , assertFixFormatted "Golden: UseMinMax fix" [useMinMaxRule]
2060+ (withPrelude "x a b = if a > b then a else b")
2061+ (withPrelude "x a b = max a b")
2062+
2063+ -- MonoidIdentity: mempty <> x -> x
2064+ , assertFixFormatted "Golden: MonoidIdentity fix" [monoidIdentityRule]
2065+ (withPrelude "x xs = mempty <> xs")
2066+ (withPrelude "x xs = xs")
2067+
2068+ -- UseFold: foldMap identity -> fold
2069+ , assertFixFormatted "Golden: UseFold fix" [useFoldRule]
2070+ (withPrelude "x = foldMap identity")
2071+ (withPrelude "x = fold")
2072+
2073+ -- UseSequence: traverse identity -> sequence
2074+ , assertFixFormatted "Golden: UseSequence fix" [useSequenceRule]
2075+ (withPrelude "x = traverse identity")
2076+ (withPrelude "x = sequence")
2077+
2078+ -- RedundantNot: not (not x) -> x
2079+ , assertFixFormatted "Golden: RedundantNot fix" [redundantNotRule]
2080+ (withPrelude "x a = not (not a)")
2081+ (withPrelude "x a = a")
2082+
2083+ -- UseFstSnd: \(Tuple x _) -> x -> fst
2084+ , assertFixFormatted "Golden: UseFstSnd fix" [useFstSndRule]
2085+ (withPrelude "x = \\(Tuple a b) -> a")
2086+ (withPrelude "x = fst")
2087+
2088+ -- UseMapMaybe: catMaybes (map f x) -> mapMaybe f x
2089+ , assertFixFormatted "Golden: UseMapMaybe fix" [useMapMaybeRule]
2090+ (withPrelude "x f xs = catMaybes (map f xs)")
2091+ (withPrelude "x f xs = mapMaybe f xs")
2092+
2093+ -- UseApplicative: pure f <*> x -> f <$> x
2094+ , assertFixFormatted "Golden: UseApplicative fix" [useApplicativeRule]
2095+ (withPrelude "x f y = pure f <*> y")
2096+ (withPrelude "x f y = f <$> y")
2097+
2098+ -- UseBindFlip: join (map f x) -> x >>= f
2099+ , assertFixFormatted "Golden: UseBindFlip fix" [useBindFlipRule]
2100+ (withPrelude "x f y = join (map f y)")
2101+ (withPrelude "x f y = y >>= f")
2102+
2103+ -- UseFor: flip traverse -> for
2104+ , assertFixFormatted "Golden: UseFor fix" [useForRule]
2105+ (withPrelude "x = flip traverse")
2106+ (withPrelude "x = for")
2107+
2108+ -- RedundantGuard: guard true -> pure unit
2109+ , assertFixFormatted "Golden: RedundantGuard fix" [redundantGuardRule]
2110+ (withPrelude "x = guard true")
2111+ (withPrelude "x = pure unit")
2112+
2113+ -- UseComparing: \a b -> compare (f a) (f b) -> comparing f
2114+ , assertFixFormatted "Golden: UseComparing fix" [useComparingRule]
2115+ (withPrelude "x f = \\a b -> compare (f a) (f b)")
2116+ (withPrelude "x f = comparing f")
2117+
2118+ -- UseOn: \a b -> f (g a) (g b) -> on f g
2119+ , assertFixFormatted "Golden: UseOn fix" [useOnRule]
2120+ (withPrelude "x f g = \\a b -> f (g a) (g b)")
2121+ (withPrelude "x f g = on f g")
2122+
2123+ -- UseFindMap: head (mapMaybe f x) -> findMap f x
2124+ , assertFixFormatted "Golden: UseFindMap fix" [useFindMapRule]
2125+ (withPrelude "x f xs = head (mapMaybe f xs)")
2126+ (withPrelude "x f xs = findMap f xs")
2127+
2128+ -- LetToDo: nested let...in -> do notation (do on previous line)
2129+ , pure $ assertFix "Golden: LetToDo nested fix" [letToDoRule]
2130+ (withPrelude "x f =\n case f of\n Just y ->\n let\n z = y + 1\n in z * 2\n Nothing -> 0")
2131+ (withPrelude "x f =\n case f of\n Just y -> do\n let\n z = y + 1\n z * 2\n Nothing -> 0")
2132+
2133+ -- LetToDo: should NOT fire on do-let (no "in" keyword)
2134+ , pure $ assertWarningCount "Golden: LetToDo no fire on do-let" [letToDoRule]
2135+ (withPrelude "x f =\n case f of\n Just y -> do\n let\n z = y + 1\n [ z ]\n Nothing -> []")
2136+ 0
2137+
2138+ -- UsePatternGuards: should preserve do-let body structure
2139+ , pure $ assertFix "Golden: UsePatternGuards preserves do-let" [usePatternGuardsRule]
2140+ (withPrelude "checkExpr expr =\n case expr of\n Just mapArgs ->\n case toArray mapArgs of\n [ x, y ] -> do\n let\n f = show x\n g = show y\n [ f, g ]\n _ -> []\n _ -> []")
2141+ (withPrelude "checkExpr expr =\n case expr of\n Just mapArgs\n | [ x, y ] <- toArray mapArgs -> do\n let\n f = show x\n g = show y\n [ f, g ]\n _ -> [ ]")
2142+
2143+ -- UseFoldMap: should preserve multiline formatting in complex body
2144+ , pure $ assertFix "Golden: UseFoldMap preserves multiline body" [useFoldMapRule]
2145+ (withPrelude "checkExpr mx =\n case mx of\n Just p ->\n let\n pText = show p\n in\n [ pText ]\n Nothing -> []")
2146+ (withPrelude "checkExpr mx =\n foldMap\n ( \\p ->\n let\n pText = show p\n in\n [ pText ]\n )\n mx")
2147+
2148+ -- UseEitherMap: either Left (Right <<< f) -> map f
2149+ , pure $ assertFix "Golden: UseEitherMap basic" [useEitherMapRule]
2150+ (withPrelude "x = either Left (Right <<< show)")
2151+ (withPrelude "x = map show")
2152+
2153+ -- UseConst: \_ -> multiline body -> const (multiline body)
2154+ , pure $ assertFix "Golden: UseConst preserves multiline body" [useConstRule]
2155+ (withPrelude "x =\n foldMap\n ( \\_ ->\n [ LintWarning\n { ruleId: RuleId \"UseEitherMap\"\n , message: WarningMessage \"either Left (Right <<< f) can be simplified to map f\"\n }\n ]\n )\n secondArg")
2156+ (withPrelude "x =\n foldMap\n ( const\n [ LintWarning\n { ruleId: RuleId \"UseEitherMap\"\n , message: WarningMessage \"either Left (Right <<< f) can be simplified to map f\"\n }\n ]\n )\n secondArg")
15402157 ]
15412158
15422159-- ============================================================================
@@ -1575,7 +2192,7 @@ tidySuggestions =
15752192 , { name: "traverse f xs", suggestion: "traverse f xs" }
15762193 , { name: "collapsed lambda", suggestion: "\\a b -> a + b" }
15772194 , { name: "map fusion", suggestion: "map (f <<< g) x" }
1578− , { name: "fmap fusion", suggestion: "(f <<< g) <$> x" }
2195+ , { name: "map fusion with <$>", suggestion: "(f <<< g) <$> x" }
15792196 , { name: "flipped map fusion", suggestion: "x <#> (g >>> f)" }
15802197 , { name: "where clause", suggestion: "y + 1\n where\n y = 1" }
15812198 ]
vscode-extension/.gitignorenew file+1
@@ -0,0 +1 @@
1+.vscode-test/
vscode-extension/out/extension.jsnew file+77
@@ -0,0 +1,77 @@
1+"use strict";
2+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+ if (k2 === undefined) k2 = k;
4+ var desc = Object.getOwnPropertyDescriptor(m, k);
5+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+ desc = { enumerable: true, get: function() { return m[k]; } };
7+ }
8+ Object.defineProperty(o, k2, desc);
9+}) : (function(o, m, k, k2) {
10+ if (k2 === undefined) k2 = k;
11+ o[k2] = m[k];
12+}));
13+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15+}) : function(o, v) {
16+ o["default"] = v;
17+});
18+var __importStar = (this && this.__importStar) || (function () {
19+ var ownKeys = function(o) {
20+ ownKeys = Object.getOwnPropertyNames || function (o) {
21+ var ar = [];
22+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23+ return ar;
24+ };
25+ return ownKeys(o);
26+ };
27+ return function (mod) {
28+ if (mod && mod.__esModule) return mod;
29+ var result = {};
30+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31+ __setModuleDefault(result, mod);
32+ return result;
33+ };
34+})();
35+Object.defineProperty(exports, "__esModule", { value: true });
36+exports.activate = activate;
37+exports.deactivate = deactivate;
38+const vscode = __importStar(require("vscode"));
39+const node_1 = require("vscode-languageclient/node");
40+let client;
41+function activate(context) {
42+ const config = vscode.workspace.getConfiguration('purslint');
43+ if (!config.get('enable', true)) {
44+ return;
45+ }
46+ // Get server path from config or use default
47+ let serverPath = config.get('serverPath', '');
48+ if (!serverPath) {
49+ // Default to the purslint project location
50+ serverPath = '/Users/mark/Developer/purslint/dist/purslint-lsp.js';
51+ }
52+ const serverOptions = {
53+ run: {
54+ module: serverPath,
55+ transport: node_1.TransportKind.stdio
56+ },
57+ debug: {
58+ module: serverPath,
59+ transport: node_1.TransportKind.stdio
60+ }
61+ };
62+ const clientOptions = {
63+ documentSelector: [{ scheme: 'file', language: 'purescript' }],
64+ synchronize: {
65+ fileEvents: vscode.workspace.createFileSystemWatcher('**/*.purs')
66+ }
67+ };
68+ client = new node_1.LanguageClient('purslint', 'Purslint', serverOptions, clientOptions);
69+ client.start();
70+}
71+function deactivate() {
72+ if (!client) {
73+ return undefined;
74+ }
75+ return client.stop();
76+}
77+//# sourceMappingURL=extension.js.map
vscode-extension/out/extension.js.mapnew file+1
@@ -0,0 +1 @@
1+{"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAWA,4BAwCC;AAED,gCAKC;AAzDD,+CAAiC;AACjC,qDAKoC;AAEpC,IAAI,MAAkC,CAAC;AAEvC,SAAgB,QAAQ,CAAC,OAAgC;IACvD,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAE7D,IAAI,CAAC,MAAM,CAAC,GAAG,CAAU,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;QACzC,OAAO;IACT,CAAC;IAED,6CAA6C;IAC7C,IAAI,UAAU,GAAG,MAAM,CAAC,GAAG,CAAS,YAAY,EAAE,EAAE,CAAC,CAAC;IACtD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,2CAA2C;QAC3C,UAAU,GAAG,qDAAqD,CAAC;IACrE,CAAC;IAED,MAAM,aAAa,GAAkB;QACnC,GAAG,EAAE;YACH,MAAM,EAAE,UAAU;YAClB,SAAS,EAAE,oBAAa,CAAC,KAAK;SAC/B;QACD,KAAK,EAAE;YACL,MAAM,EAAE,UAAU;YAClB,SAAS,EAAE,oBAAa,CAAC,KAAK;SAC/B;KACF,CAAC;IAEF,MAAM,aAAa,GAA0B;QAC3C,gBAAgB,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;QAC9D,WAAW,EAAE;YACX,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,uBAAuB,CAAC,WAAW,CAAC;SAClE;KACF,CAAC;IAEF,MAAM,GAAG,IAAI,qBAAc,CACzB,UAAU,EACV,UAAU,EACV,aAAa,EACb,aAAa,CACd,CAAC;IAEF,MAAM,CAAC,KAAK,EAAE,CAAC;AACjB,CAAC;AAED,SAAgB,UAAU;IACxB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;AACvB,CAAC"}
vscode-extension/out/test/runTest.jsnew file+55
@@ -0,0 +1,55 @@
1+"use strict";
2+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+ if (k2 === undefined) k2 = k;
4+ var desc = Object.getOwnPropertyDescriptor(m, k);
5+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+ desc = { enumerable: true, get: function() { return m[k]; } };
7+ }
8+ Object.defineProperty(o, k2, desc);
9+}) : (function(o, m, k, k2) {
10+ if (k2 === undefined) k2 = k;
11+ o[k2] = m[k];
12+}));
13+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15+}) : function(o, v) {
16+ o["default"] = v;
17+});
18+var __importStar = (this && this.__importStar) || (function () {
19+ var ownKeys = function(o) {
20+ ownKeys = Object.getOwnPropertyNames || function (o) {
21+ var ar = [];
22+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23+ return ar;
24+ };
25+ return ownKeys(o);
26+ };
27+ return function (mod) {
28+ if (mod && mod.__esModule) return mod;
29+ var result = {};
30+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31+ __setModuleDefault(result, mod);
32+ return result;
33+ };
34+})();
35+Object.defineProperty(exports, "__esModule", { value: true });
36+const path = __importStar(require("path"));
37+const test_electron_1 = require("@vscode/test-electron");
38+async function main() {
39+ try {
40+ const extensionDevelopmentPath = path.resolve(__dirname, '../../');
41+ const extensionTestsPath = path.resolve(__dirname, './suite/index');
42+ const testWorkspace = path.resolve(__dirname, '../../test-workspace');
43+ await (0, test_electron_1.runTests)({
44+ extensionDevelopmentPath,
45+ extensionTestsPath,
46+ launchArgs: [testWorkspace, '--disable-extensions']
47+ });
48+ }
49+ catch (err) {
50+ console.error('Failed to run tests', err);
51+ process.exit(1);
52+ }
53+}
54+main();
55+//# sourceMappingURL=runTest.js.map
vscode-extension/out/test/runTest.js.mapnew file+1
@@ -0,0 +1 @@
1+{"version":3,"file":"runTest.js","sourceRoot":"","sources":["../../src/test/runTest.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA6B;AAC7B,yDAAiD;AAEjD,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QACH,MAAM,wBAAwB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACnE,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QACpE,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,sBAAsB,CAAC,CAAC;QAEtE,MAAM,IAAA,wBAAQ,EAAC;YACb,wBAAwB;YACxB,kBAAkB;YAClB,UAAU,EAAE,CAAC,aAAa,EAAE,sBAAsB,CAAC;SACpD,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}
vscode-extension/out/test/suite/extension.test.jsnew file+75
@@ -0,0 +1,75 @@
1+"use strict";
2+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+ if (k2 === undefined) k2 = k;
4+ var desc = Object.getOwnPropertyDescriptor(m, k);
5+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+ desc = { enumerable: true, get: function() { return m[k]; } };
7+ }
8+ Object.defineProperty(o, k2, desc);
9+}) : (function(o, m, k, k2) {
10+ if (k2 === undefined) k2 = k;
11+ o[k2] = m[k];
12+}));
13+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15+}) : function(o, v) {
16+ o["default"] = v;
17+});
18+var __importStar = (this && this.__importStar) || (function () {
19+ var ownKeys = function(o) {
20+ ownKeys = Object.getOwnPropertyNames || function (o) {
21+ var ar = [];
22+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23+ return ar;
24+ };
25+ return ownKeys(o);
26+ };
27+ return function (mod) {
28+ if (mod && mod.__esModule) return mod;
29+ var result = {};
30+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31+ __setModuleDefault(result, mod);
32+ return result;
33+ };
34+})();
35+Object.defineProperty(exports, "__esModule", { value: true });
36+const assert = __importStar(require("assert"));
37+const vscode = __importStar(require("vscode"));
38+const path = __importStar(require("path"));
39+suite('Purslint Extension Test Suite', () => {
40+ vscode.window.showInformationMessage('Start all tests.');
41+ test('Extension should be present', () => {
42+ assert.ok(vscode.extensions.getExtension('RowtypeYoga.purslint'));
43+ });
44+ test('Extension should activate on PureScript file', async () => {
45+ const ext = vscode.extensions.getExtension('RowtypeYoga.purslint');
46+ assert.ok(ext);
47+ // Open a PureScript file to trigger activation
48+ const testFile = path.join(__dirname, '../../../test-workspace/Test.purs');
49+ const uri = vscode.Uri.file(testFile);
50+ const doc = await vscode.workspace.openTextDocument(uri);
51+ await vscode.window.showTextDocument(doc);
52+ // Wait for extension to activate
53+ await ext.activate();
54+ assert.ok(ext.isActive);
55+ });
56+ test('Should provide code actions', async () => {
57+ const ext = vscode.extensions.getExtension('RowtypeYoga.purslint');
58+ if (!ext?.isActive) {
59+ await ext?.activate();
60+ }
61+ const testFile = path.join(__dirname, '../../../test-workspace/Test.purs');
62+ const uri = vscode.Uri.file(testFile);
63+ const doc = await vscode.workspace.openTextDocument(uri);
64+ await vscode.window.showTextDocument(doc);
65+ // Wait for LSP to start and process document
66+ await new Promise(resolve => setTimeout(resolve, 3000));
67+ // Try to get code actions for the whole document
68+ const range = new vscode.Range(0, 0, doc.lineCount, 0);
69+ const codeActions = await vscode.commands.executeCommand('vscode.executeCodeActionProvider', doc.uri, range);
70+ // Code actions may or may not be present depending on LSP connection
71+ // Just verify the command doesn't throw
72+ assert.ok(Array.isArray(codeActions) || codeActions === undefined, 'Should return array or undefined');
73+ });
74+});
75+//# sourceMappingURL=extension.test.js.map
vscode-extension/out/test/suite/extension.test.js.mapnew file+1
@@ -0,0 +1 @@
1+{"version":3,"file":"extension.test.js","sourceRoot":"","sources":["../../../src/test/suite/extension.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAiC;AACjC,+CAAiC;AACjC,2CAA6B;AAE7B,KAAK,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC1C,MAAM,CAAC,MAAM,CAAC,sBAAsB,CAAC,kBAAkB,CAAC,CAAC;IAEzD,IAAI,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC9D,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC;QACnE,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QAEf,+CAA+C;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mCAAmC,CAAC,CAAC;QAC3E,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACzD,MAAM,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAE1C,iCAAiC;QACjC,MAAM,GAAG,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC;QACnE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC;YACnB,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC;QACxB,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mCAAmC,CAAC,CAAC;QAC3E,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QACzD,MAAM,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAE1C,6CAA6C;QAC7C,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAExD,iDAAiD;QACjD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QAEvD,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,CACtD,kCAAkC,EAClC,GAAG,CAAC,GAAG,EACP,KAAK,CACN,CAAC;QAEF,qEAAqE;QACrE,wCAAwC;QACxC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,WAAW,KAAK,SAAS,EAAE,kCAAkC,CAAC,CAAC;IACzG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
vscode-extension/out/test/suite/index.jsnew file+63
@@ -0,0 +1,63 @@
1+"use strict";
2+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+ if (k2 === undefined) k2 = k;
4+ var desc = Object.getOwnPropertyDescriptor(m, k);
5+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+ desc = { enumerable: true, get: function() { return m[k]; } };
7+ }
8+ Object.defineProperty(o, k2, desc);
9+}) : (function(o, m, k, k2) {
10+ if (k2 === undefined) k2 = k;
11+ o[k2] = m[k];
12+}));
13+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15+}) : function(o, v) {
16+ o["default"] = v;
17+});
18+var __importStar = (this && this.__importStar) || (function () {
19+ var ownKeys = function(o) {
20+ ownKeys = Object.getOwnPropertyNames || function (o) {
21+ var ar = [];
22+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23+ return ar;
24+ };
25+ return ownKeys(o);
26+ };
27+ return function (mod) {
28+ if (mod && mod.__esModule) return mod;
29+ var result = {};
30+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31+ __setModuleDefault(result, mod);
32+ return result;
33+ };
34+})();
35+var __importDefault = (this && this.__importDefault) || function (mod) {
36+ return (mod && mod.__esModule) ? mod : { "default": mod };
37+};
38+Object.defineProperty(exports, "__esModule", { value: true });
39+exports.run = run;
40+const path = __importStar(require("path"));
41+const mocha_1 = __importDefault(require("mocha"));
42+const glob_1 = require("glob");
43+async function run() {
44+ const mocha = new mocha_1.default({
45+ ui: 'tdd',
46+ color: true,
47+ timeout: 60000
48+ });
49+ const testsRoot = path.resolve(__dirname, '.');
50+ const files = await (0, glob_1.glob)('**/**.test.js', { cwd: testsRoot });
51+ files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
52+ return new Promise((resolve, reject) => {
53+ mocha.run(failures => {
54+ if (failures > 0) {
55+ reject(new Error(`${failures} tests failed.`));
56+ }
57+ else {
58+ resolve();
59+ }
60+ });
61+ });
62+}
63+//# sourceMappingURL=index.js.map
vscode-extension/out/test/suite/index.js.mapnew file+1
@@ -0,0 +1 @@
1+{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/test/suite/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,kBAqBC;AAzBD,2CAA6B;AAC7B,kDAA0B;AAC1B,+BAA4B;AAErB,KAAK,UAAU,GAAG;IACvB,MAAM,KAAK,GAAG,IAAI,eAAK,CAAC;QACtB,EAAE,EAAE,KAAK;QACT,KAAK,EAAE,IAAI;QACX,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,MAAM,IAAA,WAAI,EAAC,eAAe,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IAE9D,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;YACnB,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,QAAQ,gBAAgB,CAAC,CAAC,CAAC;YACjD,CAAC;iBAAM,CAAC;gBACN,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
vscode-extension/package.jsonnew file+55
@@ -0,0 +1,55 @@
1+{
2+ "name": "purslint",
3+ "displayName": "Purslint",
4+ "description": "PureScript linter inspired by hlint",
5+ "version": "0.1.1",
6+ "publisher": "RowtypeYoga",
7+ "icon": "purslint.png",
8+ "engines": {
9+ "vscode": "^1.85.0"
10+ },
11+ "categories": [
12+ "Linters",
13+ "Programming Languages"
14+ ],
15+ "activationEvents": [
16+ "onLanguage:purescript"
17+ ],
18+ "main": "./out/extension.js",
19+ "contributes": {
20+ "configuration": {
21+ "type": "object",
22+ "title": "Purslint",
23+ "properties": {
24+ "purslint.enable": {
25+ "type": "boolean",
26+ "default": true,
27+ "description": "Enable/disable purslint"
28+ },
29+ "purslint.serverPath": {
30+ "type": "string",
31+ "default": "",
32+ "description": "Path to purslint-lsp executable (uses bundled version if empty)"
33+ }
34+ }
35+ }
36+ },
37+ "scripts": {
38+ "vscode:prepublish": "npm run compile",
39+ "compile": "tsc -p ./",
40+ "watch": "tsc -watch -p ./",
41+ "test": "npm run compile && node ./out/test/runTest.js"
42+ },
43+ "dependencies": {
44+ "vscode-languageclient": "^9.0.1"
45+ },
46+ "devDependencies": {
47+ "@types/vscode": "^1.85.0",
48+ "@types/node": "^20.10.0",
49+ "@types/mocha": "^10.0.6",
50+ "@vscode/test-electron": "^2.3.8",
51+ "typescript": "^5.3.0",
52+ "mocha": "^10.2.0",
53+ "glob": "^10.3.10"
54+ }
55+}
vscode-extension/purslint-0.1.1.vsixnew file

Binary file not shown.

vscode-extension/purslint.pngnew file

Binary file not shown.

vscode-extension/src/extension.tsnew file+59
@@ -0,0 +1,59 @@
1+import * as path from 'path';
2+import * as vscode from 'vscode';
3+import {
4+ LanguageClient,
5+ LanguageClientOptions,
6+ ServerOptions,
7+ TransportKind
8+} from 'vscode-languageclient/node';
9+
10+let client: LanguageClient | undefined;
11+
12+export function activate(context: vscode.ExtensionContext) {
13+ const config = vscode.workspace.getConfiguration('purslint');
14+
15+ if (!config.get<boolean>('enable', true)) {
16+ return;
17+ }
18+
19+ // Get server path from config or use default
20+ let serverPath = config.get<string>('serverPath', '');
21+ if (!serverPath) {
22+ // Default to the purslint project location
23+ serverPath = '/Users/mark/Developer/purslint/dist/purslint-lsp.js';
24+ }
25+
26+ const serverOptions: ServerOptions = {
27+ run: {
28+ module: serverPath,
29+ transport: TransportKind.stdio
30+ },
31+ debug: {
32+ module: serverPath,
33+ transport: TransportKind.stdio
34+ }
35+ };
36+
37+ const clientOptions: LanguageClientOptions = {
38+ documentSelector: [{ scheme: 'file', language: 'purescript' }],
39+ synchronize: {
40+ fileEvents: vscode.workspace.createFileSystemWatcher('**/*.purs')
41+ }
42+ };
43+
44+ client = new LanguageClient(
45+ 'purslint',
46+ 'Purslint',
47+ serverOptions,
48+ clientOptions
49+ );
50+
51+ client.start();
52+}
53+
54+export function deactivate(): Thenable<void> | undefined {
55+ if (!client) {
56+ return undefined;
57+ }
58+ return client.stop();
59+}
vscode-extension/src/test/runTest.tsnew file+21
@@ -0,0 +1,21 @@
1+import * as path from 'path';
2+import { runTests } from '@vscode/test-electron';
3+
4+async function main() {
5+ try {
6+ const extensionDevelopmentPath = path.resolve(__dirname, '../../');
7+ const extensionTestsPath = path.resolve(__dirname, './suite/index');
8+ const testWorkspace = path.resolve(__dirname, '../../test-workspace');
9+
10+ await runTests({
11+ extensionDevelopmentPath,
12+ extensionTestsPath,
13+ launchArgs: [testWorkspace, '--disable-extensions']
14+ });
15+ } catch (err) {
16+ console.error('Failed to run tests', err);
17+ process.exit(1);
18+ }
19+}
20+
21+main();
vscode-extension/src/test/suite/extension.test.tsnew file+54
@@ -0,0 +1,54 @@
1+import * as assert from 'assert';
2+import * as vscode from 'vscode';
3+import * as path from 'path';
4+
5+suite('Purslint Extension Test Suite', () => {
6+ vscode.window.showInformationMessage('Start all tests.');
7+
8+ test('Extension should be present', () => {
9+ assert.ok(vscode.extensions.getExtension('RowtypeYoga.purslint'));
10+ });
11+
12+ test('Extension should activate on PureScript file', async () => {
13+ const ext = vscode.extensions.getExtension('RowtypeYoga.purslint');
14+ assert.ok(ext);
15+
16+ // Open a PureScript file to trigger activation
17+ const testFile = path.join(__dirname, '../../../test-workspace/Test.purs');
18+ const uri = vscode.Uri.file(testFile);
19+ const doc = await vscode.workspace.openTextDocument(uri);
20+ await vscode.window.showTextDocument(doc);
21+
22+ // Wait for extension to activate
23+ await ext.activate();
24+ assert.ok(ext.isActive);
25+ });
26+
27+ test('Should provide code actions', async () => {
28+ const ext = vscode.extensions.getExtension('RowtypeYoga.purslint');
29+ if (!ext?.isActive) {
30+ await ext?.activate();
31+ }
32+
33+ const testFile = path.join(__dirname, '../../../test-workspace/Test.purs');
34+ const uri = vscode.Uri.file(testFile);
35+ const doc = await vscode.workspace.openTextDocument(uri);
36+ await vscode.window.showTextDocument(doc);
37+
38+ // Wait for LSP to start and process document
39+ await new Promise(resolve => setTimeout(resolve, 3000));
40+
41+ // Try to get code actions for the whole document
42+ const range = new vscode.Range(0, 0, doc.lineCount, 0);
43+
44+ const codeActions = await vscode.commands.executeCommand<vscode.CodeAction[]>(
45+ 'vscode.executeCodeActionProvider',
46+ doc.uri,
47+ range
48+ );
49+
50+ // Code actions may or may not be present depending on LSP connection
51+ // Just verify the command doesn't throw
52+ assert.ok(Array.isArray(codeActions) || codeActions === undefined, 'Should return array or undefined');
53+ });
54+});
vscode-extension/src/test/suite/index.tsnew file+26
@@ -0,0 +1,26 @@
1+import * as path from 'path';
2+import Mocha from 'mocha';
3+import { glob } from 'glob';
4+
5+export async function run(): Promise<void> {
6+ const mocha = new Mocha({
7+ ui: 'tdd',
8+ color: true,
9+ timeout: 60000
10+ });
11+
12+ const testsRoot = path.resolve(__dirname, '.');
13+ const files = await glob('**/**.test.js', { cwd: testsRoot });
14+
15+ files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
16+
17+ return new Promise((resolve, reject) => {
18+ mocha.run(failures => {
19+ if (failures > 0) {
20+ reject(new Error(`${failures} tests failed.`));
21+ } else {
22+ resolve();
23+ }
24+ });
25+ });
26+}
vscode-extension/test-workspace/Test.pursnew file+13
@@ -0,0 +1,13 @@
1+module Test where
2+
3+import Prelude
4+
5+-- This should trigger a "map identity" warning
6+x = map identity [1, 2, 3]
7+
8+-- This should trigger a "redundant if" warning
9+y = if true then true else false
10+
11+-- Clean code - no warnings
12+z :: Int
13+z = 42
vscode-extension/tsconfig.jsonnew file+14
@@ -0,0 +1,14 @@
1+{
2+ "compilerOptions": {
3+ "module": "commonjs",
4+ "target": "ES2020",
5+ "outDir": "out",
6+ "lib": ["ES2020"],
7+ "sourceMap": true,
8+ "rootDir": "src",
9+ "strict": true,
10+ "esModuleInterop": true,
11+ "skipLibCheck": true
12+ },
13+ "exclude": ["node_modules", ".vscode-test"]
14+}
zed-extension/Cargo.lock+3−3
@@ -412,7 +412,7 @@ dependencies = [
412412]
413413
414414[[package]]
415−name = "purelint-zed"
415+name = "purslint-zed"
416416version = "0.0.1"
417417dependencies = [
418418 "zed_extension_api",
@@ -818,6 +818,6 @@ dependencies = [
818818
819819[[package]]
820820name = "zmij"
821−version = "1.0.16"
821+version = "1.0.17"
822822source = "registry+https://github.com/rust-lang/crates.io-index"
823−checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
823+checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
zed-extension/Cargo.toml+1−1
@@ -1,5 +1,5 @@
11[package]
2−name = "purelint-zed"
2+name = "purslint-zed"
33version = "0.0.1"
44edition = "2021"
55
zed-extension/extension.toml+3−3
@@ -1,5 +1,5 @@
1−id = "purelint"
2−name = "Purelint"
1+id = "purslint"
2+name = "Purslint"
33version = "0.0.1"
44schema_version = 1
55authors = ["Mark"]
@@ -9,5 +9,5 @@ description = "PureScript linter inspired by hlint"
99kind = "Rust"
1010version = "0.7.0"
1111
12−[language_servers.purelint]
12+[language_servers.purslint]
1313language = "PureScript"
zed-extension/extension.wasm

Binary file not shown.

zed-extension/src/lib.rs+5−5
@@ -1,10 +1,10 @@
11use zed_extension_api::{self as zed, Result};
22
3−struct PurelintExtension;
3+struct PurslintExtension;
44
5−impl zed::Extension for PurelintExtension {
5+impl zed::Extension for PurslintExtension {
66 fn new() -> Self {
7− PurelintExtension
7+ PurslintExtension
88 }
99
1010 fn language_server_command(
@@ -13,11 +13,11 @@ impl zed::Extension for PurelintExtension {
1313 _worktree: &zed::Worktree,
1414 ) -> Result<zed::Command> {
1515 Ok(zed::Command {
16− command: "/Users/mark/Developer/purelint/purelint-lsp".to_string(),
16+ command: "purslint-lsp".to_string(),
1717 args: vec![],
1818 env: Default::default(),
1919 })
2020 }
2121}
2222
23−zed::register_extension!(PurelintExtension);
23+zed::register_extension!(PurslintExtension);