git.iko.soy/rowtype-yoga/purslint
on main help sign in
160 files changed+12344−1358
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+4−14 CONTRIBUTING.md+6 LICENSE+21 README.md+100−67 bin/purelint-cli/src/Main.purs−227 bin/purelint-lsp/src/LSP.purs−318 bin/purelint-cli/spago.yaml → bin/purslint-cli/spago.yaml+6−2 bin/purslint-cli/src/Main.purs+419 bin/purslint-cli/src/Node/CLI.js+4 bin/purelint-cli/src/Node/Process.purs → bin/purslint-cli/src/Node/CLI.purs+1−1 bin/purslint-cli/src/Node/FS/Simple.js+22 bin/purelint-cli/src/Node/FS/Sync.purs → bin/purslint-cli/src/Node/FS/Simple.purs+1−1 bin/purslint-lsp.js+2 bin/purelint-lsp/spago.yaml → bin/purslint-lsp/spago.yaml+2−2 bin/purslint-lsp/src/LSP.js+14 bin/purslint-lsp/src/LSP.purs+650 bin/purslint-lsp/src/LSP/Stdio.js+92 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+2−2 bin/purslint.js+2 demo.gif docs/IMPORT_CHECKING_PLAN.md+8−8 examples/BadCode.purs+1−1 examples/TestFix.purs+5−5 go.mod−17 go.sum−8 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−53 lib/purelint/src/Purelint/Runner.purs−45 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+37−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/purslint/src/Purslint/Rules/BooleanSimplify.purs+86 lib/purslint/src/Purslint/Rules/CollapseLambdas.purs+56 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/purslint/src/Purslint/Rules/EvaluateConst.purs+54 lib/purslint/src/Purslint/Rules/EvaluateEither.purs+122 lib/purslint/src/Purslint/Rules/EvaluateFst.purs+99 lib/purslint/src/Purslint/Rules/FlattenCase.purs+145 lib/purslint/src/Purslint/Rules/FunctorLaw.purs+94 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/purslint/src/Purslint/Rules/MonoidIdentity.purs+72 lib/purelint/src/Purelint/Rules/NotEqual.purs → lib/purslint/src/Purslint/Rules/NotEqual.purs+7−5 lib/purslint/src/Purslint/Rules/NothingBind.purs+74 lib/purelint/src/Purelint/Rules/RedundantBind.purs → lib/purslint/src/Purslint/Rules/RedundantBind.purs+33−21 lib/purslint/src/Purslint/Rules/RedundantFlip.purs+64 lib/purslint/src/Purslint/Rules/RedundantGuard.purs+73 lib/purslint/src/Purslint/Rules/RedundantId.purs+130 lib/purslint/src/Purslint/Rules/RedundantIf.purs+68 lib/purslint/src/Purslint/Rules/RedundantNegate.purs+65 lib/purslint/src/Purslint/Rules/RedundantNot.purs+64 lib/purslint/src/Purslint/Rules/RedundantParens.purs+423 lib/purslint/src/Purslint/Rules/RedundantReverse.purs+60 lib/purslint/src/Purslint/Rules/UnlessNot.purs+96 lib/purslint/src/Purslint/Rules/UseAll.purs+70 lib/purslint/src/Purslint/Rules/UseAnd.purs+80 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/purslint/src/Purslint/Rules/UseComparing.purs+78 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/purslint/src/Purslint/Rules/UseElemIndex.purs+131 lib/purslint/src/Purslint/Rules/UseFindMap.purs+81 lib/purslint/src/Purslint/Rules/UseFold.purs+55 lib/purslint/src/Purslint/Rules/UseFoldBool.purs+90 lib/purslint/src/Purslint/Rules/UseFoldMap.purs+167 lib/purslint/src/Purslint/Rules/UseFoldMapId.purs+80 lib/purslint/src/Purslint/Rules/UseFor.purs+77 lib/purslint/src/Purslint/Rules/UseFromJust.purs+108 lib/purslint/src/Purslint/Rules/UseFromMaybe.purs+58 lib/purslint/src/Purslint/Rules/UseFstSnd.purs+82 lib/purelint/src/Purelint/Rules/UseGuard.purs → lib/purslint/src/Purslint/Rules/UseGuard.purs+8−5 lib/purslint/src/Purslint/Rules/UseGuardMaybe.purs+79 lib/purslint/src/Purslint/Rules/UseHead.purs+58 lib/purslint/src/Purslint/Rules/UseIsJust.purs+70 lib/purslint/src/Purslint/Rules/UseIsNothing.purs+70 lib/purslint/src/Purslint/Rules/UseJoin.purs+81 lib/purslint/src/Purslint/Rules/UseLastReverse.purs+80 lib/purslint/src/Purslint/Rules/UseMapMaybe.purs+81 lib/purslint/src/Purslint/Rules/UseMaybeMap.purs+58 lib/purslint/src/Purslint/Rules/UseMinMax.purs+80 lib/purslint/src/Purslint/Rules/UseMinimumSort.purs+80 lib/purslint/src/Purslint/Rules/UseNotElem.purs+68 lib/purslint/src/Purslint/Rules/UseNull.purs+81 lib/purslint/src/Purslint/Rules/UseOn.purs+74 lib/purslint/src/Purslint/Rules/UseOr.purs+81 lib/purslint/src/Purslint/Rules/UsePatternGuards.purs+231 lib/purslint/src/Purslint/Rules/UseReplicate.purs+84 lib/purslint/src/Purslint/Rules/UseSequence.purs+62 lib/purslint/src/Purslint/Rules/UseSpan.purs+96 lib/purelint/src/Purelint/Rules/UseTraverse.purs → lib/purslint/src/Purslint/Rules/UseTraverse.purs+36−23 lib/purslint/src/Purslint/Rules/UseTraverseSequence.purs+74 lib/purslint/src/Purslint/Rules/UseUncurry.purs+92 lib/purslint/src/Purslint/Rules/UseUnless.purs+57 lib/purslint/src/Purslint/Rules/UseUnwrap.purs+70 lib/purslint/src/Purslint/Rules/UseVoid.purs+68 lib/purslint/src/Purslint/Rules/UseWhen.purs+57 lib/purslint/src/Purslint/Rules/UseZip.purs+99 lib/purslint/src/Purslint/Rules/WhenNot.purs+96 lib/purslint/src/Purslint/Runner.purs+69 lib/purslint/src/Purslint/Tidy.js+66 lib/purslint/src/Purslint/Tidy.purs+13 lib/purelint/src/Purelint/Types.purs → lib/purslint/src/Purslint/Types.purs+16−2 package.json+49 purescript-native/ffi_loader.go−30 purescript-native/go.mod−12 purescript-native/go.sum−6 purslint-lsp+2 spago.lock+402−10 spago.yaml+3−4 test/LintExamples.purs+244−16 test/RedundantParens.purs+134 test/Test/Main.purs+1895−36 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+4−14
@@ -3,26 +3,16 @@ bower_components/
33node_modules/
44.pulp-cache/
55output/
6−output-es/
6+output-*/
7+dist/
78generated-docs/
89.psc-package/
910.psc*
1011.purs*
1112.psa*
1213.spago
13−
14−# Go build artifacts
15−main_cli.go
16−main_lsp.go
17−test_main.go
18−debug_main.go
19−
20−# Binaries (root level only)
21−/purelint
22−/purelint-lsp
23−/test-runner
24−/debug-runner
25−/Main
14+package-lock.json
2615
2716# Zed extension build
2817zed-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−227
@@ -1,227 +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.Sync as FS
14−import Node.Process as Process
15−import Purelint.Config (defaultConfig, filterRules, parseConfig)
16−import Purelint.Fix (applyAllFixes)
17−import Purelint.Rule (Rule)
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.LetToWhere (letToWhereRule)
23−import Purelint.Rules.MapFusion (mapFusionRule)
24−import Purelint.Rules.NotEqual (notEqualRule)
25−import Purelint.Rules.RedundantBind (redundantBindRule)
26−import Purelint.Rules.UseGuard (useGuardRule)
27−import Purelint.Rules.UseTraverse (useTraverseRule)
28−import Purelint.Runner (runRules)
29−import Purelint.Types (LintResult(..), LintWarning(..), Severity(..), SourceCode(..), Suggestion(..))
30−
31−-- | All available rules
32−allRules :: Array Rule
33−allRules =
34− [ useTraverseRule
35− , mapFusionRule
36− , fmapIdRule
37− , notEqualRule
38− , concatMapRule
39− , useGuardRule
40− , etaReduceRule
41− , etaReduceDeclRule
42− , redundantBindRule
43− , letToWhereRule
44− ]
45−
46−-- | Load config from .purelintignore file
47−loadConfig :: Effect _
48−loadConfig = do
49− hasConfig <- FS.exists ".purelintignore"
50− if hasConfig then do
51− content <- FS.readTextFile ".purelintignore"
52− pure $ parseConfig content
53− else
54− pure defaultConfig
55−
56−-- | Format a source range for display
57−formatRange :: { start :: { line :: Int, column :: Int }, end :: { line :: Int, column :: Int } } -> String
58−formatRange r = show (r.start.line + 1) <> ":" <> show (r.start.column + 1) <> "-" <> show (r.end.line + 1) <> ":" <> show (r.end.column + 1)
59−
60−-- | Format severity for display
61−formatSeverity :: Severity -> String
62−formatSeverity Hint = "[hint]"
63−formatSeverity Warning = "[warning]"
64−formatSeverity Error = "[error]"
65−
66−-- | Format a single warning for display
67−formatWarning :: String -> LintWarning -> String
68−formatWarning filePath (LintWarning w) =
69− filePath <> ":" <> formatRange w.range <> " " <> formatSeverity w.severity <> " " <> unwrap w.ruleId <> ": " <> unwrap w.message <>
70− case w.suggestion of
71− Nothing -> ""
72− Just (Suggestion s) -> "\n Replace with: " <> unwrap s.replacement
73−
74−-- | Lint a single file with config
75−lintFileWithConfig :: Array Rule -> String -> Effect Int
76−lintFileWithConfig rules filePath = do
77− content <- FS.readTextFile filePath
78− case runRules rules (SourceCode content) of
79− Left err -> do
80− log $ filePath <> ": " <> err
81− pure 0
82− Right (LintResult result) -> do
83− for_ result.warnings \w -> do
84− log $ formatWarning filePath w
85− pure (Array.length result.warnings)
86−
87−-- | Fix a single file
88−fixFile :: Array Rule -> String -> Effect Int
89−fixFile rules filePath = do
90− content <- FS.readTextFile filePath
91− case runRules rules (SourceCode content) of
92− Left err -> do
93− log $ filePath <> ": " <> err
94− pure 0
95− Right result@(LintResult r) -> do
96− let fixedContent = applyAllFixes content result
97− when (fixedContent /= content) do
98− FS.writeTextFile filePath fixedContent
99− log $ filePath <> ": fixed " <> show (Array.length r.warnings) <> " issue(s)"
100− pure (Array.length r.warnings)
101−
102−-- | Recursively find all .purs files in a directory
103−findPursFiles :: String -> Effect (Array String)
104−findPursFiles dir = do
105− entries <- FS.readdir dir
106− files <- Array.foldM (collectFiles dir) [] entries
107− pure files
108− where
109− collectFiles :: String -> Array String -> String -> Effect (Array String)
110− collectFiles parentDir acc entry = do
111− let path = parentDir <> "/" <> entry
112− stats <- FS.stat path
113− if stats.isDirectory then do
114− subFiles <- findPursFiles path
115− pure (acc <> subFiles)
116− else if String.takeRight 5 entry == ".purs" then
117− pure (Array.snoc acc path)
118− else
119− pure acc
120−
121−-- | Lint a file or directory with config
122−lintPathWithConfig :: Array Rule -> String -> Effect Int
123−lintPathWithConfig rules path = do
124− stats <- FS.stat path
125− if stats.isDirectory then do
126− files <- findPursFiles path
127− counts <- Array.foldM (\acc file -> do
128− count <- lintFileWithConfig rules file
129− pure (acc + count)) 0 files
130− pure counts
131− else
132− lintFileWithConfig rules path
133−
134−-- | Fix a file or directory
135−fixPath :: Array Rule -> String -> Effect Int
136−fixPath rules path = do
137− stats <- FS.stat path
138− if stats.isDirectory then do
139− files <- findPursFiles path
140− counts <- Array.foldM (\acc file -> do
141− count <- fixFile rules file
142− pure (acc + count)) 0 files
143− pure counts
144− else
145− fixFile rules path
146−
147−-- | Show usage
148−showUsage :: Effect Unit
149−showUsage = do
150− log "purelint - a linter for PureScript"
151− log ""
152− log "Usage: purelint [OPTIONS] [FILES/DIRECTORIES...]"
153− log ""
154− log "Options:"
155− log " --help Show this help"
156− log " --fix Auto-fix issues (where possible)"
157− log " --init Create a sample .purelintignore"
158− log " --list List all available rules"
159− log ""
160− log "Configuration:"
161− log " Create .purelintignore in your project root to disable rules."
162− log " Add one rule name per line (lines starting with # are comments)."
163− log ""
164− log "Examples:"
165− log " purelint src/ Lint all .purs files in src/"
166− log " purelint --fix src/ Auto-fix issues in src/"
167− log " purelint src/Main.purs Lint a single file"
168−
169−-- | List all available rules
170−listRules :: Effect Unit
171−listRules = do
172− log "Available rules:"
173− log ""
174− for_ allRules \rule -> do
175− log $ " " <> unwrap rule.ruleId
176−
177−-- | Create sample config file
178−initConfig :: Effect Unit
179−initConfig = do
180− hasConfig <- FS.exists ".purelintignore"
181− if hasConfig then
182− log ".purelintignore already exists"
183− else do
184− let sampleConfig = """# purelint ignore file
185−# Add rule names (one per line) to disable them
186−# Example:
187−# EtaReduce
188−# MapFusion
189−"""
190− FS.writeTextFile ".purelintignore" sampleConfig
191− log "Created .purelintignore"
192−
193−main :: Effect Unit
194−main = do
195− args <- Process.argv
196− -- args[0] is the executable, args[1..] are the actual arguments
197− let userArgs = Array.drop 1 args
198− case userArgs of
199− [] -> showUsage
200− ["--help"] -> showUsage
201− ["-h"] -> showUsage
202− ["--list"] -> listRules
203− ["--init"] -> initConfig
204− _ -> do
205− -- Check for --fix flag
206− let hasFix = Array.elem "--fix" userArgs
207− let paths = Array.filter (\a -> a /= "--fix") userArgs
208−
209− -- Load config and filter rules
210− config <- loadConfig
211− let enabledRules = filterRules allRules config
212−
213− if hasFix then do
214− -- Fix mode
215− _ <- Array.foldM (\acc path -> do
216− count <- fixPath enabledRules path
217− pure (acc + count)) 0 paths
218− pure unit
219− else do
220− -- Lint mode
221− totalWarnings <- Array.foldM (\acc path -> do
222− count <- lintPathWithConfig enabledRules path
223− pure (acc + count)) 0 paths
224− when (totalWarnings > 0) do
225− log ""
226− log $ "Total: " <> show totalWarnings <> " warning" <> if totalWarnings == 1 then "" else "s"
227− Process.exit 1
bin/purelint-lsp/src/LSP.pursdeleted−318
@@ -1,318 +0,0 @@
1−module Purelint.LSP where
2−
3−import Prelude
4−
5−import Data.Array as Array
6−import Data.Either (Either(..))
7−import Data.Maybe (Maybe(..), fromMaybe)
8−import Data.Newtype (unwrap)
9−import Effect (Effect)
10−import Foreign (Foreign)
11−import LSP.Stdio as Stdio
12−import Purelint.Config (defaultConfig, filterRules)
13−import Purelint.Rule (Rule)
14−import Purelint.Rules.ConcatMap (concatMapRule)
15−import Purelint.Rules.EtaReduce (etaReduceRule)
16−import Purelint.Rules.EtaReduceDecl (etaReduceDeclRule)
17−import Purelint.Rules.FmapId (fmapIdRule)
18−import Purelint.Rules.LetToWhere (letToWhereRule)
19−import Purelint.Rules.MapFusion (mapFusionRule)
20−import Purelint.Rules.NotEqual (notEqualRule)
21−import Purelint.Rules.RedundantBind (redundantBindRule)
22−import Purelint.Rules.UseGuard (useGuardRule)
23−import Purelint.Rules.UseTraverse (useTraverseRule)
24−import Purelint.Runner (runRules)
25−import Purelint.Types (LintResult(..), LintWarning(..), Severity(..), SourceCode(..), Suggestion(..))
26−import Unsafe.Coerce (unsafeCoerce)
27−
28−-- | All available rules
29−allRules :: Array Rule
30−allRules =
31− [ useTraverseRule
32− , mapFusionRule
33− , fmapIdRule
34− , notEqualRule
35− , concatMapRule
36− , useGuardRule
37− , etaReduceRule
38− , etaReduceDeclRule
39− , redundantBindRule
40− , letToWhereRule
41− ]
42−
43−-- | Get enabled rules
44−enabledRules :: Array Rule
45−enabledRules = filterRules allRules defaultConfig
46−
47−-- | Convert lint warning to LSP diagnostic (with fix data embedded)
48−warningToDiagnostic :: LintWarning -> Foreign
49−warningToDiagnostic (LintWarning w) =
50− let
51− range =
52− { start: { line: w.range.start.line, character: w.range.start.column }
53− , end: { line: w.range.end.line, character: w.range.end.column }
54− }
55− base =
56− { range: range
57− , severity: severityToLSP w.severity
58− , source: "purelint"
59− , message: unwrap w.ruleId <> ": " <> unwrap w.message
60− }
61− in case w.suggestion of
62− Nothing -> unsafeCoerce base
63− Just (Suggestion sug) -> unsafeCoerce
64− { range: range
65− , severity: severityToLSP w.severity
66− , source: "purelint"
67− , message: unwrap w.ruleId <> ": " <> unwrap w.message
68− , data:
69− { hasFix: true
70− , replacement: unwrap sug.replacement
71− , description: unwrap sug.description
72− }
73− }
74−
75−severityToLSP :: Severity -> Int
76−severityToLSP Error = 1
77−severityToLSP Warning = 2
78−severityToLSP Hint = 4
79−
80−-- | Lint a document and return diagnostics
81−lintDocument :: String -> Array Foreign
82−lintDocument content =
83− case runRules enabledRules (SourceCode content) of
84− Left _ -> []
85− Right (LintResult result) -> map warningToDiagnostic result.warnings
86−
87−-- | Create a publishDiagnostics notification
88−mkPublishDiagnostics :: String -> Array Foreign -> String
89−mkPublishDiagnostics uri diagnostics =
90− Stdio.stringifyJSON $ unsafeCoerce
91− { jsonrpc: "2.0"
92− , method: "textDocument/publishDiagnostics"
93− , params:
94− { uri: uri
95− , diagnostics: diagnostics
96− }
97− }
98−
99−-- | Create an initialize response
100−mkInitializeResponse :: Int -> String
101−mkInitializeResponse reqId =
102− Stdio.stringifyJSON $ unsafeCoerce
103− { jsonrpc: "2.0"
104− , id: reqId
105− , result:
106− { capabilities:
107− { textDocumentSync: 1 -- Full sync
108− , codeActionProvider: true
109− }
110− , serverInfo:
111− { name: "purelint-lsp"
112− , version: "0.1.0"
113− }
114− }
115− }
116−
117−-- | Create a simple response (null result)
118−mkResponse :: Int -> String
119−mkResponse reqId =
120− Stdio.stringifyJSON $ unsafeCoerce
121− { jsonrpc: "2.0"
122− , id: reqId
123− , result: mkNull unit
124− }
125−
126−-- | Create code action response
127−mkCodeActionResponse :: Int -> Array Foreign -> String
128−mkCodeActionResponse reqId actions =
129− Stdio.stringifyJSON $ unsafeCoerce
130− { jsonrpc: "2.0"
131− , id: reqId
132− , result: actions
133− }
134−
135−foreign import mkNull :: forall a. a -> Foreign
136−
137−-- | Helper to get a string field from Foreign
138−getField :: String -> Foreign -> Maybe String
139−getField key obj =
140− if hasKey key obj
141− then Just (unsafeCoerce (getKey key obj))
142− else Nothing
143−
144−getFieldInt :: String -> Foreign -> Maybe Int
145−getFieldInt key obj =
146− if hasKey key obj
147− then Just (unsafeCoerce (getKey key obj))
148− else Nothing
149−
150−getFieldObj :: String -> Foreign -> Maybe Foreign
151−getFieldObj key obj =
152− if hasKey key obj
153− then Just (getKey key obj)
154− else Nothing
155−
156−getFieldArr :: String -> Foreign -> Maybe (Array Foreign)
157−getFieldArr key obj =
158− if hasKey key obj
159− then Just (unsafeCoerce (getKey key obj))
160− else Nothing
161−
162−foreign import hasKey :: String -> Foreign -> Boolean
163−foreign import getKey :: String -> Foreign -> Foreign
164−
165−-- | Handle a single message
166−handleMessage :: Foreign -> Effect Unit
167−handleMessage msg = do
168− let method = fromMaybe "" (getField "method" msg)
169− let reqId = fromMaybe 0 (getFieldInt "id" msg)
170−
171− Stdio.logMessage $ "Method: " <> method
172−
173− if method == "initialize" then do
174− Stdio.logMessage "Handling initialize"
175− Stdio.writeMessage $ mkInitializeResponse reqId
176− else if method == "initialized" then do
177− Stdio.logMessage "Client initialized"
178− else if method == "textDocument/didOpen" then do
179− handleDidOpen msg
180− else if method == "textDocument/didChange" then do
181− handleDidChange msg
182− else if method == "textDocument/didClose" then do
183− handleDidClose msg
184− else if method == "textDocument/codeAction" then do
185− handleCodeAction msg reqId
186− else if method == "shutdown" then do
187− Stdio.logMessage "Shutdown"
188− Stdio.writeMessage $ mkResponse reqId
189− else if method == "exit" then do
190− Stdio.logMessage "Exit"
191− else do
192− Stdio.logMessage $ "Unknown method: " <> method
193−
194−handleDidOpen :: Foreign -> Effect Unit
195−handleDidOpen msg = do
196− Stdio.logMessage "Document opened"
197− case getFieldObj "params" msg of
198− Nothing -> pure unit
199− Just params ->
200− case getFieldObj "textDocument" params of
201− Nothing -> pure unit
202− Just textDoc -> do
203− let uri = fromMaybe "" (getField "uri" textDoc)
204− let text = fromMaybe "" (getField "text" textDoc)
205− let diagnostics = lintDocument text
206− Stdio.writeMessage $ mkPublishDiagnostics uri diagnostics
207−
208−handleDidChange :: Foreign -> Effect Unit
209−handleDidChange msg = do
210− Stdio.logMessage "Document changed"
211− case getFieldObj "params" msg of
212− Nothing -> pure unit
213− Just params -> do
214− let uri = fromMaybe "" $ do
215− textDoc <- getFieldObj "textDocument" params
216− getField "uri" textDoc
217− case getFieldArr "contentChanges" params of
218− Nothing -> pure unit
219− Just changes ->
220− case Array.head changes of
221− Nothing -> pure unit
222− Just change -> do
223− let text = fromMaybe "" (getField "text" change)
224− let diagnostics = lintDocument text
225− Stdio.writeMessage $ mkPublishDiagnostics uri diagnostics
226−
227−handleDidClose :: Foreign -> Effect Unit
228−handleDidClose msg = do
229− Stdio.logMessage "Document closed"
230− case getFieldObj "params" msg of
231− Nothing -> pure unit
232− Just params ->
233− case getFieldObj "textDocument" params of
234− Nothing -> pure unit
235− Just textDoc -> do
236− let uri = fromMaybe "" (getField "uri" textDoc)
237− Stdio.writeMessage $ mkPublishDiagnostics uri []
238−
239−handleCodeAction :: Foreign -> Int -> Effect Unit
240−handleCodeAction msg reqId = do
241− Stdio.logMessage "Code action requested"
242− case getFieldObj "params" msg of
243− Nothing -> Stdio.writeMessage $ mkCodeActionResponse reqId []
244− Just params -> do
245− -- Get the document URI
246− let uri = fromMaybe "" $ do
247− textDoc <- getFieldObj "textDocument" params
248− getField "uri" textDoc
249−
250− -- Get diagnostics from the request context
251− let diagnostics = fromMaybe [] $ do
252− context <- getFieldObj "context" params
253− getFieldArr "diagnostics" context
254−
255− -- Convert diagnostics to code actions
256− let actions = Array.catMaybes $ map (diagnosticToCodeAction uri) diagnostics
257−
258− Stdio.logMessage $ "Returning " <> show (Array.length actions) <> " code actions"
259− Stdio.writeMessage $ mkCodeActionResponse reqId actions
260−
261−-- | Convert a diagnostic with fix data to a code action
262−diagnosticToCodeAction :: String -> Foreign -> Maybe Foreign
263−diagnosticToCodeAction uri diag = do
264− dataField <- getFieldObj "data" diag
265− if hasKey "replacement" dataField && hasKey "description" dataField
266− then do
267− replacement <- getField "replacement" dataField
268− description <- getField "description" dataField
269− range <- getFieldObj "range" diag
270− pure $ mkCodeAction description uri range replacement diag
271− else Nothing
272−
273−-- | Create a code action with a workspace edit
274−mkCodeAction :: String -> String -> Foreign -> String -> Foreign -> Foreign
275−mkCodeAction title uri range newText diagnostic =
276− unsafeCoerce
277− { title: title
278− , kind: "quickfix"
279− , diagnostics: [diagnostic]
280− , isPreferred: true
281− , edit:
282− { documentChanges:
283− [ { textDocument:
284− { uri: uri
285− , version: mkNull unit
286− }
287− , edits: [{ range: range, newText: newText }]
288− }
289− ]
290− }
291− }
292−
293−-- | Main loop
294−mainLoop :: Effect Unit
295−mainLoop = do
296− msgStr <- Stdio.readMessage
297− case msgStr of
298− Nothing -> do
299− Stdio.logMessage "No message, exiting"
300− Just str -> do
301− Stdio.logMessage $ "Got message: " <> str
302− case Stdio.parseJSON str of
303− Nothing -> do
304− Stdio.logMessage "Failed to parse message"
305− mainLoop
306− Just msg -> do
307− handleMessage msg
308− -- Check if it was exit
309− let method = fromMaybe "" (getField "method" msg)
310− if method == "exit"
311− then pure unit
312− else mainLoop
313−
314−main :: Effect Unit
315−main = do
316− Stdio.logMessage "purelint-lsp starting..."
317− mainLoop
318− Stdio.logMessage "purelint-lsp exiting"
bin/purelint-cli/spago.yaml → bin/purslint-cli/spago.yamlrenamed+6−2
@@ -1,5 +1,5 @@
11package:
2− name: purelint-cli
2+ name: purslint-cli
33 dependencies:
44 - arrays
55 - console
@@ -8,6 +8,10 @@ package:
88 - foldable-traversable
99 - maybe
1010 - newtype
11+ - optparse
1112 - prelude
12− - purelint
13+ - purslint
1314 - strings
15+ - node-buffer
16+ - node-fs
17+ - node-process
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/purslint-cli/src/Node/CLI.jsnew file+4
@@ -0,0 +1,4 @@
1+// Node.CLI FFI for JavaScript
2+export const argv = () => process.argv;
3+export const cwd = () => process.cwd();
4+export const exit = (code) => () => process.exit(code);
bin/purelint-cli/src/Node/Process.purs → bin/purslint-cli/src/Node/CLI.pursrenamed+1−1
@@ -1,4 +1,4 @@
1−module Node.Process where
1+module Node.CLI where
22
33import Prelude
44import Effect (Effect)
bin/purslint-cli/src/Node/FS/Simple.jsnew file+22
@@ -0,0 +1,22 @@
1+// Node.FS.Simple FFI for JavaScript
2+import * as fs from 'fs';
3+
4+export const readTextFile = (path) => () => fs.readFileSync(path, 'utf8');
5+
6+export const writeTextFile = (path) => (content) => () => {
7+ fs.writeFileSync(path, content, 'utf8');
8+};
9+
10+export const exists = (path) => () => fs.existsSync(path);
11+
12+export const readdir = (path) => () => fs.readdirSync(path);
13+
14+export const stat = (path) => () => {
15+ const s = fs.statSync(path);
16+ return {
17+ isFile: s.isFile(),
18+ isDirectory: s.isDirectory(),
19+ isSymbolicLink: s.isSymbolicLink(),
20+ size: s.size
21+ };
22+};
bin/purelint-cli/src/Node/FS/Sync.purs → bin/purslint-cli/src/Node/FS/Simple.pursrenamed+1−1
@@ -1,4 +1,4 @@
1−module Node.FS.Sync where
1+module Node.FS.Simple where
22
33import Prelude
44import Effect (Effect)
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/purslint-lsp/src/LSP.jsnew file+14
@@ -0,0 +1,14 @@
1+// Purslint.LSP FFI for JavaScript
2+
3+// Create a null value
4+export const mkNull = (_) => null;
5+
6+// Check if a key exists in an object
7+export const hasKey = (key) => (obj) => {
8+ return obj != null && typeof obj === 'object' && key in obj;
9+};
10+
11+// Get a key from an object
12+export const getKey = (key) => (obj) => {
13+ return obj[key];
14+};
bin/purslint-lsp/src/LSP.pursnew file+650
@@ -0,0 +1,650 @@
1+module Purslint.LSP where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Either (Either(..))
7+import Data.Map (Map)
8+import Data.Map as Map
9+import Data.Maybe (Maybe(..), fromMaybe)
10+import Data.Newtype (unwrap)
11+import Effect (Effect)
12+import Effect.Ref (Ref)
13+import Effect.Ref as Ref
14+import Foreign (Foreign)
15+import LSP.Stdio as Stdio
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(..))
101+import Unsafe.Coerce (unsafeCoerce)
102+
103+-- | All available rules
104+allRules :: Array Rule
105+allRules =
106+ [ useTraverseRule
107+ , mapFusionRule
108+ , mapIdentityRule
109+ , notEqualRule
110+ , concatMapRule
111+ , useGuardRule
112+ , etaReduceRule
113+ , etaReduceDeclRule
114+ , redundantBindRule
115+ , letToDoRule
116+ , letToWhereRule
117+ , redundantIfRule
118+ , functorLawRule
119+ , useUnwrapRule
120+ , useVoidRule
121+ , useJoinRule
122+ , useAnyRule
123+ , useAllRule
124+ , booleanSimplifyRule
125+ , collapseLambdasRule
126+ , useConstRule
127+ , useNullRule
128+ , useFromMaybeRule
129+ , useIsJustRule
130+ , useIsNothingRule
131+ , useWhenRule
132+ , useUnlessRule
133+ , redundantFlipRule
134+ , useNotElemRule
135+ , useMinMaxRule
136+ , monoidIdentityRule
137+ , useFoldRule
138+ , useSequenceRule
139+ , useTraverseSequenceRule
140+ , redundantNotRule
141+ , useFstSndRule
142+ , useMapMaybeRule
143+ , useGuardMaybeRule
144+ , useMaybeMapRule
145+ , useApplicativeRule
146+ , useApplyFlippedRule
147+ , useBindFlipRule
148+ , useForRule
149+ , redundantGuardRule
150+ , useComparingRule
151+ , useOnRule
152+ , useFindMapRule
153+ -- New rules
154+ , useLastReverseRule
155+ , redundantReverseRule
156+ , useBreakRule
157+ , useSpanRule
158+ , useMinimumSortRule
159+ , useBimapRule
160+ , useEitherMapRule
161+ , evaluateFstRule
162+ , evaluateBoolRule
163+ , evaluateEitherRule
164+ , evaluateConstRule
165+ , useOrRule
166+ , useAndRule
167+ , useFoldMapRule
168+ , useFoldMapIdRule
169+ , useFoldBoolRule
170+ , monadLawRule
171+ , alternativeLawRule
172+ , whenNotRule
173+ , unlessNotRule
174+ , useZipRule
175+ , useReplicateRule
176+ , useUncurryRule
177+ , redundantNegateRule
178+ , useFromJustRule
179+ , useHeadRule
180+ , redundantIdRule
181+ , nothingBindRule
182+ , useElemIndexRule
183+ , usePatternGuardsRule
184+ , useCaseOfRule
185+ , flattenCaseRule
186+ , redundantParensRule
187+ , useDollarRule
188+ ]
189+
190+-- | Get enabled rules based on config
191+getEnabledRules :: Config -> Array Rule
192+getEnabledRules config = filterRules allRules config
193+
194+-- | LSP server state
195+type LSPState =
196+ { config :: Config
197+ , documents :: Map String String -- URI -> content
198+ }
199+
200+-- | Convert lint warning to LSP diagnostic (with fix data embedded)
201+warningToDiagnostic :: LintWarning -> Foreign
202+warningToDiagnostic (LintWarning w) =
203+ let
204+ range =
205+ { start: { line: w.range.start.line, character: w.range.start.column }
206+ , end: { line: w.range.end.line, character: w.range.end.column }
207+ }
208+ base =
209+ { range: range
210+ , severity: severityToLSP w.severity
211+ , source: "purslint"
212+ , message: unwrap w.ruleId <> ": " <> unwrap w.message
213+ }
214+ in case w.suggestion of
215+ Nothing -> unsafeCoerce base
216+ Just (Suggestion sug) -> unsafeCoerce
217+ { range: range
218+ , severity: severityToLSP w.severity
219+ , source: "purslint"
220+ , message: unwrap w.ruleId <> ": " <> unwrap w.message
221+ , data:
222+ { hasFix: true
223+ , replacement: unwrap sug.replacement
224+ , description: unwrap sug.description
225+ , ruleId: unwrap w.ruleId
226+ }
227+ }
228+
229+severityToLSP :: Severity -> Int
230+severityToLSP Error = 1
231+severityToLSP Warning = 2
232+severityToLSP Hint = 4
233+severityToLSP Refactor = 4 -- Same as Hint, but filtered out
234+
235+-- | Lint a document and return diagnostics (excludes Refactor severity)
236+lintDocument :: Config -> String -> Array Foreign
237+lintDocument config content =
238+ let rules = getEnabledRules config
239+ in case runRules rules (SourceCode content) of
240+ Left _ -> []
241+ Right (LintResult result) ->
242+ map warningToDiagnostic $ Array.filter (not <<< isRefactor) result.warnings
243+ where
244+ isRefactor (LintWarning w) = w.severity == Refactor
245+
246+-- | Create a publishDiagnostics notification
247+mkPublishDiagnostics :: String -> Array Foreign -> String
248+mkPublishDiagnostics uri diagnostics =
249+ Stdio.stringifyJSON $ unsafeCoerce
250+ { jsonrpc: "2.0"
251+ , method: "textDocument/publishDiagnostics"
252+ , params:
253+ { uri: uri
254+ , diagnostics: diagnostics
255+ }
256+ }
257+
258+-- | Create an initialize response with workspace configuration support
259+mkInitializeResponse :: Int -> String
260+mkInitializeResponse reqId =
261+ Stdio.stringifyJSON $ unsafeCoerce
262+ { jsonrpc: "2.0"
263+ , id: reqId
264+ , result:
265+ { capabilities:
266+ { textDocumentSync: 1 -- Full sync
267+ , codeActionProvider: true
268+ , workspace:
269+ { workspaceFolders:
270+ { supported: true
271+ , changeNotifications: true
272+ }
273+ }
274+ }
275+ , serverInfo:
276+ { name: "purslint-lsp"
277+ , version: "0.1.0"
278+ }
279+ }
280+ }
281+
282+-- | Create a simple response (null result)
283+mkResponse :: Int -> String
284+mkResponse reqId =
285+ Stdio.stringifyJSON $ unsafeCoerce
286+ { jsonrpc: "2.0"
287+ , id: reqId
288+ , result: mkNull unit
289+ }
290+
291+-- | Create code action response
292+mkCodeActionResponse :: Int -> Array Foreign -> String
293+mkCodeActionResponse reqId actions =
294+ Stdio.stringifyJSON $ unsafeCoerce
295+ { jsonrpc: "2.0"
296+ , id: reqId
297+ , result: actions
298+ }
299+
300+foreign import mkNull :: forall a. a -> Foreign
301+
302+-- | Helper to get a string field from Foreign
303+getField :: String -> Foreign -> Maybe String
304+getField key obj =
305+ if hasKey key obj
306+ then Just (unsafeCoerce (getKey key obj))
307+ else Nothing
308+
309+getFieldInt :: String -> Foreign -> Maybe Int
310+getFieldInt key obj =
311+ if hasKey key obj
312+ then Just (unsafeCoerce (getKey key obj))
313+ else Nothing
314+
315+getFieldObj :: String -> Foreign -> Maybe Foreign
316+getFieldObj key obj =
317+ if hasKey key obj
318+ then Just (getKey key obj)
319+ else Nothing
320+
321+getFieldArr :: String -> Foreign -> Maybe (Array Foreign)
322+getFieldArr key obj =
323+ if hasKey key obj
324+ then Just (unsafeCoerce (getKey key obj))
325+ else Nothing
326+
327+getFieldBool :: String -> Foreign -> Maybe Boolean
328+getFieldBool key obj =
329+ if hasKey key obj
330+ then Just (unsafeCoerce (getKey key obj))
331+ else Nothing
332+
333+foreign import hasKey :: String -> Foreign -> Boolean
334+foreign import getKey :: String -> Foreign -> Foreign
335+
336+-- | Parse settings to get disabled rules
337+-- | Expected format: { "purslint": { "disabledRules": ["RuleId1", "RuleId2"] } }
338+-- | Or: { "purslint.disabledRules": ["RuleId1", "RuleId2"] }
339+-- | Or: { "purslint.enabled": false } to disable all rules
340+parseSettings :: Foreign -> Config
341+parseSettings settings =
342+ -- Try to get purslint.enabled first
343+ case getFieldObj "purslint" settings of
344+ Just purslintObj ->
345+ -- Check if all rules are disabled
346+ case getFieldBool "enabled" purslintObj of
347+ Just false ->
348+ { disabledRules: map (\r -> r.ruleId) allRules } -- Disable all rules
349+ _ ->
350+ -- Get disabled rules list
351+ case getFieldArr "disabledRules" purslintObj of
352+ Just arr ->
353+ { disabledRules: Array.mapMaybe parseRuleId arr }
354+ Nothing -> defaultConfig
355+ Nothing ->
356+ -- Try flat format: purslint.disabledRules
357+ case getFieldArr "purslint.disabledRules" settings of
358+ Just arr ->
359+ { disabledRules: Array.mapMaybe parseRuleId arr }
360+ Nothing ->
361+ -- Try purslint.enabled
362+ case getFieldBool "purslint.enabled" settings of
363+ Just false ->
364+ { disabledRules: map (\r -> r.ruleId) allRules }
365+ _ -> defaultConfig
366+
367+ where
368+ parseRuleId :: Foreign -> Maybe RuleId
369+ parseRuleId f =
370+ -- Try to coerce to string
371+ let s = unsafeCoerce f :: String
372+ in if s == "" then Nothing else Just (RuleId s)
373+
374+-- | Handle a single message
375+handleMessage :: Ref LSPState -> Foreign -> Effect Unit
376+handleMessage stateRef msg = do
377+ let method = fromMaybe "" (getField "method" msg)
378+ let reqId = fromMaybe 0 (getFieldInt "id" msg)
379+
380+ Stdio.logMessage $ "Method: " <> method
381+
382+ if method == "initialize" then do
383+ Stdio.logMessage "Handling initialize"
384+ -- Parse initial settings from initializationOptions if present
385+ case getFieldObj "params" msg of
386+ Just params ->
387+ case getFieldObj "initializationOptions" params of
388+ Just options -> do
389+ let config = parseSettings options
390+ Ref.modify_ (_ { config = config }) stateRef
391+ Stdio.logMessage $ "Initial config loaded, disabled: " <> show (Array.length config.disabledRules)
392+ Nothing -> pure unit
393+ Nothing -> pure unit
394+ Stdio.writeMessage $ mkInitializeResponse reqId
395+ else if method == "initialized" then do
396+ Stdio.logMessage "Client initialized"
397+ else if method == "workspace/didChangeConfiguration" then do
398+ handleDidChangeConfiguration stateRef msg
399+ else if method == "textDocument/didOpen" then do
400+ handleDidOpen stateRef msg
401+ else if method == "textDocument/didChange" then do
402+ handleDidChange stateRef msg
403+ else if method == "textDocument/didClose" then do
404+ handleDidClose stateRef msg
405+ else if method == "textDocument/codeAction" then do
406+ handleCodeAction stateRef msg reqId
407+ else if method == "shutdown" then do
408+ Stdio.logMessage "Shutdown"
409+ Stdio.writeMessage $ mkResponse reqId
410+ else if method == "exit" then do
411+ Stdio.logMessage "Exit"
412+ else do
413+ Stdio.logMessage $ "Unknown method: " <> method
414+
415+handleDidChangeConfiguration :: Ref LSPState -> Foreign -> Effect Unit
416+handleDidChangeConfiguration stateRef msg = do
417+ Stdio.logMessage "Configuration changed"
418+ case getFieldObj "params" msg of
419+ Nothing -> pure unit
420+ Just params ->
421+ case getFieldObj "settings" params of
422+ Nothing -> pure unit
423+ Just settings -> do
424+ let config = parseSettings settings
425+ Ref.modify_ (_ { config = config }) stateRef
426+ Stdio.logMessage $ "Config updated, disabled rules: " <> show (Array.length config.disabledRules)
427+
428+handleDidOpen :: Ref LSPState -> Foreign -> Effect Unit
429+handleDidOpen stateRef msg = do
430+ Stdio.logMessage "Document opened"
431+ case getFieldObj "params" msg of
432+ Nothing -> pure unit
433+ Just params ->
434+ case getFieldObj "textDocument" params of
435+ Nothing -> pure unit
436+ Just textDoc -> do
437+ let uri = fromMaybe "" (getField "uri" textDoc)
438+ let text = fromMaybe "" (getField "text" textDoc)
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
443+ Stdio.writeMessage $ mkPublishDiagnostics uri diagnostics
444+
445+handleDidChange :: Ref LSPState -> Foreign -> Effect Unit
446+handleDidChange stateRef msg = do
447+ Stdio.logMessage "Document changed"
448+ case getFieldObj "params" msg of
449+ Nothing -> pure unit
450+ Just params -> do
451+ let uri = fromMaybe "" $ do
452+ textDoc <- getFieldObj "textDocument" params
453+ getField "uri" textDoc
454+ case getFieldArr "contentChanges" params of
455+ Nothing -> pure unit
456+ Just changes ->
457+ case Array.head changes of
458+ Nothing -> pure unit
459+ Just change -> do
460+ let text = fromMaybe "" (getField "text" change)
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
465+ Stdio.writeMessage $ mkPublishDiagnostics uri diagnostics
466+
467+handleDidClose :: Ref LSPState -> Foreign -> Effect Unit
468+handleDidClose stateRef msg = do
469+ Stdio.logMessage "Document closed"
470+ case getFieldObj "params" msg of
471+ Nothing -> pure unit
472+ Just params ->
473+ case getFieldObj "textDocument" params of
474+ Nothing -> pure unit
475+ Just textDoc -> do
476+ let uri = fromMaybe "" (getField "uri" textDoc)
477+ -- Remove document from store
478+ Ref.modify_ (\s -> s { documents = Map.delete uri s.documents }) stateRef
479+ Stdio.writeMessage $ mkPublishDiagnostics uri []
480+
481+handleCodeAction :: Ref LSPState -> Foreign -> Int -> Effect Unit
482+handleCodeAction stateRef msg reqId = do
483+ Stdio.logMessage "Code action requested"
484+ state <- Ref.read stateRef
485+ case getFieldObj "params" msg of
486+ Nothing -> Stdio.writeMessage $ mkCodeActionResponse reqId []
487+ Just params -> do
488+ -- Get the document URI
489+ let uri = fromMaybe "" $ do
490+ textDoc <- getFieldObj "textDocument" params
491+ getField "uri" textDoc
492+
493+ -- Get the requested range
494+ let requestedRange = getFieldObj "range" params
495+
496+ -- Get diagnostics from the request context (for regular rules)
497+ let diagnostics = fromMaybe [] $ do
498+ context <- getFieldObj "context" params
499+ getFieldArr "diagnostics" context
500+
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
510+
511+ Stdio.logMessage $ "Returning " <> show (Array.length actions) <> " code actions"
512+ Stdio.writeMessage $ mkCodeActionResponse reqId actions
513+
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+
565+-- | Convert a diagnostic to code actions (quickfix + disable rule)
566+diagnosticToCodeActions :: String -> Foreign -> Array Foreign
567+diagnosticToCodeActions uri diag =
568+ case getFieldObj "data" diag of
569+ Nothing -> []
570+ Just dataField ->
571+ let
572+ -- Try to create quickfix action
573+ quickfixAction =
574+ if hasKey "replacement" dataField && hasKey "description" dataField
575+ then do
576+ replacement <- getField "replacement" dataField
577+ description <- getField "description" dataField
578+ range <- getFieldObj "range" diag
579+ pure $ mkCodeAction description uri range replacement diag
580+ else Nothing
581+
582+ -- Try to create disable rule action
583+ disableAction = do
584+ ruleId <- getField "ruleId" dataField
585+ pure $ mkDisableRuleAction ruleId diag
586+ in
587+ Array.catMaybes [quickfixAction, disableAction]
588+
589+-- | Create a code action with a workspace edit
590+mkCodeAction :: String -> String -> Foreign -> String -> Foreign -> Foreign
591+mkCodeAction title uri range newText diagnostic =
592+ unsafeCoerce
593+ { title: title
594+ , kind: "quickfix"
595+ , diagnostics: [diagnostic]
596+ , isPreferred: true
597+ , edit:
598+ { documentChanges:
599+ [ { textDocument:
600+ { uri: uri
601+ , version: mkNull unit
602+ }
603+ , edits: [{ range: range, newText: newText }]
604+ }
605+ ]
606+ }
607+ }
608+
609+-- | Create a "Disable rule" code action (uses a command the client can execute)
610+mkDisableRuleAction :: String -> Foreign -> Foreign
611+mkDisableRuleAction ruleId diagnostic =
612+ unsafeCoerce
613+ { title: "Disable rule: " <> ruleId
614+ , kind: "quickfix"
615+ , diagnostics: [diagnostic]
616+ , isPreferred: false
617+ , command:
618+ { title: "Disable rule " <> ruleId
619+ , command: "purslint.disableRule"
620+ , arguments: [ruleId]
621+ }
622+ }
623+
624+-- | Main loop
625+mainLoop :: Ref LSPState -> Effect Unit
626+mainLoop stateRef = do
627+ msgStr <- Stdio.readMessage
628+ case msgStr of
629+ Nothing -> do
630+ Stdio.logMessage "No message, exiting"
631+ Just str -> do
632+ Stdio.logMessage $ "Got message: " <> str
633+ case Stdio.parseJSON str of
634+ Nothing -> do
635+ Stdio.logMessage "Failed to parse message"
636+ mainLoop stateRef
637+ Just msg -> do
638+ handleMessage stateRef msg
639+ -- Check if it was exit
640+ let method = fromMaybe "" (getField "method" msg)
641+ if method == "exit"
642+ then pure unit
643+ else mainLoop stateRef
644+
645+main :: Effect Unit
646+main = do
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/purslint-lsp/src/LSP/Stdio.jsnew file+92
@@ -0,0 +1,92 @@
1+// LSP stdio FFI for JavaScript
2+import * as fs from 'fs';
3+
4+// PureScript Maybe representation that works for both standard and optimized backends
5+const mkJust = (x) => ({ tag: 1, _1: x, value0: x });
6+const mkNothing = { tag: 0 };
7+
8+// Buffer for reading LSP messages
9+let buffer = '';
10+
11+// Try to parse a complete LSP message from buffer
12+function tryParseMessage() {
13+ // Look for Content-Length header
14+ const headerMatch = buffer.match(/Content-Length:\s*(\d+)\r?\n\r?\n/);
15+ if (!headerMatch) return null;
16+
17+ const headerEnd = headerMatch.index + headerMatch[0].length;
18+ const contentLength = parseInt(headerMatch[1], 10);
19+
20+ // Check if we have the full content
21+ if (buffer.length < headerEnd + contentLength) return null;
22+
23+ // Extract the message content
24+ const content = buffer.slice(headerEnd, headerEnd + contentLength);
25+ buffer = buffer.slice(headerEnd + contentLength);
26+
27+ return content;
28+}
29+
30+// Read a message synchronously (blocking)
31+// Returns Maybe String
32+export const readMessage = () => {
33+ // First check if we already have a complete message in buffer
34+ let msg = tryParseMessage();
35+ if (msg !== null) {
36+ return mkJust(msg);
37+ }
38+
39+ // Read synchronously from stdin fd (0)
40+ try {
41+ while (true) {
42+ const chunk = Buffer.alloc(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+ }
50+
51+ if (bytesRead === 0) {
52+ // EOF
53+ return mkNothing;
54+ }
55+
56+ buffer += chunk.toString('utf8', 0, bytesRead);
57+
58+ msg = tryParseMessage();
59+ if (msg !== null) {
60+ return mkJust(msg);
61+ }
62+ }
63+ } catch (e) {
64+ return mkNothing;
65+ }
66+};
67+
68+// Write a JSON-RPC message to stdout
69+export const writeMessage = (msg) => () => {
70+ const content = msg;
71+ const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
72+ process.stdout.write(header + content);
73+};
74+
75+// Log a message to stderr (for debugging)
76+export const logMessage = (msg) => () => {
77+ process.stderr.write('[purslint-lsp] ' + msg + '\n');
78+};
79+
80+// Parse JSON string to Foreign (returns Maybe Foreign)
81+export const parseJSON = (str) => {
82+ try {
83+ return mkJust(JSON.parse(str));
84+ } catch (e) {
85+ return mkNothing;
86+ }
87+};
88+
89+// Stringify Foreign to JSON
90+export const stringifyJSON = (obj) => {
91+ return JSON.stringify(obj);
92+};
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+2−2
@@ -1,8 +1,8 @@
1−module Main where
1+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
go.moddeleted−17
@@ -1,17 +0,0 @@
1−module project.localhost/example
2−
3−go 1.23.2
4−
5−replace project.localhost/purescript-native/ffi-loader => ./purescript-native
6−
7−replace project.localhost/purescript-native/output => ./output
8−
9−replace github.com/i-am-the-slime/go-ffi => /Users/mark/Developer/go-ffi
10−
11−require (
12− github.com/dlclark/regexp2 v1.4.0 // indirect
13− github.com/i-am-the-slime/go-ffi v0.0.0-20250107191809-8aebdb8aad2e // indirect
14− github.com/purescript-native/go-runtime v0.1.2 // indirect
15− project.localhost/purescript-native/ffi-loader v0.0.0-00010101000000-000000000000 // indirect
16− project.localhost/purescript-native/output v0.0.0-00010101000000-000000000000 // indirect
17−)
go.sumdeleted−8
@@ -1,8 +0,0 @@
1−github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
2−github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
3−github.com/i-am-the-slime/go-ffi v0.0.0-20250107191809-8aebdb8aad2e h1:1XdE49AEa1EpmMPOVMqY7jVaqGUssrdHuSz6gY4LYP0=
4−github.com/i-am-the-slime/go-ffi v0.0.0-20250107191809-8aebdb8aad2e/go.mod h1:G20T9FXpgn6M3s1pnXDAOTwAXKQgDwl/uhMLfo5h6PA=
5−github.com/joprice/purescript-native-go-ffi v0.0.0-20230319170919-df001aa880fd/go.mod h1:1TvaJdjRUFHRmoLQ5QvpR1pnivyd7eBjNA+j1PuktBM=
6−github.com/purescript-native/go-runtime v0.1.0/go.mod h1:Pl7TR9c9Al8GMlY/R2D7joWjlQuTiB9eBN2a9aXd6GQ=
7−github.com/purescript-native/go-runtime v0.1.2 h1:vxVmLXojzc6nkP29qMxHmEd6iwolF309yP/FskRY88Y=
8−github.com/purescript-native/go-runtime v0.1.2/go.mod h1:Pl7TR9c9Al8GMlY/R2D7joWjlQuTiB9eBN2a9aXd6GQ=
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−53
@@ -1,53 +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.String (joinWith) as String
9−import Purelint.Rule (Rule, RuleContext, mkRule)
10−import Purelint.Types (LintWarning(..), RuleId(..), Severity(..), Suggestion(..), SuggestionDescription(..), ReplacementText(..), WarningMessage(..))
11−import PureScript.CST.Print (printToken)
12−import PureScript.CST.Range (rangeOf, tokensOf)
13−import PureScript.CST.Range.TokenList as TokenList
14−import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
15−import PureScript.CST.Types (Expr(..), LetBinding, Module)
16−
17−-- | Rule: let x = y in z -> z where x = y
18−-- | Suggests replacing let...in expressions with where clauses
19−letToWhereRule :: Rule
20−letToWhereRule = mkRule (RuleId "LetToWhere") run
21− where
22− run :: RuleContext -> Module Void -> Array LintWarning
23− run _ctx = foldMapModule visitor
24− where
25− visitor = defaultMonoidalVisitor { onExpr = checkExpr }
26−
27− checkExpr :: Expr Void -> Array LintWarning
28− checkExpr expr = case expr of
29− ExprLet letIn ->
30− let
31− bodyText = printExpr letIn.body
32− bindings = NEA.toArray letIn.bindings # map printBinding
33− bindingsText = String.joinWith "\n " bindings
34− replacement = bodyText <> "\n where\n " <> bindingsText
35− in
36− [ LintWarning
37− { ruleId: RuleId "LetToWhere"
38− , message: WarningMessage "Use where clause instead of let...in"
39− , range: rangeOf expr
40− , severity: Hint
41− , suggestion: Just $ Suggestion
42− { replacement: ReplacementText replacement
43− , description: SuggestionDescription "let x = y in z can be rewritten as z where x = y"
44− }
45− }
46− ]
47− _ -> []
48−
49− printExpr :: Expr Void -> String
50− printExpr e = foldMap (\tok -> printToken tok.value) (TokenList.toArray (tokensOf e))
51−
52− printBinding :: LetBinding Void -> String
53− printBinding b = foldMap (\tok -> printToken tok.value) (TokenList.toArray (tokensOf b))
lib/purelint/src/Purelint/Runner.pursdeleted−45
@@ -1,45 +0,0 @@
1−module Purelint.Runner where
2−
3−import Prelude
4−
5−import Data.Array as Array
6−import Data.Either (Either(..))
7−import Data.Newtype (unwrap)
8−import Purelint.Imports (getImportInfo)
9−import Purelint.Rule (Rule, RuleContext)
10−import Purelint.Types (LintResult(..), SourceCode(..))
11−import PureScript.CST (RecoveredParserResult(..), parseModule)
12−import PureScript.CST.Errors (printParseError)
13−import PureScript.CST.Types (Module, ModuleHeader(..), ModuleName)
14−import Unsafe.Coerce (unsafeCoerce)
15−
16−-- | Run a set of rules on source code
17−runRules :: Array Rule -> SourceCode -> Either String LintResult
18−runRules rules (SourceCode source) =
19− case parseModule source of
20− ParseSucceeded m -> Right $ runOnModule rules (toVoidModule m)
21− ParseSucceededWithErrors m _ -> Right $ runOnModule rules (toVoidModule m)
22− ParseFailed errRec -> Left $ "Parse error at " <> show (errRec.position.line + 1) <> ":" <> show (errRec.position.column + 1) <> ": " <> printParseError errRec.error
23−
24−-- | Run rules on a successfully parsed module
25−runOnModule :: Array Rule -> Module Void -> LintResult
26−runOnModule rules mod =
27− let
28− imports = getImportInfo mod
29− ctx :: RuleContext
30− ctx = { imports }
31− warnings = Array.concatMap (\rule -> rule.run ctx mod) rules
32− modName = getModuleName mod
33− in
34− LintResult { warnings, moduleName: modName }
35−
36−-- | Get the module name from a parsed module
37−getModuleName :: forall e. Module e -> ModuleName
38−getModuleName mod =
39− let (ModuleHeader header) = (unwrap mod).header
40− in (unwrap header.name).name
41−
42−-- | Convert a recovered module to a Void module (assuming no errors)
43−-- | This is safe after ParseSucceeded because there are no error nodes
44−toVoidModule :: forall e. Module e -> Module Void
45−toVoidModule = unsafeCoerce
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+37−5
@@ -1,11 +1,10 @@
1−module Purelint.Imports where
1+module Purslint.Imports where
22
33import Prelude
44
55import Data.Array as Array
66import Data.Foldable (foldr)
77import Data.Maybe (Maybe(..))
8−import Data.Newtype (unwrap)
98import Data.Set (Set)
109import Data.Set as Set
1110import Data.Tuple (Tuple(..), snd)
@@ -22,6 +21,10 @@ type ImportInfo =
2221 , hasPrelude :: Boolean
2322 }
2423
24+-- | Check if a module is prelude-like (for debugging)
25+isPreludeModule :: String -> Boolean
26+isPreludeModule name = Set.member name preludeModules
27+
2528-- | Modules considered "Prelude-like" for our purposes
2629preludeModules :: Set String
2730preludeModules = Set.fromFoldable
@@ -32,24 +35,53 @@ preludeModules = Set.fromFoldable
3235 , "Control.Applicative"
3336 , "Control.Monad"
3437 , "Control.Bind"
38+ , "Control.Alternative"
3539 , "Data.Function"
3640 , "Data.Eq"
3741 , "Data.Boolean"
42+ , "Data.Maybe"
43+ , "Data.Tuple"
44+ , "Data.Monoid"
45+ , "Data.Semigroup"
46+ , "Data.Ord"
47+ , "Data.Array"
48+ , "Data.Either"
49+ , "Data.Bifunctor"
50+ , "Data.List"
3851 ]
3952
4053-- | Standard Prelude values we care about
4154standardValues :: Set String
4255standardValues = Set.fromFoldable
43− [ "map", "fmap", "identity", "id", "not"
44− , "traverse", "sequenceA", "sequence"
56+ [ "map", "identity", "id", "not"
57+ , "traverse", "traverse_", "sequenceA", "sequence", "for", "for_"
4558 , "concat", "concatMap", "bind", "pure", "join"
4659 , "when", "unless"
60+ , "or", "and", "any", "all"
61+ , "length", "null", "head", "last", "take", "reverse", "sort"
62+ , "const", "void"
63+ , "maybe", "fromMaybe", "isJust", "isNothing", "Nothing", "Just"
64+ , "flip", "elem", "notElem", "elemIndex", "findIndex"
65+ , "mempty", "fold", "foldMap", "foldr", "foldl"
66+ , "fst", "snd", "uncurry"
67+ , "max", "min", "minimum", "maximum"
68+ , "compare", "comparing", "on"
69+ , "guard", "empty"
70+ , "catMaybes", "mapMaybe", "findMap"
71+ , "either", "Left", "Right"
72+ , "bimap", "first", "second"
73+ , "span", "break"
74+ , "negate"
75+ , "zip", "zipWith"
76+ , "repeat", "replicate"
4777 ]
4878
4979-- | Standard Prelude operators we care about
5080standardOps :: Set String
5181standardOps = Set.fromFoldable
52− [ "<$>", "<#>", "<*>", ">>=", "=<<", ">>", "<<<", ">>>", "==", "/=" ]
82+ [ "<$>", "<#>", "<*>", ">>=", "=<<", ">>", "*>", "<<<", ">>>", "==", "/="
83+ , "<>", ">", ">=", "<", "<=", "$>", "!!", "<|>", "&&", "||"
84+ ]
5385
5486-- | Extract import information from a module
5587getImportInfo :: Module Void -> ImportInfo
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/purslint/src/Purslint/Rules/BooleanSimplify.pursnew file+86
@@ -0,0 +1,86 @@
1+module Purslint.Rules.BooleanSimplify 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, 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 (Expr(..), Module, Operator(..), QualifiedName(..))
16+
17+-- | Rule: x == true -> x
18+-- | Also: x == false -> not x
19+-- | Also: true == x -> x
20+-- | Also: false == x -> not x
21+booleanSimplifyRule :: Rule
22+booleanSimplifyRule = run # mkRule (RuleId "BooleanSimplify")
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+
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" ]
64+ _ -> []
65+
66+ mkWarning :: Expr Void -> String -> String -> String -> LintWarning
67+ mkWarning fullExpr replacement from to =
68+ LintWarning
69+ { ruleId: RuleId "BooleanSimplify"
70+ , message: WarningMessage $ "Redundant comparison: " <> from
71+ , range: rangeOf fullExpr
72+ , severity: Warning
73+ , suggestion: Just $ Suggestion
74+ { replacement: ReplacementText replacement
75+ , description: SuggestionDescription $ from <> " can be simplified to " <> to
76+ , requiredImports: []
77+ }
78+ }
79+
80+ isEqOp :: QualifiedName Operator -> Boolean
81+ isEqOp (QualifiedName { name: Operator op }) = op == "=="
82+
83+ isBoolLit :: Boolean -> Expr Void -> Boolean
84+ isBoolLit expected (ExprBoolean _ val) = val == expected
85+ isBoolLit _ _ = false
86+
lib/purslint/src/Purslint/Rules/CollapseLambdas.pursnew file+56
@@ -0,0 +1,56 @@
1+module Purslint.Rules.CollapseLambdas 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 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 (Binder(..), Expr(..), Ident(..), Module, Name(..))
15+
16+-- | Rule: \x -> \y -> body -> \x y -> body
17+collapseLambdasRule :: Rule
18+collapseLambdasRule = run # mkRule (RuleId "CollapseLambdas")
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+ ExprLambda lambda ->
28+ -- Check if body is another lambda
29+ case lambda.body of
30+ ExprLambda innerLambda -> do
31+ let
32+ outerBinders = NEA.toArray lambda.binders
33+ innerBinders = NEA.toArray innerLambda.binders
34+ allBinders = outerBinders <> innerBinders
35+ binderNames = Array.mapMaybe getBinderName allBinders
36+ bodyText = printExpr innerLambda.body
37+ replacement = "\\" <> String.joinWith " " binderNames <> " -> " <> bodyText
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+ ]
50+ _ -> []
51+ _ -> []
52+
53+ getBinderName :: Binder Void -> Maybe String
54+ getBinderName (BinderVar (Name { name: Ident n })) = Just n
55+ 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/purslint/src/Purslint/Rules/EvaluateConst.pursnew file+54
@@ -0,0 +1,54 @@
1+module Purslint.Rules.EvaluateConst 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(..))
15+
16+-- | Rule: const x y -> x
17+evaluateConstRule :: Rule
18+evaluateConstRule = mkRule (RuleId "EvaluateConst") 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: const x y (fully applied)
29+ ExprApp fn args
30+ | isConst imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm xArg, AppTerm _yArg] ->
33+ let x = printExpr xArg
34+ in
35+ [ LintWarning
36+ { ruleId: RuleId "EvaluateConst"
37+ , message: WarningMessage "const x y can be simplified to x"
38+ , range: rangeOf expr
39+ , severity: Warning
40+ , suggestion: Just $ Suggestion
41+ { replacement: ReplacementText x
42+ , description: SuggestionDescription "Evaluate const"
43+ , requiredImports: []
44+ }
45+ }
46+ ]
47+ _ -> []
48+ _ -> []
49+
50+ isConst :: ImportInfo -> Expr Void -> Boolean
51+ isConst imports (ExprIdent (QualifiedName { name: Ident name })) =
52+ name == "const" && hasValue imports "const"
53+ 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/purslint/src/Purslint/Rules/EvaluateFst.pursnew file+99
@@ -0,0 +1,99 @@
1+module Purslint.Rules.EvaluateFst 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, Proper(..), QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: fst (Tuple x y) -> x
17+-- | Also: snd (Tuple x y) -> y
18+evaluateFstRule :: Rule
19+evaluateFstRule = mkRule (RuleId "EvaluateFst") 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: fst (Tuple x y) or fst (x /\ y)
30+ ExprApp fn args
31+ | isFst imports fn ->
32+ case NEA.toArray args of
33+ [AppTerm arg] ->
34+ case getTupleElements (unwrapParens arg) of
35+ Just { first, second: _ } ->
36+ let x = printExpr first
37+ in
38+ [ LintWarning
39+ { ruleId: RuleId "EvaluateFst"
40+ , message: WarningMessage "fst (Tuple x y) can be simplified to x"
41+ , range: rangeOf expr
42+ , severity: Warning
43+ , suggestion: Just $ Suggestion
44+ { replacement: ReplacementText x
45+ , description: SuggestionDescription "Extract first element directly"
46+ , requiredImports: []
47+ }
48+ }
49+ ]
50+ Nothing -> []
51+ _ -> []
52+ | isSnd imports fn ->
53+ case NEA.toArray args of
54+ [AppTerm arg] ->
55+ case getTupleElements (unwrapParens arg) of
56+ Just { first: _, second } ->
57+ let y = printExpr second
58+ in
59+ [ LintWarning
60+ { ruleId: RuleId "EvaluateFst"
61+ , message: WarningMessage "snd (Tuple x y) can be simplified to y"
62+ , range: rangeOf expr
63+ , severity: Warning
64+ , suggestion: Just $ Suggestion
65+ { replacement: ReplacementText y
66+ , description: SuggestionDescription "Extract second element directly"
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+ isFst :: ImportInfo -> Expr Void -> Boolean
80+ isFst imports (ExprIdent (QualifiedName { name: Ident name })) =
81+ name == "fst" && hasValue imports "fst"
82+ isFst _ _ = false
83+
84+ isSnd :: ImportInfo -> Expr Void -> Boolean
85+ isSnd imports (ExprIdent (QualifiedName { name: Ident name })) =
86+ name == "snd" && hasValue imports "snd"
87+ isSnd _ _ = false
88+
89+ -- Extract tuple elements from Tuple x y constructor
90+ getTupleElements :: Expr Void -> Maybe { first :: Expr Void, second :: Expr Void }
91+ getTupleElements (ExprApp ctor args) =
92+ case ctor of
93+ ExprConstructor (QualifiedName { name: Proper "Tuple" }) ->
94+ case NEA.toArray args of
95+ [AppTerm first, AppTerm second] -> Just { first, second }
96+ _ -> Nothing
97+ _ -> Nothing
98+ 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/purslint/src/Purslint/Rules/FunctorLaw.pursnew file+94
@@ -0,0 +1,94 @@
1+module Purslint.Rules.FunctorLaw 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: map f (map g x) -> map (f <<< g) x (functor fusion)
18+-- | Also: f <$> (g <$> x) -> (f <<< g) <$> x
19+functorLawRule :: Rule
20+functorLawRule = mkRule (RuleId "FunctorLaw") 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: map f (map g x)
31+ ExprApp fnExpr args
32+ | isMapLike imports fnExpr
33+ , [AppTerm fExpr, AppTerm innerExpr] <- NEA.toArray args ->
34+ checkInnerMap imports expr fExpr (unwrapParens innerExpr)
35+ -- Match: f <$> (g <$> x)
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 "<$>"
57+ _ -> []
58+
59+ mkWarning :: Expr Void -> Expr Void -> Expr Void -> Expr Void -> String -> Array LintWarning
60+ mkWarning fullExpr fExpr gExpr xExpr style =
61+ let
62+ fText = printExpr fExpr
63+ gText = printExpr gExpr
64+ xText = printExpr xExpr
65+ replacement = if style == "map"
66+ then "map (" <> fText <> " <<< " <> gText <> ") " <> xText
67+ else "(" <> fText <> " <<< " <> gText <> ") <$> " <> xText
68+ in
69+ [ LintWarning
70+ { ruleId: RuleId "FunctorLaw"
71+ , message: WarningMessage "Functor law: fuse nested maps"
72+ , range: rangeOf fullExpr
73+ , severity: Warning
74+ , suggestion: Just $ Suggestion
75+ { replacement: ReplacementText replacement
76+ , description: SuggestionDescription $ style <> " f (" <> style <> " g x) can be fused to " <> style <> " (f <<< g) x"
77+ , requiredImports: []
78+ }
79+ }
80+ ]
81+
82+ unwrapParens :: Expr Void -> Expr Void
83+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
84+ unwrapParens e = e
85+
86+ isMapLike :: ImportInfo -> Expr Void -> Boolean
87+ isMapLike imports (ExprIdent (QualifiedName { name: Ident name })) =
88+ name == "map" && hasValue imports "map"
89+ isMapLike _ _ = false
90+
91+ isMapOp :: ImportInfo -> QualifiedName Operator -> Boolean
92+ isMapOp imports (QualifiedName { name: Operator op }) =
93+ 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/purslint/src/Purslint/Rules/MonoidIdentity.pursnew file+72
@@ -0,0 +1,72 @@
1+module Purslint.Rules.MonoidIdentity 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 (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
16+
17+-- | Rule: mempty <> x -> x
18+-- | Also: x <> mempty -> x
19+monoidIdentityRule :: Rule
20+monoidIdentityRule = mkRule (RuleId "MonoidIdentity") 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: mempty <> x or x <> mempty
31+ ExprOp lhs ops | hasOp imports "<>" ->
32+ case NEA.toArray ops of
33+ [Tuple (QualifiedName { name: Operator op }) rhs] | op == "<>" ->
34+ if isMempty imports lhs then
35+ let rhsText = printExpr rhs
36+ in
37+ [ LintWarning
38+ { ruleId: RuleId "MonoidIdentity"
39+ , message: WarningMessage "mempty <> x is redundant"
40+ , range: rangeOf expr
41+ , severity: Warning
42+ , suggestion: Just $ Suggestion
43+ { replacement: ReplacementText rhsText
44+ , description: SuggestionDescription "mempty <> x can be simplified to x (Monoid left identity)"
45+ , requiredImports: []
46+ }
47+ }
48+ ]
49+ else if isMempty imports rhs then
50+ let lhsText = printExpr lhs
51+ in
52+ [ LintWarning
53+ { ruleId: RuleId "MonoidIdentity"
54+ , message: WarningMessage "x <> mempty is redundant"
55+ , range: rangeOf expr
56+ , severity: Warning
57+ , suggestion: Just $ Suggestion
58+ { replacement: ReplacementText lhsText
59+ , description: SuggestionDescription "x <> mempty can be simplified to x (Monoid right identity)"
60+ , requiredImports: []
61+ }
62+ }
63+ ]
64+ else []
65+ _ -> []
66+ _ -> []
67+
68+ isMempty :: ImportInfo -> Expr Void -> Boolean
69+ isMempty imports (ExprIdent (QualifiedName { name: Ident name })) =
70+ name == "mempty" && hasValue imports "mempty"
71+ 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/purslint/src/Purslint/Rules/NothingBind.pursnew file+74
@@ -0,0 +1,74 @@
1+module Purslint.Rules.NothingBind 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, hasOp)
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(..), Proper(..), QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: Nothing >>= f -> Nothing
17+-- | Also: f =<< Nothing -> Nothing
18+nothingBindRule :: Rule
19+nothingBindRule = mkRule (RuleId "NothingBind") 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: Nothing >>= f
30+ ExprOp lhs ops
31+ | hasOp imports ">>=" ->
32+ case NEA.toArray ops of
33+ [Tuple (QualifiedName { name: Operator ">>=" }) _rhs] ->
34+ if isNothing (unwrapParens lhs) then
35+ [ LintWarning
36+ { ruleId: RuleId "NothingBind"
37+ , message: WarningMessage "Nothing >>= f is always Nothing"
38+ , range: rangeOf expr
39+ , severity: Warning
40+ , suggestion: Just $ Suggestion
41+ { replacement: ReplacementText "Nothing"
42+ , description: SuggestionDescription "Simplify Nothing >>= f to Nothing"
43+ , requiredImports: []
44+ }
45+ }
46+ ]
47+ else []
48+ [Tuple (QualifiedName { name: Operator "=<<" }) rhs] ->
49+ if isNothing (unwrapParens rhs) then
50+ [ LintWarning
51+ { ruleId: RuleId "NothingBind"
52+ , message: WarningMessage "f =<< Nothing is always Nothing"
53+ , range: rangeOf expr
54+ , severity: Warning
55+ , suggestion: Just $ Suggestion
56+ { replacement: ReplacementText "Nothing"
57+ , description: SuggestionDescription "Simplify f =<< Nothing to Nothing"
58+ , requiredImports: []
59+ }
60+ }
61+ ]
62+ else []
63+ _ -> []
64+ _ -> []
65+
66+ unwrapParens :: Expr Void -> Expr Void
67+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
68+ unwrapParens e = e
69+
70+ isNothing :: Expr Void -> Boolean
71+ isNothing (ExprIdent (QualifiedName { name: Ident "Nothing" })) = true
72+ isNothing (ExprConstructor (QualifiedName { name: Proper "Nothing" })) = true
73+ isNothing _ = false
74+
lib/purelint/src/Purelint/Rules/RedundantBind.purs → lib/purslint/src/Purslint/Rules/RedundantBind.pursrenamed+33−21
@@ -1,17 +1,17 @@
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)
14−import PureScript.CST.Types (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
14+import PureScript.CST.Types (AppSpine(..), Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
1515import Data.Void (Void)
1616
1717-- | Rule: x >>= pure -> x
@@ -50,27 +50,39 @@ 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 ]
56− else if isPureApp imports lhs then
57− -- pure x >>= f -> f x (don't auto-fix this one, too complex)
58− [ LintWarning
59− { ruleId: RuleId "RedundantBind"
60− , message: WarningMessage "pure x >>= f can be simplified"
61− , range: rangeOf fullExpr
62− , severity: Hint
63− , suggestion: Nothing -- No auto-fix for this pattern
64− }
65− ]
66− else
67− []
57+ else case getPureArg imports lhs of
58+ Just arg ->
59+ -- pure x >>= f -> f x
60+ let replacement = printExpr rhs <> " " <> printExpr arg
61+ in
62+ [ LintWarning
63+ { ruleId: RuleId "RedundantBind"
64+ , message: WarningMessage "pure x >>= f can be simplified to f x"
65+ , range: rangeOf fullExpr
66+ , severity: Warning
67+ , suggestion: Just $ Suggestion
68+ { replacement: ReplacementText replacement
69+ , description: SuggestionDescription "Apply f directly to x"
70+ , requiredImports: []
71+ }
72+ }
73+ ]
74+ Nothing -> []
6875
6976 isPure :: ImportInfo -> Expr Void -> Boolean
7077 isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
7178 (name == "pure" && hasValue imports "pure") || (name == "return" && hasValue imports "pure")
7279 isPure _ _ = false
7380
74− isPureApp :: ImportInfo -> Expr Void -> Boolean
75− isPureApp imports (ExprApp fn _) = isPure imports fn
76− isPureApp _ _ = false
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+
lib/purslint/src/Purslint/Rules/RedundantFlip.pursnew file+64
@@ -0,0 +1,64 @@
1+module Purslint.Rules.RedundantFlip 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: flip (flip f) -> f
17+redundantFlipRule :: Rule
18+redundantFlipRule = mkRule (RuleId "RedundantFlip") 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: flip (flip f)
29+ ExprApp fnExpr args | isFlip imports fnExpr ->
30+ case NEA.toArray args of
31+ [AppTerm innerExpr] ->
32+ case getFlipArg imports innerExpr of
33+ Just f ->
34+ let fText = printExpr f
35+ in
36+ [ LintWarning
37+ { ruleId: RuleId "RedundantFlip"
38+ , message: WarningMessage "flip (flip f) is redundant"
39+ , range: rangeOf expr
40+ , severity: Warning
41+ , suggestion: Just $ Suggestion
42+ { replacement: ReplacementText fText
43+ , description: SuggestionDescription "flip (flip f) can be simplified to f"
44+ , requiredImports: []
45+ }
46+ }
47+ ]
48+ Nothing -> []
49+ _ -> []
50+ _ -> []
51+
52+ isFlip :: ImportInfo -> Expr Void -> Boolean
53+ isFlip imports (ExprIdent (QualifiedName { name: Ident name })) =
54+ name == "flip" && hasValue imports "flip"
55+ isFlip _ _ = false
56+
57+ getFlipArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
58+ getFlipArg imports (ExprApp fnExpr args) | isFlip imports fnExpr =
59+ case NEA.toArray args of
60+ [AppTerm f] -> Just f
61+ _ -> Nothing
62+ getFlipArg imports (ExprParens (Wrapped { value })) = getFlipArg imports value
63+ getFlipArg _ _ = Nothing
64+
lib/purslint/src/Purslint/Rules/RedundantGuard.pursnew file+73
@@ -0,0 +1,73 @@
1+module Purslint.Rules.RedundantGuard 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.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(..), Wrapped(..))
14+
15+-- | Rule: guard true -> pure unit
16+redundantGuardRule :: Rule
17+redundantGuardRule = mkRule (RuleId "RedundantGuard") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run ctx = foldMapModule visitor
21+ where
22+ imports = ctx.imports
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
24+
25+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
26+ checkExpr imports expr = case expr of
27+ -- Match: guard true
28+ ExprApp fnExpr args
29+ | isGuard imports fnExpr ->
30+ case NEA.toArray args of
31+ [AppTerm argExpr]
32+ | isTrue (unwrapParens argExpr) ->
33+ [ LintWarning
34+ { ruleId: RuleId "RedundantGuard"
35+ , message: WarningMessage "guard true is always successful"
36+ , range: rangeOf expr
37+ , severity: Warning
38+ , suggestion: Just $ Suggestion
39+ { replacement: ReplacementText "pure unit"
40+ , description: SuggestionDescription "guard true can be replaced with pure unit"
41+ , requiredImports: []
42+ }
43+ }
44+ ]
45+ | isFalse (unwrapParens argExpr) ->
46+ [ LintWarning
47+ { ruleId: RuleId "RedundantGuard"
48+ , message: WarningMessage "guard false always fails"
49+ , range: rangeOf expr
50+ , severity: Warning
51+ , suggestion: Nothing
52+ }
53+ ]
54+ _ -> []
55+ _ -> []
56+
57+ unwrapParens :: Expr Void -> Expr Void
58+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
59+ unwrapParens e = e
60+
61+ isGuard :: ImportInfo -> Expr Void -> Boolean
62+ isGuard imports (ExprIdent (QualifiedName { name: Ident name })) =
63+ name == "guard" && hasValue imports "guard"
64+ isGuard _ _ = false
65+
66+ isTrue :: Expr Void -> Boolean
67+ isTrue (ExprBoolean _ b) = b
68+ isTrue _ = false
69+
70+ isFalse :: Expr Void -> Boolean
71+ isFalse (ExprBoolean _ b) = not b
72+ isFalse _ = false
73+
lib/purslint/src/Purslint/Rules/RedundantId.pursnew file+130
@@ -0,0 +1,130 @@
1+module Purslint.Rules.RedundantId 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: identity x -> x
18+-- | Also: x <<< identity -> x
19+-- | Also: identity <<< x -> x
20+redundantIdRule :: Rule
21+redundantIdRule = mkRule (RuleId "RedundantId") 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: identity x (applied)
32+ ExprApp fn args
33+ | isIdentity imports fn ->
34+ case NEA.toArray args of
35+ [AppTerm arg] ->
36+ let x = printExpr arg
37+ in
38+ [ LintWarning
39+ { ruleId: RuleId "RedundantId"
40+ , message: WarningMessage "identity x is redundant"
41+ , range: rangeOf expr
42+ , severity: Warning
43+ , suggestion: Just $ Suggestion
44+ { replacement: ReplacementText x
45+ , description: SuggestionDescription "Remove redundant identity application"
46+ , requiredImports: []
47+ }
48+ }
49+ ]
50+ _ -> []
51+ -- Match: x <<< identity or identity <<< x
52+ ExprOp lhs ops
53+ | hasOp imports "<<<" ->
54+ case NEA.toArray ops of
55+ [Tuple (QualifiedName { name: Operator "<<<" }) rhs] ->
56+ if isIdentity imports (unwrapParens rhs) then
57+ let x = printExpr lhs
58+ in
59+ [ LintWarning
60+ { ruleId: RuleId "RedundantId"
61+ , message: WarningMessage "x <<< identity is redundant"
62+ , range: rangeOf expr
63+ , severity: Warning
64+ , suggestion: Just $ Suggestion
65+ { replacement: ReplacementText x
66+ , description: SuggestionDescription "Remove redundant identity composition"
67+ , requiredImports: []
68+ }
69+ }
70+ ]
71+ else if isIdentity imports (unwrapParens lhs) then
72+ let x = printExpr rhs
73+ in
74+ [ LintWarning
75+ { ruleId: RuleId "RedundantId"
76+ , message: WarningMessage "identity <<< x is redundant"
77+ , range: rangeOf expr
78+ , severity: Warning
79+ , suggestion: Just $ Suggestion
80+ { replacement: ReplacementText x
81+ , description: SuggestionDescription "Remove redundant identity composition"
82+ , requiredImports: []
83+ }
84+ }
85+ ]
86+ else []
87+ [Tuple (QualifiedName { name: Operator ">>>" }) rhs] ->
88+ if isIdentity imports (unwrapParens lhs) then
89+ let x = printExpr rhs
90+ in
91+ [ LintWarning
92+ { ruleId: RuleId "RedundantId"
93+ , message: WarningMessage "identity >>> x is redundant"
94+ , range: rangeOf expr
95+ , severity: Warning
96+ , suggestion: Just $ Suggestion
97+ { replacement: ReplacementText x
98+ , description: SuggestionDescription "Remove redundant identity composition"
99+ , requiredImports: []
100+ }
101+ }
102+ ]
103+ else if isIdentity imports (unwrapParens rhs) then
104+ let x = printExpr lhs
105+ in
106+ [ LintWarning
107+ { ruleId: RuleId "RedundantId"
108+ , message: WarningMessage "x >>> identity is redundant"
109+ , range: rangeOf expr
110+ , severity: Warning
111+ , suggestion: Just $ Suggestion
112+ { replacement: ReplacementText x
113+ , description: SuggestionDescription "Remove redundant identity composition"
114+ , requiredImports: []
115+ }
116+ }
117+ ]
118+ else []
119+ _ -> []
120+ _ -> []
121+
122+ unwrapParens :: Expr Void -> Expr Void
123+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
124+ unwrapParens e = e
125+
126+ isIdentity :: ImportInfo -> Expr Void -> Boolean
127+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
128+ (name == "identity" || name == "id") && hasValue imports "identity"
129+ isIdentity _ _ = false
130+
lib/purslint/src/Purslint/Rules/RedundantIf.pursnew file+68
@@ -0,0 +1,68 @@
1+module Purslint.Rules.RedundantIf where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Data.Void (Void)
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 (Expr(..), Module)
14+
15+-- | Rule: if a then true else false -> a
16+-- | Also: if a then false else true -> not a
17+redundantIfRule :: Rule
18+redundantIfRule = mkRule (RuleId "RedundantIf") 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+ ExprIf ifExpr ->
29+ let
30+ thenExpr = ifExpr.true
31+ elseExpr = ifExpr.false
32+ condExpr = ifExpr.cond
33+ in
34+ -- if a then true else false -> a
35+ if isBoolLit true thenExpr && isBoolLit false elseExpr then
36+ [ LintWarning
37+ { ruleId: RuleId "RedundantIf"
38+ , message: WarningMessage "Redundant if: if a then true else false"
39+ , range: rangeOf expr
40+ , severity: Warning
41+ , suggestion: Just $ Suggestion
42+ { replacement: ReplacementText (printExpr condExpr)
43+ , description: SuggestionDescription "if a then true else false can be simplified to a"
44+ , requiredImports: []
45+ }
46+ }
47+ ]
48+ -- if a then false else true -> not a
49+ else if isBoolLit false thenExpr && isBoolLit true elseExpr && hasValue imports "not" then
50+ [ LintWarning
51+ { ruleId: RuleId "RedundantIf"
52+ , message: WarningMessage "Redundant if: if a then false else true"
53+ , range: rangeOf expr
54+ , severity: Warning
55+ , suggestion: Just $ Suggestion
56+ { replacement: ReplacementText ("not " <> printExpr condExpr)
57+ , description: SuggestionDescription "if a then false else true can be simplified to not a"
58+ , requiredImports: []
59+ }
60+ }
61+ ]
62+ else []
63+ _ -> []
64+
65+ isBoolLit :: Boolean -> Expr Void -> Boolean
66+ isBoolLit expected (ExprBoolean _ val) = val == expected
67+ isBoolLit _ _ = false
68+
lib/purslint/src/Purslint/Rules/RedundantNegate.pursnew file+65
@@ -0,0 +1,65 @@
1+module Purslint.Rules.RedundantNegate 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: negate (negate x) -> x
17+redundantNegateRule :: Rule
18+redundantNegateRule = mkRule (RuleId "RedundantNegate") 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: negate (negate x)
29+ ExprApp fn args
30+ | isNegate imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm arg] ->
33+ case unwrapParens arg of
34+ ExprApp innerFn innerArgs
35+ | isNegate imports innerFn ->
36+ case NEA.toArray innerArgs of
37+ [AppTerm innerArg] ->
38+ let x = printExpr innerArg
39+ in
40+ [ LintWarning
41+ { ruleId: RuleId "RedundantNegate"
42+ , message: WarningMessage "negate (negate x) is redundant"
43+ , range: rangeOf expr
44+ , severity: Warning
45+ , suggestion: Just $ Suggestion
46+ { replacement: ReplacementText x
47+ , description: SuggestionDescription "Remove redundant double negation"
48+ , requiredImports: []
49+ }
50+ }
51+ ]
52+ _ -> []
53+ _ -> []
54+ _ -> []
55+ _ -> []
56+
57+ unwrapParens :: Expr Void -> Expr Void
58+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
59+ unwrapParens e = e
60+
61+ isNegate :: ImportInfo -> Expr Void -> Boolean
62+ isNegate imports (ExprIdent (QualifiedName { name: Ident name })) =
63+ name == "negate" && hasValue imports "negate"
64+ isNegate _ _ = false
65+
lib/purslint/src/Purslint/Rules/RedundantNot.pursnew file+64
@@ -0,0 +1,64 @@
1+module Purslint.Rules.RedundantNot 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: not (not x) -> x
17+redundantNotRule :: Rule
18+redundantNotRule = mkRule (RuleId "RedundantNot") 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: not (not x)
29+ ExprApp fnExpr args | isNot imports fnExpr ->
30+ case NEA.toArray args of
31+ [AppTerm innerExpr] ->
32+ case getNotArg imports innerExpr of
33+ Just x ->
34+ let xText = printExpr x
35+ in
36+ [ LintWarning
37+ { ruleId: RuleId "RedundantNot"
38+ , message: WarningMessage "not (not x) is redundant"
39+ , range: rangeOf expr
40+ , severity: Warning
41+ , suggestion: Just $ Suggestion
42+ { replacement: ReplacementText xText
43+ , description: SuggestionDescription "not (not x) can be simplified to x"
44+ , requiredImports: []
45+ }
46+ }
47+ ]
48+ Nothing -> []
49+ _ -> []
50+ _ -> []
51+
52+ isNot :: ImportInfo -> Expr Void -> Boolean
53+ isNot imports (ExprIdent (QualifiedName { name: Ident name })) =
54+ name == "not" && hasValue imports "not"
55+ isNot _ _ = false
56+
57+ getNotArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
58+ getNotArg imports (ExprApp fnExpr args) | isNot imports fnExpr =
59+ case NEA.toArray args of
60+ [AppTerm x] -> Just x
61+ _ -> Nothing
62+ getNotArg imports (ExprParens (Wrapped { value })) = getNotArg imports value
63+ 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/purslint/src/Purslint/Rules/UnlessNot.pursnew file+96
@@ -0,0 +1,96 @@
1+module Purslint.Rules.UnlessNot 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: unless (not x) -> when x
17+unlessNotRule :: Rule
18+unlessNotRule = mkRule (RuleId "UnlessNot") 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: unless (not x) action
29+ ExprApp fn args
30+ | isUnless imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm condArg, AppTerm actionArg] ->
33+ case getNotArg imports (unwrapParens condArg) of
34+ Just x ->
35+ let
36+ xText = printExpr x
37+ action = printExpr actionArg
38+ in
39+ [ LintWarning
40+ { ruleId: RuleId "UnlessNot"
41+ , message: WarningMessage "unless (not x) can be simplified to when x"
42+ , range: rangeOf expr
43+ , severity: Hint
44+ , suggestion: Just $ Suggestion
45+ { replacement: ReplacementText ("when " <> xText <> " " <> action)
46+ , description: SuggestionDescription "Use when instead of unless (not ...)"
47+ , requiredImports: []
48+ }
49+ }
50+ ]
51+ Nothing -> []
52+ [AppTerm condArg] ->
53+ case getNotArg imports (unwrapParens condArg) of
54+ Just x ->
55+ let xText = printExpr x
56+ in
57+ [ LintWarning
58+ { ruleId: RuleId "UnlessNot"
59+ , message: WarningMessage "unless (not x) can be simplified to when x"
60+ , range: rangeOf expr
61+ , severity: Hint
62+ , suggestion: Just $ Suggestion
63+ { replacement: ReplacementText ("when " <> xText)
64+ , description: SuggestionDescription "Use when instead of unless (not ...)"
65+ , requiredImports: []
66+ }
67+ }
68+ ]
69+ Nothing -> []
70+ _ -> []
71+ _ -> []
72+
73+ unwrapParens :: Expr Void -> Expr Void
74+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
75+ unwrapParens e = e
76+
77+ isUnless :: ImportInfo -> Expr Void -> Boolean
78+ isUnless imports (ExprIdent (QualifiedName { name: Ident name })) =
79+ name == "unless" && hasValue imports "unless"
80+ isUnless _ _ = false
81+
82+ isNot :: ImportInfo -> Expr Void -> Boolean
83+ isNot imports (ExprIdent (QualifiedName { name: Ident name })) =
84+ name == "not" && hasValue imports "not"
85+ isNot _ _ = false
86+
87+ getNotArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
88+ getNotArg imports e =
89+ case e of
90+ ExprApp notFn args
91+ | isNot imports notFn ->
92+ case NEA.toArray args of
93+ [AppTerm arg] -> Just arg
94+ _ -> Nothing
95+ _ -> Nothing
96+
lib/purslint/src/Purslint/Rules/UseAll.pursnew file+70
@@ -0,0 +1,70 @@
1+module Purslint.Rules.UseAll 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: and (map f x) -> all f x
17+useAllRule :: Rule
18+useAllRule = mkRule (RuleId "UseAll") 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: and (map f x)
29+ ExprApp fnExpr args | isAnd 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 "UseAll"
42+ , message: WarningMessage "Use all instead of and (map f x)"
43+ , range: rangeOf expr
44+ , severity: Warning
45+ , suggestion: Just $ Suggestion
46+ { replacement: ReplacementText ("all " <> fText <> " " <> xText)
47+ , description: SuggestionDescription "and (map f x) can be simplified to all f x"
48+ , requiredImports: []
49+ }
50+ }
51+ ]
52+ _ -> []
53+ _ -> []
54+ _ -> []
55+ _ -> []
56+
57+ unwrapParens :: Expr Void -> Expr Void
58+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
59+ unwrapParens e = e
60+
61+ isAnd :: ImportInfo -> Expr Void -> Boolean
62+ isAnd imports (ExprIdent (QualifiedName { name: Ident name })) =
63+ name == "and" && hasValue imports "and"
64+ isAnd _ _ = false
65+
66+ isMapLike :: ImportInfo -> Expr Void -> Boolean
67+ isMapLike imports (ExprIdent (QualifiedName { name: Ident name })) =
68+ name == "map" && hasValue imports "map"
69+ isMapLike _ _ = false
70+
lib/purslint/src/Purslint/Rules/UseAnd.pursnew file+80
@@ -0,0 +1,80 @@
1+module Purslint.Rules.UseAnd 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: all identity -> and
17+useAndRule :: Rule
18+useAndRule = mkRule (RuleId "UseAnd") 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: all identity xs
29+ ExprApp fn args
30+ | isAll imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm predArg] ->
33+ if isIdentity imports (unwrapParens predArg) then
34+ [ LintWarning
35+ { ruleId: RuleId "UseAnd"
36+ , message: WarningMessage "all identity can be simplified to and"
37+ , range: rangeOf expr
38+ , severity: Warning
39+ , suggestion: Just $ Suggestion
40+ { replacement: ReplacementText "and"
41+ , description: SuggestionDescription "Use and instead of all identity"
42+ , requiredImports: []
43+ }
44+ }
45+ ]
46+ else []
47+ [AppTerm predArg, AppTerm xArg] ->
48+ if isIdentity imports (unwrapParens predArg) then
49+ let x = printExpr xArg
50+ in
51+ [ LintWarning
52+ { ruleId: RuleId "UseAnd"
53+ , message: WarningMessage "all identity xs can be simplified to and xs"
54+ , range: rangeOf expr
55+ , severity: Warning
56+ , suggestion: Just $ Suggestion
57+ { replacement: ReplacementText ("and " <> x)
58+ , description: SuggestionDescription "Use and instead of all identity"
59+ , requiredImports: []
60+ }
61+ }
62+ ]
63+ else []
64+ _ -> []
65+ _ -> []
66+
67+ unwrapParens :: Expr Void -> Expr Void
68+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
69+ unwrapParens e = e
70+
71+ isAll :: ImportInfo -> Expr Void -> Boolean
72+ isAll imports (ExprIdent (QualifiedName { name: Ident name })) =
73+ name == "all" && hasValue imports "all"
74+ isAll _ _ = false
75+
76+ isIdentity :: ImportInfo -> Expr Void -> Boolean
77+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
78+ (name == "identity" || name == "id") && hasValue imports "identity"
79+ 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/purslint/src/Purslint/Rules/UseComparing.pursnew file+78
@@ -0,0 +1,78 @@
1+module Purslint.Rules.UseComparing 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(..), Binder(..), Expr(..), Ident(..), Module, Name(..), QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: \x y -> compare (f x) (f y) -> comparing f
17+useComparingRule :: Rule
18+useComparingRule = mkRule (RuleId "UseComparing") 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: \x y -> compare (f x) (f y)
29+ ExprLambda { binders, body }
30+ | 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+ _ -> []
35+ _ -> []
36+
37+ unwrapParens :: Expr Void -> Expr Void
38+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
39+ unwrapParens e = e
40+
41+ checkCompareBody :: ImportInfo -> Expr Void -> String -> String -> Expr Void -> Array LintWarning
42+ checkCompareBody imports fullExpr x y body =
43+ case body of
44+ ExprApp fnExpr args
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+ ]
62+ _ -> []
63+
64+ isCompare :: ImportInfo -> Expr Void -> Boolean
65+ isCompare = case _, _ of
66+ imports, ExprIdent (QualifiedName { name: Ident name }) -> name == "compare" && hasValue imports "compare"
67+ _, _ -> false
68+
69+ extractFunctionCall :: Expr Void -> Maybe { fn :: String, arg :: String }
70+ extractFunctionCall (ExprApp fnExpr args) =
71+ case fnExpr, NEA.toArray args of
72+ ExprIdent (QualifiedName { name: Ident fn }), [ AppTerm argExpr ] ->
73+ case argExpr of
74+ ExprIdent (QualifiedName { name: Ident arg }) -> Just { fn, arg }
75+ _ -> Nothing
76+ _, _ -> Nothing
77+ 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/purslint/src/Purslint/Rules/UseElemIndex.pursnew file+131
@@ -0,0 +1,131 @@
1+module Purslint.Rules.UseElemIndex 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(..), Binder(..), Expr(..), Ident(..), Module, Name(..), Operator(..), QualifiedName(..), Wrapped(..))
16+
17+-- | Rule: findIndex (== a) -> elemIndex a
18+-- | Also: findIndex (\x -> x == a) -> elemIndex a
19+useElemIndexRule :: Rule
20+useElemIndexRule = mkRule (RuleId "UseElemIndex") 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: findIndex (== a) xs or findIndex (\x -> x == a)
31+ ExprApp fn args
32+ | isFindIndex imports fn ->
33+ case NEA.toArray args of
34+ [AppTerm predArg] ->
35+ case getEqArg imports (unwrapParens predArg) of
36+ Just a ->
37+ let aText = printExpr a
38+ in
39+ [ LintWarning
40+ { ruleId: RuleId "UseElemIndex"
41+ , message: WarningMessage "findIndex (== a) can be simplified to elemIndex a"
42+ , range: rangeOf expr
43+ , severity: Warning
44+ , suggestion: Just $ Suggestion
45+ { replacement: ReplacementText ("elemIndex " <> aText)
46+ , description: SuggestionDescription "Use elemIndex instead of findIndex (== a)"
47+ , requiredImports: []
48+ }
49+ }
50+ ]
51+ Nothing -> []
52+ [AppTerm predArg, AppTerm xArg] ->
53+ case getEqArg imports (unwrapParens predArg) of
54+ Just a ->
55+ let
56+ aText = printExpr a
57+ x = printExpr xArg
58+ in
59+ [ LintWarning
60+ { ruleId: RuleId "UseElemIndex"
61+ , message: WarningMessage "findIndex (== a) xs can be simplified to elemIndex a xs"
62+ , range: rangeOf expr
63+ , severity: Warning
64+ , suggestion: Just $ Suggestion
65+ { replacement: ReplacementText ("elemIndex " <> aText <> " " <> x)
66+ , description: SuggestionDescription "Use elemIndex instead of findIndex (== a)"
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+ isFindIndex :: ImportInfo -> Expr Void -> Boolean
80+ isFindIndex imports (ExprIdent (QualifiedName { name: Ident name })) =
81+ name == "findIndex" && hasValue imports "findIndex"
82+ isFindIndex _ _ = false
83+
84+ -- Check for (== a) section or (\x -> x == a) lambda
85+ getEqArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
86+ getEqArg imports e = case e of
87+ -- Section: (== a)
88+ ExprSection _ ->
89+ -- Sections are tricky to parse, skip for now
90+ Nothing
91+ -- Operator section in parens: (== a)
92+ ExprOp lhs ops
93+ | hasOp imports "==" ->
94+ case NEA.toArray ops of
95+ [Tuple (QualifiedName { name: Operator "==" }) rhs] ->
96+ -- This would be `lhs == rhs`, not a section
97+ -- Sections appear differently
98+ Nothing
99+ _ -> Nothing
100+ -- Lambda: \x -> x == a
101+ ExprLambda { binders, body } ->
102+ case NEA.toArray binders of
103+ [binder] ->
104+ case getBinderName binder of
105+ Just varName ->
106+ case body of
107+ ExprOp lhs ops
108+ | hasOp imports "==" ->
109+ case NEA.toArray ops of
110+ [Tuple (QualifiedName { name: Operator "==" }) rhs] ->
111+ -- Check if lhs is the bound variable
112+ case lhs of
113+ ExprIdent (QualifiedName { name: Ident name })
114+ | name == varName -> Just rhs
115+ _ ->
116+ -- Check if rhs is the bound variable
117+ case rhs of
118+ ExprIdent (QualifiedName { name: Ident name })
119+ | name == varName -> Just lhs
120+ _ -> Nothing
121+ _ -> Nothing
122+ _ -> Nothing
123+ Nothing -> Nothing
124+ _ -> Nothing
125+ _ -> Nothing
126+
127+ getBinderName :: Binder Void -> Maybe String
128+ getBinderName (BinderVar (Name { name: Ident name })) = Just name
129+ getBinderName (BinderParens (Wrapped { value: inner })) = getBinderName inner
130+ getBinderName _ = Nothing
131+
lib/purslint/src/Purslint/Rules/UseFindMap.pursnew file+81
@@ -0,0 +1,81 @@
1+module Purslint.Rules.UseFindMap 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: head (mapMaybe f x) -> findMap f x
17+useFindMapRule :: Rule
18+useFindMapRule = mkRule (RuleId "UseFindMap") 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: head (mapMaybe f x)
29+ ExprApp fnExpr args
30+ | isHead imports fnExpr ->
31+ case NEA.toArray args of
32+ [AppTerm innerExpr] ->
33+ checkMapMaybe 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+ checkMapMaybe :: ImportInfo -> Expr Void -> Expr Void -> Array LintWarning
42+ checkMapMaybe imports fullExpr innerExpr =
43+ case innerExpr of
44+ ExprApp mapMaybeFn mapMaybeArgs | isMapMaybe imports mapMaybeFn ->
45+ case NEA.toArray mapMaybeArgs of
46+ [AppTerm fExpr, AppTerm xExpr] ->
47+ let
48+ f = printExpr fExpr
49+ x = printExpr xExpr
50+ in
51+ [ LintWarning
52+ { ruleId: RuleId "UseFindMap"
53+ , message: WarningMessage "head (mapMaybe f x) can be simplified to findMap f x"
54+ , range: rangeOf fullExpr
55+ , severity: Warning
56+ , suggestion: Just $ Suggestion
57+ { replacement: ReplacementText ("findMap " <> f <> " " <> x)
58+ , 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+ ]
66+ }
67+ }
68+ ]
69+ _ -> []
70+ _ -> []
71+
72+ isHead :: ImportInfo -> Expr Void -> Boolean
73+ isHead imports (ExprIdent (QualifiedName { name: Ident name })) =
74+ name == "head" && hasValue imports "head"
75+ isHead _ _ = false
76+
77+ isMapMaybe :: ImportInfo -> Expr Void -> Boolean
78+ isMapMaybe imports (ExprIdent (QualifiedName { name: Ident name })) =
79+ name == "mapMaybe" && hasValue imports "mapMaybe"
80+ isMapMaybe _ _ = false
81+
lib/purslint/src/Purslint/Rules/UseFold.pursnew file+55
@@ -0,0 +1,55 @@
1+module Purslint.Rules.UseFold 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.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: foldMap identity -> fold
16+useFoldRule :: Rule
17+useFoldRule = mkRule (RuleId "UseFold") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run ctx = foldMapModule visitor
21+ where
22+ imports = ctx.imports
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
24+
25+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
26+ checkExpr imports expr = case expr of
27+ -- Match: foldMap identity
28+ ExprApp fnExpr args | isFoldMap imports fnExpr ->
29+ case NEA.toArray args of
30+ [AppTerm idExpr] | isIdentity imports idExpr ->
31+ [ LintWarning
32+ { ruleId: RuleId "UseFold"
33+ , message: WarningMessage "foldMap identity can be simplified to fold"
34+ , range: rangeOf expr
35+ , severity: Warning
36+ , suggestion: Just $ Suggestion
37+ { replacement: ReplacementText "fold"
38+ , description: SuggestionDescription "Use fold instead of foldMap identity"
39+ , requiredImports: []
40+ }
41+ }
42+ ]
43+ _ -> []
44+ _ -> []
45+
46+ isFoldMap :: ImportInfo -> Expr Void -> Boolean
47+ isFoldMap imports (ExprIdent (QualifiedName { name: Ident name })) =
48+ name == "foldMap" && hasValue imports "foldMap"
49+ isFoldMap _ _ = false
50+
51+ isIdentity :: ImportInfo -> Expr Void -> Boolean
52+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
53+ name == "identity" && hasValue imports "identity"
54+ 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/purslint/src/Purslint/Rules/UseFoldMapId.pursnew file+80
@@ -0,0 +1,80 @@
1+module Purslint.Rules.UseFoldMapId 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: foldMap identity -> fold
17+useFoldMapIdRule :: Rule
18+useFoldMapIdRule = mkRule (RuleId "UseFoldMapId") 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: foldMap identity xs
29+ ExprApp fn args
30+ | isFoldMap imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm predArg] ->
33+ if isIdentity imports (unwrapParens predArg) then
34+ [ LintWarning
35+ { ruleId: RuleId "UseFoldMapId"
36+ , message: WarningMessage "foldMap identity can be simplified to fold"
37+ , range: rangeOf expr
38+ , severity: Warning
39+ , suggestion: Just $ Suggestion
40+ { replacement: ReplacementText "fold"
41+ , description: SuggestionDescription "Use fold instead of foldMap identity"
42+ , requiredImports: []
43+ }
44+ }
45+ ]
46+ else []
47+ [AppTerm predArg, AppTerm xArg] ->
48+ if isIdentity imports (unwrapParens predArg) then
49+ let x = printExpr xArg
50+ in
51+ [ LintWarning
52+ { ruleId: RuleId "UseFoldMapId"
53+ , message: WarningMessage "foldMap identity xs can be simplified to fold xs"
54+ , range: rangeOf expr
55+ , severity: Warning
56+ , suggestion: Just $ Suggestion
57+ { replacement: ReplacementText ("fold " <> x)
58+ , description: SuggestionDescription "Use fold instead of foldMap identity"
59+ , requiredImports: []
60+ }
61+ }
62+ ]
63+ else []
64+ _ -> []
65+ _ -> []
66+
67+ unwrapParens :: Expr Void -> Expr Void
68+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
69+ unwrapParens e = e
70+
71+ isFoldMap :: ImportInfo -> Expr Void -> Boolean
72+ isFoldMap imports (ExprIdent (QualifiedName { name: Ident name })) =
73+ name == "foldMap" && hasValue imports "foldMap"
74+ isFoldMap _ _ = false
75+
76+ isIdentity :: ImportInfo -> Expr Void -> Boolean
77+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
78+ (name == "identity" || name == "id") && hasValue imports "identity"
79+ isIdentity _ _ = false
80+
lib/purslint/src/Purslint/Rules/UseFor.pursnew file+77
@@ -0,0 +1,77 @@
1+module Purslint.Rules.UseFor 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: flip traverse -> for
17+useForRule :: Rule
18+useForRule = mkRule (RuleId "UseFor") 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: flip traverse
29+ ExprApp fnExpr args
30+ | isFlip imports fnExpr ->
31+ case NEA.toArray args of
32+ [AppTerm innerExpr] ->
33+ checkTraverse 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+ checkTraverse :: ImportInfo -> Expr Void -> Expr Void -> Array LintWarning
42+ checkTraverse imports fullExpr innerExpr =
43+ case innerExpr of
44+ ExprIdent (QualifiedName { name: Ident name })
45+ | name == "traverse" && hasValue imports "traverse" ->
46+ [ LintWarning
47+ { ruleId: RuleId "UseFor"
48+ , message: WarningMessage "flip traverse can be simplified to for"
49+ , range: rangeOf fullExpr
50+ , severity: Warning
51+ , suggestion: Just $ Suggestion
52+ { replacement: ReplacementText "for"
53+ , description: SuggestionDescription "Use for instead of flip traverse"
54+ , requiredImports: []
55+ }
56+ }
57+ ]
58+ | name == "traverse_" && hasValue imports "traverse_" ->
59+ [ LintWarning
60+ { ruleId: RuleId "UseFor"
61+ , message: WarningMessage "flip traverse_ can be simplified to for_"
62+ , range: rangeOf fullExpr
63+ , severity: Warning
64+ , suggestion: Just $ Suggestion
65+ { replacement: ReplacementText "for_"
66+ , description: SuggestionDescription "Use for_ instead of flip traverse_"
67+ , requiredImports: []
68+ }
69+ }
70+ ]
71+ _ -> []
72+
73+ isFlip :: ImportInfo -> Expr Void -> Boolean
74+ isFlip imports (ExprIdent (QualifiedName { name: Ident name })) =
75+ name == "flip" && hasValue imports "flip"
76+ isFlip _ _ = false
77+
lib/purslint/src/Purslint/Rules/UseFromJust.pursnew file+108
@@ -0,0 +1,108 @@
1+module Purslint.Rules.UseFromJust 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, Proper(..), QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: fromJust (Just x) -> x (evaluate)
17+-- | Note: This is for constant folding; actual usage of fromJust is discouraged
18+useFromJustRule :: Rule
19+useFromJustRule = mkRule (RuleId "UseFromJust") 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: fromJust (Just x) - but this is a partial function warning opportunity
30+ ExprApp fn args
31+ | isFromMaybe imports fn ->
32+ case NEA.toArray args of
33+ [AppTerm defaultArg, AppTerm maybeArg] ->
34+ case getJustArg (unwrapParens maybeArg) of
35+ Just x ->
36+ let xText = printExpr x
37+ in
38+ [ LintWarning
39+ { ruleId: RuleId "UseFromJust"
40+ , message: WarningMessage "fromMaybe d (Just x) can be simplified to x"
41+ , range: rangeOf expr
42+ , severity: Warning
43+ , suggestion: Just $ Suggestion
44+ { replacement: ReplacementText xText
45+ , description: SuggestionDescription "Simplify fromMaybe with Just"
46+ , requiredImports: []
47+ }
48+ }
49+ ]
50+ Nothing ->
51+ case unwrapParens maybeArg of
52+ ExprIdent (QualifiedName { name: Ident "Nothing" }) ->
53+ let d = printExpr defaultArg
54+ in
55+ [ LintWarning
56+ { ruleId: RuleId "UseFromJust"
57+ , message: WarningMessage "fromMaybe d Nothing can be simplified to d"
58+ , range: rangeOf expr
59+ , severity: Warning
60+ , suggestion: Just $ Suggestion
61+ { replacement: ReplacementText d
62+ , description: SuggestionDescription "Simplify fromMaybe with Nothing"
63+ , requiredImports: []
64+ }
65+ }
66+ ]
67+ ExprConstructor (QualifiedName { name: Proper "Nothing" }) ->
68+ let d = printExpr defaultArg
69+ in
70+ [ LintWarning
71+ { ruleId: RuleId "UseFromJust"
72+ , message: WarningMessage "fromMaybe d Nothing can be simplified to d"
73+ , range: rangeOf expr
74+ , severity: Warning
75+ , suggestion: Just $ Suggestion
76+ { replacement: ReplacementText d
77+ , description: SuggestionDescription "Simplify fromMaybe with Nothing"
78+ , requiredImports: []
79+ }
80+ }
81+ ]
82+ _ -> []
83+ _ -> []
84+ _ -> []
85+
86+ unwrapParens :: Expr Void -> Expr Void
87+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
88+ unwrapParens e = e
89+
90+ isFromMaybe :: ImportInfo -> Expr Void -> Boolean
91+ isFromMaybe imports (ExprIdent (QualifiedName { name: Ident name })) =
92+ name == "fromMaybe" && hasValue imports "fromMaybe"
93+ isFromMaybe _ _ = false
94+
95+ getJustArg :: Expr Void -> Maybe (Expr Void)
96+ getJustArg (ExprApp ctor args) =
97+ case ctor of
98+ ExprConstructor (QualifiedName { name: Proper "Just" }) ->
99+ case NEA.toArray args of
100+ [AppTerm x] -> Just x
101+ _ -> Nothing
102+ ExprIdent (QualifiedName { name: Ident "Just" }) ->
103+ case NEA.toArray args of
104+ [AppTerm x] -> Just x
105+ _ -> Nothing
106+ _ -> Nothing
107+ getJustArg _ = Nothing
108+
lib/purslint/src/Purslint/Rules/UseFromMaybe.pursnew file+58
@@ -0,0 +1,58 @@
1+module Purslint.Rules.UseFromMaybe 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(..))
15+
16+-- | Rule: maybe x id -> fromMaybe x
17+useFromMaybeRule :: Rule
18+useFromMaybeRule = mkRule (RuleId "UseFromMaybe") 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: maybe x id
29+ ExprApp fnExpr args | isMaybe imports fnExpr ->
30+ case NEA.toArray args of
31+ [AppTerm defaultVal, AppTerm fn] | isId imports fn ->
32+ let defaultText = printExpr defaultVal
33+ in
34+ [ LintWarning
35+ { ruleId: RuleId "UseFromMaybe"
36+ , message: WarningMessage "maybe x id can be simplified to fromMaybe x"
37+ , range: rangeOf expr
38+ , severity: Warning
39+ , suggestion: Just $ Suggestion
40+ { replacement: ReplacementText ("fromMaybe " <> defaultText)
41+ , description: SuggestionDescription "Use fromMaybe instead of maybe x id"
42+ , requiredImports: []
43+ }
44+ }
45+ ]
46+ _ -> []
47+ _ -> []
48+
49+ isMaybe :: ImportInfo -> Expr Void -> Boolean
50+ isMaybe imports (ExprIdent (QualifiedName { name: Ident name })) =
51+ name == "maybe" && hasValue imports "maybe"
52+ isMaybe _ _ = false
53+
54+ isId :: ImportInfo -> Expr Void -> Boolean
55+ isId imports (ExprIdent (QualifiedName { name: Ident name })) =
56+ name == "identity" && hasValue imports "identity"
57+ isId _ _ = false
58+
lib/purslint/src/Purslint/Rules/UseFstSnd.pursnew file+82
@@ -0,0 +1,82 @@
1+module Purslint.Rules.UseFstSnd 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.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 (Binder(..), Expr(..), Ident(..), Module, Name(..), Proper(..), QualifiedName(..), Wrapped(..))
13+
14+-- | Rule: \(Tuple x y) -> x -> fst
15+-- | Also: \(Tuple x y) -> y -> snd
16+useFstSndRule :: Rule
17+useFstSndRule = mkRule (RuleId "UseFstSnd") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run _ctx = foldMapModule visitor
21+ where
22+ visitor = defaultMonoidalVisitor { onExpr = checkExpr }
23+
24+ checkExpr :: Expr Void -> Array LintWarning
25+ checkExpr expr = case expr of
26+ -- Match: \(Tuple x y) -> x or \(Tuple x y) -> y
27+ ExprLambda { binders, body } ->
28+ case NEA.toArray binders of
29+ [binder] ->
30+ case getTupleBinderNames binder of
31+ Just { first, second } ->
32+ case body of
33+ ExprIdent (QualifiedName { name: Ident bodyName })
34+ | bodyName == first ->
35+ [ LintWarning
36+ { ruleId: RuleId "UseFstSnd"
37+ , message: WarningMessage "\\(Tuple x y) -> x can be simplified to fst"
38+ , range: rangeOf expr
39+ , severity: Warning
40+ , suggestion: Just $ Suggestion
41+ { replacement: ReplacementText "fst"
42+ , description: SuggestionDescription "Use fst instead of lambda extracting first element"
43+ , requiredImports: []
44+ }
45+ }
46+ ]
47+ | bodyName == second ->
48+ [ LintWarning
49+ { ruleId: RuleId "UseFstSnd"
50+ , message: WarningMessage "\\(Tuple x y) -> y can be simplified to snd"
51+ , range: rangeOf expr
52+ , severity: Warning
53+ , suggestion: Just $ Suggestion
54+ { replacement: ReplacementText "snd"
55+ , description: SuggestionDescription "Use snd instead of lambda extracting second element"
56+ , requiredImports: []
57+ }
58+ }
59+ ]
60+ _ -> []
61+ Nothing -> []
62+ _ -> []
63+ _ -> []
64+
65+ getTupleBinderNames :: Binder Void -> Maybe { first :: String, second :: String }
66+ getTupleBinderNames (BinderParens (Wrapped { value: inner })) = getTupleBinderNames inner
67+ getTupleBinderNames (BinderConstructor (QualifiedName { name: Proper ctorName }) binders)
68+ | ctorName == "Tuple" =
69+ case binders of
70+ [firstBinder, secondBinder] ->
71+ case getBinderName firstBinder, getBinderName secondBinder of
72+ Just first, Just second -> Just { first, second }
73+ _, _ -> Nothing
74+ _ -> Nothing
75+ getTupleBinderNames _ = Nothing
76+
77+ getBinderName :: Binder Void -> Maybe String
78+ getBinderName (BinderVar (Name { name: Ident name })) = Just name
79+ getBinderName (BinderWildcard _) = Nothing -- wildcards can't be referenced in body
80+ getBinderName (BinderParens (Wrapped { value: inner })) = getBinderName inner
81+ 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/purslint/src/Purslint/Rules/UseGuardMaybe.pursnew file+79
@@ -0,0 +1,79 @@
1+module Purslint.Rules.UseGuardMaybe where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Data.Void (Void)
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 (Expr(..), Ident(..), Module, QualifiedName(..), Wrapped(..))
14+
15+-- | Rule: if c then Just x else Nothing -> guard c $> x
16+useGuardMaybeRule :: Rule
17+useGuardMaybeRule = mkRule (RuleId "UseGuardMaybe") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run ctx = foldMapModule visitor
21+ where
22+ imports = ctx.imports
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
24+
25+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
26+ checkExpr imports expr = case expr of
27+ -- Match: if c then Just x else Nothing
28+ ExprIf { cond, true: trueExpr, false: falseExpr }
29+ | isJustApp imports (unwrapParens trueExpr)
30+ , isNothing imports (unwrapParens falseExpr) ->
31+ case getJustArg (unwrapParens trueExpr) of
32+ Just xExpr ->
33+ let
34+ c = printExpr cond
35+ x = printExpr xExpr
36+ in
37+ [ LintWarning
38+ { ruleId: RuleId "UseGuardMaybe"
39+ , message: WarningMessage "if c then Just x else Nothing can be simplified"
40+ , range: rangeOf expr
41+ , severity: Hint
42+ , suggestion: Just $ Suggestion
43+ { replacement: ReplacementText ("guard " <> c <> " $> " <> x)
44+ , description: SuggestionDescription "Use guard c $> x instead of if-then-else with Maybe"
45+ , requiredImports: []
46+ }
47+ }
48+ ]
49+ Nothing -> []
50+ _ -> []
51+
52+ unwrapParens :: Expr Void -> Expr Void
53+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
54+ unwrapParens e = e
55+
56+ isJustApp :: ImportInfo -> Expr Void -> Boolean
57+ isJustApp imports (ExprApp fn _) = isJust imports fn
58+ isJustApp _ _ = false
59+
60+ isJust :: ImportInfo -> Expr Void -> Boolean
61+ isJust imports (ExprIdent (QualifiedName { name: Ident name })) =
62+ name == "Just" && hasValue imports "Just"
63+ isJust imports (ExprConstructor (QualifiedName { name: _ })) =
64+ hasValue imports "Just"
65+ isJust _ _ = false
66+
67+ getJustArg :: Expr Void -> Maybe (Expr Void)
68+ getJustArg (ExprApp _ args) =
69+ case args of
70+ _ -> Nothing -- simplified - would need NEA access
71+ getJustArg _ = Nothing
72+
73+ isNothing :: ImportInfo -> Expr Void -> Boolean
74+ isNothing imports (ExprIdent (QualifiedName { name: Ident name })) =
75+ name == "Nothing" && hasValue imports "Nothing"
76+ isNothing imports (ExprConstructor (QualifiedName { name: _ })) =
77+ hasValue imports "Nothing"
78+ isNothing _ _ = false
79+
lib/purslint/src/Purslint/Rules/UseHead.pursnew file+58
@@ -0,0 +1,58 @@
1+module Purslint.Rules.UseHead where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Data.Void (Void)
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 (Expr(..), Ident(..), IntValue(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
13+import Data.Array.NonEmpty as NEA
14+import Data.Tuple (Tuple(..))
15+
16+-- | Rule: xs !! 0 -> head xs
17+useHeadRule :: Rule
18+useHeadRule = mkRule (RuleId "UseHead") 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: xs !! 0
29+ ExprOp lhs ops
30+ | hasValue imports "head" ->
31+ case NEA.toArray ops of
32+ [Tuple (QualifiedName { name: Operator "!!" }) rhs] ->
33+ if isZeroLiteral (unwrapParens rhs) then
34+ [ LintWarning
35+ { ruleId: RuleId "UseHead"
36+ , message: WarningMessage "xs !! 0 can be simplified to head xs"
37+ , range: rangeOf expr
38+ , severity: Hint
39+ , suggestion: Just $ Suggestion
40+ { replacement: ReplacementText "head"
41+ , description: SuggestionDescription "Use head instead of !! 0"
42+ , requiredImports: []
43+ }
44+ }
45+ ]
46+ else []
47+ _ -> []
48+ _ -> []
49+
50+ unwrapParens :: Expr Void -> Expr Void
51+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
52+ unwrapParens e = e
53+
54+ isZeroLiteral :: Expr Void -> Boolean
55+ isZeroLiteral (ExprInt _ (SmallInt 0)) = true
56+ isZeroLiteral (ExprInt _ (BigInt s)) = s == "0"
57+ isZeroLiteral _ = false
58+
lib/purslint/src/Purslint/Rules/UseIsJust.pursnew file+70
@@ -0,0 +1,70 @@
1+module Purslint.Rules.UseIsJust 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.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(..), Wrapped(..))
14+
15+-- | Rule: maybe false (const true) -> isJust
16+useIsJustRule :: Rule
17+useIsJustRule = mkRule (RuleId "UseIsJust") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run ctx = foldMapModule visitor
21+ where
22+ imports = ctx.imports
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
24+
25+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
26+ checkExpr imports expr = case expr of
27+ -- Match: maybe false (const true)
28+ ExprApp fnExpr args | isMaybe imports fnExpr ->
29+ case NEA.toArray args of
30+ [AppTerm falseExpr, AppTerm constTrueExpr]
31+ | isFalse falseExpr && isConstTrue imports constTrueExpr ->
32+ [ LintWarning
33+ { ruleId: RuleId "UseIsJust"
34+ , message: WarningMessage "maybe false (const true) can be simplified to isJust"
35+ , range: rangeOf expr
36+ , severity: Warning
37+ , suggestion: Just $ Suggestion
38+ { replacement: ReplacementText "isJust"
39+ , description: SuggestionDescription "Use isJust instead of maybe false (const true)"
40+ , requiredImports: []
41+ }
42+ }
43+ ]
44+ _ -> []
45+ _ -> []
46+
47+ isMaybe :: ImportInfo -> Expr Void -> Boolean
48+ isMaybe imports (ExprIdent (QualifiedName { name: Ident name })) =
49+ name == "maybe" && hasValue imports "maybe"
50+ isMaybe _ _ = false
51+
52+ isFalse :: Expr Void -> Boolean
53+ isFalse (ExprBoolean _ b) = not b
54+ isFalse _ = false
55+
56+ isConstTrue :: ImportInfo -> Expr Void -> Boolean
57+ isConstTrue imports (ExprApp fnExpr args) =
58+ case fnExpr of
59+ ExprIdent (QualifiedName { name: Ident name }) | name == "const" && hasValue imports "const" ->
60+ case NEA.toArray args of
61+ [AppTerm trueExpr] -> isTrue trueExpr
62+ _ -> false
63+ _ -> false
64+ isConstTrue imports (ExprParens (Wrapped { value })) = isConstTrue imports value
65+ isConstTrue _ _ = false
66+
67+ isTrue :: Expr Void -> Boolean
68+ isTrue (ExprBoolean _ b) = b
69+ isTrue _ = false
70+
lib/purslint/src/Purslint/Rules/UseIsNothing.pursnew file+70
@@ -0,0 +1,70 @@
1+module Purslint.Rules.UseIsNothing 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.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(..), Wrapped(..))
14+
15+-- | Rule: maybe true (const false) -> isNothing
16+useIsNothingRule :: Rule
17+useIsNothingRule = mkRule (RuleId "UseIsNothing") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run ctx = foldMapModule visitor
21+ where
22+ imports = ctx.imports
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
24+
25+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
26+ checkExpr imports expr = case expr of
27+ -- Match: maybe true (const false)
28+ ExprApp fnExpr args | isMaybe imports fnExpr ->
29+ case NEA.toArray args of
30+ [AppTerm trueExpr, AppTerm constFalseExpr]
31+ | isTrue trueExpr && isConstFalse imports constFalseExpr ->
32+ [ LintWarning
33+ { ruleId: RuleId "UseIsNothing"
34+ , message: WarningMessage "maybe true (const false) can be simplified to isNothing"
35+ , range: rangeOf expr
36+ , severity: Warning
37+ , suggestion: Just $ Suggestion
38+ { replacement: ReplacementText "isNothing"
39+ , description: SuggestionDescription "Use isNothing instead of maybe true (const false)"
40+ , requiredImports: []
41+ }
42+ }
43+ ]
44+ _ -> []
45+ _ -> []
46+
47+ isMaybe :: ImportInfo -> Expr Void -> Boolean
48+ isMaybe imports (ExprIdent (QualifiedName { name: Ident name })) =
49+ name == "maybe" && hasValue imports "maybe"
50+ isMaybe _ _ = false
51+
52+ isTrue :: Expr Void -> Boolean
53+ isTrue (ExprBoolean _ b) = b
54+ isTrue _ = false
55+
56+ isConstFalse :: ImportInfo -> Expr Void -> Boolean
57+ isConstFalse imports (ExprApp fnExpr args) =
58+ case fnExpr of
59+ ExprIdent (QualifiedName { name: Ident name }) | name == "const" && hasValue imports "const" ->
60+ case NEA.toArray args of
61+ [AppTerm falseExpr] -> isFalse falseExpr
62+ _ -> false
63+ _ -> false
64+ isConstFalse imports (ExprParens (Wrapped { value })) = isConstFalse imports value
65+ isConstFalse _ _ = false
66+
67+ isFalse :: Expr Void -> Boolean
68+ isFalse (ExprBoolean _ b) = not b
69+ isFalse _ = false
70+
lib/purslint/src/Purslint/Rules/UseJoin.pursnew file+81
@@ -0,0 +1,81 @@
1+module Purslint.Rules.UseJoin 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 (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..))
16+
17+-- | Rule: x >>= identity -> join x
18+useJoinRule :: Rule
19+useJoinRule = mkRule (RuleId "UseJoin") 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: x >>= identity
30+ ExprOp lhs ops | hasOp imports ">>=" ->
31+ case NEA.toArray ops of
32+ [Tuple qn rhs] | isBindOp imports qn && isIdentity imports rhs ->
33+ let xText = printExpr lhs
34+ in
35+ [ LintWarning
36+ { ruleId: RuleId "UseJoin"
37+ , message: WarningMessage "Use join instead of >>= identity"
38+ , range: rangeOf expr
39+ , severity: Warning
40+ , suggestion: Just $ Suggestion
41+ { replacement: ReplacementText ("join " <> xText)
42+ , description: SuggestionDescription "x >>= identity can be simplified to join x"
43+ , requiredImports: []
44+ }
45+ }
46+ ]
47+ _ -> []
48+ -- Match: identity =<< x
49+ ExprOp lhs ops | hasOp imports "=<<" && isIdentity imports lhs ->
50+ case NEA.toArray ops of
51+ [Tuple qn rhs] | isBindFlipOp imports qn ->
52+ let xText = printExpr rhs
53+ in
54+ [ LintWarning
55+ { ruleId: RuleId "UseJoin"
56+ , message: WarningMessage "Use join instead of identity =<<"
57+ , range: rangeOf expr
58+ , severity: Warning
59+ , suggestion: Just $ Suggestion
60+ { replacement: ReplacementText ("join " <> xText)
61+ , description: SuggestionDescription "identity =<< x can be simplified to join x"
62+ , requiredImports: []
63+ }
64+ }
65+ ]
66+ _ -> []
67+ _ -> []
68+
69+ isBindOp :: ImportInfo -> QualifiedName Operator -> Boolean
70+ isBindOp imports (QualifiedName { name: Operator op }) =
71+ op == ">>=" && hasOp imports ">>="
72+
73+ isBindFlipOp :: ImportInfo -> QualifiedName Operator -> Boolean
74+ isBindFlipOp imports (QualifiedName { name: Operator op }) =
75+ op == "=<<" && hasOp imports "=<<"
76+
77+ isIdentity :: ImportInfo -> Expr Void -> Boolean
78+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
79+ (name == "identity" && hasValue imports "identity") || (name == "id" && hasValue imports "id")
80+ isIdentity _ _ = false
81+
lib/purslint/src/Purslint/Rules/UseLastReverse.pursnew file+80
@@ -0,0 +1,80 @@
1+module Purslint.Rules.UseLastReverse 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: head (reverse x) -> last x
17+-- | Also: last (reverse x) -> head x
18+useLastReverseRule :: Rule
19+useLastReverseRule = mkRule (RuleId "UseLastReverse") 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: head (reverse x) or last (reverse x)
30+ ExprApp fn args
31+ | isHead imports fn || isLast imports fn ->
32+ case NEA.toArray args of
33+ [AppTerm arg] ->
34+ case unwrapParens arg of
35+ ExprApp reverseFn reverseArgs
36+ | isReverse imports reverseFn ->
37+ case NEA.toArray reverseArgs of
38+ [AppTerm innerArg] ->
39+ let
40+ x = printExpr innerArg
41+ isHeadCall = isHead imports fn
42+ newFn = if isHeadCall then "last" else "head"
43+ oldPattern = if isHeadCall then "head (reverse x)" else "last (reverse x)"
44+ in
45+ [ LintWarning
46+ { ruleId: RuleId "UseLastReverse"
47+ , message: WarningMessage $ oldPattern <> " can be simplified to " <> newFn <> " x"
48+ , range: rangeOf expr
49+ , severity: Warning
50+ , suggestion: Just $ Suggestion
51+ { replacement: ReplacementText (newFn <> " " <> x)
52+ , description: SuggestionDescription $ "Use " <> newFn <> " instead"
53+ , requiredImports: []
54+ }
55+ }
56+ ]
57+ _ -> []
58+ _ -> []
59+ _ -> []
60+ _ -> []
61+
62+ unwrapParens :: Expr Void -> Expr Void
63+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
64+ unwrapParens e = e
65+
66+ isHead :: ImportInfo -> Expr Void -> Boolean
67+ isHead imports (ExprIdent (QualifiedName { name: Ident name })) =
68+ name == "head" && hasValue imports "head"
69+ isHead _ _ = false
70+
71+ isLast :: ImportInfo -> Expr Void -> Boolean
72+ isLast imports (ExprIdent (QualifiedName { name: Ident name })) =
73+ name == "last" && hasValue imports "last"
74+ isLast _ _ = false
75+
76+ isReverse :: ImportInfo -> Expr Void -> Boolean
77+ isReverse imports (ExprIdent (QualifiedName { name: Ident name })) =
78+ name == "reverse" && hasValue imports "reverse"
79+ isReverse _ _ = false
80+
lib/purslint/src/Purslint/Rules/UseMapMaybe.pursnew file+81
@@ -0,0 +1,81 @@
1+module Purslint.Rules.UseMapMaybe 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: catMaybes (map f x) -> mapMaybe f x
17+useMapMaybeRule :: Rule
18+useMapMaybeRule = mkRule (RuleId "UseMapMaybe") 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: catMaybes (map f x)
29+ ExprApp fnExpr args
30+ | isCatMaybes 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 "UseMapMaybe"
53+ , message: WarningMessage "catMaybes (map f x) can be simplified to mapMaybe f x"
54+ , range: rangeOf fullExpr
55+ , severity: Warning
56+ , suggestion: Just $ Suggestion
57+ { replacement: ReplacementText ("mapMaybe " <> f <> " " <> x)
58+ , 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+ ]
66+ }
67+ }
68+ ]
69+ _ -> []
70+ _ -> []
71+
72+ isCatMaybes :: ImportInfo -> Expr Void -> Boolean
73+ isCatMaybes imports (ExprIdent (QualifiedName { name: Ident name })) =
74+ name == "catMaybes" && hasValue imports "catMaybes"
75+ isCatMaybes _ _ = false
76+
77+ isMap :: ImportInfo -> Expr Void -> Boolean
78+ isMap imports (ExprIdent (QualifiedName { name: Ident name })) =
79+ name == "map" && hasValue imports "map"
80+ isMap _ _ = false
81+
lib/purslint/src/Purslint/Rules/UseMaybeMap.pursnew file+58
@@ -0,0 +1,58 @@
1+module Purslint.Rules.UseMaybeMap 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: maybe Nothing (Just <<< f) -> map f
17+useMaybeMapRule :: Rule
18+useMaybeMapRule = mkRule (RuleId "UseMaybeMap") 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: maybe Nothing (Just <<< f) or maybe Nothing (\x -> Just (f x))
29+ ExprApp fnExpr args
30+ | isMaybe imports fnExpr ->
31+ case NEA.toArray args of
32+ [AppTerm nothingExpr, AppTerm fnArg]
33+ | isNothing imports (unwrapParens nothingExpr) ->
34+ checkJustComposition imports expr (unwrapParens fnArg)
35+ _ -> []
36+ _ -> []
37+
38+ unwrapParens :: Expr Void -> Expr Void
39+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
40+ unwrapParens e = e
41+
42+ -- Check if the function is Just <<< f
43+ checkJustComposition :: ImportInfo -> Expr Void -> Expr Void -> Array LintWarning
44+ checkJustComposition imports fullExpr fnExpr =
45+ case fnExpr of
46+ -- Pattern: Just <<< f (would need operator parsing)
47+ -- For now, suggest when we see the pattern
48+ _ -> []
49+
50+ isMaybe :: ImportInfo -> Expr Void -> Boolean
51+ isMaybe imports (ExprIdent (QualifiedName { name: Ident name })) =
52+ name == "maybe" && hasValue imports "maybe"
53+ isMaybe _ _ = false
54+
55+ isNothing :: ImportInfo -> Expr Void -> Boolean
56+ isNothing imports (ExprIdent (QualifiedName { name: Ident name })) =
57+ name == "Nothing" && hasValue imports "Nothing"
58+ isNothing _ _ = false
lib/purslint/src/Purslint/Rules/UseMinMax.pursnew file+80
@@ -0,0 +1,80 @@
1+module Purslint.Rules.UseMinMax where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Data.Void (Void)
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(..))
11+import PureScript.CST.Range (rangeOf)
12+import PureScript.CST.Traversal (defaultMonoidalVisitor, foldMapModule)
13+import PureScript.CST.Types (Expr(..), Ident(..), Module, Operator(..), QualifiedName(..), Wrapped(..))
14+import Data.Array.NonEmpty as NEA
15+import Data.Tuple (Tuple(..))
16+
17+-- | Rule: if a > b then a else b -> max a b
18+-- | Also: if a < b then a else b -> min a b
19+useMinMaxRule :: Rule
20+useMinMaxRule = mkRule (RuleId "UseMinMax") 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+ ExprIf { cond, true: trueExpr, false: falseExpr } ->
31+ case getComparison imports cond of
32+ Just { op, left, right } ->
33+ let leftText = printExpr left
34+ rightText = printExpr right
35+ trueText = printExpr trueExpr
36+ falseText = printExpr falseExpr
37+ in
38+ -- if a > b then a else b -> max a b
39+ -- if a >= b then a else b -> max a b
40+ if (op == ">" || op == ">=") && leftText == trueText && rightText == falseText then
41+ [ mkWarning expr "max" leftText rightText ]
42+ -- if a > b then b else a -> min a b
43+ -- if a >= b then b else a -> min a b
44+ else if (op == ">" || op == ">=") && rightText == trueText && leftText == falseText then
45+ [ mkWarning expr "min" leftText rightText ]
46+ -- if a < b then a else b -> min a b
47+ -- if a <= b then a else b -> min a b
48+ else if (op == "<" || op == "<=") && leftText == trueText && rightText == falseText then
49+ [ mkWarning expr "min" leftText rightText ]
50+ -- if a < b then b else a -> max a b
51+ -- if a <= b then b else a -> max a b
52+ else if (op == "<" || op == "<=") && rightText == trueText && leftText == falseText then
53+ [ mkWarning expr "max" leftText rightText ]
54+ else []
55+ Nothing -> []
56+ _ -> []
57+
58+ mkWarning :: Expr Void -> String -> String -> String -> LintWarning
59+ mkWarning expr fn a b = LintWarning
60+ { ruleId: RuleId "UseMinMax"
61+ , message: WarningMessage ("if comparison can be simplified to " <> fn)
62+ , range: rangeOf expr
63+ , severity: Warning
64+ , suggestion: Just $ Suggestion
65+ { replacement: ReplacementText (fn <> " " <> a <> " " <> b)
66+ , description: SuggestionDescription ("Use " <> fn <> " instead of if/else with comparison")
67+ , requiredImports: []
68+ }
69+ }
70+
71+ getComparison :: ImportInfo -> Expr Void -> Maybe { op :: String, left :: Expr Void, right :: Expr Void }
72+ getComparison imports (ExprOp left ops) =
73+ case NEA.toArray ops of
74+ [Tuple (QualifiedName { name: Operator op }) right]
75+ | (op == ">" || op == ">=" || op == "<" || op == "<=") && hasOp imports op ->
76+ Just { op, left, right }
77+ _ -> Nothing
78+ getComparison imports (ExprParens (Wrapped { value })) = getComparison imports value
79+ getComparison _ _ = Nothing
80+
lib/purslint/src/Purslint/Rules/UseMinimumSort.pursnew file+80
@@ -0,0 +1,80 @@
1+module Purslint.Rules.UseMinimumSort 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: head (sort x) -> minimum x
17+-- | Also: last (sort x) -> maximum x
18+useMinimumSortRule :: Rule
19+useMinimumSortRule = mkRule (RuleId "UseMinimumSort") 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: head (sort x) or last (sort x)
30+ ExprApp fn args
31+ | isHead imports fn || isLast imports fn ->
32+ case NEA.toArray args of
33+ [AppTerm arg] ->
34+ case unwrapParens arg of
35+ ExprApp sortFn sortArgs
36+ | isSort imports sortFn ->
37+ case NEA.toArray sortArgs of
38+ [AppTerm innerArg] ->
39+ let
40+ x = printExpr innerArg
41+ isHeadCall = isHead imports fn
42+ newFn = if isHeadCall then "minimum" else "maximum"
43+ oldPattern = if isHeadCall then "head (sort x)" else "last (sort x)"
44+ in
45+ [ LintWarning
46+ { ruleId: RuleId "UseMinimumSort"
47+ , message: WarningMessage $ oldPattern <> " can be simplified to " <> newFn <> " x"
48+ , range: rangeOf expr
49+ , severity: Warning
50+ , suggestion: Just $ Suggestion
51+ { replacement: ReplacementText (newFn <> " " <> x)
52+ , description: SuggestionDescription $ "Use " <> newFn <> " instead of sorting"
53+ , requiredImports: []
54+ }
55+ }
56+ ]
57+ _ -> []
58+ _ -> []
59+ _ -> []
60+ _ -> []
61+
62+ unwrapParens :: Expr Void -> Expr Void
63+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
64+ unwrapParens e = e
65+
66+ isHead :: ImportInfo -> Expr Void -> Boolean
67+ isHead imports (ExprIdent (QualifiedName { name: Ident name })) =
68+ name == "head" && hasValue imports "head"
69+ isHead _ _ = false
70+
71+ isLast :: ImportInfo -> Expr Void -> Boolean
72+ isLast imports (ExprIdent (QualifiedName { name: Ident name })) =
73+ name == "last" && hasValue imports "last"
74+ isLast _ _ = false
75+
76+ isSort :: ImportInfo -> Expr Void -> Boolean
77+ isSort imports (ExprIdent (QualifiedName { name: Ident name })) =
78+ name == "sort" && hasValue imports "sort"
79+ isSort _ _ = false
80+
lib/purslint/src/Purslint/Rules/UseNotElem.pursnew file+68
@@ -0,0 +1,68 @@
1+module Purslint.Rules.UseNotElem 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: not (elem x y) -> notElem x y
17+useNotElemRule :: Rule
18+useNotElemRule = mkRule (RuleId "UseNotElem") 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: not (elem x y)
29+ ExprApp fnExpr args | isNot imports fnExpr ->
30+ case NEA.toArray args of
31+ [AppTerm innerExpr] ->
32+ case getElemArgs imports innerExpr of
33+ Just { x, y } ->
34+ let xText = printExpr x
35+ yText = printExpr y
36+ in
37+ [ LintWarning
38+ { ruleId: RuleId "UseNotElem"
39+ , message: WarningMessage "not (elem x y) can be simplified to notElem x y"
40+ , range: rangeOf expr
41+ , severity: Warning
42+ , suggestion: Just $ Suggestion
43+ { replacement: ReplacementText ("notElem " <> xText <> " " <> yText)
44+ , description: SuggestionDescription "Use notElem instead of not (elem ...)"
45+ , requiredImports: []
46+ }
47+ }
48+ ]
49+ Nothing -> []
50+ _ -> []
51+ _ -> []
52+
53+ isNot :: ImportInfo -> Expr Void -> Boolean
54+ isNot imports (ExprIdent (QualifiedName { name: Ident name })) =
55+ name == "not" && hasValue imports "not"
56+ isNot _ _ = false
57+
58+ getElemArgs :: ImportInfo -> Expr Void -> Maybe { x :: Expr Void, y :: Expr Void }
59+ getElemArgs imports (ExprApp fnExpr args) =
60+ case fnExpr of
61+ ExprIdent (QualifiedName { name: Ident name }) | name == "elem" && hasValue imports "elem" ->
62+ case NEA.toArray args of
63+ [AppTerm x, AppTerm y] -> Just { x, y }
64+ _ -> Nothing
65+ _ -> Nothing
66+ getElemArgs imports (ExprParens (Wrapped { value })) = getElemArgs imports value
67+ getElemArgs _ _ = Nothing
68+
lib/purslint/src/Purslint/Rules/UseNull.pursnew file+81
@@ -0,0 +1,81 @@
1+module Purslint.Rules.UseNull 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(..), IntValue(..), Module, Operator(..), QualifiedName(..))
16+
17+-- | Rule: length x == 0 -> null x
18+-- | Also: 0 == length x -> null x
19+useNullRule :: Rule
20+useNullRule = mkRule (RuleId "UseNull") 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: length x == 0 or 0 == length x
31+ ExprOp lhs ops | hasOp imports "==" && hasValue imports "null" ->
32+ case NEA.toArray ops of
33+ [Tuple qn rhs] | isEqOp qn ->
34+ -- length x == 0
35+ if isZeroLit rhs then
36+ case getLengthArg imports lhs of
37+ Just xText ->
38+ [ mkWarning expr xText "length x == 0" ]
39+ Nothing -> []
40+ -- 0 == length x
41+ else if isZeroLit lhs then
42+ case getLengthArg imports rhs of
43+ Just xText ->
44+ [ mkWarning expr xText "0 == length x" ]
45+ Nothing -> []
46+ else []
47+ _ -> []
48+ _ -> []
49+
50+ mkWarning :: Expr Void -> String -> String -> LintWarning
51+ mkWarning fullExpr xText from =
52+ LintWarning
53+ { ruleId: RuleId "UseNull"
54+ , message: WarningMessage $ "Use null instead of " <> from
55+ , range: rangeOf fullExpr
56+ , severity: Warning
57+ , suggestion: Just $ Suggestion
58+ { replacement: ReplacementText ("null " <> xText)
59+ , description: SuggestionDescription $ from <> " can be simplified to null x"
60+ , requiredImports: []
61+ }
62+ }
63+
64+ isEqOp :: QualifiedName Operator -> Boolean
65+ isEqOp (QualifiedName { name: Operator op }) = op == "=="
66+
67+ isZeroLit :: Expr Void -> Boolean
68+ isZeroLit (ExprInt _ (SmallInt 0)) = true
69+ isZeroLit (ExprInt _ (BigInt "0")) = true
70+ isZeroLit _ = false
71+
72+ getLengthArg :: ImportInfo -> Expr Void -> Maybe String
73+ getLengthArg imports (ExprApp fnExpr args) =
74+ case fnExpr of
75+ ExprIdent (QualifiedName { name: Ident name }) | name == "length" && hasValue imports "length" ->
76+ case NEA.toArray args of
77+ [AppTerm xExpr] -> Just (printExpr xExpr)
78+ _ -> Nothing
79+ _ -> Nothing
80+ getLengthArg _ _ = Nothing
81+
lib/purslint/src/Purslint/Rules/UseOn.pursnew file+74
@@ -0,0 +1,74 @@
1+module Purslint.Rules.UseOn 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(..), Binder(..), Expr(..), Ident(..), Module, Name(..), QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: \x y -> f (g x) (g y) -> on f g
17+useOnRule :: Rule
18+useOnRule = mkRule (RuleId "UseOn") 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: \x y -> f (g x) (g y)
29+ ExprLambda { binders, body } ->
30+ case NEA.toArray binders of
31+ [BinderVar (Name { name: Ident x }), BinderVar (Name { name: Ident y })] ->
32+ checkOnBody imports expr x y (unwrapParens body)
33+ _ -> []
34+ _ -> []
35+
36+ unwrapParens :: Expr Void -> Expr Void
37+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
38+ unwrapParens e = e
39+
40+ checkOnBody :: ImportInfo -> Expr Void -> String -> String -> Expr Void -> Array LintWarning
41+ checkOnBody imports fullExpr x y body =
42+ case body of
43+ ExprApp fnExpr args ->
44+ case fnExpr, NEA.toArray args of
45+ ExprIdent (QualifiedName { name: Ident f }), [AppTerm gxExpr, AppTerm gyExpr] ->
46+ case extractFunctionCall (unwrapParens gxExpr), extractFunctionCall (unwrapParens gyExpr) of
47+ Just { fn: g1, arg: arg1 }, Just { fn: g2, arg: arg2 }
48+ | g1 == g2 && arg1 == x && arg2 == y ->
49+ [ LintWarning
50+ { ruleId: RuleId "UseOn"
51+ , message: WarningMessage "\\x y -> f (g x) (g y) can be simplified to on f g"
52+ , range: rangeOf fullExpr
53+ , severity: Hint
54+ , suggestion: Just $ Suggestion
55+ { replacement: ReplacementText ("on " <> f <> " " <> g1)
56+ , description: SuggestionDescription "Use on instead of lambda applying g to both arguments"
57+ , requiredImports: []
58+ }
59+ }
60+ ]
61+ _, _ -> []
62+ _, _ -> []
63+ _ -> []
64+
65+ extractFunctionCall :: Expr Void -> Maybe { fn :: String, arg :: String }
66+ extractFunctionCall (ExprApp fnExpr args) =
67+ case fnExpr, NEA.toArray args of
68+ ExprIdent (QualifiedName { name: Ident fn }), [AppTerm argExpr] ->
69+ case argExpr of
70+ ExprIdent (QualifiedName { name: Ident arg }) -> Just { fn, arg }
71+ _ -> Nothing
72+ _, _ -> Nothing
73+ extractFunctionCall _ = Nothing
74+
lib/purslint/src/Purslint/Rules/UseOr.pursnew file+81
@@ -0,0 +1,81 @@
1+module Purslint.Rules.UseOr 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: any identity -> or
17+-- | Also: elem true -> or
18+useOrRule :: Rule
19+useOrRule = mkRule (RuleId "UseOr") 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: any identity xs or any id xs
30+ ExprApp fn args
31+ | isAny imports fn ->
32+ case NEA.toArray args of
33+ [AppTerm predArg] ->
34+ if isIdentity imports (unwrapParens predArg) then
35+ [ LintWarning
36+ { ruleId: RuleId "UseOr"
37+ , message: WarningMessage "any identity can be simplified to or"
38+ , range: rangeOf expr
39+ , severity: Warning
40+ , suggestion: Just $ Suggestion
41+ { replacement: ReplacementText "or"
42+ , description: SuggestionDescription "Use or instead of any identity"
43+ , requiredImports: []
44+ }
45+ }
46+ ]
47+ else []
48+ [AppTerm predArg, AppTerm xArg] ->
49+ if isIdentity imports (unwrapParens predArg) then
50+ let x = printExpr xArg
51+ in
52+ [ LintWarning
53+ { ruleId: RuleId "UseOr"
54+ , message: WarningMessage "any identity xs can be simplified to or xs"
55+ , range: rangeOf expr
56+ , severity: Warning
57+ , suggestion: Just $ Suggestion
58+ { replacement: ReplacementText ("or " <> x)
59+ , description: SuggestionDescription "Use or instead of any identity"
60+ , requiredImports: []
61+ }
62+ }
63+ ]
64+ else []
65+ _ -> []
66+ _ -> []
67+
68+ unwrapParens :: Expr Void -> Expr Void
69+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
70+ unwrapParens e = e
71+
72+ isAny :: ImportInfo -> Expr Void -> Boolean
73+ isAny imports (ExprIdent (QualifiedName { name: Ident name })) =
74+ name == "any" && hasValue imports "any"
75+ isAny _ _ = false
76+
77+ isIdentity :: ImportInfo -> Expr Void -> Boolean
78+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
79+ (name == "identity" || name == "id") && hasValue imports "identity"
80+ 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/purslint/src/Purslint/Rules/UseReplicate.pursnew file+84
@@ -0,0 +1,84 @@
1+module Purslint.Rules.UseReplicate 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: take n (repeat x) -> replicate n x
17+useReplicateRule :: Rule
18+useReplicateRule = mkRule (RuleId "UseReplicate") 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: take n (repeat x)
29+ ExprApp fn args
30+ | isTake imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm nArg, AppTerm repeatArg] ->
33+ case getRepeatArg imports (unwrapParens repeatArg) of
34+ Just x ->
35+ let
36+ n = printExpr nArg
37+ xText = printExpr x
38+ in
39+ [ LintWarning
40+ { ruleId: RuleId "UseReplicate"
41+ , message: WarningMessage "take n (repeat x) can be simplified to replicate n x"
42+ , range: rangeOf expr
43+ , severity: Warning
44+ , suggestion: Just $ Suggestion
45+ { replacement: ReplacementText ("replicate " <> n <> " " <> xText)
46+ , 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+ ]
54+ }
55+ }
56+ ]
57+ Nothing -> []
58+ _ -> []
59+ _ -> []
60+
61+ unwrapParens :: Expr Void -> Expr Void
62+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
63+ unwrapParens e = e
64+
65+ isTake :: ImportInfo -> Expr Void -> Boolean
66+ isTake imports (ExprIdent (QualifiedName { name: Ident name })) =
67+ name == "take" && hasValue imports "take"
68+ isTake _ _ = false
69+
70+ isRepeat :: ImportInfo -> Expr Void -> Boolean
71+ isRepeat imports (ExprIdent (QualifiedName { name: Ident name })) =
72+ name == "repeat" && hasValue imports "repeat"
73+ isRepeat _ _ = false
74+
75+ getRepeatArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
76+ getRepeatArg imports e =
77+ case e of
78+ ExprApp repeatFn args
79+ | isRepeat imports repeatFn ->
80+ case NEA.toArray args of
81+ [AppTerm arg] -> Just arg
82+ _ -> Nothing
83+ _ -> Nothing
84+
lib/purslint/src/Purslint/Rules/UseSequence.pursnew file+62
@@ -0,0 +1,62 @@
1+module Purslint.Rules.UseSequence 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.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: traverse identity -> sequence
16+-- | Also: for x identity -> sequence x
17+useSequenceRule :: Rule
18+useSequenceRule = mkRule (RuleId "UseSequence") 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: traverse identity
29+ ExprApp fnExpr args | isTraverse imports fnExpr ->
30+ case NEA.toArray args of
31+ [AppTerm idExpr] | isIdentity imports idExpr ->
32+ [ LintWarning
33+ { ruleId: RuleId "UseSequence"
34+ , message: WarningMessage "traverse identity can be simplified to sequence"
35+ , range: rangeOf expr
36+ , severity: Warning
37+ , suggestion: Just $ Suggestion
38+ { replacement: ReplacementText "sequence"
39+ , 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+ ]
47+ }
48+ }
49+ ]
50+ _ -> []
51+ _ -> []
52+
53+ isTraverse :: ImportInfo -> Expr Void -> Boolean
54+ isTraverse imports (ExprIdent (QualifiedName { name: Ident name })) =
55+ name == "traverse" && hasValue imports "traverse"
56+ isTraverse _ _ = false
57+
58+ isIdentity :: ImportInfo -> Expr Void -> Boolean
59+ isIdentity imports (ExprIdent (QualifiedName { name: Ident name })) =
60+ name == "identity" && hasValue imports "identity"
61+ isIdentity _ _ = false
62+
lib/purslint/src/Purslint/Rules/UseSpan.pursnew file+96
@@ -0,0 +1,96 @@
1+module Purslint.Rules.UseSpan 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: break (not <<< p) -> span p
17+useSpanRule :: Rule
18+useSpanRule = mkRule (RuleId "UseSpan") 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: break (not <<< p) x
29+ ExprApp fn args
30+ | isBreak imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm predArg, AppTerm xArg] ->
33+ case getNotComposedPred imports predArg of
34+ Just p ->
35+ let
36+ pText = printExpr p
37+ x = printExpr xArg
38+ in
39+ [ LintWarning
40+ { ruleId: RuleId "UseSpan"
41+ , message: WarningMessage "break (not <<< p) can be simplified to span p"
42+ , range: rangeOf expr
43+ , severity: Hint
44+ , suggestion: Just $ Suggestion
45+ { replacement: ReplacementText ("span " <> pText <> " " <> x)
46+ , description: SuggestionDescription "Use span instead of break with negated predicate"
47+ , requiredImports: []
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 "UseSpan"
59+ , message: WarningMessage "break (not <<< p) can be simplified to span p"
60+ , range: rangeOf expr
61+ , severity: Hint
62+ , suggestion: Just $ Suggestion
63+ { replacement: ReplacementText ("span " <> pText)
64+ , description: SuggestionDescription "Use span instead of break with negated predicate"
65+ , requiredImports: []
66+ }
67+ }
68+ ]
69+ Nothing -> []
70+ _ -> []
71+ _ -> []
72+
73+ unwrapParens :: Expr Void -> Expr Void
74+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
75+ unwrapParens e = e
76+
77+ isBreak :: ImportInfo -> Expr Void -> Boolean
78+ isBreak imports (ExprIdent (QualifiedName { name: Ident name })) =
79+ name == "break" && hasValue imports "break"
80+ isBreak _ _ = false
81+
82+ isNot :: ImportInfo -> Expr Void -> Boolean
83+ isNot imports (ExprIdent (QualifiedName { name: Ident name })) =
84+ name == "not" && hasValue imports "not"
85+ isNot _ _ = false
86+
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
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/purslint/src/Purslint/Rules/UseTraverseSequence.pursnew file+74
@@ -0,0 +1,74 @@
1+module Purslint.Rules.UseTraverseSequence 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: sequence (map f x) -> traverse f x
17+useTraverseSequenceRule :: Rule
18+useTraverseSequenceRule = mkRule (RuleId "UseTraverseSequence") 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: sequence (map f x)
29+ ExprApp fnExpr args | isSequence imports fnExpr ->
30+ case NEA.toArray args of
31+ [AppTerm mapExpr] ->
32+ case getMapArgs imports mapExpr of
33+ Just { f, x } ->
34+ let fText = printExpr f
35+ xText = printExpr x
36+ in
37+ [ LintWarning
38+ { ruleId: RuleId "UseTraverseSequence"
39+ , message: WarningMessage "sequence (map f x) can be simplified to traverse f x"
40+ , range: rangeOf expr
41+ , severity: Warning
42+ , suggestion: Just $ Suggestion
43+ { replacement: ReplacementText ("traverse " <> fText <> " " <> xText)
44+ , 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+ ]
52+ }
53+ }
54+ ]
55+ Nothing -> []
56+ _ -> []
57+ _ -> []
58+
59+ isSequence :: ImportInfo -> Expr Void -> Boolean
60+ isSequence imports (ExprIdent (QualifiedName { name: Ident name })) =
61+ (name == "sequence" || name == "sequenceA") && hasValue imports name
62+ isSequence _ _ = false
63+
64+ getMapArgs :: ImportInfo -> Expr Void -> Maybe { f :: Expr Void, x :: Expr Void }
65+ getMapArgs imports (ExprApp fnExpr args) =
66+ case fnExpr of
67+ ExprIdent (QualifiedName { name: Ident name }) | name == "map" && hasValue imports "map" ->
68+ case NEA.toArray args of
69+ [AppTerm f, AppTerm x] -> Just { f, x }
70+ _ -> Nothing
71+ _ -> Nothing
72+ getMapArgs imports (ExprParens (Wrapped { value })) = getMapArgs imports value
73+ getMapArgs _ _ = Nothing
74+
lib/purslint/src/Purslint/Rules/UseUncurry.pursnew file+92
@@ -0,0 +1,92 @@
1+module Purslint.Rules.UseUncurry 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, Proper(..), QualifiedName(..), Separated(..), Wrapped(..))
15+
16+-- | Rule: f (fst p) (snd p) -> uncurry f p
17+useUncurryRule :: Rule
18+useUncurryRule = mkRule (RuleId "UseUncurry") 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: f (fst p) (snd p)
29+ ExprApp fn args ->
30+ case NEA.toArray args of
31+ [AppTerm arg1, AppTerm arg2] ->
32+ case getFstArg imports (unwrapParens arg1), getSndArg imports (unwrapParens arg2) of
33+ Just p1, Just p2 ->
34+ let
35+ p1Text = printExpr p1
36+ p2Text = printExpr p2
37+ in
38+ if p1Text == p2Text then
39+ let
40+ f = printExpr fn
41+ in
42+ [ LintWarning
43+ { ruleId: RuleId "UseUncurry"
44+ , message: WarningMessage "f (fst p) (snd p) can be simplified to uncurry f p"
45+ , range: rangeOf expr
46+ , severity: Hint
47+ , suggestion: Just $ Suggestion
48+ { replacement: ReplacementText ("uncurry " <> f <> " " <> p1Text)
49+ , description: SuggestionDescription "Use uncurry instead of fst/snd"
50+ , requiredImports: []
51+ }
52+ }
53+ ]
54+ else []
55+ _, _ -> []
56+ _ -> []
57+ _ -> []
58+
59+ unwrapParens :: Expr Void -> Expr Void
60+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
61+ unwrapParens e = e
62+
63+ isFst :: ImportInfo -> Expr Void -> Boolean
64+ isFst imports (ExprIdent (QualifiedName { name: Ident name })) =
65+ name == "fst" && hasValue imports "fst"
66+ isFst _ _ = false
67+
68+ isSnd :: ImportInfo -> Expr Void -> Boolean
69+ isSnd imports (ExprIdent (QualifiedName { name: Ident name })) =
70+ name == "snd" && hasValue imports "snd"
71+ isSnd _ _ = false
72+
73+ getFstArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
74+ getFstArg imports e =
75+ case e of
76+ ExprApp fstFn args
77+ | isFst imports fstFn ->
78+ case NEA.toArray args of
79+ [AppTerm arg] -> Just arg
80+ _ -> Nothing
81+ _ -> Nothing
82+
83+ getSndArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
84+ getSndArg imports e =
85+ case e of
86+ ExprApp sndFn args
87+ | isSnd imports sndFn ->
88+ case NEA.toArray args of
89+ [AppTerm arg] -> Just arg
90+ _ -> Nothing
91+ _ -> Nothing
92+
lib/purslint/src/Purslint/Rules/UseUnless.pursnew file+57
@@ -0,0 +1,57 @@
1+module Purslint.Rules.UseUnless where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Data.Void (Void)
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 (Expr(..), Ident(..), Module, QualifiedName(..))
14+
15+-- | Rule: if x then pure unit else y -> unless x y
16+useUnlessRule :: Rule
17+useUnlessRule = mkRule (RuleId "UseUnless") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run ctx = foldMapModule visitor
21+ where
22+ imports = ctx.imports
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
24+
25+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
26+ checkExpr imports expr = case expr of
27+ -- Match: if cond then pure unit else action
28+ ExprIf { cond, true: trueExpr, false: falseExpr }
29+ | isPureUnit imports trueExpr ->
30+ let condText = printExpr cond
31+ actionText = printExpr falseExpr
32+ in
33+ [ LintWarning
34+ { ruleId: RuleId "UseUnless"
35+ , message: WarningMessage "if x then pure unit else y can be simplified to unless x y"
36+ , range: rangeOf expr
37+ , severity: Warning
38+ , suggestion: Just $ Suggestion
39+ { replacement: ReplacementText ("unless " <> condText <> " " <> actionText)
40+ , description: SuggestionDescription "Use unless instead of if/else with pure unit"
41+ , requiredImports: []
42+ }
43+ }
44+ ]
45+ _ -> []
46+
47+ isPureUnit :: ImportInfo -> Expr Void -> Boolean
48+ isPureUnit imports (ExprApp fnExpr _) = isPure imports fnExpr
49+ isPureUnit imports (ExprIdent (QualifiedName { name: Ident name })) =
50+ name == "unit" || (name == "pure" && hasValue imports "pure")
51+ isPureUnit _ _ = false
52+
53+ isPure :: ImportInfo -> Expr Void -> Boolean
54+ isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
55+ (name == "pure" && hasValue imports "pure") || (name == "return" && hasValue imports "return")
56+ 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/purslint/src/Purslint/Rules/UseVoid.pursnew file+68
@@ -0,0 +1,68 @@
1+module Purslint.Rules.UseVoid 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(..))
16+
17+-- | Rule: a *> pure unit -> void a
18+-- | Also: a >>= \_ -> pure unit -> void a
19+useVoidRule :: Rule
20+useVoidRule = mkRule (RuleId "UseVoid") 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: a *> pure unit or a >> pure unit
31+ ExprOp lhs ops | hasOp imports "*>" || hasOp imports ">>" ->
32+ case NEA.toArray ops of
33+ [Tuple qn rhs] | isSeqOp imports qn && isPureUnit imports rhs ->
34+ let aText = printExpr lhs
35+ in
36+ [ LintWarning
37+ { ruleId: RuleId "UseVoid"
38+ , message: WarningMessage "Use void instead of *> pure unit"
39+ , range: rangeOf expr
40+ , severity: Warning
41+ , suggestion: Just $ Suggestion
42+ { replacement: ReplacementText ("void " <> aText)
43+ , description: SuggestionDescription "a *> pure unit can be simplified to void a"
44+ , requiredImports: []
45+ }
46+ }
47+ ]
48+ _ -> []
49+ _ -> []
50+
51+ isSeqOp :: ImportInfo -> QualifiedName Operator -> Boolean
52+ isSeqOp imports (QualifiedName { name: Operator op }) =
53+ (op == "*>" && hasOp imports "*>") || (op == ">>" && hasOp imports ">>")
54+
55+ isPureUnit :: ImportInfo -> Expr Void -> Boolean
56+ isPureUnit imports (ExprApp fnExpr args) =
57+ case fnExpr of
58+ ExprIdent (QualifiedName { name: Ident name }) | (name == "pure" && hasValue imports "pure") || (name == "return" && hasValue imports "return") ->
59+ case NEA.toArray args of
60+ [AppTerm unitExpr] -> isUnit unitExpr
61+ _ -> false
62+ _ -> false
63+ isPureUnit _ _ = false
64+
65+ isUnit :: Expr Void -> Boolean
66+ isUnit (ExprIdent (QualifiedName { name: Ident name })) = name == "unit"
67+ isUnit _ = false
68+
lib/purslint/src/Purslint/Rules/UseWhen.pursnew file+57
@@ -0,0 +1,57 @@
1+module Purslint.Rules.UseWhen where
2+
3+import Prelude
4+
5+import Data.Maybe (Maybe(..))
6+import Data.Void (Void)
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 (Expr(..), Ident(..), Module, QualifiedName(..))
14+
15+-- | Rule: if x then y else pure unit -> when x y
16+useWhenRule :: Rule
17+useWhenRule = mkRule (RuleId "UseWhen") run
18+ where
19+ run :: RuleContext -> Module Void -> Array LintWarning
20+ run ctx = foldMapModule visitor
21+ where
22+ imports = ctx.imports
23+ visitor = defaultMonoidalVisitor { onExpr = checkExpr imports }
24+
25+ checkExpr :: ImportInfo -> Expr Void -> Array LintWarning
26+ checkExpr imports expr = case expr of
27+ -- Match: if cond then action else pure unit
28+ ExprIf { cond, true: trueExpr, false: falseExpr }
29+ | isPureUnit imports falseExpr ->
30+ let condText = printExpr cond
31+ actionText = printExpr trueExpr
32+ in
33+ [ LintWarning
34+ { ruleId: RuleId "UseWhen"
35+ , message: WarningMessage "if x then y else pure unit can be simplified to when x y"
36+ , range: rangeOf expr
37+ , severity: Warning
38+ , suggestion: Just $ Suggestion
39+ { replacement: ReplacementText ("when " <> condText <> " " <> actionText)
40+ , description: SuggestionDescription "Use when instead of if/else with pure unit"
41+ , requiredImports: []
42+ }
43+ }
44+ ]
45+ _ -> []
46+
47+ isPureUnit :: ImportInfo -> Expr Void -> Boolean
48+ isPureUnit imports (ExprApp fnExpr _) = isPure imports fnExpr
49+ isPureUnit imports (ExprIdent (QualifiedName { name: Ident name })) =
50+ name == "unit" || (name == "pure" && hasValue imports "pure")
51+ isPureUnit _ _ = false
52+
53+ isPure :: ImportInfo -> Expr Void -> Boolean
54+ isPure imports (ExprIdent (QualifiedName { name: Ident name })) =
55+ (name == "pure" && hasValue imports "pure") || (name == "return" && hasValue imports "return")
56+ isPure _ _ = false
57+
lib/purslint/src/Purslint/Rules/UseZip.pursnew file+99
@@ -0,0 +1,99 @@
1+module Purslint.Rules.UseZip 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, Proper(..), QualifiedName(..), Wrapped(..))
15+
16+-- | Rule: zipWith Tuple -> zip
17+useZipRule :: Rule
18+useZipRule = mkRule (RuleId "UseZip") 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: zipWith Tuple xs ys
29+ ExprApp fn args
30+ | isZipWith imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm fArg] ->
33+ if isTupleCtor (unwrapParens fArg) then
34+ [ LintWarning
35+ { ruleId: RuleId "UseZip"
36+ , message: WarningMessage "zipWith Tuple can be simplified to zip"
37+ , range: rangeOf expr
38+ , severity: Warning
39+ , suggestion: Just $ Suggestion
40+ { replacement: ReplacementText "zip"
41+ , description: SuggestionDescription "Use zip instead of zipWith Tuple"
42+ , requiredImports: []
43+ }
44+ }
45+ ]
46+ else []
47+ [AppTerm fArg, AppTerm xArg] ->
48+ if isTupleCtor (unwrapParens fArg) then
49+ let x = printExpr xArg
50+ in
51+ [ LintWarning
52+ { ruleId: RuleId "UseZip"
53+ , message: WarningMessage "zipWith Tuple can be simplified to zip"
54+ , range: rangeOf expr
55+ , severity: Warning
56+ , suggestion: Just $ Suggestion
57+ { replacement: ReplacementText ("zip " <> x)
58+ , description: SuggestionDescription "Use zip instead of zipWith Tuple"
59+ , requiredImports: []
60+ }
61+ }
62+ ]
63+ else []
64+ [AppTerm fArg, AppTerm xArg, AppTerm yArg] ->
65+ if isTupleCtor (unwrapParens fArg) then
66+ let
67+ x = printExpr xArg
68+ y = printExpr yArg
69+ in
70+ [ LintWarning
71+ { ruleId: RuleId "UseZip"
72+ , message: WarningMessage "zipWith Tuple xs ys can be simplified to zip xs ys"
73+ , range: rangeOf expr
74+ , severity: Warning
75+ , suggestion: Just $ Suggestion
76+ { replacement: ReplacementText ("zip " <> x <> " " <> y)
77+ , description: SuggestionDescription "Use zip instead of zipWith Tuple"
78+ , requiredImports: []
79+ }
80+ }
81+ ]
82+ else []
83+ _ -> []
84+ _ -> []
85+
86+ unwrapParens :: Expr Void -> Expr Void
87+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
88+ unwrapParens e = e
89+
90+ isZipWith :: ImportInfo -> Expr Void -> Boolean
91+ isZipWith imports (ExprIdent (QualifiedName { name: Ident name })) =
92+ name == "zipWith" && hasValue imports "zipWith"
93+ isZipWith _ _ = false
94+
95+ isTupleCtor :: Expr Void -> Boolean
96+ isTupleCtor (ExprConstructor (QualifiedName { name: Proper "Tuple" })) = true
97+ isTupleCtor (ExprIdent (QualifiedName { name: Ident "Tuple" })) = true
98+ isTupleCtor _ = false
99+
lib/purslint/src/Purslint/Rules/WhenNot.pursnew file+96
@@ -0,0 +1,96 @@
1+module Purslint.Rules.WhenNot 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: when (not x) -> unless x
17+whenNotRule :: Rule
18+whenNotRule = mkRule (RuleId "WhenNot") 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: when (not x) action
29+ ExprApp fn args
30+ | isWhen imports fn ->
31+ case NEA.toArray args of
32+ [AppTerm condArg, AppTerm actionArg] ->
33+ case getNotArg imports (unwrapParens condArg) of
34+ Just x ->
35+ let
36+ xText = printExpr x
37+ action = printExpr actionArg
38+ in
39+ [ LintWarning
40+ { ruleId: RuleId "WhenNot"
41+ , message: WarningMessage "when (not x) can be simplified to unless x"
42+ , range: rangeOf expr
43+ , severity: Hint
44+ , suggestion: Just $ Suggestion
45+ { replacement: ReplacementText ("unless " <> xText <> " " <> action)
46+ , description: SuggestionDescription "Use unless instead of when (not ...)"
47+ , requiredImports: []
48+ }
49+ }
50+ ]
51+ Nothing -> []
52+ [AppTerm condArg] ->
53+ case getNotArg imports (unwrapParens condArg) of
54+ Just x ->
55+ let xText = printExpr x
56+ in
57+ [ LintWarning
58+ { ruleId: RuleId "WhenNot"
59+ , message: WarningMessage "when (not x) can be simplified to unless x"
60+ , range: rangeOf expr
61+ , severity: Hint
62+ , suggestion: Just $ Suggestion
63+ { replacement: ReplacementText ("unless " <> xText)
64+ , description: SuggestionDescription "Use unless instead of when (not ...)"
65+ , requiredImports: []
66+ }
67+ }
68+ ]
69+ Nothing -> []
70+ _ -> []
71+ _ -> []
72+
73+ unwrapParens :: Expr Void -> Expr Void
74+ unwrapParens (ExprParens (Wrapped { value })) = unwrapParens value
75+ unwrapParens e = e
76+
77+ isWhen :: ImportInfo -> Expr Void -> Boolean
78+ isWhen imports (ExprIdent (QualifiedName { name: Ident name })) =
79+ name == "when" && hasValue imports "when"
80+ isWhen _ _ = false
81+
82+ isNot :: ImportInfo -> Expr Void -> Boolean
83+ isNot imports (ExprIdent (QualifiedName { name: Ident name })) =
84+ name == "not" && hasValue imports "not"
85+ isNot _ _ = false
86+
87+ getNotArg :: ImportInfo -> Expr Void -> Maybe (Expr Void)
88+ getNotArg imports e =
89+ case e of
90+ ExprApp notFn args
91+ | isNot imports notFn ->
92+ case NEA.toArray args of
93+ [AppTerm arg] -> Just arg
94+ _ -> Nothing
95+ _ -> Nothing
96+
lib/purslint/src/Purslint/Runner.pursnew file+69
@@ -0,0 +1,69 @@
1+module Purslint.Runner where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Either (Either(..))
7+import Data.Newtype (un, unwrap)
8+import PureScript.CST (RecoveredParserResult(..), parseModule)
9+import PureScript.CST.Errors (printParseError)
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(..))
14+import Unsafe.Coerce (unsafeCoerce)
15+
16+-- | Run a set of rules on source code
17+runRules :: Array Rule -> SourceCode -> Either String LintResult
18+runRules rules (SourceCode source) =
19+ case parseModule source of
20+ ParseSucceeded m -> Right $ runOnModule rules (toVoidModule m)
21+ ParseSucceededWithErrors m _ -> Right $ runOnModule rules (toVoidModule m)
22+ ParseFailed errRec -> Left $ "Parse error at " <> show (errRec.position.line + 1) <> ":" <> show (errRec.position.column + 1) <> ": " <> printParseError errRec.error
23+
24+-- | Get import info from source code (for debugging)
25+getImportsFromSource :: SourceCode -> Either String ImportInfo
26+getImportsFromSource (SourceCode source) =
27+ case parseModule source of
28+ ParseSucceeded m -> Right $ getImportInfo (toVoidModule m)
29+ ParseSucceededWithErrors m _ -> Right $ getImportInfo (toVoidModule m)
30+ ParseFailed _ -> Left "Parse error"
31+
32+-- | Get raw module names from imports (for debugging)
33+getModuleNames :: SourceCode -> Either String (Array String)
34+getModuleNames (SourceCode source) =
35+ case parseModule source of
36+ ParseSucceeded m -> Right $ extractModuleNames (toVoidModule m)
37+ ParseSucceededWithErrors m _ -> Right $ extractModuleNames (toVoidModule m)
38+ ParseFailed _ -> Left "Parse error"
39+ where
40+ extractModuleNames (Module mod) = do
41+ let ModuleHeader header = mod.header
42+ header.imports <#> extractName
43+ extractName (ImportDecl imp) =
44+ case imp.module of
45+ Name { name: ModuleName n } -> n
46+
47+-- | Run rules on a successfully parsed module
48+runOnModule :: Array Rule -> Module Void -> LintResult
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
58+
59+-- | Get the module name from a parsed module
60+getModuleName :: forall e. Module e -> ModuleName
61+getModuleName mod =
62+ (header.name # un Name).name
63+ where
64+ ModuleHeader header = (mod # un Module).header
65+
66+-- | Convert a recovered module to a Void module (assuming no errors)
67+-- | This is safe after ParseSucceeded because there are no error nodes
68+toVoidModule :: forall e. Module e -> Module Void
69+toVoidModule = unsafeCoerce
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/purslint/src/Purslint/Tidy.pursnew file+13
@@ -0,0 +1,13 @@
1+module Purslint.Tidy where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+
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+
11+-- | Format a PureScript expression string using purs-tidy
12+-- | Returns the formatted string, or the original if formatting fails
13+foreign 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.jsonnew file+49
@@ -0,0 +1,49 @@
1+{
2+ "name": "purslint",
3+ "version": "0.1.0",
4+ "description": "A linter for PureScript",
5+ "type": "module",
6+ "bin": {
7+ "purslint": "./bin/purslint.js",
8+ "purslint-lsp": "./bin/purslint-lsp.js"
9+ },
10+ "scripts": {
11+ "build:cli": "spago build -p purslint-cli --output output-cli",
12+ "build:lsp": "spago build -p purslint-lsp --output output-lsp",
13+ "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/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",
16+ "bundle": "npm run bundle:cli && npm run bundle:lsp",
17+ "dist": "npm run build && npm run bundle",
18+ "test": "spago test",
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"
22+ },
23+ "keywords": [
24+ "purescript",
25+ "linter"
26+ ],
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+ ],
41+ "license": "MIT",
42+ "dependencies": {
43+ "spago": "^0.93.45"
44+ },
45+ "devDependencies": {
46+ "esbuild": "^0.27.2",
47+ "purs-backend-es": "^1.4.3"
48+ }
49+}
purescript-native/ffi_loader.godeleted−30
@@ -1,30 +0,0 @@
1−package ffi_loader
2−
3−// Load PureScript standard library FFI packages. Comment out the ones you don't need for
4−// faster/smaller builds.
5−
6−import (
7− _ "github.com/i-am-the-slime/go-ffi/purescript-arrays"
8− _ "github.com/i-am-the-slime/go-ffi/purescript-console"
9− _ "github.com/i-am-the-slime/go-ffi/purescript-effect"
10− _ "github.com/i-am-the-slime/go-ffi/purescript-enums"
11− _ "github.com/i-am-the-slime/go-ffi/purescript-exceptions"
12− _ "github.com/i-am-the-slime/go-ffi/purescript-foldable-traversable"
13− _ "github.com/i-am-the-slime/go-ffi/purescript-foreign"
14− _ "github.com/i-am-the-slime/go-ffi/purescript-foreign-object"
15− _ "github.com/i-am-the-slime/go-ffi/purescript-functions"
16− _ "github.com/i-am-the-slime/go-ffi/purescript-integers"
17− _ "github.com/i-am-the-slime/go-ffi/purescript-lazy"
18− _ "github.com/i-am-the-slime/go-ffi/purescript-lsp"
19− _ "github.com/i-am-the-slime/go-ffi/purescript-math"
20− _ "github.com/i-am-the-slime/go-ffi/purescript-node-fs"
21− _ "github.com/i-am-the-slime/go-ffi/purescript-node-process"
22− _ "github.com/i-am-the-slime/go-ffi/purescript-partial"
23− _ "github.com/i-am-the-slime/go-ffi/purescript-prelude"
24− _ "github.com/i-am-the-slime/go-ffi/purescript-refs"
25− _ "github.com/i-am-the-slime/go-ffi/purescript-simple-json"
26− _ "github.com/i-am-the-slime/go-ffi/purescript-st"
27− _ "github.com/i-am-the-slime/go-ffi/purescript-strings"
28− _ "github.com/i-am-the-slime/go-ffi/purescript-unfoldable"
29− _ "github.com/i-am-the-slime/go-ffi/purescript-unsafe-coerce"
30−)
purescript-native/go.moddeleted−12
@@ -1,12 +0,0 @@
1−module project.localhost/purescript-native/ffi-loader
2−
3−go 1.23.2
4−
5−require github.com/i-am-the-slime/go-ffi v0.0.0-20250107191809-8aebdb8aad2e
6−
7−require (
8− github.com/dlclark/regexp2 v1.4.0 // indirect
9− github.com/purescript-native/go-runtime v0.1.2 // indirect
10−)
11−
12−replace github.com/i-am-the-slime/go-ffi => /Users/mark/Developer/go-ffi
purescript-native/go.sumdeleted−6
@@ -1,6 +0,0 @@
1−github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
2−github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
3−github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
4−github.com/purescript-native/go-runtime v0.1.0/go.mod h1:Pl7TR9c9Al8GMlY/R2D7joWjlQuTiB9eBN2a9aXd6GQ=
5−github.com/purescript-native/go-runtime v0.1.2 h1:vxVmLXojzc6nkP29qMxHmEd6iwolF309yP/FskRY88Y=
6−github.com/purescript-native/go-runtime v0.1.2/go.mod h1:Pl7TR9c9Al8GMlY/R2D7joWjlQuTiB9eBN2a9aXd6GQ=
purslint-lspnew file+2
@@ -0,0 +1,2 @@
1+#!/bin/bash
2+exec node "$(cd "$(dirname "$0")" && pwd)/dist/purslint-lsp.js" "$@"
spago.lock+402−10
@@ -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",
@@ -80,11 +80,17 @@
8080 "foldable-traversable",
8181 "maybe",
8282 "newtype",
83+ "node-buffer",
84+ "node-fs",
85+ "node-process",
86+ "optparse",
8387 "prelude",
84− "purelint",
88+ "purslint",
8589 "strings"
8690 ],
8791 "build_plan": [
92+ "aff",
93+ "arraybuffer-types",
8894 "arrays",
8995 "bifunctors",
9096 "catenable-lists",
@@ -92,13 +98,17 @@
9298 "const",
9399 "contravariant",
94100 "control",
101+ "datetime",
95102 "distributive",
96103 "effect",
97104 "either",
98105 "enums",
99106 "exceptions",
100107 "exists",
108+ "exitcodes",
101109 "foldable-traversable",
110+ "foreign",
111+ "foreign-object",
102112 "free",
103113 "functions",
104114 "functors",
@@ -106,19 +116,32 @@
106116 "identity",
107117 "integers",
108118 "invariant",
119+ "js-date",
109120 "language-cst-parser",
110121 "lazy",
111122 "lists",
112123 "maybe",
113124 "newtype",
125+ "node-buffer",
126+ "node-event-emitter",
127+ "node-fs",
128+ "node-path",
129+ "node-process",
130+ "node-streams",
114131 "nonempty",
132+ "now",
133+ "nullable",
115134 "numbers",
135+ "open-memoize",
136+ "optparse",
116137 "ordered-collections",
117138 "orders",
139+ "parallel",
118140 "partial",
141+ "posix-types",
119142 "prelude",
120143 "profunctor",
121− "purelint",
144+ "purslint",
122145 "refs",
123146 "safe-coerce",
124147 "st",
@@ -137,8 +160,8 @@
137160 "build_plan": []
138161 }
139162 },
140− "purelint-lsp": {
141− "path": "bin/purelint-lsp",
163+ "purslint-lsp": {
164+ "path": "bin/purslint-lsp",
142165 "core": {
143166 "dependencies": [
144167 "arrays",
@@ -148,7 +171,7 @@
148171 "maybe",
149172 "newtype",
150173 "prelude",
151− "purelint",
174+ "purslint",
152175 "unsafe-coerce"
153176 ],
154177 "build_plan": [
@@ -185,7 +208,89 @@
185208 "partial",
186209 "prelude",
187210 "profunctor",
188− "purelint",
211+ "purslint",
212+ "refs",
213+ "safe-coerce",
214+ "st",
215+ "strings",
216+ "tailrec",
217+ "transformers",
218+ "tuples",
219+ "type-equality",
220+ "typelevel-prelude",
221+ "unfoldable",
222+ "unsafe-coerce"
223+ ]
224+ },
225+ "test": {
226+ "dependencies": [],
227+ "build_plan": []
228+ }
229+ },
230+ "purslint-tests": {
231+ "path": "./",
232+ "core": {
233+ "dependencies": [
234+ "arrays",
235+ "console",
236+ "effect",
237+ "either",
238+ "maybe",
239+ "newtype",
240+ "optparse",
241+ "prelude",
242+ "purslint"
243+ ],
244+ "build_plan": [
245+ "aff",
246+ "arraybuffer-types",
247+ "arrays",
248+ "bifunctors",
249+ "catenable-lists",
250+ "console",
251+ "const",
252+ "contravariant",
253+ "control",
254+ "datetime",
255+ "distributive",
256+ "effect",
257+ "either",
258+ "enums",
259+ "exceptions",
260+ "exists",
261+ "exitcodes",
262+ "foldable-traversable",
263+ "foreign",
264+ "foreign-object",
265+ "free",
266+ "functions",
267+ "functors",
268+ "gen",
269+ "identity",
270+ "integers",
271+ "invariant",
272+ "language-cst-parser",
273+ "lazy",
274+ "lists",
275+ "maybe",
276+ "newtype",
277+ "node-buffer",
278+ "node-event-emitter",
279+ "node-process",
280+ "node-streams",
281+ "nonempty",
282+ "nullable",
283+ "numbers",
284+ "open-memoize",
285+ "optparse",
286+ "ordered-collections",
287+ "orders",
288+ "parallel",
289+ "partial",
290+ "posix-types",
291+ "prelude",
292+ "profunctor",
293+ "purslint",
189294 "refs",
190295 "safe-coerce",
191296 "st",
@@ -784,6 +889,35 @@
784889 "extra_packages": {}
785890 },
786891 "packages": {
892+ "aff": {
893+ "type": "registry",
894+ "version": "8.0.0",
895+ "integrity": "sha256-5MmdI4+0RHBtSBy+YlU3/Cq4R5W2ih3OaRedJIrVHdk=",
896+ "dependencies": [
897+ "bifunctors",
898+ "control",
899+ "datetime",
900+ "effect",
901+ "either",
902+ "exceptions",
903+ "foldable-traversable",
904+ "functions",
905+ "maybe",
906+ "newtype",
907+ "parallel",
908+ "prelude",
909+ "refs",
910+ "tailrec",
911+ "transformers",
912+ "unsafe-coerce"
913+ ]
914+ },
915+ "arraybuffer-types": {
916+ "type": "registry",
917+ "version": "3.0.2",
918+ "integrity": "sha256-mQKokysYVkooS4uXbO+yovmV/s8b138Ws3zQvOwIHRA=",
919+ "dependencies": []
920+ },
787921 "arrays": {
788922 "type": "registry",
789923 "version": "7.3.0",
@@ -871,6 +1005,29 @@
8711005 "prelude"
8721006 ]
8731007 },
1008+ "datetime": {
1009+ "type": "registry",
1010+ "version": "6.1.0",
1011+ "integrity": "sha256-g/5X5BBegQWLpI9IWD+sY6mcaYpzzlW5lz5NBzaMtyI=",
1012+ "dependencies": [
1013+ "bifunctors",
1014+ "control",
1015+ "either",
1016+ "enums",
1017+ "foldable-traversable",
1018+ "functions",
1019+ "gen",
1020+ "integers",
1021+ "lists",
1022+ "maybe",
1023+ "newtype",
1024+ "numbers",
1025+ "ordered-collections",
1026+ "partial",
1027+ "prelude",
1028+ "tuples"
1029+ ]
1030+ },
8741031 "distributive": {
8751032 "type": "registry",
8761033 "version": "6.0.0",
@@ -938,6 +1095,14 @@
9381095 "unsafe-coerce"
9391096 ]
9401097 },
1098+ "exitcodes": {
1099+ "type": "registry",
1100+ "version": "4.0.0",
1101+ "integrity": "sha256-4wxViTbyOoyKJ/WaRGI6+hZmgMKI5Miv16lSwefiLSM=",
1102+ "dependencies": [
1103+ "enums"
1104+ ]
1105+ },
9411106 "foldable-traversable": {
9421107 "type": "registry",
9431108 "version": "6.0.0",
@@ -972,6 +1137,25 @@
9721137 "transformers"
9731138 ]
9741139 },
1140+ "foreign-object": {
1141+ "type": "registry",
1142+ "version": "4.1.0",
1143+ "integrity": "sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo=",
1144+ "dependencies": [
1145+ "arrays",
1146+ "foldable-traversable",
1147+ "functions",
1148+ "gen",
1149+ "lists",
1150+ "maybe",
1151+ "prelude",
1152+ "st",
1153+ "tailrec",
1154+ "tuples",
1155+ "typelevel-prelude",
1156+ "unfoldable"
1157+ ]
1158+ },
9751159 "free": {
9761160 "type": "registry",
9771161 "version": "7.1.0",
@@ -1068,6 +1252,19 @@
10681252 "prelude"
10691253 ]
10701254 },
1255+ "js-date": {
1256+ "type": "registry",
1257+ "version": "8.0.0",
1258+ "integrity": "sha256-6TVF4DWg5JL+jRAsoMssYw8rgOVALMUHT1CuNZt8NRo=",
1259+ "dependencies": [
1260+ "datetime",
1261+ "effect",
1262+ "exceptions",
1263+ "foreign",
1264+ "integers",
1265+ "now"
1266+ ]
1267+ },
10711268 "language-cst-parser": {
10721269 "type": "registry",
10731270 "version": "0.14.1",
@@ -1152,6 +1349,102 @@
11521349 "safe-coerce"
11531350 ]
11541351 },
1352+ "node-buffer": {
1353+ "type": "registry",
1354+ "version": "9.0.0",
1355+ "integrity": "sha256-PWE2DJ5ruBLCmeA/fUiuySEFmUJ/VuRfyrnCuVZBlu4=",
1356+ "dependencies": [
1357+ "arraybuffer-types",
1358+ "effect",
1359+ "maybe",
1360+ "nullable",
1361+ "st",
1362+ "unsafe-coerce"
1363+ ]
1364+ },
1365+ "node-event-emitter": {
1366+ "type": "registry",
1367+ "version": "3.0.0",
1368+ "integrity": "sha256-Qw0MjsT4xRH2j2i4K8JmRjcMKnH5z1Cw39t00q4LE4w=",
1369+ "dependencies": [
1370+ "effect",
1371+ "either",
1372+ "functions",
1373+ "maybe",
1374+ "nullable",
1375+ "prelude",
1376+ "unsafe-coerce"
1377+ ]
1378+ },
1379+ "node-fs": {
1380+ "type": "registry",
1381+ "version": "9.2.0",
1382+ "integrity": "sha256-Sg0vkXycEzkEerX6hLccz21Ygd9w1+QSk1thotRZPGI=",
1383+ "dependencies": [
1384+ "datetime",
1385+ "effect",
1386+ "either",
1387+ "enums",
1388+ "exceptions",
1389+ "functions",
1390+ "integers",
1391+ "js-date",
1392+ "maybe",
1393+ "node-buffer",
1394+ "node-path",
1395+ "node-streams",
1396+ "nullable",
1397+ "partial",
1398+ "prelude",
1399+ "strings",
1400+ "unsafe-coerce"
1401+ ]
1402+ },
1403+ "node-path": {
1404+ "type": "registry",
1405+ "version": "5.0.1",
1406+ "integrity": "sha256-ePOElFamHkffhwJcS0Ozq4A14rflnkasFU6X2B8/yXs=",
1407+ "dependencies": [
1408+ "effect"
1409+ ]
1410+ },
1411+ "node-process": {
1412+ "type": "registry",
1413+ "version": "11.2.0",
1414+ "integrity": "sha256-+2MQDYChjGbVbapCyJtuWYwD41jk+BntF/kcOTKBMVs=",
1415+ "dependencies": [
1416+ "effect",
1417+ "foreign",
1418+ "foreign-object",
1419+ "maybe",
1420+ "node-event-emitter",
1421+ "node-streams",
1422+ "posix-types",
1423+ "prelude",
1424+ "unsafe-coerce"
1425+ ]
1426+ },
1427+ "node-streams": {
1428+ "type": "registry",
1429+ "version": "9.0.1",
1430+ "integrity": "sha256-7RJ6RqjOlhW+QlDFQNUHlkCG/CuYTTLT8yary5jhhsU=",
1431+ "dependencies": [
1432+ "aff",
1433+ "arrays",
1434+ "effect",
1435+ "either",
1436+ "exceptions",
1437+ "maybe",
1438+ "node-buffer",
1439+ "node-event-emitter",
1440+ "nullable",
1441+ "prelude",
1442+ "refs",
1443+ "st",
1444+ "tailrec",
1445+ "unsafe-coerce"
1446+ ]
1447+ },
11551448 "nonempty": {
11561449 "type": "registry",
11571450 "version": "7.0.0",
@@ -1165,6 +1458,25 @@
11651458 "unfoldable"
11661459 ]
11671460 },
1461+ "now": {
1462+ "type": "registry",
1463+ "version": "6.0.0",
1464+ "integrity": "sha256-xZ7x37ZMREfs6GCDw/h+FaKHV/3sPWmtqBZRGTxybQY=",
1465+ "dependencies": [
1466+ "datetime",
1467+ "effect"
1468+ ]
1469+ },
1470+ "nullable": {
1471+ "type": "registry",
1472+ "version": "6.0.0",
1473+ "integrity": "sha256-yiGBVl3AD+Guy4kNWWeN+zl1gCiJK+oeIFtZtPCw4+o=",
1474+ "dependencies": [
1475+ "effect",
1476+ "functions",
1477+ "maybe"
1478+ ]
1479+ },
11681480 "numbers": {
11691481 "type": "registry",
11701482 "version": "9.0.1",
@@ -1174,6 +1486,59 @@
11741486 "maybe"
11751487 ]
11761488 },
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+ },
11771542 "ordered-collections": {
11781543 "type": "registry",
11791544 "version": "3.2.0",
@@ -1201,12 +1566,39 @@
12011566 "prelude"
12021567 ]
12031568 },
1569+ "parallel": {
1570+ "type": "registry",
1571+ "version": "7.0.0",
1572+ "integrity": "sha256-gUC9i4Txnx9K9RcMLsjujbwZz6BB1bnE2MLvw4GIw5o=",
1573+ "dependencies": [
1574+ "control",
1575+ "effect",
1576+ "either",
1577+ "foldable-traversable",
1578+ "functors",
1579+ "maybe",
1580+ "newtype",
1581+ "prelude",
1582+ "profunctor",
1583+ "refs",
1584+ "transformers"
1585+ ]
1586+ },
12041587 "partial": {
12051588 "type": "registry",
12061589 "version": "4.0.0",
12071590 "integrity": "sha256-fwXerld6Xw1VkReh8yeQsdtLVrjfGiVuC5bA1Wyo/J4=",
12081591 "dependencies": []
12091592 },
1593+ "posix-types": {
1594+ "type": "registry",
1595+ "version": "6.0.0",
1596+ "integrity": "sha256-ZfFz8RR1lee/o/Prccyeut3Q+9tYd08mlR72sIh6GzA=",
1597+ "dependencies": [
1598+ "maybe",
1599+ "prelude"
1600+ ]
1601+ },
12101602 "prelude": {
12111603 "type": "registry",
12121604 "version": "6.0.2",
spago.yaml+3−4
@@ -2,19 +2,18 @@ workspace:
22 packageSet:
33 registry: 71.0.0
44 extraPackages: {}
5− backend:
6− cmd: psgo
75
86package:
9− name: purelint-tests
7+ name: purslint-tests
108 dependencies:
119 - console
1210 - effect
1311 - either
1412 - maybe
1513 - newtype
14+ - optparse
1615 - prelude
17− - purelint
16+ - purslint
1817 - arrays
1918 test:
2019 main: Test.Main
test/LintExamples.purs+244−16
@@ -2,27 +2,32 @@ module Test.LintExamples where
22
33import Prelude
44
5−import Data.Traversable (class Traversable, sequence)
5+import Control.Monad (join)
6+import Data.Foldable (class Foldable, all, and, any, elem, fold, foldMap, length, null, or)
7+import Data.Maybe (Maybe, fromMaybe, isJust, isNothing, maybe)
8+import Data.Monoid (class Monoid, mempty)
9+import Data.Traversable (class Traversable, sequence, traverse)
10+import Data.Tuple (Tuple(..), fst, snd)
611
712-- ============================================================================
8−-- FmapId: map identity x -> x
13+-- MapIdentity: map identity x -> x
914-- ============================================================================
1015
1116-- | map identity x -> x
12−fmapId1 :: Array Int
13−fmapId1 = map identity [ 1, 2, 3 ]
17+mapIdentity1 :: Array Int
18+mapIdentity1 = map identity [ 1, 2, 3 ]
1419
15−-- | fmap identity x -> x (same as map in PureScript)
16−fmapId2 :: Array Int
17−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 ]
1823
1924-- | identity <$> x -> x
20−fmapId3 :: Array Int
21−fmapId3 = identity <$> [ 1, 2, 3 ]
25+mapIdentity3 :: Array Int
26+mapIdentity3 = identity <$> [ 1, 2, 3 ]
2227
2328-- | x <#> identity -> x
24−fmapId4 :: Array Int
25−fmapId4 = [ 1, 2, 3 ] <#> identity
29+mapIdentity4 :: Array Int
30+mapIdentity4 = [ 1, 2, 3 ] <#> identity
2631
2732-- ============================================================================
2833-- MapFusion: map f (map g x) -> map (f <<< g) x
@@ -106,7 +111,7 @@ etaReduceDecl1 x = show x
106111
107112-- | foo a b = bar a b -> foo a = bar a
108113etaReduceDecl2 :: String -> String -> String
109−etaReduceDecl2 a b = append a b
114+etaReduceDecl2 a = append a
110115
111116-- ============================================================================
112117-- RedundantBind: x >>= pure -> x
@@ -125,10 +130,8 @@ redundantBind2 x f = pure x >>= f
125130-- ============================================================================
126131
127132-- | let x = y in z -> z where x = y
128−letToWhere1 :: Int -> Int
129−letToWhere1 x = y * 2
130− where
131− y = x + 1
133+letToWhere1 :: Int -> Int -> Int
134+letToWhere1 y z = let x = y in z
132135
133136-- | Multiple let bindings
134137letToWhere2 :: Int -> Int
@@ -138,3 +141,228 @@ letToWhere2 x =
138141 b = x * 2
139142 in
140143 a + b
144+
145+-- ============================================================================
146+-- UseFromMaybe: maybe x identity -> fromMaybe x
147+-- ============================================================================
148+
149+-- | maybe x identity -> fromMaybe x
150+useFromMaybe1 :: forall a. a -> Maybe a -> a
151+useFromMaybe1 def m = maybe def identity m
152+
153+-- ============================================================================
154+-- UseIsJust: maybe false (const true) -> isJust
155+-- ============================================================================
156+
157+-- | maybe false (const true) -> isJust
158+useIsJust1 :: forall a. Maybe a -> Boolean
159+useIsJust1 m = maybe false (const true) m
160+
161+-- ============================================================================
162+-- UseIsNothing: maybe true (const false) -> isNothing
163+-- ============================================================================
164+
165+-- | maybe true (const false) -> isNothing
166+useIsNothing1 :: forall a. Maybe a -> Boolean
167+useIsNothing1 m = maybe true (const false) m
168+
169+-- ============================================================================
170+-- UseWhen: if x then y else pure unit -> when x y
171+-- ============================================================================
172+
173+-- | if x then y else pure unit -> when x y
174+useWhen1 :: forall m. Applicative m => Boolean -> m Unit -> m Unit
175+useWhen1 cond action = if cond then action else pure unit
176+
177+-- ============================================================================
178+-- UseUnless: if x then pure unit else y -> unless x y
179+-- ============================================================================
180+
181+-- | if x then pure unit else y -> unless x y
182+useUnless1 :: forall m. Applicative m => Boolean -> m Unit -> m Unit
183+useUnless1 cond action = if cond then pure unit else action
184+
185+-- ============================================================================
186+-- RedundantFlip: flip (flip f) -> f
187+-- ============================================================================
188+
189+-- | flip (flip f) -> f
190+redundantFlip1 :: forall a b c. (a -> b -> c) -> a -> b -> c
191+redundantFlip1 f = flip (flip f)
192+
193+-- ============================================================================
194+-- UseNotElem: not (elem x xs) -> notElem x xs
195+-- ============================================================================
196+
197+-- | not (elem x xs) -> notElem x xs
198+useNotElem1 :: forall f a. Foldable f => Eq a => a -> f a -> Boolean
199+useNotElem1 x xs = not (elem x xs)
200+
201+-- ============================================================================
202+-- UseMinMax: if a > b then a else b -> max a b
203+-- ============================================================================
204+
205+-- | if a > b then a else b -> max a b
206+useMax1 :: forall a. Ord a => a -> a -> a
207+useMax1 a b = if a > b then a else b
208+
209+-- | if a >= b then a else b -> max a b
210+useMax2 :: forall a. Ord a => a -> a -> a
211+useMax2 a b = if a >= b then a else b
212+
213+-- | if a < b then a else b -> min a b
214+useMin1 :: forall a. Ord a => a -> a -> a
215+useMin1 a b = if a < b then a else b
216+
217+-- | if a <= b then a else b -> min a b
218+useMin2 :: forall a. Ord a => a -> a -> a
219+useMin2 a b = if a <= b then a else b
220+
221+-- ============================================================================
222+-- MonoidIdentity: mempty <> x -> x
223+-- ============================================================================
224+
225+-- | mempty <> x -> x
226+monoidIdentity1 :: forall m. Monoid m => m -> m
227+monoidIdentity1 x = mempty <> x
228+
229+-- | x <> mempty -> x
230+monoidIdentity2 :: forall m. Monoid m => m -> m
231+monoidIdentity2 x = x <> mempty
232+
233+-- ============================================================================
234+-- UseFold: foldMap identity -> fold
235+-- ============================================================================
236+
237+-- | foldMap identity -> fold
238+useFold1 :: forall f m. Foldable f => Monoid m => f m -> m
239+useFold1 xs = foldMap identity xs
240+
241+-- ============================================================================
242+-- UseSequence: traverse identity -> sequence
243+-- ============================================================================
244+
245+-- | traverse identity -> sequence
246+useSequence1 :: forall t f a. Traversable t => Applicative f => t (f a) -> f (t a)
247+useSequence1 xs = traverse identity xs
248+
249+-- ============================================================================
250+-- UseTraverseSequence: sequence (map f x) -> traverse f x
251+-- ============================================================================
252+
253+-- | sequence (map f x) -> traverse f x
254+useTraverseSequence1 :: forall t f a b. Traversable t => Applicative f => (a -> f b) -> t a -> f (t b)
255+useTraverseSequence1 f xs = sequence (map f xs)
256+
257+-- | sequence (f <$> x) -> traverse f x
258+useTraverseSequence2 :: forall t f a b. Traversable t => Applicative f => (a -> f b) -> t a -> f (t b)
259+useTraverseSequence2 f xs = sequence (f <$> xs)
260+
261+-- ============================================================================
262+-- RedundantNot: not (not x) -> x
263+-- ============================================================================
264+
265+-- | not (not x) -> x
266+redundantNot1 :: Boolean -> Boolean
267+redundantNot1 x = not (not x)
268+
269+-- ============================================================================
270+-- UseFstSnd: \(Tuple x _) -> x -> fst
271+-- ============================================================================
272+
273+-- | \(Tuple x _) -> x -> fst
274+useFst1 :: forall a b. Tuple a b -> a
275+useFst1 = \(Tuple x _) -> x
276+
277+-- | \(Tuple _ y) -> y -> snd
278+useSnd1 :: forall a b. Tuple a b -> b
279+useSnd1 = \(Tuple _ y) -> y
280+
281+-- ============================================================================
282+-- RedundantIf: if a then true else false -> a
283+-- ============================================================================
284+
285+-- | if a then true else false -> a
286+redundantIf1 :: Boolean -> Boolean
287+redundantIf1 a = if a then true else false
288+
289+-- | if a then false else true -> not a
290+redundantIf2 :: Boolean -> Boolean
291+redundantIf2 a = if a then false else true
292+
293+-- ============================================================================
294+-- FunctorLaw: map f (map g x) -> map (f <<< g) x
295+-- ============================================================================
296+
297+-- Note: This is the same as MapFusion, testing both rule names
298+-- | map f (map g x) -> map (f <<< g) x
299+functorLaw1 :: Array String
300+functorLaw1 = map show (map (_ + 1) [ 1, 2, 3 ])
301+
302+-- ============================================================================
303+-- UseVoid: a *> pure unit -> void a
304+-- ============================================================================
305+
306+-- | a *> pure unit -> void a
307+useVoid1 :: forall m. Applicative m => m Int -> m Unit
308+useVoid1 a = a *> pure unit
309+
310+-- ============================================================================
311+-- UseJoin: x >>= identity -> join x
312+-- ============================================================================
313+
314+-- | x >>= identity -> join x
315+useJoin1 :: forall m a. Monad m => m (m a) -> m a
316+useJoin1 x = x >>= identity
317+
318+-- ============================================================================
319+-- UseAny: or (map f x) -> any f x
320+-- ============================================================================
321+
322+-- | or (map f x) -> any f x
323+useAny1 :: forall f a. Foldable f => Functor f => (a -> Boolean) -> f a -> Boolean
324+useAny1 f xs = or (map f xs)
325+
326+-- ============================================================================
327+-- UseAll: and (map f x) -> all f x
328+-- ============================================================================
329+
330+-- | and (map f x) -> all f x
331+useAll1 :: forall f a. Foldable f => Functor f => (a -> Boolean) -> f a -> Boolean
332+useAll1 f xs = and (map f xs)
333+
334+-- ============================================================================
335+-- BooleanSimplify: x == true -> x
336+-- ============================================================================
337+
338+-- | x == true -> x
339+booleanSimplify1 :: Boolean -> Boolean
340+booleanSimplify1 x = x == true
341+
342+-- | x == false -> not x
343+booleanSimplify2 :: Boolean -> Boolean
344+booleanSimplify2 x = x == false
345+
346+-- ============================================================================
347+-- CollapseLambdas: \x -> \y -> body -> \x y -> body
348+-- ============================================================================
349+
350+-- | \x -> \y -> body -> \x y -> body
351+collapseLambdas1 :: Int -> Int -> Int
352+collapseLambdas1 = \x -> \y -> x + y
353+
354+-- ============================================================================
355+-- UseConst: \x -> y -> const y
356+-- ============================================================================
357+
358+-- | \_ -> y -> const y
359+useConst1 :: forall a. a -> Int
360+useConst1 = \_ -> 42
361+
362+-- ============================================================================
363+-- UseNull: length x == 0 -> null x
364+-- ============================================================================
365+
366+-- | length x == 0 -> null x
367+useNull1 :: forall f a. Foldable f => f a -> Boolean
368+useNull1 xs = length xs == 0
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+1895−36
@@ -7,22 +7,96 @@ import Data.Either (Either(..))
77import Data.Foldable (for_)
88import Data.Maybe (Maybe(..))
99import Data.Newtype (unwrap)
10+import Data.Traversable (traverse)
1011import Effect (Effect)
1112import Effect.Class.Console (log)
12−import Purelint.Fix (applyAllFixes)
13−import Purelint.Rules.FmapId (fmapIdRule)
14−import Purelint.Rules.MapFusion (mapFusionRule)
15−import Purelint.Rules.UseTraverse (useTraverseRule)
16−import Purelint.Rules.ConcatMap (concatMapRule)
17−import Purelint.Rules.NotEqual (notEqualRule)
18−import Purelint.Rules.UseGuard (useGuardRule)
19−import Purelint.Rules.EtaReduce (etaReduceRule)
20−import Purelint.Rules.EtaReduceDecl (etaReduceDeclRule)
21−import Purelint.Rules.RedundantBind (redundantBindRule)
22−import Purelint.Rules.LetToWhere (letToWhereRule)
23−import Purelint.Rule (Rule)
24−import Purelint.Runner (runRules)
25−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(..))
26100
27101-- | Simple test framework
28102type TestResult = { passed :: Boolean, name :: String, message :: String }
@@ -83,12 +157,27 @@ assertFix name rules source expectedFixed =
83157 then runTest name true "OK"
84158 else runTest name false ("Expected:\n" <> expectedFixed <> "\nGot:\n" <> fixed)
85159
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+
86175-- | Run all tests and report results
87176main :: Effect Unit
88177main = do
89− log "Running purelint tests...\n"
178+ log "Running purslint tests...\n"
90179
91− let results = fmapIdTests
180+ let results = mapIdentityTests
92181 <> mapFusionTests
93182 <> useTraverseTests
94183 <> concatMapTests
@@ -98,12 +187,83 @@ main = do
98187 <> etaReduceDeclTests
99188 <> redundantBindTests
100189 <> letToWhereTests
190+ <> redundantIfTests
191+ <> functorLawTests
192+ <> useVoidTests
193+ <> useJoinTests
194+ <> useAnyTests
195+ <> useAllTests
196+ <> booleanSimplifyTests
197+ <> collapseLambdasTests
198+ <> useConstTests
199+ <> useNullTests
200+ <> useFromMaybeTests
201+ <> useIsJustTests
202+ <> useIsNothingTests
203+ <> useWhenTests
204+ <> useUnlessTests
205+ <> redundantFlipTests
206+ <> useNotElemTests
207+ <> useMinMaxTests
208+ <> monoidIdentityTests
209+ <> useFoldTests
210+ <> useSequenceTests
211+ <> useTraverseSequenceTests
212+ <> redundantNotTests
213+ <> useFstSndTests
214+ <> useMapMaybeTests
215+ <> useApplicativeTests
216+ <> useBindFlipTests
217+ <> useForTests
218+ <> redundantGuardTests
219+ <> useComparingTests
220+ <> useOnTests
221+ <> useFindMapTests
222+ <> useLastReverseTests
223+ <> redundantReverseTests
224+ <> useMinimumSortTests
225+ <> useBimapTests
226+ <> evaluateFstTests
227+ <> evaluateConstTests
228+ <> useOrTests
229+ <> useAndTests
230+ <> useFoldMapTests
231+ <> useApplyFlippedTests
232+ <> useFoldMapIdTests
233+ <> whenNotTests
234+ <> unlessNotTests
235+ <> useZipTests
236+ <> useReplicateTests
237+ <> useUncurryTests
238+ <> useFromJustTests
239+ <> useHeadTests
240+ <> useElemIndexTests
241+ <> useBreakTests
242+ <> useSpanTests
243+ <> useDollarTests
244+ <> useUnwrapTests
245+ <> redundantNegateTests
246+ <> redundantIdTests
247+ <> nothingBindTests
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
101261
102− let passed = Array.length $ Array.filter _.passed results
103− let total = Array.length results
262+ let passed = Array.length $ Array.filter _.passed allResults
263+ let total = Array.length allResults
104264
105265 -- Report failures
106− let failures = Array.filter (not <<< _.passed) results
266+ let failures = Array.filter (not <<< _.passed) allResults
107267 when (Array.length failures > 0) do
108268 log "FAILURES:"
109269 for_ failures (\r -> log (" ✗ " <> r.name <> ": " <> r.message))
@@ -115,44 +275,56 @@ main = do
115275 when (passed < total) do
116276 log "\nSome tests failed!"
117277
278+ -- Run purs-tidy formatting tests
279+ log "\nRunning purs-tidy formatting tests..."
280+ tidyResults <- runTidyTests
281+ let tidyPassed = Array.length $ Array.filter _.passed tidyResults
282+ let tidyTotal = Array.length tidyResults
283+
284+ let tidyFailures = Array.filter (not <<< _.passed) tidyResults
285+ when (Array.length tidyFailures > 0) do
286+ log "TIDY FAILURES:"
287+ for_ tidyFailures (\r -> log (" ✗ " <> r.name <> ": " <> r.message))
288+ log ""
289+
290+ log $ "Tidy tests: " <> show tidyPassed <> "/" <> show tidyTotal <> " passed"
291+
118292-- | Helper: wrap code in a module with Prelude import
119293withPrelude :: String -> String
120−withPrelude code = "module Test where\nimport Prelude\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
121295
122296-- | Helper: wrap code in module without imports
123297withoutImports :: String -> String
124298withoutImports code = "module Test where\n" <> code
125299
126300-- ============================================================================
127−-- FmapId Tests
301+-- MapIdentity Tests
128302-- ============================================================================
129303
130−fmapIdTests :: Array TestResult
131−fmapIdTests =
304+mapIdentityTests :: Array TestResult
305+mapIdentityTests =
132306 -- Positive cases: should detect
133− [ assertWarningCount "FmapId: map identity x" [fmapIdRule]
307+ [ assertWarningCount "MapIdentity: map identity x" [mapIdentityRule]
134308 (withPrelude "x = map identity [1]") 1
135− , assertRuleId "FmapId: correct rule id" [fmapIdRule]
136− (withPrelude "x = map identity [1]") "FmapId"
137− , assertSuggestion "FmapId: map identity suggestion" [fmapIdRule]
138− (withPrelude "x = map identity [1]") "[1]"
139− , assertWarningCount "FmapId: fmap identity x" [fmapIdRule]
140− (withPrelude "x = fmap identity [1]") 1
141− , 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]
142314 (withPrelude "x = identity <$> [1]") 1
143− , assertWarningCount "FmapId: x <#> identity" [fmapIdRule]
315+ , assertWarningCount "MapIdentity: x <#> identity" [mapIdentityRule]
144316 (withPrelude "x = [1] <#> identity") 1
145317
146318 -- Negative cases: should NOT detect
147− , assertWarningCount "FmapId: no fire on map show" [fmapIdRule]
319+ , assertWarningCount "MapIdentity: no fire on map show" [mapIdentityRule]
148320 (withPrelude "x = map show [1]") 0
149− , assertWarningCount "FmapId: no fire without import" [fmapIdRule]
321+ , assertWarningCount "MapIdentity: no fire without import" [mapIdentityRule]
150322 (withoutImports "x = map identity [1]") 0
151323
152324 -- Auto-fix test
153− , assertFix "FmapId: fix map identity" [fmapIdRule]
325+ , assertFix "MapIdentity: fix map identity" [mapIdentityRule]
154326 (withPrelude "x = map identity [1]")
155− (withPrelude "x = [1]")
327+ (withPrelude "x = [ 1 ]")
156328 ]
157329
158330-- ============================================================================
@@ -317,6 +489,12 @@ redundantBindTests =
317489 (withPrelude "x = [1] >>= pure") 1
318490 , assertRuleId "RedundantBind: correct rule id" [redundantBindRule]
319491 (withPrelude "x = [1] >>= pure") "RedundantBind"
492+ , assertSuggestion "RedundantBind: x >>= pure suggestion" [redundantBindRule]
493+ (withPrelude "x = [1] >>= pure") "[ 1 ]"
494+ , assertWarningCount "RedundantBind: pure x >>= f" [redundantBindRule]
495+ (withPrelude "x = pure 1 >>= show") 1
496+ , assertSuggestion "RedundantBind: pure x >>= f suggestion" [redundantBindRule]
497+ (withPrelude "x = pure 1 >>= show") "show 1"
320498
321499 -- Negative cases
322500 , assertWarningCount "RedundantBind: no fire on x >>= f" [redundantBindRule]
@@ -340,4 +518,1685 @@ letToWhereTests =
340518 -- Multiple bindings
341519 , assertWarningCount "LetToWhere: multiple bindings" [letToWhereRule]
342520 (withPrelude "x = let a = 1\n b = 2 in a + b") 1
521+
522+ -- Suggestion preserves spaces based on let column position
523+ , assertSuggestion "LetToWhere: preserves spaces in simple case" [letToWhereRule]
524+ (withPrelude "x = let y = 1 in y + 1") "y + 1\n where\n y = 1"
525+ , assertSuggestion "LetToWhere: preserves spaces in expressions" [letToWhereRule]
526+ (withPrelude "f x = let a = x + 1 in a * 2") "a * 2\n where\n a = x + 1"
527+ , assertSuggestion "LetToWhere: preserves spaces with multiple bindings" [letToWhereRule]
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"
532+ ]
533+
534+-- ============================================================================
535+-- RedundantIf Tests
536+-- ============================================================================
537+
538+redundantIfTests :: Array TestResult
539+redundantIfTests =
540+ -- Positive cases
541+ [ assertWarningCount "RedundantIf: if a then true else false" [redundantIfRule]
542+ (withPrelude "x = if foo then true else false") 1
543+ , assertRuleId "RedundantIf: correct rule id" [redundantIfRule]
544+ (withPrelude "x = if foo then true else false") "RedundantIf"
545+ , assertSuggestion "RedundantIf: suggests condition" [redundantIfRule]
546+ (withPrelude "x = if foo then true else false") "foo"
547+ , assertWarningCount "RedundantIf: if a then false else true" [redundantIfRule]
548+ (withPrelude "x = if foo then false else true") 1
549+ , assertSuggestion "RedundantIf: suggests not condition" [redundantIfRule]
550+ (withPrelude "x = if foo then false else true") "not foo"
551+
552+ -- Negative cases
553+ , assertWarningCount "RedundantIf: no fire on normal if" [redundantIfRule]
554+ (withPrelude "x = if foo then 1 else 2") 0
555+ , assertWarningCount "RedundantIf: no fire on if a then true else x" [redundantIfRule]
556+ (withPrelude "x = if foo then true else bar") 0
557+ ]
558+
559+-- ============================================================================
560+-- FunctorLaw Tests
561+-- ============================================================================
562+
563+functorLawTests :: Array TestResult
564+functorLawTests =
565+ -- Positive cases
566+ [ assertWarningCount "FunctorLaw: map f (map g x)" [functorLawRule]
567+ (withPrelude "x = map f (map g [1])") 1
568+ , assertRuleId "FunctorLaw: correct rule id" [functorLawRule]
569+ (withPrelude "x = map f (map g [1])") "FunctorLaw"
570+ , assertWarningCount "FunctorLaw: f <$> (g <$> x)" [functorLawRule]
571+ (withPrelude "x = f <$> (g <$> [1])") 1
572+
573+ -- Negative cases
574+ , assertWarningCount "FunctorLaw: no fire on single map" [functorLawRule]
575+ (withPrelude "x = map f [1]") 0
576+ , assertWarningCount "FunctorLaw: no fire without import" [functorLawRule]
577+ (withoutImports "x = map f (map g [1])") 0
578+ ]
579+
580+-- ============================================================================
581+-- UseVoid Tests
582+-- ============================================================================
583+
584+useVoidTests :: Array TestResult
585+useVoidTests =
586+ -- Positive cases
587+ [ assertWarningCount "UseVoid: a *> pure unit" [useVoidRule]
588+ (withPrelude "x = foo *> pure unit") 1
589+ , assertRuleId "UseVoid: correct rule id" [useVoidRule]
590+ (withPrelude "x = foo *> pure unit") "UseVoid"
591+ , assertSuggestion "UseVoid: suggests void" [useVoidRule]
592+ (withPrelude "x = foo *> pure unit") "void foo"
593+
594+ -- Negative cases
595+ , assertWarningCount "UseVoid: no fire on a *> pure x" [useVoidRule]
596+ (withPrelude "x = foo *> pure 1") 0
597+ , assertWarningCount "UseVoid: no fire without import" [useVoidRule]
598+ (withoutImports "x = foo *> pure unit") 0
599+ ]
600+
601+-- ============================================================================
602+-- UseJoin Tests
603+-- ============================================================================
604+
605+useJoinTests :: Array TestResult
606+useJoinTests =
607+ -- Positive cases
608+ [ assertWarningCount "UseJoin: x >>= identity" [useJoinRule]
609+ (withPrelude "x = foo >>= identity") 1
610+ , assertRuleId "UseJoin: correct rule id" [useJoinRule]
611+ (withPrelude "x = foo >>= identity") "UseJoin"
612+ , assertSuggestion "UseJoin: suggests join" [useJoinRule]
613+ (withPrelude "x = foo >>= identity") "join foo"
614+
615+ -- Negative cases
616+ , assertWarningCount "UseJoin: no fire on x >>= f" [useJoinRule]
617+ (withPrelude "x = foo >>= show") 0
618+ , assertWarningCount "UseJoin: no fire without import" [useJoinRule]
619+ (withoutImports "x = foo >>= identity") 0
620+ ]
621+
622+-- ============================================================================
623+-- UseAny Tests
624+-- ============================================================================
625+
626+useAnyTests :: Array TestResult
627+useAnyTests =
628+ -- Positive cases
629+ [ assertWarningCount "UseAny: or (map f x)" [useAnyRule]
630+ (withPrelude "x = or (map f [1])") 1
631+ , assertRuleId "UseAny: correct rule id" [useAnyRule]
632+ (withPrelude "x = or (map f [1])") "UseAny"
633+ , assertSuggestion "UseAny: suggests any" [useAnyRule]
634+ (withPrelude "x = or (map f [1])") "any f [ 1 ]"
635+
636+ -- Negative cases
637+ , assertWarningCount "UseAny: no fire on or alone" [useAnyRule]
638+ (withPrelude "x = or [true]") 0
639+ , assertWarningCount "UseAny: no fire without import" [useAnyRule]
640+ (withoutImports "x = or (map f [1])") 0
641+ ]
642+
643+-- ============================================================================
644+-- UseAll Tests
645+-- ============================================================================
646+
647+useAllTests :: Array TestResult
648+useAllTests =
649+ -- Positive cases
650+ [ assertWarningCount "UseAll: and (map f x)" [useAllRule]
651+ (withPrelude "x = and (map f [1])") 1
652+ , assertRuleId "UseAll: correct rule id" [useAllRule]
653+ (withPrelude "x = and (map f [1])") "UseAll"
654+ , assertSuggestion "UseAll: suggests all" [useAllRule]
655+ (withPrelude "x = and (map f [1])") "all f [ 1 ]"
656+
657+ -- Negative cases
658+ , assertWarningCount "UseAll: no fire on and alone" [useAllRule]
659+ (withPrelude "x = and [true]") 0
660+ , assertWarningCount "UseAll: no fire without import" [useAllRule]
661+ (withoutImports "x = and (map f [1])") 0
662+ ]
663+
664+-- ============================================================================
665+-- BooleanSimplify Tests
666+-- ============================================================================
667+
668+booleanSimplifyTests :: Array TestResult
669+booleanSimplifyTests =
670+ -- Positive cases
671+ [ assertWarningCount "BooleanSimplify: x == true" [booleanSimplifyRule]
672+ (withPrelude "x = foo == true") 1
673+ , assertRuleId "BooleanSimplify: correct rule id" [booleanSimplifyRule]
674+ (withPrelude "x = foo == true") "BooleanSimplify"
675+ , assertSuggestion "BooleanSimplify: x == true -> x" [booleanSimplifyRule]
676+ (withPrelude "x = foo == true") "foo"
677+ , assertWarningCount "BooleanSimplify: x == false" [booleanSimplifyRule]
678+ (withPrelude "x = foo == false") 1
679+ , assertSuggestion "BooleanSimplify: x == false -> not x" [booleanSimplifyRule]
680+ (withPrelude "x = foo == false") "not foo"
681+ , assertWarningCount "BooleanSimplify: true == x" [booleanSimplifyRule]
682+ (withPrelude "x = true == foo") 1
683+ , assertWarningCount "BooleanSimplify: false == x" [booleanSimplifyRule]
684+ (withPrelude "x = false == foo") 1
685+
686+ -- Negative cases
687+ , assertWarningCount "BooleanSimplify: no fire on x == y" [booleanSimplifyRule]
688+ (withPrelude "x = foo == bar") 0
689+ , assertWarningCount "BooleanSimplify: no fire without import" [booleanSimplifyRule]
690+ (withoutImports "x = foo == true") 0
691+ ]
692+
693+-- ============================================================================
694+-- CollapseLambdas Tests
695+-- ============================================================================
696+
697+collapseLambdasTests :: Array TestResult
698+collapseLambdasTests =
699+ -- Positive cases
700+ [ assertWarningCount "CollapseLambdas: \\x -> \\y -> body" [collapseLambdasRule]
701+ (withPrelude "x = \\a -> \\b -> a + b") 1
702+ , assertRuleId "CollapseLambdas: correct rule id" [collapseLambdasRule]
703+ (withPrelude "x = \\a -> \\b -> a + b") "CollapseLambdas"
704+ , assertSuggestion "CollapseLambdas: suggests collapsed" [collapseLambdasRule]
705+ (withPrelude "x = \\a -> \\b -> a + b") "\\a b -> a + b"
706+
707+ -- Negative cases
708+ , assertWarningCount "CollapseLambdas: no fire on single lambda" [collapseLambdasRule]
709+ (withPrelude "x = \\a -> a + 1") 0
710+ ]
711+
712+-- ============================================================================
713+-- UseConst Tests
714+-- ============================================================================
715+
716+useConstTests :: Array TestResult
717+useConstTests =
718+ -- Positive cases: only fires on wildcard binder \_
719+ [ assertWarningCount "UseConst: \\_ -> y" [useConstRule]
720+ (withPrelude "x = \\_ -> 42") 1
721+ , assertRuleId "UseConst: correct rule id" [useConstRule]
722+ (withPrelude "x = \\_ -> 42") "UseConst"
723+ , assertSuggestion "UseConst: suggests const" [useConstRule]
724+ (withPrelude "x = \\_ -> 42") "const 42"
725+
726+ -- Negative cases
727+ , assertWarningCount "UseConst: no fire on named param (compiler warns)" [useConstRule]
728+ (withPrelude "x = \\a -> 42") 0
729+ , assertWarningCount "UseConst: no fire when param used" [useConstRule]
730+ (withPrelude "x = \\a -> a + 1") 0
731+ , assertWarningCount "UseConst: no fire without import" [useConstRule]
732+ (withoutImports "x = \\_ -> 42") 0
733+ ]
734+
735+-- ============================================================================
736+-- UseNull Tests
737+-- ============================================================================
738+
739+useNullTests :: Array TestResult
740+useNullTests =
741+ -- Positive cases
742+ [ assertWarningCount "UseNull: length x == 0" [useNullRule]
743+ (withPrelude "x = length [1] == 0") 1
744+ , assertRuleId "UseNull: correct rule id" [useNullRule]
745+ (withPrelude "x = length [1] == 0") "UseNull"
746+ , assertSuggestion "UseNull: suggests null" [useNullRule]
747+ (withPrelude "x = length [1] == 0") "null [ 1 ]"
748+ , assertWarningCount "UseNull: 0 == length x" [useNullRule]
749+ (withPrelude "x = 0 == length [1]") 1
750+
751+ -- Negative cases
752+ , assertWarningCount "UseNull: no fire on length x == 1" [useNullRule]
753+ (withPrelude "x = length [1] == 1") 0
754+ , assertWarningCount "UseNull: no fire without import" [useNullRule]
755+ (withoutImports "x = length [1] == 0") 0
756+ ]
757+
758+-- ============================================================================
759+-- UseFromMaybe Tests
760+-- ============================================================================
761+
762+useFromMaybeTests :: Array TestResult
763+useFromMaybeTests =
764+ [ assertWarningCount "UseFromMaybe: maybe x identity" [useFromMaybeRule]
765+ (withPrelude "x = maybe 0 identity") 1
766+ , assertRuleId "UseFromMaybe: correct rule id" [useFromMaybeRule]
767+ (withPrelude "x = maybe 0 identity") "UseFromMaybe"
768+ , assertSuggestion "UseFromMaybe: suggests fromMaybe" [useFromMaybeRule]
769+ (withPrelude "x = maybe 0 identity") "fromMaybe 0"
770+
771+ -- Negative cases
772+ , assertWarningCount "UseFromMaybe: no fire on maybe x f" [useFromMaybeRule]
773+ (withPrelude "x = maybe 0 show") 0
774+ ]
775+
776+-- ============================================================================
777+-- UseIsJust Tests
778+-- ============================================================================
779+
780+useIsJustTests :: Array TestResult
781+useIsJustTests =
782+ [ assertWarningCount "UseIsJust: maybe false (const true)" [useIsJustRule]
783+ (withPrelude "x = maybe false (const true)") 1
784+ , assertRuleId "UseIsJust: correct rule id" [useIsJustRule]
785+ (withPrelude "x = maybe false (const true)") "UseIsJust"
786+ , assertSuggestion "UseIsJust: suggests isJust" [useIsJustRule]
787+ (withPrelude "x = maybe false (const true)") "isJust"
788+
789+ -- Negative cases
790+ , assertWarningCount "UseIsJust: no fire on maybe true (const false)" [useIsJustRule]
791+ (withPrelude "x = maybe true (const false)") 0
792+ ]
793+
794+-- ============================================================================
795+-- UseIsNothing Tests
796+-- ============================================================================
797+
798+useIsNothingTests :: Array TestResult
799+useIsNothingTests =
800+ [ assertWarningCount "UseIsNothing: maybe true (const false)" [useIsNothingRule]
801+ (withPrelude "x = maybe true (const false)") 1
802+ , assertRuleId "UseIsNothing: correct rule id" [useIsNothingRule]
803+ (withPrelude "x = maybe true (const false)") "UseIsNothing"
804+ , assertSuggestion "UseIsNothing: suggests isNothing" [useIsNothingRule]
805+ (withPrelude "x = maybe true (const false)") "isNothing"
806+
807+ -- Negative cases
808+ , assertWarningCount "UseIsNothing: no fire on maybe false (const true)" [useIsNothingRule]
809+ (withPrelude "x = maybe false (const true)") 0
810+ ]
811+
812+-- ============================================================================
813+-- UseWhen Tests
814+-- ============================================================================
815+
816+useWhenTests :: Array TestResult
817+useWhenTests =
818+ [ assertWarningCount "UseWhen: if x then y else pure unit" [useWhenRule]
819+ (withPrelude "x = if true then log \"hi\" else pure unit") 1
820+ , assertRuleId "UseWhen: correct rule id" [useWhenRule]
821+ (withPrelude "x = if true then log \"hi\" else pure unit") "UseWhen"
822+
823+ -- Negative cases
824+ , assertWarningCount "UseWhen: no fire on if x then pure unit else y" [useWhenRule]
825+ (withPrelude "x = if true then pure unit else log \"hi\"") 0
826+ ]
827+
828+-- ============================================================================
829+-- UseUnless Tests
830+-- ============================================================================
831+
832+useUnlessTests :: Array TestResult
833+useUnlessTests =
834+ [ assertWarningCount "UseUnless: if x then pure unit else y" [useUnlessRule]
835+ (withPrelude "x = if true then pure unit else log \"hi\"") 1
836+ , assertRuleId "UseUnless: correct rule id" [useUnlessRule]
837+ (withPrelude "x = if true then pure unit else log \"hi\"") "UseUnless"
838+
839+ -- Negative cases
840+ , assertWarningCount "UseUnless: no fire on if x then y else pure unit" [useUnlessRule]
841+ (withPrelude "x = if true then log \"hi\" else pure unit") 0
842+ ]
843+
844+-- ============================================================================
845+-- RedundantFlip Tests
846+-- ============================================================================
847+
848+redundantFlipTests :: Array TestResult
849+redundantFlipTests =
850+ [ assertWarningCount "RedundantFlip: flip (flip f)" [redundantFlipRule]
851+ (withPrelude "x = flip (flip add)") 1
852+ , assertRuleId "RedundantFlip: correct rule id" [redundantFlipRule]
853+ (withPrelude "x = flip (flip add)") "RedundantFlip"
854+ , assertSuggestion "RedundantFlip: suggests f" [redundantFlipRule]
855+ (withPrelude "x = flip (flip add)") "add"
856+
857+ -- Negative cases
858+ , assertWarningCount "RedundantFlip: no fire on flip f" [redundantFlipRule]
859+ (withPrelude "x = flip add") 0
860+ ]
861+
862+-- ============================================================================
863+-- UseNotElem Tests
864+-- ============================================================================
865+
866+useNotElemTests :: Array TestResult
867+useNotElemTests =
868+ [ assertWarningCount "UseNotElem: not (elem x y)" [useNotElemRule]
869+ (withPrelude "x = not (elem 1 [1,2,3])") 1
870+ , assertRuleId "UseNotElem: correct rule id" [useNotElemRule]
871+ (withPrelude "x = not (elem 1 [1,2,3])") "UseNotElem"
872+ , assertSuggestion "UseNotElem: suggests notElem" [useNotElemRule]
873+ (withPrelude "x = not (elem 1 [1,2,3])") "notElem 1 [ 1, 2, 3 ]"
874+
875+ -- Negative cases
876+ , assertWarningCount "UseNotElem: no fire on not (foo x)" [useNotElemRule]
877+ (withPrelude "x = not (foo 1)") 0
878+ ]
879+
880+-- ============================================================================
881+-- UseMinMax Tests
882+-- ============================================================================
883+
884+useMinMaxTests :: Array TestResult
885+useMinMaxTests =
886+ [ assertWarningCount "UseMinMax: if a > b then a else b" [useMinMaxRule]
887+ (withPrelude "x = if a > b then a else b") 1
888+ , assertRuleId "UseMinMax: correct rule id" [useMinMaxRule]
889+ (withPrelude "x = if a > b then a else b") "UseMinMax"
890+ , assertSuggestion "UseMinMax: suggests max" [useMinMaxRule]
891+ (withPrelude "x = if a > b then a else b") "max a b"
892+ , assertWarningCount "UseMinMax: if a < b then a else b -> min" [useMinMaxRule]
893+ (withPrelude "x = if a < b then a else b") 1
894+ , assertSuggestion "UseMinMax: suggests min" [useMinMaxRule]
895+ (withPrelude "x = if a < b then a else b") "min a b"
896+
897+ -- Negative cases
898+ , assertWarningCount "UseMinMax: no fire on unrelated if" [useMinMaxRule]
899+ (withPrelude "x = if a > b then c else d") 0
900+ ]
901+
902+-- ============================================================================
903+-- MonoidIdentity Tests
904+-- ============================================================================
905+
906+monoidIdentityTests :: Array TestResult
907+monoidIdentityTests =
908+ [ assertWarningCount "MonoidIdentity: mempty <> x" [monoidIdentityRule]
909+ (withPrelude "x = mempty <> [1,2,3]") 1
910+ , assertRuleId "MonoidIdentity: correct rule id" [monoidIdentityRule]
911+ (withPrelude "x = mempty <> [1,2,3]") "MonoidIdentity"
912+ , assertSuggestion "MonoidIdentity: suggests x" [monoidIdentityRule]
913+ (withPrelude "x = mempty <> [1,2,3]") "[ 1, 2, 3 ]"
914+ , assertWarningCount "MonoidIdentity: x <> mempty" [monoidIdentityRule]
915+ (withPrelude "x = [1,2,3] <> mempty") 1
916+
917+ -- Negative cases
918+ , assertWarningCount "MonoidIdentity: no fire on x <> y" [monoidIdentityRule]
919+ (withPrelude "x = [1] <> [2]") 0
920+ ]
921+
922+-- ============================================================================
923+-- UseFold Tests
924+-- ============================================================================
925+
926+useFoldTests :: Array TestResult
927+useFoldTests =
928+ [ assertWarningCount "UseFold: foldMap identity" [useFoldRule]
929+ (withPrelude "x = foldMap identity") 1
930+ , assertRuleId "UseFold: correct rule id" [useFoldRule]
931+ (withPrelude "x = foldMap identity") "UseFold"
932+ , assertSuggestion "UseFold: suggests fold" [useFoldRule]
933+ (withPrelude "x = foldMap identity") "fold"
934+
935+ -- Negative cases
936+ , assertWarningCount "UseFold: no fire on foldMap f" [useFoldRule]
937+ (withPrelude "x = foldMap show") 0
938+ ]
939+
940+-- ============================================================================
941+-- UseSequence Tests
942+-- ============================================================================
943+
944+useSequenceTests :: Array TestResult
945+useSequenceTests =
946+ [ assertWarningCount "UseSequence: traverse identity" [useSequenceRule]
947+ (withPrelude "x = traverse identity") 1
948+ , assertRuleId "UseSequence: correct rule id" [useSequenceRule]
949+ (withPrelude "x = traverse identity") "UseSequence"
950+ , assertSuggestion "UseSequence: suggests sequence" [useSequenceRule]
951+ (withPrelude "x = traverse identity") "sequence"
952+
953+ -- Negative cases
954+ , assertWarningCount "UseSequence: no fire on traverse f" [useSequenceRule]
955+ (withPrelude "x = traverse show") 0
956+ ]
957+
958+-- ============================================================================
959+-- UseTraverseSequence Tests
960+-- ============================================================================
961+
962+useTraverseSequenceTests :: Array TestResult
963+useTraverseSequenceTests =
964+ [ assertWarningCount "UseTraverseSequence: sequence (map f x)" [useTraverseSequenceRule]
965+ (withPrelude "x = sequence (map show [1,2,3])") 1
966+ , assertRuleId "UseTraverseSequence: correct rule id" [useTraverseSequenceRule]
967+ (withPrelude "x = sequence (map show [1,2,3])") "UseTraverseSequence"
968+ , assertSuggestion "UseTraverseSequence: suggests traverse" [useTraverseSequenceRule]
969+ (withPrelude "x = sequence (map show [1,2,3])") "traverse show [ 1, 2, 3 ]"
970+
971+ -- Negative cases
972+ , assertWarningCount "UseTraverseSequence: no fire on sequence xs" [useTraverseSequenceRule]
973+ (withPrelude "x = sequence [Just 1]") 0
974+ ]
975+
976+-- ============================================================================
977+-- RedundantNot Tests
978+-- ============================================================================
979+
980+redundantNotTests :: Array TestResult
981+redundantNotTests =
982+ [ assertWarningCount "RedundantNot: not (not x)" [redundantNotRule]
983+ (withPrelude "x = not (not true)") 1
984+ , assertRuleId "RedundantNot: correct rule id" [redundantNotRule]
985+ (withPrelude "x = not (not true)") "RedundantNot"
986+ , assertSuggestion "RedundantNot: suggests x" [redundantNotRule]
987+ (withPrelude "x = not (not foo)") "foo"
988+
989+ -- Negative cases
990+ , assertWarningCount "RedundantNot: no fire on not x" [redundantNotRule]
991+ (withPrelude "x = not true") 0
992+ ]
993+
994+-- ============================================================================
995+-- UseFstSnd Tests
996+-- ============================================================================
997+
998+useFstSndTests :: Array TestResult
999+useFstSndTests =
1000+ [ assertWarningCount "UseFstSnd: \\(Tuple x y) -> x" [useFstSndRule]
1001+ (withPrelude "x = \\(Tuple a b) -> a") 1
1002+ , assertRuleId "UseFstSnd: correct rule id" [useFstSndRule]
1003+ (withPrelude "x = \\(Tuple a b) -> a") "UseFstSnd"
1004+ , assertSuggestion "UseFstSnd: suggests fst" [useFstSndRule]
1005+ (withPrelude "x = \\(Tuple a b) -> a") "fst"
1006+ , assertWarningCount "UseFstSnd: \\(Tuple x y) -> y" [useFstSndRule]
1007+ (withPrelude "x = \\(Tuple a b) -> b") 1
1008+ , assertSuggestion "UseFstSnd: suggests snd" [useFstSndRule]
1009+ (withPrelude "x = \\(Tuple a b) -> b") "snd"
1010+
1011+ -- Negative cases
1012+ , assertWarningCount "UseFstSnd: no fire when using both" [useFstSndRule]
1013+ (withPrelude "x = \\(Tuple a b) -> a + b") 0
1014+ ]
1015+
1016+-- ============================================================================
1017+-- UseMapMaybe Tests
1018+-- ============================================================================
1019+
1020+useMapMaybeTests :: Array TestResult
1021+useMapMaybeTests =
1022+ [ assertWarningCount "UseMapMaybe: catMaybes (map f x)" [useMapMaybeRule]
1023+ (withPrelude "x = catMaybes (map f [1,2,3])") 1
1024+ , assertRuleId "UseMapMaybe: correct rule id" [useMapMaybeRule]
1025+ (withPrelude "x = catMaybes (map f [1,2,3])") "UseMapMaybe"
1026+ , assertSuggestion "UseMapMaybe: suggests mapMaybe" [useMapMaybeRule]
1027+ (withPrelude "x = catMaybes (map f [1,2,3])") "mapMaybe f [ 1, 2, 3 ]"
1028+ ]
1029+
1030+-- ============================================================================
1031+-- UseApplicative Tests
1032+-- ============================================================================
1033+
1034+useApplicativeTests :: Array TestResult
1035+useApplicativeTests =
1036+ [ assertWarningCount "UseApplicative: pure f <*> x" [useApplicativeRule]
1037+ (withPrelude "x = pure f <*> y") 1
1038+ , assertRuleId "UseApplicative: correct rule id" [useApplicativeRule]
1039+ (withPrelude "x = pure f <*> y") "UseApplicative"
1040+ , assertSuggestion "UseApplicative: suggests f <$> x" [useApplicativeRule]
1041+ (withPrelude "x = pure f <*> y") "f <$> y"
1042+ ]
1043+
1044+-- ============================================================================
1045+-- UseBindFlip Tests
1046+-- ============================================================================
1047+
1048+useBindFlipTests :: Array TestResult
1049+useBindFlipTests =
1050+ [ assertWarningCount "UseBindFlip: join (map f x)" [useBindFlipRule]
1051+ (withPrelude "x = join (map f y)") 1
1052+ , assertRuleId "UseBindFlip: correct rule id" [useBindFlipRule]
1053+ (withPrelude "x = join (map f y)") "UseBindFlip"
1054+ , assertSuggestion "UseBindFlip: suggests x >>= f" [useBindFlipRule]
1055+ (withPrelude "x = join (map f y)") "y >>= f"
1056+ ]
1057+
1058+-- ============================================================================
1059+-- UseFor Tests
1060+-- ============================================================================
1061+
1062+useForTests :: Array TestResult
1063+useForTests =
1064+ [ assertWarningCount "UseFor: flip traverse" [useForRule]
1065+ (withPrelude "x = flip traverse") 1
1066+ , assertRuleId "UseFor: correct rule id" [useForRule]
1067+ (withPrelude "x = flip traverse") "UseFor"
1068+ , assertSuggestion "UseFor: suggests for" [useForRule]
1069+ (withPrelude "x = flip traverse") "for"
1070+ , assertWarningCount "UseFor: flip traverse_" [useForRule]
1071+ (withPrelude "x = flip traverse_") 1
1072+ , assertSuggestion "UseFor: suggests for_" [useForRule]
1073+ (withPrelude "x = flip traverse_") "for_"
1074+ ]
1075+
1076+-- ============================================================================
1077+-- RedundantGuard Tests
1078+-- ============================================================================
1079+
1080+redundantGuardTests :: Array TestResult
1081+redundantGuardTests =
1082+ [ assertWarningCount "RedundantGuard: guard true" [redundantGuardRule]
1083+ (withPrelude "x = guard true") 1
1084+ , assertRuleId "RedundantGuard: correct rule id" [redundantGuardRule]
1085+ (withPrelude "x = guard true") "RedundantGuard"
1086+ , assertSuggestion "RedundantGuard: suggests pure unit" [redundantGuardRule]
1087+ (withPrelude "x = guard true") "pure unit"
1088+ , assertWarningCount "RedundantGuard: guard false" [redundantGuardRule]
1089+ (withPrelude "x = guard false") 1
1090+ ]
1091+
1092+-- ============================================================================
1093+-- UseComparing Tests
1094+-- ============================================================================
1095+
1096+useComparingTests :: Array TestResult
1097+useComparingTests =
1098+ [ assertWarningCount "UseComparing: \\x y -> compare (f x) (f y)" [useComparingRule]
1099+ (withPrelude "x = \\a b -> compare (f a) (f b)") 1
1100+ , assertRuleId "UseComparing: correct rule id" [useComparingRule]
1101+ (withPrelude "x = \\a b -> compare (f a) (f b)") "UseComparing"
1102+ , assertSuggestion "UseComparing: suggests comparing" [useComparingRule]
1103+ (withPrelude "x = \\a b -> compare (f a) (f b)") "comparing f"
1104+ ]
1105+
1106+-- ============================================================================
1107+-- UseOn Tests
1108+-- ============================================================================
1109+
1110+useOnTests :: Array TestResult
1111+useOnTests =
1112+ [ assertWarningCount "UseOn: \\x y -> f (g x) (g y)" [useOnRule]
1113+ (withPrelude "x = \\a b -> f (g a) (g b)") 1
1114+ , assertRuleId "UseOn: correct rule id" [useOnRule]
1115+ (withPrelude "x = \\a b -> f (g a) (g b)") "UseOn"
1116+ , assertSuggestion "UseOn: suggests on f g" [useOnRule]
1117+ (withPrelude "x = \\a b -> f (g a) (g b)") "on f g"
1118+ ]
1119+
1120+-- ============================================================================
1121+-- UseFindMap Tests
1122+-- ============================================================================
1123+
1124+useFindMapTests :: Array TestResult
1125+useFindMapTests =
1126+ [ assertWarningCount "UseFindMap: head (mapMaybe f x)" [useFindMapRule]
1127+ (withPrelude "x = head (mapMaybe f [1,2,3])") 1
1128+ , assertRuleId "UseFindMap: correct rule id" [useFindMapRule]
1129+ (withPrelude "x = head (mapMaybe f [1,2,3])") "UseFindMap"
1130+ , assertSuggestion "UseFindMap: suggests findMap" [useFindMapRule]
1131+ (withPrelude "x = head (mapMaybe f [1,2,3])") "findMap f [ 1, 2, 3 ]"
1132+ ]
1133+
1134+-- ============================================================================
1135+-- UseLastReverse Tests
1136+-- ============================================================================
1137+
1138+useLastReverseTests :: Array TestResult
1139+useLastReverseTests =
1140+ [ assertWarningCount "UseLastReverse: head (reverse x)" [useLastReverseRule]
1141+ (withPrelude "x = head (reverse [1,2,3])") 1
1142+ , assertRuleId "UseLastReverse: correct rule id" [useLastReverseRule]
1143+ (withPrelude "x = head (reverse [1,2,3])") "UseLastReverse"
1144+ , assertSuggestion "UseLastReverse: suggests last" [useLastReverseRule]
1145+ (withPrelude "x = head (reverse [1,2,3])") "last [ 1, 2, 3 ]"
1146+ ]
1147+
1148+-- ============================================================================
1149+-- RedundantReverse Tests
1150+-- ============================================================================
1151+
1152+redundantReverseTests :: Array TestResult
1153+redundantReverseTests =
1154+ [ assertWarningCount "RedundantReverse: reverse (reverse x)" [redundantReverseRule]
1155+ (withPrelude "x = reverse (reverse [1,2,3])") 1
1156+ , assertRuleId "RedundantReverse: correct rule id" [redundantReverseRule]
1157+ (withPrelude "x = reverse (reverse [1,2,3])") "RedundantReverse"
1158+ , assertSuggestion "RedundantReverse: suggests x" [redundantReverseRule]
1159+ (withPrelude "x = reverse (reverse foo)") "foo"
1160+ ]
1161+
1162+-- ============================================================================
1163+-- UseMinimumSort Tests
1164+-- ============================================================================
1165+
1166+useMinimumSortTests :: Array TestResult
1167+useMinimumSortTests =
1168+ [ assertWarningCount "UseMinimumSort: head (sort x)" [useMinimumSortRule]
1169+ (withPrelude "x = head (sort [1,2,3])") 1
1170+ , assertRuleId "UseMinimumSort: correct rule id" [useMinimumSortRule]
1171+ (withPrelude "x = head (sort [1,2,3])") "UseMinimumSort"
1172+ , assertSuggestion "UseMinimumSort: suggests minimum" [useMinimumSortRule]
1173+ (withPrelude "x = head (sort [1,2,3])") "minimum [ 1, 2, 3 ]"
3431174 ]
1175+
1176+-- ============================================================================
1177+-- UseBimap Tests
1178+-- ============================================================================
1179+
1180+useBimapTests :: Array TestResult
1181+useBimapTests =
1182+ [ assertWarningCount "UseBimap: bimap id g" [useBimapRule]
1183+ (withPrelude "x = bimap identity show") 1
1184+ , assertRuleId "UseBimap: correct rule id" [useBimapRule]
1185+ (withPrelude "x = bimap identity show") "UseBimap"
1186+ , assertSuggestion "UseBimap: suggests second" [useBimapRule]
1187+ (withPrelude "x = bimap identity show") "second show"
1188+ ]
1189+
1190+-- ============================================================================
1191+-- EvaluateFst Tests
1192+-- ============================================================================
1193+
1194+evaluateFstTests :: Array TestResult
1195+evaluateFstTests =
1196+ [ assertWarningCount "EvaluateFst: fst (Tuple x y)" [evaluateFstRule]
1197+ (withPrelude "x = fst (Tuple 1 2)") 1
1198+ , assertRuleId "EvaluateFst: correct rule id" [evaluateFstRule]
1199+ (withPrelude "x = fst (Tuple 1 2)") "EvaluateFst"
1200+ , assertSuggestion "EvaluateFst: suggests x" [evaluateFstRule]
1201+ (withPrelude "x = fst (Tuple 1 2)") "1"
1202+ , assertWarningCount "EvaluateFst: snd (Tuple x y)" [evaluateFstRule]
1203+ (withPrelude "x = snd (Tuple 1 2)") 1
1204+ , assertSuggestion "EvaluateFst: suggests y" [evaluateFstRule]
1205+ (withPrelude "x = snd (Tuple 1 2)") "2"
1206+ ]
1207+
1208+-- ============================================================================
1209+-- EvaluateConst Tests
1210+-- ============================================================================
1211+
1212+evaluateConstTests :: Array TestResult
1213+evaluateConstTests =
1214+ [ assertWarningCount "EvaluateConst: const x y" [evaluateConstRule]
1215+ (withPrelude "x = const 42 foo") 1
1216+ , assertRuleId "EvaluateConst: correct rule id" [evaluateConstRule]
1217+ (withPrelude "x = const 42 foo") "EvaluateConst"
1218+ , assertSuggestion "EvaluateConst: suggests x" [evaluateConstRule]
1219+ (withPrelude "x = const 42 foo") "42"
1220+ ]
1221+
1222+-- ============================================================================
1223+-- UseOr Tests
1224+-- ============================================================================
1225+
1226+useOrTests :: Array TestResult
1227+useOrTests =
1228+ [ assertWarningCount "UseOr: any identity" [useOrRule]
1229+ (withPrelude "x = any identity [true, false]") 1
1230+ , assertRuleId "UseOr: correct rule id" [useOrRule]
1231+ (withPrelude "x = any identity [true, false]") "UseOr"
1232+ , assertSuggestion "UseOr: suggests or" [useOrRule]
1233+ (withPrelude "x = any identity [true, false]") "or [ true, false ]"
1234+ ]
1235+
1236+-- ============================================================================
1237+-- UseAnd Tests
1238+-- ============================================================================
1239+
1240+useAndTests :: Array TestResult
1241+useAndTests =
1242+ [ assertWarningCount "UseAnd: all identity" [useAndRule]
1243+ (withPrelude "x = all identity [true, false]") 1
1244+ , assertRuleId "UseAnd: correct rule id" [useAndRule]
1245+ (withPrelude "x = all identity [true, false]") "UseAnd"
1246+ , assertSuggestion "UseAnd: suggests and" [useAndRule]
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 <> \"!\""
1294+ ]
1295+
1296+-- ============================================================================
1297+-- UseFoldMapId Tests
1298+-- ============================================================================
1299+
1300+useFoldMapIdTests :: Array TestResult
1301+useFoldMapIdTests =
1302+ [ assertWarningCount "UseFoldMapId: foldMap identity" [useFoldMapIdRule]
1303+ (withPrelude "x = foldMap identity") 1
1304+ , assertRuleId "UseFoldMapId: correct rule id" [useFoldMapIdRule]
1305+ (withPrelude "x = foldMap identity") "UseFoldMapId"
1306+ , assertSuggestion "UseFoldMapId: suggests fold" [useFoldMapIdRule]
1307+ (withPrelude "x = foldMap identity") "fold"
1308+ ]
1309+
1310+-- ============================================================================
1311+-- WhenNot Tests
1312+-- ============================================================================
1313+
1314+whenNotTests :: Array TestResult
1315+whenNotTests =
1316+ [ assertWarningCount "WhenNot: when (not x)" [whenNotRule]
1317+ (withPrelude "x = when (not cond) action") 1
1318+ , assertRuleId "WhenNot: correct rule id" [whenNotRule]
1319+ (withPrelude "x = when (not cond) action") "WhenNot"
1320+ , assertSuggestion "WhenNot: suggests unless" [whenNotRule]
1321+ (withPrelude "x = when (not cond) action") "unless cond action"
1322+ ]
1323+
1324+-- ============================================================================
1325+-- UnlessNot Tests
1326+-- ============================================================================
1327+
1328+unlessNotTests :: Array TestResult
1329+unlessNotTests =
1330+ [ assertWarningCount "UnlessNot: unless (not x)" [unlessNotRule]
1331+ (withPrelude "x = unless (not cond) action") 1
1332+ , assertRuleId "UnlessNot: correct rule id" [unlessNotRule]
1333+ (withPrelude "x = unless (not cond) action") "UnlessNot"
1334+ , assertSuggestion "UnlessNot: suggests when" [unlessNotRule]
1335+ (withPrelude "x = unless (not cond) action") "when cond action"
1336+ ]
1337+
1338+-- ============================================================================
1339+-- UseZip Tests
1340+-- ============================================================================
1341+
1342+useZipTests :: Array TestResult
1343+useZipTests =
1344+ [ assertWarningCount "UseZip: zipWith Tuple" [useZipRule]
1345+ (withPrelude "x = zipWith Tuple [1] [2]") 1
1346+ , assertRuleId "UseZip: correct rule id" [useZipRule]
1347+ (withPrelude "x = zipWith Tuple [1] [2]") "UseZip"
1348+ , assertSuggestion "UseZip: suggests zip" [useZipRule]
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"
1506+ ]
1507+
1508+-- ============================================================================
1509+-- RedundantNegate Tests
1510+-- ============================================================================
1511+
1512+redundantNegateTests :: Array TestResult
1513+redundantNegateTests =
1514+ [ assertWarningCount "RedundantNegate: negate (negate x)" [redundantNegateRule]
1515+ (withPrelude "x = negate (negate 5)") 1
1516+ , assertRuleId "RedundantNegate: correct rule id" [redundantNegateRule]
1517+ (withPrelude "x = negate (negate 5)") "RedundantNegate"
1518+ , assertSuggestion "RedundantNegate: suggests x" [redundantNegateRule]
1519+ (withPrelude "x = negate (negate foo)") "foo"
1520+ ]
1521+
1522+-- ============================================================================
1523+-- RedundantId Tests
1524+-- ============================================================================
1525+
1526+redundantIdTests :: Array TestResult
1527+redundantIdTests =
1528+ [ assertWarningCount "RedundantId: identity x" [redundantIdRule]
1529+ (withPrelude "x = identity 42") 1
1530+ , assertRuleId "RedundantId: correct rule id" [redundantIdRule]
1531+ (withPrelude "x = identity 42") "RedundantId"
1532+ , assertSuggestion "RedundantId: suggests x" [redundantIdRule]
1533+ (withPrelude "x = identity foo") "foo"
1534+ ]
1535+
1536+-- ============================================================================
1537+-- NothingBind Tests
1538+-- ============================================================================
1539+
1540+nothingBindTests :: Array TestResult
1541+nothingBindTests =
1542+ [ assertWarningCount "NothingBind: Nothing >>= f" [nothingBindRule]
1543+ (withPrelude "x = Nothing >>= show") 1
1544+ , assertRuleId "NothingBind: correct rule id" [nothingBindRule]
1545+ (withPrelude "x = Nothing >>= show") "NothingBind"
1546+ , assertSuggestion "NothingBind: suggests Nothing" [nothingBindRule]
1547+ (withPrelude "x = Nothing >>= show") "Nothing"
1548+ ]
1549+
1550+-- ============================================================================
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
1736+-- ============================================================================
1737+
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+
1892+-- | All rules for golden testing
1893+allRulesForGolden :: Array Rule
1894+allRulesForGolden =
1895+ [ mapIdentityRule
1896+ , mapFusionRule
1897+ , useTraverseRule
1898+ , concatMapRule
1899+ , notEqualRule
1900+ , useGuardRule
1901+ , etaReduceRule
1902+ , etaReduceDeclRule
1903+ , redundantBindRule
1904+ , letToWhereRule
1905+ , redundantIfRule
1906+ , functorLawRule
1907+ , useVoidRule
1908+ , useJoinRule
1909+ , useAnyRule
1910+ , useAllRule
1911+ , booleanSimplifyRule
1912+ , collapseLambdasRule
1913+ , useConstRule
1914+ , useNullRule
1915+ , useFromMaybeRule
1916+ , useIsJustRule
1917+ , useIsNothingRule
1918+ , useWhenRule
1919+ , useUnlessRule
1920+ , redundantFlipRule
1921+ , useNotElemRule
1922+ , useMinMaxRule
1923+ , monoidIdentityRule
1924+ , useFoldRule
1925+ , useSequenceRule
1926+ , useTraverseSequenceRule
1927+ , redundantNotRule
1928+ , useFstSndRule
1929+ , useMapMaybeRule
1930+ , useGuardMaybeRule
1931+ , useMaybeMapRule
1932+ , useApplicativeRule
1933+ , useBindFlipRule
1934+ , useForRule
1935+ , redundantGuardRule
1936+ , useComparingRule
1937+ , useOnRule
1938+ , useFindMapRule
1939+ ]
1940+
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")
2157+ ]
2158+
2159+-- ============================================================================
2160+-- Purs-Tidy Formatting Tests
2161+-- ============================================================================
2162+
2163+-- | Test that a suggestion is already purs-tidy formatted
2164+assertTidyFormatted :: String -> String -> Effect TestResult
2165+assertTidyFormatted name suggestion = do
2166+ formatted <- formatExpr suggestion
2167+ pure $ if formatted == suggestion
2168+ then runTest name true "OK"
2169+ else runTest name false ("Not tidy-formatted. Got: '" <> formatted <> "' Expected: '" <> suggestion <> "'")
2170+
2171+-- | Suggestions that should be purs-tidy formatted
2172+tidySuggestions :: Array { name :: String, suggestion :: String }
2173+tidySuggestions =
2174+ [ { name: "void foo", suggestion: "void foo" }
2175+ , { name: "join foo", suggestion: "join foo" }
2176+ , { name: "any f xs", suggestion: "any f xs" }
2177+ , { name: "all f xs", suggestion: "all f xs" }
2178+ , { name: "const 42", suggestion: "const 42" }
2179+ , { name: "notElem x xs", suggestion: "notElem x xs" }
2180+ , { name: "max a b", suggestion: "max a b" }
2181+ , { name: "min a b", suggestion: "min a b" }
2182+ , { name: "null xs", suggestion: "null xs" }
2183+ , { name: "fromMaybe x", suggestion: "fromMaybe x" }
2184+ , { name: "isJust", suggestion: "isJust" }
2185+ , { name: "isNothing", suggestion: "isNothing" }
2186+ , { name: "when cond action", suggestion: "when cond action" }
2187+ , { name: "unless cond action", suggestion: "unless cond action" }
2188+ , { name: "fst", suggestion: "fst" }
2189+ , { name: "snd", suggestion: "snd" }
2190+ , { name: "fold", suggestion: "fold" }
2191+ , { name: "sequence", suggestion: "sequence" }
2192+ , { name: "traverse f xs", suggestion: "traverse f xs" }
2193+ , { name: "collapsed lambda", suggestion: "\\a b -> a + b" }
2194+ , { name: "map fusion", suggestion: "map (f <<< g) x" }
2195+ , { name: "map fusion with <$>", suggestion: "(f <<< g) <$> x" }
2196+ , { name: "flipped map fusion", suggestion: "x <#> (g >>> f)" }
2197+ , { name: "where clause", suggestion: "y + 1\n where\n y = 1" }
2198+ ]
2199+
2200+-- | Run all tidy formatting tests
2201+runTidyTests :: Effect (Array TestResult)
2202+runTidyTests = traverse (\t -> assertTidyFormatted ("Tidy: " <> t.name) t.suggestion) tidySuggestions
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);