├── .gitignore ├── elm-tooling.json ├── src ├── NoUnused │ ├── LamderaSupport.elm │ ├── Variables │ │ └── ElmPrelude.elm │ ├── NonemptyList.elm │ ├── Modules.elm │ ├── Patterns │ │ └── NameVisitor.elm │ ├── Parameters.elm │ ├── Dependencies.elm │ └── CustomTypeConstructorArgs.elm ├── String │ └── Extra.elm └── List │ └── Extra.elm ├── elm-review-package-tests ├── helpers │ ├── ansi.js │ └── find-configurations.js ├── check-examples-were-updated.js └── check-previews-compile.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── new-rule-idea.md └── workflows │ └── test.yml ├── example ├── src │ └── ReviewConfig.elm └── elm.json ├── preview ├── src │ └── ReviewConfig.elm └── elm.json ├── elm.json ├── preview-ignore-tests ├── elm.json └── src │ └── ReviewConfig.elm ├── example-ignore-tests ├── elm.json └── src │ └── ReviewConfig.elm ├── package.json ├── review ├── elm.json └── src │ └── ReviewConfig.elm ├── maintenance ├── update-examples-from-preview.js └── MAINTENANCE.md ├── LICENSE ├── README.md ├── tests ├── TestProject.elm ├── NoUnused │ ├── ModulesTest.elm │ └── CustomTypeConstructorArgsTest.elm └── Dependencies │ └── ElmJson.elm ├── CHANGELOG.md └── docs.json /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | *.log 3 | node_modules/ 4 | .idea/ 5 | -------------------------------------------------------------------------------- /elm-tooling.json: -------------------------------------------------------------------------------- 1 | { 2 | "tools": { 3 | "elm": "0.19.1", 4 | "elm-format": "0.8.5" 5 | } 6 | } -------------------------------------------------------------------------------- /src/NoUnused/LamderaSupport.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.LamderaSupport exposing (isLamderaApplication) 2 | 3 | import Elm.Package 4 | 5 | 6 | isLamderaApplication : List ( Elm.Package.Name, b ) -> Bool 7 | isLamderaApplication depsDirect = 8 | List.any (\( name, _ ) -> Elm.Package.toString name == "lamdera/core") depsDirect 9 | -------------------------------------------------------------------------------- /elm-review-package-tests/helpers/ansi.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | red, 3 | green, 4 | yellow 5 | }; 6 | 7 | function red(text) { 8 | return '\u001B[31m' + text + '\u001B[39m'; 9 | } 10 | 11 | function green(text) { 12 | return '\u001B[32m' + text + '\u001B[39m'; 13 | } 14 | 15 | function yellow(text) { 16 | return '\u001B[33m' + text + '\u001B[39m'; 17 | } 18 | -------------------------------------------------------------------------------- /elm-review-package-tests/helpers/find-configurations.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | 4 | const root = path 5 | .resolve(__dirname, '../../') 6 | .replace(/.:/, '') 7 | .replace(/\\/g, '/'); 8 | 9 | module.exports = { 10 | findPreviewConfigurations 11 | }; 12 | 13 | function findPreviewConfigurations() { 14 | return glob 15 | .sync(`${root}/preview*/**/elm.json`, { 16 | ignore: ['**/elm-stuff/**'], 17 | nodir: true 18 | }) 19 | .map(path.dirname); 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **SSCCE (Short Self-Contained Correct Example)** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /src/String/Extra.elm: -------------------------------------------------------------------------------- 1 | module String.Extra exposing (isCapitalized) 2 | 3 | {-| Some utilities. 4 | -} 5 | 6 | 7 | {-| Check if the first character of a string is upper case, unicode aware. 8 | 9 | Note that `Char.isUpper` returns the correct result only for ASCII characters, even though it can be called with any Unicode character. 10 | See 11 | 12 | -} 13 | isCapitalized : String -> Bool 14 | isCapitalized string = 15 | case String.uncons string of 16 | Just ( char, _ ) -> 17 | -- 1. The character is its own upper variant. 18 | (char == Char.toUpper char) 19 | -- 2. The character is not its own lower variant. 20 | && (char /= Char.toLower char) 21 | 22 | Nothing -> 23 | False 24 | -------------------------------------------------------------------------------- /example/src/ReviewConfig.elm: -------------------------------------------------------------------------------- 1 | module ReviewConfig exposing (config) 2 | 3 | {-| Do not rename the ReviewConfig module or the config function, because 4 | `elm-review` will look for these. 5 | 6 | To add packages that contain rules, add them to this review project using 7 | 8 | `elm install author/packagename` 9 | 10 | when inside the directory containing this file. 11 | 12 | -} 13 | 14 | import NoUnused.CustomTypeConstructorArgs 15 | import NoUnused.CustomTypeConstructors 16 | import NoUnused.Dependencies 17 | import NoUnused.Exports 18 | import NoUnused.Parameters 19 | import NoUnused.Patterns 20 | import NoUnused.Variables 21 | import Review.Rule exposing (Rule) 22 | 23 | 24 | config : List Rule 25 | config = 26 | [ NoUnused.CustomTypeConstructors.rule [] 27 | , NoUnused.CustomTypeConstructorArgs.rule 28 | , NoUnused.Dependencies.rule 29 | , NoUnused.Exports.rule 30 | , NoUnused.Parameters.rule 31 | , NoUnused.Patterns.rule 32 | , NoUnused.Variables.rule 33 | ] 34 | -------------------------------------------------------------------------------- /preview/src/ReviewConfig.elm: -------------------------------------------------------------------------------- 1 | module ReviewConfig exposing (config) 2 | 3 | {-| Do not rename the ReviewConfig module or the config function, because 4 | `elm-review` will look for these. 5 | 6 | To add packages that contain rules, add them to this review project using 7 | 8 | `elm install author/packagename` 9 | 10 | when inside the directory containing this file. 11 | 12 | -} 13 | 14 | import NoUnused.CustomTypeConstructorArgs 15 | import NoUnused.CustomTypeConstructors 16 | import NoUnused.Dependencies 17 | import NoUnused.Exports 18 | import NoUnused.Parameters 19 | import NoUnused.Patterns 20 | import NoUnused.Variables 21 | import Review.Rule exposing (Rule) 22 | 23 | 24 | config : List Rule 25 | config = 26 | [ NoUnused.CustomTypeConstructors.rule [] 27 | , NoUnused.CustomTypeConstructorArgs.rule 28 | , NoUnused.Dependencies.rule 29 | , NoUnused.Exports.rule 30 | , NoUnused.Parameters.rule 31 | , NoUnused.Patterns.rule 32 | , NoUnused.Variables.rule 33 | ] 34 | -------------------------------------------------------------------------------- /preview/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "../src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/core": "1.0.5", 11 | "elm/project-metadata-utils": "1.0.2", 12 | "jfmengels/elm-review": "2.15.3", 13 | "stil4m/elm-syntax": "7.3.9" 14 | }, 15 | "indirect": { 16 | "elm/bytes": "1.0.8", 17 | "elm/html": "1.0.0", 18 | "elm/json": "1.1.3", 19 | "elm/parser": "1.1.0", 20 | "elm/random": "1.0.0", 21 | "elm/regex": "1.0.0", 22 | "elm/time": "1.0.0", 23 | "elm/virtual-dom": "1.0.4", 24 | "elm-explorations/test": "2.2.0", 25 | "rtfeldman/elm-hex": "1.0.0", 26 | "stil4m/structured-writer": "1.0.3" 27 | } 28 | }, 29 | "test-dependencies": { 30 | "direct": {}, 31 | "indirect": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "jfmengels/elm-review-unused", 4 | "summary": "Provides elm-review rules to detect unused elements in your Elm project", 5 | "license": "BSD-3-Clause", 6 | "version": "1.2.4", 7 | "exposed-modules": [ 8 | "NoUnused.CustomTypeConstructorArgs", 9 | "NoUnused.CustomTypeConstructors", 10 | "NoUnused.Dependencies", 11 | "NoUnused.Exports", 12 | "NoUnused.Modules", 13 | "NoUnused.Parameters", 14 | "NoUnused.Patterns", 15 | "NoUnused.Variables" 16 | ], 17 | "elm-version": "0.19.0 <= v < 0.20.0", 18 | "dependencies": { 19 | "elm/core": "1.0.5 <= v < 2.0.0", 20 | "elm/project-metadata-utils": "1.0.1 <= v < 2.0.0", 21 | "jfmengels/elm-review": "2.15.3 <= v < 3.0.0", 22 | "stil4m/elm-syntax": "7.3.2 <= v < 8.0.0" 23 | }, 24 | "test-dependencies": { 25 | "elm/json": "1.1.3 <= v < 2.0.0", 26 | "elm-explorations/test": "2.0.1 <= v < 3.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /preview-ignore-tests/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "../src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/core": "1.0.5", 11 | "elm/project-metadata-utils": "1.0.2", 12 | "jfmengels/elm-review": "2.15.3", 13 | "stil4m/elm-syntax": "7.3.9" 14 | }, 15 | "indirect": { 16 | "elm/bytes": "1.0.8", 17 | "elm/html": "1.0.0", 18 | "elm/json": "1.1.3", 19 | "elm/parser": "1.1.0", 20 | "elm/random": "1.0.0", 21 | "elm/regex": "1.0.0", 22 | "elm/time": "1.0.0", 23 | "elm/virtual-dom": "1.0.4", 24 | "elm-explorations/test": "2.2.0", 25 | "rtfeldman/elm-hex": "1.0.0", 26 | "stil4m/structured-writer": "1.0.3" 27 | } 28 | }, 29 | "test-dependencies": { 30 | "direct": {}, 31 | "indirect": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.5", 10 | "elm/project-metadata-utils": "1.0.2", 11 | "jfmengels/elm-review": "2.15.0", 12 | "stil4m/elm-syntax": "7.3.8", 13 | "jfmengels/elm-review-unused": "1.2.4" 14 | }, 15 | "indirect": { 16 | "elm/bytes": "1.0.8", 17 | "elm/html": "1.0.0", 18 | "elm/json": "1.1.3", 19 | "elm/parser": "1.1.0", 20 | "elm/random": "1.0.0", 21 | "elm/regex": "1.0.0", 22 | "elm/time": "1.0.0", 23 | "elm/virtual-dom": "1.0.3", 24 | "elm-explorations/test": "2.2.0", 25 | "rtfeldman/elm-hex": "1.0.0", 26 | "stil4m/structured-writer": "1.0.3" 27 | } 28 | }, 29 | "test-dependencies": { 30 | "direct": {}, 31 | "indirect": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example-ignore-tests/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/core": "1.0.5", 10 | "elm/project-metadata-utils": "1.0.2", 11 | "jfmengels/elm-review": "2.15.0", 12 | "stil4m/elm-syntax": "7.3.8", 13 | "jfmengels/elm-review-unused": "1.2.4" 14 | }, 15 | "indirect": { 16 | "elm/bytes": "1.0.8", 17 | "elm/html": "1.0.0", 18 | "elm/json": "1.1.3", 19 | "elm/parser": "1.1.0", 20 | "elm/random": "1.0.0", 21 | "elm/regex": "1.0.0", 22 | "elm/time": "1.0.0", 23 | "elm/virtual-dom": "1.0.3", 24 | "elm-explorations/test": "2.2.0", 25 | "rtfeldman/elm-hex": "1.0.0", 26 | "stil4m/structured-writer": "1.0.3" 27 | } 28 | }, 29 | "test-dependencies": { 30 | "direct": {}, 31 | "indirect": {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /elm-review-package-tests/check-examples-were-updated.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const Ansi = require('./helpers/ansi'); 4 | const {execSync} = require('child_process'); 5 | const updateExamplesFromPreview = require('../maintenance/update-examples-from-preview'); 6 | 7 | const preCheckGitStatus = execSync('git status --porcelain').toString().trim(); 8 | if (preCheckGitStatus !== '') { 9 | console.error( 10 | `${Ansi.red( 11 | '✖' 12 | )} Check aborted: There are uncommitted changes in the project.` 13 | ); 14 | process.exit(1); 15 | } 16 | 17 | updateExamplesFromPreview(); 18 | 19 | const postCheckGitStatus = execSync('git status --porcelain').toString().trim(); 20 | if (postCheckGitStatus !== '') { 21 | console.error('\u001B[31m✖\u001B[39m Your examples were not up to date.'); 22 | console.log( 23 | `Please commit the changes I made. If you see this message from GitHub Actions, then run 24 | ${Ansi.yellow('node maintenance/update-examples-from-preview.js')} 25 | to update your examples.` 26 | ); 27 | process.exit(1); 28 | } 29 | 30 | process.exit(0); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "scripts": { 4 | "test": "npm-run-all --print-name --silent --sequential test:make test:format test:run test:review test:package", 5 | "test:make": "elm make --docs=docs.json", 6 | "test:format": "elm-format src/ tests/ --validate", 7 | "test:run": "elm-test", 8 | "test:review": "elm-review", 9 | "test:package": "node elm-review-package-tests/check-previews-compile.js", 10 | "preview-docs": "elm-doc-preview", 11 | "elm-bump": "npm-run-all --print-name --silent --sequential test bump-version 'test:review -- --fix-all-without-prompt' update-examples", 12 | "bump-version": "(yes | elm bump)", 13 | "update-examples": "node maintenance/update-examples-from-preview.js", 14 | "postinstall": "elm-tooling install" 15 | }, 16 | "dependencies": { 17 | "elm-doc-preview": "^5.0.5", 18 | "elm-review": "^2.13.4", 19 | "elm-test": "^0.19.1-revision9", 20 | "elm-tooling": "^1.8.0", 21 | "fs-extra": "9.0.0", 22 | "glob": "7.1.6", 23 | "npm-run-all": "^4.1.5" 24 | }, 25 | "license": "BSD-3-Clause" 26 | } 27 | -------------------------------------------------------------------------------- /src/NoUnused/Variables/ElmPrelude.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.Variables.ElmPrelude exposing (elmPrelude) 2 | 3 | import Dict exposing (Dict) 4 | 5 | 6 | elmPrelude : 7 | Dict 8 | (List String) 9 | { exposes : List String 10 | , alias : Maybe (List String) 11 | } 12 | elmPrelude = 13 | -- These are the default imports implicitly added by the Elm compiler 14 | -- https://package.elm-lang.org/packages/elm/core/latest 15 | Dict.fromList 16 | [ ( [ "Basics" ], { exposes = [], alias = Nothing } ) 17 | , ( [ "List" ], { exposes = [ "List", "::" ], alias = Nothing } ) 18 | , ( [ "Maybe" ], { exposes = [ "Maybe" ], alias = Nothing } ) 19 | , ( [ "Result" ], { exposes = [ "Result" ], alias = Nothing } ) 20 | , ( [ "String" ], { exposes = [ "String" ], alias = Nothing } ) 21 | , ( [ "Char" ], { exposes = [ "Char" ], alias = Nothing } ) 22 | , ( [ "Tuple" ], { exposes = [], alias = Nothing } ) 23 | , ( [ "Debug" ], { exposes = [], alias = Nothing } ) 24 | , ( [ "Platform" ], { exposes = [ "Program" ], alias = Nothing } ) 25 | , ( [ "Platform", "Cmd" ], { exposes = [ "Cmd" ], alias = Just [ "Cmd" ] } ) 26 | , ( [ "Platform", "Sub" ], { exposes = [ "Sub" ], alias = Just [ "Sub" ] } ) 27 | ] 28 | -------------------------------------------------------------------------------- /example-ignore-tests/src/ReviewConfig.elm: -------------------------------------------------------------------------------- 1 | module ReviewConfig exposing (config) 2 | 3 | {-| Do not rename the ReviewConfig module or the config function, because 4 | `elm-review` will look for these. 5 | 6 | To add packages that contain rules, add them to this review project using 7 | 8 | `elm install author/packagename` 9 | 10 | when inside the directory containing this file. 11 | 12 | -} 13 | 14 | import NoUnused.CustomTypeConstructorArgs 15 | import NoUnused.CustomTypeConstructors 16 | import NoUnused.Dependencies 17 | import NoUnused.Exports exposing (annotatedBy, suffixedBy) 18 | import NoUnused.Parameters 19 | import NoUnused.Patterns 20 | import NoUnused.Variables 21 | import Review.Rule exposing (Rule) 22 | 23 | 24 | config : List Rule 25 | config = 26 | [ NoUnused.CustomTypeConstructors.rule [] 27 | , NoUnused.CustomTypeConstructorArgs.rule 28 | , NoUnused.Dependencies.rule 29 | , NoUnused.Exports.defaults 30 | |> NoUnused.Exports.reportUnusedProductionExports 31 | { isProductionFile = \{ moduleName, filePath, isInSourceDirectories } -> isInSourceDirectories 32 | , exceptionsAre = [ annotatedBy "@test-helper", suffixedBy "_FOR_TESTS" ] 33 | } 34 | |> NoUnused.Exports.toRule 35 | , NoUnused.Parameters.rule 36 | , NoUnused.Patterns.rule 37 | , NoUnused.Variables.rule 38 | ] 39 | -------------------------------------------------------------------------------- /preview-ignore-tests/src/ReviewConfig.elm: -------------------------------------------------------------------------------- 1 | module ReviewConfig exposing (config) 2 | 3 | {-| Do not rename the ReviewConfig module or the config function, because 4 | `elm-review` will look for these. 5 | 6 | To add packages that contain rules, add them to this review project using 7 | 8 | `elm install author/packagename` 9 | 10 | when inside the directory containing this file. 11 | 12 | -} 13 | 14 | import NoUnused.CustomTypeConstructorArgs 15 | import NoUnused.CustomTypeConstructors 16 | import NoUnused.Dependencies 17 | import NoUnused.Exports exposing (annotatedBy, suffixedBy) 18 | import NoUnused.Parameters 19 | import NoUnused.Patterns 20 | import NoUnused.Variables 21 | import Review.Rule exposing (Rule) 22 | 23 | 24 | config : List Rule 25 | config = 26 | [ NoUnused.CustomTypeConstructors.rule [] 27 | , NoUnused.CustomTypeConstructorArgs.rule 28 | , NoUnused.Dependencies.rule 29 | , NoUnused.Exports.defaults 30 | |> NoUnused.Exports.reportUnusedProductionExports 31 | { isProductionFile = \{ moduleName, filePath, isInSourceDirectories } -> isInSourceDirectories 32 | , exceptionsAre = [ annotatedBy "@test-helper", suffixedBy "_FOR_TESTS" ] 33 | } 34 | |> NoUnused.Exports.toRule 35 | , NoUnused.Parameters.rule 36 | , NoUnused.Patterns.rule 37 | , NoUnused.Variables.rule 38 | ] 39 | -------------------------------------------------------------------------------- /review/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "../src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "elm/core": "1.0.5", 11 | "elm/json": "1.1.3", 12 | "elm/project-metadata-utils": "1.0.2", 13 | "elm/regex": "1.0.0", 14 | "jfmengels/elm-review": "2.15.1", 15 | "jfmengels/elm-review-code-style": "1.2.0", 16 | "jfmengels/elm-review-cognitive-complexity": "1.0.3", 17 | "jfmengels/elm-review-common": "1.3.3", 18 | "jfmengels/elm-review-debug": "1.0.8", 19 | "jfmengels/elm-review-documentation": "2.0.4", 20 | "jfmengels/elm-review-simplify": "2.1.9", 21 | "sparksp/elm-review-forbidden-words": "1.0.1", 22 | "stil4m/elm-syntax": "7.3.9" 23 | }, 24 | "indirect": { 25 | "elm/bytes": "1.0.8", 26 | "elm/html": "1.0.0", 27 | "elm/parser": "1.1.0", 28 | "elm/random": "1.0.0", 29 | "elm/time": "1.0.0", 30 | "elm/virtual-dom": "1.0.4", 31 | "elm-explorations/test": "2.2.0", 32 | "pzp1997/assoc-list": "1.0.0", 33 | "rtfeldman/elm-hex": "1.0.0", 34 | "stil4m/structured-writer": "1.0.3" 35 | } 36 | }, 37 | "test-dependencies": { 38 | "direct": {}, 39 | "indirect": {} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-rule-idea.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New rule idea 3 | about: Propose a new rule idea 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | 18 | **What the rule should do:** 19 | 20 | 21 | **What problems does it solve:** 22 | 23 | 24 | **Example of things the rule would report:** 25 | 26 | ```elm 27 | 28 | ``` 29 | 30 | **Example of things the rule would not report:** 31 | 32 | ```elm 33 | 34 | ``` 35 | 36 | **When (not) to enable this rule:** 37 | 38 | 40 | 41 | **I am looking for:** 42 | 43 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | -------------------------------------------------------------------------------- /maintenance/update-examples-from-preview.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const root = path.resolve(__dirname, '..'); 6 | const packageElmJson = require(`${root}/elm.json`); 7 | const { 8 | findPreviewConfigurations 9 | } = require('../elm-review-package-tests/helpers/find-configurations'); 10 | 11 | if (require.main === module) { 12 | copyPreviewsToExamples(); 13 | } else { 14 | module.exports = copyPreviewsToExamples; 15 | } 16 | 17 | // Find all elm.json files 18 | 19 | function copyPreviewsToExamples() { 20 | const previewFolders = findPreviewConfigurations(); 21 | previewFolders.forEach(copyPreviewToExample); 22 | } 23 | 24 | function copyPreviewToExample(pathToPreviewFolder) { 25 | const pathToExampleFolder = `${pathToPreviewFolder}/`.replace( 26 | /preview/g, 27 | 'example' 28 | ); 29 | fs.removeSync(pathToExampleFolder); 30 | fs.copySync(pathToPreviewFolder, pathToExampleFolder, {overwrite: true}); 31 | 32 | const pathToElmJson = path.resolve(pathToExampleFolder, 'elm.json'); 33 | const elmJson = fs.readJsonSync(pathToElmJson); 34 | 35 | // Remove the source directory pointing to the package's src/ 36 | elmJson['source-directories'] = elmJson['source-directories'].filter( 37 | (sourceDirectory) => 38 | path.resolve(pathToExampleFolder, sourceDirectory) !== 39 | path.resolve(root, 'src') 40 | ); 41 | elmJson.dependencies.direct[packageElmJson.name] = packageElmJson.version; 42 | fs.writeJsonSync(pathToElmJson, elmJson, {spaces: 4}); 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Jeroen Engels 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of elm-review-unused nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /review/src/ReviewConfig.elm: -------------------------------------------------------------------------------- 1 | module ReviewConfig exposing (config) 2 | 3 | {-| Do not rename the ReviewConfig module or the config function, because 4 | `elm-review` will look for these. 5 | 6 | To add packages that contain rules, add them to this review project using 7 | 8 | `elm install author/packagename` 9 | 10 | when inside the directory containing this file. 11 | 12 | -} 13 | 14 | import CognitiveComplexity 15 | import Docs.NoMissing exposing (exposedModules, onlyExposed) 16 | import Docs.ReviewAtDocs 17 | import Docs.ReviewLinksAndSections 18 | import Docs.UpToDateReadmeLinks 19 | import NoDebug.Log 20 | import NoDebug.TodoOrToString 21 | import NoExposingEverything 22 | import NoForbiddenWords 23 | import NoImportingEverything 24 | import NoMissingTypeAnnotation 25 | import NoMissingTypeAnnotationInLetIn 26 | import NoMissingTypeExpose 27 | import NoPrematureLetComputation 28 | import NoSimpleLetBody 29 | import NoUnused.CustomTypeConstructorArgs 30 | import NoUnused.CustomTypeConstructors 31 | import NoUnused.Dependencies 32 | import NoUnused.Exports 33 | import NoUnused.Parameters 34 | import NoUnused.Patterns 35 | import NoUnused.Variables 36 | import Review.Rule as Rule exposing (Rule) 37 | import Simplify 38 | 39 | 40 | config : List Rule 41 | config = 42 | [ Docs.ReviewAtDocs.rule 43 | , Docs.NoMissing.rule { document = onlyExposed, from = exposedModules } 44 | , Docs.ReviewLinksAndSections.rule 45 | , Docs.UpToDateReadmeLinks.rule 46 | , NoDebug.Log.rule 47 | , NoDebug.TodoOrToString.rule 48 | |> Rule.ignoreErrorsForDirectories [ "tests/" ] 49 | , NoExposingEverything.rule 50 | , NoForbiddenWords.rule [ "REPLACEME" ] 51 | , NoImportingEverything.rule [] 52 | , NoMissingTypeAnnotation.rule 53 | , NoMissingTypeAnnotationInLetIn.rule 54 | , NoMissingTypeExpose.rule 55 | , NoSimpleLetBody.rule 56 | , NoPrematureLetComputation.rule 57 | , NoUnused.CustomTypeConstructors.rule [] 58 | , NoUnused.CustomTypeConstructorArgs.rule 59 | , NoUnused.Dependencies.rule 60 | , NoUnused.Exports.rule 61 | , NoUnused.Parameters.rule 62 | , NoUnused.Patterns.rule 63 | , NoUnused.Variables.rule 64 | , Simplify.rule Simplify.defaults 65 | , -- TODO Reduce to 10 66 | CognitiveComplexity.rule 29 67 | ] 68 | |> List.map (Rule.ignoreErrorsForFiles [ "src/NoUnused/Patterns/NameVisitor.elm" ]) 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-review-unused 2 | 3 | Provides [`elm-review`](https://package.elm-lang.org/packages/jfmengels/elm-review/latest/) rules to detect unused elements in your Elm project. 4 | 5 | ## Provided rules 6 | 7 | - [🔧 `NoUnused.Variables`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Variables/ "Provides automatic fixes") - Reports unused top-level variables and types, imports and imported variables and types inside of a module. 8 | - [🔧 `NoUnused.CustomTypeConstructors`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-CustomTypeConstructors/ "Provides automatic fixes") - Reports unused constructors for a custom type. 9 | - [`NoUnused.CustomTypeConstructorArgs`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-CustomTypeConstructorArgs/ "Provides automatic fixes") - Reports arguments of custom type constructors that are never used. 10 | - [🔧 `NoUnused.Exports`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Exports/ "Provides automatic fixes") - Reports unused exposed elements from a module. 11 | - [🔧 `NoUnused.Dependencies`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Dependencies/ "Provides automatic fixes") - Reports unused dependencies in the project. 12 | - [🔧 `NoUnused.Parameters`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Parameters/ "Provides automatic fixes") - Report unused parameters. 13 | - [🔧 `NoUnused.Patterns`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Patterns/ "Provides automatic fixes") - Report useless patterns and pattern values that are not used. 14 | 15 | 16 | - **(DEPRECATED)** [`NoUnused.Modules`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Modules/ "Provides automatic fixes") - Reports unused modules in the project. 17 | 18 | ## Example configuration 19 | 20 | ```elm 21 | module ReviewConfig exposing (config) 22 | 23 | import NoUnused.CustomTypeConstructorArgs 24 | import NoUnused.CustomTypeConstructors 25 | import NoUnused.Dependencies 26 | import NoUnused.Exports 27 | import NoUnused.Parameters 28 | import NoUnused.Patterns 29 | import NoUnused.Variables 30 | import Review.Rule exposing (Rule) 31 | 32 | 33 | config : List Rule 34 | config = 35 | [ NoUnused.CustomTypeConstructors.rule [] 36 | , NoUnused.CustomTypeConstructorArgs.rule 37 | , NoUnused.Dependencies.rule 38 | , NoUnused.Exports.rule 39 | , NoUnused.Parameters.rule 40 | , NoUnused.Patterns.rule 41 | , NoUnused.Variables.rule 42 | ] 43 | ``` 44 | 45 | 46 | ## How this package works 47 | 48 | This package works by having several rules that check for different unused elements, and that complement each other. 49 | 50 | This allows for fine-grained control over what you want the rules to do. If you add these rules to an existing project, you will likely get a lot of errors, and fixing them will take time (though the autofixing will definitely help). Instead, you can introduce these rules gradually in batches. For cases where the errors are too time-consuming to fix, you can ignore them in the configuration, until you take care of them. 51 | 52 | A few of these rules provide automatic fixes using `elm-review --fix` or `elm-review --fix-all`. 53 | 54 | 55 | ## Try it out 56 | 57 | You can try the example configuration above out by running the following command: 58 | 59 | ```bash 60 | elm-review --template jfmengels/elm-review-unused/example 61 | ``` 62 | 63 | 64 | ## Thanks 65 | 66 | Thanks to @sparksp for writing [`NoUnused.Parameters`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Parameters/) 67 | and [`NoUnused.Patterns`](https://package.elm-lang.org/packages/jfmengels/elm-review-unused/1.2.4/NoUnused-Patterns/). 68 | -------------------------------------------------------------------------------- /src/List/Extra.elm: -------------------------------------------------------------------------------- 1 | module List.Extra exposing (dictToListFilterAndMap, dictToListMap, find, findMap, findMapWithIndex, indexedFilterMap, insertAllJusts, listFilterThenMapInto) 2 | 3 | {-| Some utilities. 4 | -} 5 | 6 | import Dict exposing (Dict) 7 | import Set exposing (Set) 8 | 9 | 10 | {-| Find the first element that satisfies a predicate and return 11 | Just that element. If none match, return Nothing. 12 | 13 | find (\num -> num > 5) [ 2, 4, 6, 8 ] == Just 6 14 | 15 | -} 16 | find : (a -> Bool) -> List a -> Maybe a 17 | find predicate list = 18 | case list of 19 | [] -> 20 | Nothing 21 | 22 | first :: rest -> 23 | if predicate first then 24 | Just first 25 | 26 | else 27 | find predicate rest 28 | 29 | 30 | findMap : (a -> Maybe b) -> List a -> Maybe b 31 | findMap mapper list = 32 | case list of 33 | [] -> 34 | Nothing 35 | 36 | first :: rest -> 37 | case mapper first of 38 | Just value -> 39 | Just value 40 | 41 | Nothing -> 42 | findMap mapper rest 43 | 44 | 45 | findMapWithIndex : (Int -> a -> Maybe b) -> List a -> Maybe b 46 | findMapWithIndex mapper list = 47 | findMapWithIndexHelp mapper 0 list 48 | 49 | 50 | findMapWithIndexHelp : (Int -> a -> Maybe b) -> Int -> List a -> Maybe b 51 | findMapWithIndexHelp mapper index list = 52 | case list of 53 | [] -> 54 | Nothing 55 | 56 | first :: rest -> 57 | case mapper index first of 58 | Just value -> 59 | Just value 60 | 61 | Nothing -> 62 | findMapWithIndexHelp mapper (index + 1) rest 63 | 64 | 65 | indexedFilterMap : (Int -> a -> Maybe b) -> Int -> List a -> List b -> List b 66 | indexedFilterMap predicate index list acc = 67 | case list of 68 | [] -> 69 | acc 70 | 71 | x :: xs -> 72 | indexedFilterMap predicate 73 | (index + 1) 74 | xs 75 | (case predicate index x of 76 | Just b -> 77 | b :: acc 78 | 79 | Nothing -> 80 | acc 81 | ) 82 | 83 | 84 | {-| Note: Doesn't preserve order of the list. 85 | -} 86 | listFilterThenMapInto : (a -> Bool) -> (a -> b) -> List a -> List b -> List b 87 | listFilterThenMapInto predicate mapper list acc = 88 | case list of 89 | [] -> 90 | acc 91 | 92 | x :: xs -> 93 | if predicate x then 94 | mapper x :: acc 95 | 96 | else 97 | listFilterThenMapInto predicate mapper xs acc 98 | 99 | 100 | dictToListMap : (k -> v -> a) -> Dict k v -> List a -> List a 101 | dictToListMap mapper dict baseAcc = 102 | Dict.foldr (\k v acc -> mapper k v :: acc) baseAcc dict 103 | 104 | 105 | dictToListFilterAndMap : (k -> Bool) -> (k -> v -> a) -> Dict k v -> List a -> List a 106 | dictToListFilterAndMap predicate mapper dict baseAcc = 107 | Dict.foldr 108 | (\k v acc -> 109 | if predicate k then 110 | mapper k v :: acc 111 | 112 | else 113 | acc 114 | ) 115 | baseAcc 116 | dict 117 | 118 | 119 | insertAllJusts : List ( a, Maybe comparable ) -> Set comparable -> Set comparable 120 | insertAllJusts list set = 121 | case list of 122 | [] -> 123 | set 124 | 125 | ( _, head ) :: rest -> 126 | case head of 127 | Nothing -> 128 | insertAllJusts rest set 129 | 130 | Just value -> 131 | insertAllJusts rest (Set.insert value set) 132 | -------------------------------------------------------------------------------- /src/NoUnused/NonemptyList.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.NonemptyList exposing 2 | ( Nonempty(..) 3 | , fromElement 4 | , head 5 | , cons, pop 6 | , mapHead 7 | ) 8 | 9 | {-| Copied contents of mgold/elm-nonempty-list, and trimmed down unused functions. 10 | 11 | This is to avoid dependency conflicts when mgold/elm-nonempty-list would release a new major version. 12 | 13 | A list that cannot be empty. The head and tail can be accessed without Maybes. Most other list functions are 14 | available. 15 | 16 | 17 | # Definition 18 | 19 | @docs Nonempty 20 | 21 | 22 | # Create 23 | 24 | @docs fromElement 25 | 26 | 27 | # Access 28 | 29 | @docs head 30 | 31 | 32 | # Convert 33 | 34 | @docs cons, pop 35 | 36 | 37 | # Map 38 | 39 | @docs mapHead 40 | 41 | 42 | # Original copyright notice 43 | 44 | Copyright (c) 2015, Max Goldstein 45 | 46 | All rights reserved. 47 | 48 | Redistribution and use in source and binary forms, with or without 49 | modification, are permitted provided that the following conditions are met: 50 | 51 | * Redistributions of source code must retain the above copyright 52 | notice, this list of conditions and the following disclaimer. 53 | 54 | * Redistributions in binary form must reproduce the above 55 | copyright notice, this list of conditions and the following 56 | disclaimer in the documentation and/or other materials provided 57 | with the distribution. 58 | 59 | * Neither the name of Max Goldstein nor the names of other 60 | contributors may be used to endorse or promote products derived 61 | from this software without specific prior written permission. 62 | 63 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 64 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 65 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 66 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 67 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 68 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 69 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 70 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 71 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 72 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 73 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 74 | 75 | -} 76 | 77 | 78 | {-| The Nonempty type. If you have both a head and tail, you can construct a 79 | nonempty list directly. Otherwise use the helpers below instead. 80 | -} 81 | type Nonempty a 82 | = Nonempty a (List a) 83 | 84 | 85 | {-| Create a singleton list with the given element. 86 | -} 87 | fromElement : a -> Nonempty a 88 | fromElement x = 89 | Nonempty x [] 90 | 91 | 92 | {-| Return the head of the list. 93 | -} 94 | head : Nonempty a -> a 95 | head (Nonempty x _) = 96 | x 97 | 98 | 99 | {-| Add another element as the head of the list, pushing the previous head to the tail. 100 | -} 101 | cons : a -> Nonempty a -> Nonempty a 102 | cons y (Nonempty x xs) = 103 | Nonempty y (x :: xs) 104 | 105 | 106 | {-| Pop and discard the head, or do nothing for a singleton list. Useful if you 107 | want to exhaust a list but hang on to the last item indefinitely. 108 | pop (Nonempty 3 [ 2, 1 ]) --> Nonempty 2 [1] 109 | pop (Nonempty 1 []) --> Nonempty 1 [] 110 | -} 111 | pop : Nonempty a -> Nonempty a 112 | pop ((Nonempty _ xs) as untouched) = 113 | case xs of 114 | [] -> 115 | untouched 116 | 117 | y :: ys -> 118 | Nonempty y ys 119 | 120 | 121 | {-| Map the head to a value of the same type 122 | -} 123 | mapHead : (a -> a) -> Nonempty a -> Nonempty a 124 | mapHead fn (Nonempty x xs) = 125 | Nonempty (fn x) xs 126 | -------------------------------------------------------------------------------- /tests/TestProject.elm: -------------------------------------------------------------------------------- 1 | module TestProject exposing (application, lamderaApplication, package) 2 | 3 | import Elm.Package 4 | import Elm.Project 5 | import Elm.Version 6 | import Json.Decode as Decode 7 | import Review.Project as Project exposing (Project) 8 | 9 | 10 | application : Project 11 | application = 12 | Project.new 13 | |> Project.addElmJson applicationElmJson 14 | 15 | 16 | lamderaApplication : Project 17 | lamderaApplication = 18 | Project.new 19 | |> Project.addElmJson lamderaApplicationElmJson 20 | 21 | 22 | applicationElmJson : { path : String, raw : String, project : Elm.Project.Project } 23 | applicationElmJson = 24 | { path = "elm.json" 25 | , raw = """{ 26 | "type": "application", 27 | "source-directories": [ 28 | "src" 29 | ], 30 | "elm-version": "0.19.1", 31 | "dependencies": { 32 | "direct": { 33 | "elm/core": "1.0.0" 34 | }, 35 | "indirect": {} 36 | }, 37 | "test-dependencies": { 38 | "direct": {}, 39 | "indirect": {} 40 | } 41 | }""" 42 | , project = 43 | Elm.Project.Application 44 | { elm = Elm.Version.one 45 | , dirs = [] 46 | , depsDirect = [ ( unsafePackageName "elm/core", Elm.Version.one ) ] 47 | , depsIndirect = [] 48 | , testDepsDirect = [] 49 | , testDepsIndirect = [] 50 | } 51 | } 52 | 53 | 54 | lamderaApplicationElmJson : { path : String, raw : String, project : Elm.Project.Project } 55 | lamderaApplicationElmJson = 56 | { path = "elm.json" 57 | , raw = """{ 58 | "type": "application", 59 | "source-directories": [ 60 | "src" 61 | ], 62 | "elm-version": "0.19.1", 63 | "dependencies": { 64 | "direct": { 65 | "elm/core": "1.0.0", 66 | "lamdera/core": "1.0.0" 67 | }, 68 | "indirect": {} 69 | }, 70 | "test-dependencies": { 71 | "direct": {}, 72 | "indirect": {} 73 | } 74 | }""" 75 | , project = 76 | Elm.Project.Application 77 | { elm = Elm.Version.one 78 | , dirs = [] 79 | , depsDirect = 80 | [ ( unsafePackageName "elm/core", Elm.Version.one ) 81 | , ( unsafePackageName "lamdera/core", Elm.Version.one ) 82 | ] 83 | , depsIndirect = [] 84 | , testDepsDirect = [] 85 | , testDepsIndirect = [] 86 | } 87 | } 88 | 89 | 90 | unsafePackageName : String -> Elm.Package.Name 91 | unsafePackageName packageName = 92 | case Elm.Package.fromString packageName of 93 | Just name -> 94 | name 95 | 96 | Nothing -> 97 | Debug.todo ("Failed to convert " ++ packageName ++ " to a package name") 98 | 99 | 100 | package : Project 101 | package = 102 | Project.new 103 | |> Project.addElmJson (createPackageElmJson ()) 104 | 105 | 106 | createPackageElmJson : () -> { path : String, raw : String, project : Elm.Project.Project } 107 | createPackageElmJson () = 108 | case Decode.decodeString Elm.Project.decoder rawPackageElmJson of 109 | Ok elmJson -> 110 | { path = "elm.json" 111 | , raw = rawPackageElmJson 112 | , project = elmJson 113 | } 114 | 115 | Err err -> 116 | Debug.todo ("Invalid elm.json supplied to test: " ++ Debug.toString err) 117 | 118 | 119 | rawPackageElmJson : String 120 | rawPackageElmJson = 121 | """{ 122 | "type": "package", 123 | "name": "author/package", 124 | "summary": "Summary", 125 | "license": "BSD-3-Clause", 126 | "version": "1.0.0", 127 | "exposed-modules": [ 128 | "Exposed" 129 | ], 130 | "elm-version": "0.19.0 <= v < 0.20.0", 131 | "dependencies": {}, 132 | "test-dependencies": {} 133 | }""" 134 | -------------------------------------------------------------------------------- /elm-review-package-tests/check-previews-compile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const Ansi = require('./helpers/ansi'); 5 | const {execSync} = require('child_process'); 6 | const {findPreviewConfigurations} = require('./helpers/find-configurations'); 7 | const packageDependencies = require('../elm.json').dependencies; 8 | 9 | const root = path.dirname(__dirname); 10 | 11 | // Find all elm.json files 12 | 13 | findPreviewConfigurations().forEach(checkThatExampleCompiles); 14 | 15 | function checkThatExampleCompiles(exampleConfiguration) { 16 | const exampleConfigurationElmJson = require(`${exampleConfiguration}/elm.json`); 17 | 18 | checkDepsAreCompatible( 19 | path.basename(exampleConfiguration), 20 | exampleConfigurationElmJson.dependencies.direct 21 | ); 22 | 23 | try { 24 | execSync(`npx elm-review --config ${exampleConfiguration} --report=json`, { 25 | encoding: 'utf8', 26 | stdio: 'pipe', 27 | cwd: path.resolve(__dirname, '..') 28 | }).toString(); 29 | success(exampleConfiguration); 30 | } catch (error) { 31 | try { 32 | const output = JSON.parse(error.stdout); 33 | // We don't care whether there were any reported errors. 34 | // If the root type is not "error", then the configuration compiled 35 | // successfully, which is all we care about in this test. 36 | if (output.type !== 'review-errors') { 37 | console.log( 38 | `${Ansi.red('✖')} ${Ansi.yellow( 39 | `${path.relative(root, exampleConfiguration)}/` 40 | )} does not compile.` 41 | ); 42 | console.log( 43 | `Please run 44 | ${Ansi.yellow(`npx elm-review --config ${exampleConfiguration}/`)} 45 | and make the necessary changes to make it compile.` 46 | ); 47 | process.exit(1); 48 | } 49 | 50 | success(exampleConfiguration); 51 | return; 52 | } catch { 53 | console.log( 54 | `An error occurred while trying to check whether the ${Ansi.yellow( 55 | path.relative(root, exampleConfiguration) 56 | )} configuration compiles.` 57 | ); 58 | console.error(error); 59 | process.exit(1); 60 | } 61 | } 62 | } 63 | 64 | function success(config) { 65 | console.log(`${Ansi.green('✔')} ${path.relative(root, config)}/ compiles`); 66 | } 67 | 68 | function checkDepsAreCompatible(exampleConfiguration, previewDependencies) { 69 | Object.entries(packageDependencies).forEach(([depName, constraint]) => { 70 | if (!(depName in previewDependencies)) { 71 | console.error( 72 | `Dependency ${depName} is missing in the ${exampleConfiguration}/ configuration` 73 | ); 74 | process.exit(1); 75 | } 76 | 77 | checkConstraint( 78 | exampleConfiguration, 79 | depName, 80 | constraint, 81 | previewDependencies[depName] 82 | ); 83 | delete previewDependencies[depName]; 84 | }); 85 | 86 | const remainingKeys = Object.keys(previewDependencies); 87 | if (remainingKeys.length !== 0) { 88 | console.error( 89 | `There are extraneous dependencies in the ${exampleConfiguration}/ configuration: ${remainingKeys}` 90 | ); 91 | process.exit(1); 92 | } 93 | } 94 | 95 | function checkConstraint(exampleConfiguration, depName, constraint, version) { 96 | const [minVersion] = constraint.split(' <= v < ').map(splitVersion); 97 | const previewVersion = splitVersion(version); 98 | const isValid = 99 | previewVersion[0] === minVersion[0] && 100 | (previewVersion[1] > minVersion[1] || 101 | (previewVersion[1] === minVersion[1] && 102 | previewVersion[2] >= minVersion[2])); 103 | if (!isValid) { 104 | console.error( 105 | `The constraint for ${depName} in ${exampleConfiguration}/ is not in the expected range. It was ${version} but it should be in ${constraint} to be in sync with the package's elm.json's dependencies.` 106 | ); 107 | process.exit(1); 108 | } 109 | } 110 | 111 | function splitVersion(version) { 112 | return version.split('.').map((n) => Number.parseInt(n, 10)); 113 | } 114 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the main branch 5 | on: [push, pull_request] 6 | 7 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 8 | jobs: 9 | test: 10 | # The type of runner that the job will run on 11 | runs-on: ubuntu-latest 12 | 13 | # Steps represent a sequence of tasks that will be executed as part of the job 14 | steps: 15 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 16 | - uses: actions/checkout@v3 17 | 18 | - name: Setup Node.js environment 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: lts/* 22 | 23 | # Re-use node_modules between runs until package-lock.json changes. 24 | - name: Cache node_modules 25 | id: internal-cache-node_modules 26 | uses: actions/cache@v3 27 | with: 28 | path: node_modules 29 | key: internal-node_modules-ubuntu-latest.x-${{ hashFiles('package-lock.json') }} 30 | 31 | # Re-use ~/.elm between runs until elm.json, elm-tooling.json or 32 | # review/elm.json changes. The Elm compiler saves downloaded Elm packages 33 | # to ~/.elm, and elm-tooling saves downloaded tool executables there. 34 | - name: Cache ~/.elm 35 | uses: actions/cache@v3 36 | with: 37 | path: ~/.elm 38 | key: elm-${{ hashFiles('elm.json', 'elm-tooling.json', 'review/elm.json') }} 39 | 40 | - name: Install npm dependencies 41 | if: steps.cache-node_modules.outputs.cache-hit != 'true' 42 | env: 43 | # If you have a `"postinstall": "elm-tooling install"` script in your 44 | # package.json, this turns it into a no-op. We’ll run it in the next 45 | # step because of the caching. If elm-tooling.json changes but 46 | # package-lock.json does not, the postinstall script needs running 47 | # but this step won’t. 48 | NO_ELM_TOOLING_INSTALL: 1 49 | run: npm ci 50 | 51 | # Install tools from elm-tooling.json, unless we restored them from 52 | # cache. package-lock.json and elm-tooling.json can change independently, 53 | # so we need to install separately based on what was restored from cache. 54 | # This is run even if we restored ~/.elm from cache to be 100% sure 55 | # node_modules/.bin/ contains links to all your tools. `elm-tooling 56 | # install` runs very fast when there’s nothing new to download so 57 | # skipping the step doesn’t save much time. 58 | - name: elm-tooling install 59 | run: npx --no-install elm-tooling install 60 | 61 | - name: Run tests 62 | run: npm test 63 | 64 | publish: 65 | needs: [test] # make sure all your other jobs succeed before trying to publish 66 | 67 | # The type of runner that the job will run on 68 | runs-on: ubuntu-latest 69 | 70 | # Steps represent a sequence of tasks that will be executed as part of the job 71 | steps: 72 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 73 | - uses: actions/checkout@v3 74 | 75 | - name: Setup Node.js environment 76 | uses: actions/setup-node@v3 77 | with: 78 | node-version: lts/* 79 | 80 | # Re-use node_modules between runs until package-lock.json changes. 81 | - name: Cache node_modules 82 | id: internal-cache-node_modules 83 | uses: actions/cache@v3 84 | with: 85 | path: node_modules 86 | key: internal-node_modules-ubuntu-latest.x-${{ hashFiles('package-lock.json') }} 87 | 88 | # Re-use ~/.elm between runs until elm.json, elm-tooling.json or 89 | # review/elm.json changes. The Elm compiler saves downloaded Elm packages 90 | # to ~/.elm, and elm-tooling saves downloaded tool executables there. 91 | - name: Cache ~/.elm 92 | uses: actions/cache@v3 93 | with: 94 | path: ~/.elm 95 | key: elm-${{ hashFiles('elm.json', 'elm-tooling.json', 'review/elm.json') }} 96 | 97 | - name: Install npm dependencies 98 | if: steps.cache-node_modules.outputs.cache-hit != 'true' 99 | env: 100 | # If you have a `"postinstall": "elm-tooling install"` script in your 101 | # package.json, this turns it into a no-op. We’ll run it in the next 102 | # step because of the caching. If elm-tooling.json changes but 103 | # package-lock.json does not, the postinstall script needs running 104 | # but this step won’t. 105 | NO_ELM_TOOLING_INSTALL: 1 106 | run: npm ci 107 | 108 | # Install tools from elm-tooling.json, unless we restored them from 109 | # cache. package-lock.json and elm-tooling.json can change independently, 110 | # so we need to install separately based on what was restored from cache. 111 | # This is run even if we restored ~/.elm from cache to be 100% sure 112 | # node_modules/.bin/ contains links to all your tools. `elm-tooling 113 | # install` runs very fast when there’s nothing new to download so 114 | # skipping the step doesn’t save much time. 115 | - name: elm-tooling install 116 | run: npx --no-install elm-tooling install 117 | 118 | - name: Check if package needs to be published 119 | uses: dillonkearns/elm-publish-action@v1 120 | id: publish 121 | with: 122 | dry-run: true 123 | path-to-elm: ./node_modules/.bin/elm 124 | 125 | - name: Check that examples are up to date 126 | if: steps.publish.outputs.is-publishable == 'true' 127 | run: node elm-review-package-tests/check-examples-were-updated.js 128 | 129 | # Runs a single command using the runners shell 130 | - name: Elm Publish 131 | if: steps.publish.outputs.is-publishable == 'true' 132 | uses: dillonkearns/elm-publish-action@v1 133 | with: 134 | # Token provided by GitHub 135 | github-token: ${{ secrets.GITHUB_TOKEN }} 136 | path-to-elm: ./node_modules/.bin/elm 137 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | - Fixed an issue for [`NoUnused.Exports`] where a custom type did not get reported when one of its constructors was named like another type. 6 | Example, where type `Unused` was incorrectly considered as used: 7 | ```elm 8 | type alias T = () 9 | type Unused = T 10 | value : T 11 | value = () 12 | ``` 13 | - [`NoUnused.Exports`] now reports (and fixes) unnecessary exposing of a custom type's variants in a module's `exposing` list. 14 | ```diff 15 | -module A exposing (Type(..)) 16 | +module A exposing (Type) 17 | ``` 18 | - [`NoUnused.Variables`] now reports (and fixes) unnecessary imports to functions/types available by default (`Basics`, `List`, etc.) 19 | - [`NoUnused.Dependencies`] now doesn't report `elm/json` as an unused dependency for applications (as not depending on it yields compiler errors). 20 | - [`NoUnused.CustomTypeConstructor`] now gives a hint on how to not have phantom errors reported. 21 | 22 | ## [1.2.4] - 2025-02-11 23 | 24 | Now requires `jfmengels/elm-review` v2.15.0. 25 | `NoUnused.CustomTypeConstructors` provides automatic fixes even if the unused constructor is referenced in other files. 26 | 27 | `NoUnused.Exports` now provides an automatic fix to remove unused modules when using `elm-review --fix --allow-remove-files`. 28 | 29 | ## [1.2.3] - 2024-04-10 30 | 31 | `NoUnused.Variables` now finds unused imported functions when they have been shadowed by let destructuring variables. Thanks [@matzko](https://github.com/matzko). 32 | 33 | ## [1.2.2] - 2024-04-02 34 | 35 | Fixed issues in [`NoUnused.Exports`] where type aliases and custom types would incorrectly be reported. Thanks Eric from the Elm Slack for reporting! 36 | 37 | ## [1.2.1] - 2024-04-01 38 | 39 | - [`NoUnused.Exports`] now reports (and removes using `--fix`) elements even if the module is exposing everything (`module X exposing (..)`) if the element is unused both locally and across the project. Thanks [@tfausak](https://github.com/tfausak). 40 | 41 | ## [1.2.0] - 2023-07-28 42 | 43 | Add configuration setting for [`NoUnused.Exports`] for reporting exports that is only used in non-production code. This includes multiple new functions and types in that module centered around `reportUnusedProductionExports`. 44 | 45 | ## [1.1.30] - 2023-06-16 46 | 47 | Fix false positive in [`NoUnused.CustomTypeConstructors`] related to non-ASCII constructor names ([#79](https://github.com/jfmengels/elm-review-unused/pull/79)). Thanks [@lydell](https://github.com/lydell) and [@marc136](https://github.com/marc136). 48 | 49 | ## [1.1.29] - 2022-12-07 50 | 51 | Fix false positive in [`NoUnused.CustomTypeConstructors`]. 52 | 53 | ## [1.1.28] - 2022-11-08 54 | 55 | Add better support for `jfmengels/elm-review` v2.10.0. 56 | 57 | ## [1.1.27] - 2022-09-19 58 | 59 | This release contains HUGE performance updates. Rough benchmarks show some of the slow rules ([`NoUnused.Exports`], [`NoUnused.CustomTypeConstructors`] and [`NoUnused.CustomTypeConstructorArgs`]) are now 20-40 times faster compared to the previous release. 60 | 61 | ## [1.1.26] - 2022-09-11 62 | 63 | - Fixed an issue in [`NoUnused.Dependencies`] that led the `elm.json` to become corrupt 64 | 65 | ## [1.1.25] - 2022-09-10 66 | 67 | - [`NoUnused.Patterns`] now reports multiple aliases `(((A a) as y) as z)` 68 | - [`NoUnused.Patterns`] now reports aliases on a variable pattern `(name as otherName)` 69 | - [`NoUnused.Patterns`] now reports aliases to wildcard `(_ as thing)` in parameters, and [`NoUnused.Parameters`] now doesn't 70 | - Improved the fixes for [`NoUnused.Patterns`] to not include unnecessary patterns 71 | - Improved the fixes for [`NoUnused.Patterns`] to have formatting look more like `elm-format` 72 | 73 | ## [1.1.24] - 2022-09-02 74 | 75 | - [`NoUnused.Variables`] now reports imports that get shadowed by other imports ([252475888b79a88f107571c1002d0ed650622ddb]) 76 | 77 | ## [1.1.23] - 2022-08-24 78 | 79 | This version merges the [`NoUnused.Modules`] into the [`NoUnused.Exports`] rule. 80 | 81 | A common issue when running `elm-review --fix` (or `--fix-all`) was when you had the two rules enabled and encountered an unused module. 82 | 83 | While in fix mode, `NoUnused.Exports` would remove every export one at a time, which would likely be followed by 84 | [`NoUnused.Variables`] removing the previously exported element. This would go on until the module is as empty as it can 85 | be. At this point, you would finally be able to see `NoUnused.Modules`'s error indicating that the module is unused. 86 | 87 | Whether you want to remove the module or use it somewhere in response to this message, this is a lot of unnecessary work 88 | for you and/or the tool, making `--fix-all` painfully long. 89 | 90 | By having the `NoUnused.Exports` do the work of both rules, and not reporting any unused exports when the entire module 91 | is unused, this situation should not happen anymore, or not as exacerbated. 92 | 93 | [`NoUnused.Modules`] is therefore now deprecated and should not be used anymore. It is removed from the `example` 94 | configuration. 95 | 96 | 97 | ## [1.1.22] - 2022-04-14 98 | 99 | - Fixed an issue in [`NoUnused.Variables`] where removing a let declaration would not always remove its type annotation ([24237116ada98791d8ff79630ca5d4eb632ef6ea]) 100 | 101 | 102 | ## Missing changelog 103 | 104 | Help would be appreciated to fill the blanks! 105 | 106 | [Unreleased]: https://github.com/jfmengels/elm-review-unused/compare/v1.2.4...HEAD 107 | [1.2.4]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.2.4 108 | [1.2.3]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.2.3 109 | [1.2.2]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.2.2 110 | [1.2.1]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.2.1 111 | [1.2.0]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.2.0 112 | [1.1.30]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.30 113 | [1.1.29]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.29 114 | [1.1.28]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.28 115 | [1.1.27]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.27 116 | [1.1.26]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.26 117 | [1.1.25]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.25 118 | [1.1.24]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.24 119 | [1.1.23]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.23 120 | [1.1.22]: https://github.com/jfmengels/elm-review-unused/releases/tag/1.1.22 121 | 122 | [252475888b79a88f107571c1002d0ed650622ddb]: https://github.com/jfmengels/elm-review-unused/commit/252475888b79a88f107571c1002d0ed650622ddb 123 | [24237116ada98791d8ff79630ca5d4eb632ef6ea]: https://github.com/jfmengels/elm-review-unused/commit/24237116ada98791d8ff79630ca5d4eb632ef6ea 124 | 125 | [`NoUnused.Modules`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-Modules) 126 | [`NoUnused.Exports`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-Exports) 127 | [`NoUnused.Variables`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-Variables) 128 | [`NoUnused.Patterns`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-Patterns) 129 | [`NoUnused.Parameters`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-Parameters) 130 | [`NoUnused.Dependencies`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-Dependencies) 131 | [`NoUnused.CustomTypeConstructors`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-CustomTypeConstructors) 132 | [`NoUnused.CustomTypeConstructorArgs`]: (https://package.elm-lang.org/packages/jfmengels/elm-review-unused/latest/NoUnused-CustomTypeConstructorArgs) 133 | -------------------------------------------------------------------------------- /maintenance/MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # How to maintain this package 2 | 3 | This document has been created along with the project through the `elm-review new-package` command. 4 | 5 | A lot of things should happen automatically for you. Here is an overview of what you should know and what to expect. 6 | 7 | We'll discuss 8 | - [After creation](#after-creation) 9 | - [Writing rules](#writing-rules) 10 | - [Example and preview configurations](#example-and-preview-configurations) 11 | - [Publishing](#publishing) 12 | 13 | This document and the set up created for you is aimed at helping you work and improving the quality of the package. You can however opt out of all of this if you encounter problems or it doesn't suit you for any reason. If that happens, please contact the `elm-review` maintainers so that they can work on improvements. 14 | 15 | 16 | ## After creation 17 | 18 | Right after you have created the package, you should 19 | 20 | ### 1. Install the `npm` dependencies 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | If you prefer using `yarn` or another package manager, you can do so, but you should update the 2 "Install npm dependencies" scripts in `.github/workflows/test.yml` so that they use your preferred package manager. 27 | 28 | Note that [`elm-tooling`](https://elm-tooling.github.io/elm-tooling-cli/) takes care of some of the Elm dependencies, notably `elm` and `elm-format`. Their versions are defined in the `elm-tooling.json` file, and are automatically installed through the `postinstall` script/hook in `package.json`. 29 | 30 | ### 2. Set up `Git` 31 | 32 | ```bash 33 | git init 34 | git add --all 35 | git commit --message="Initialize project" 36 | ``` 37 | 38 | ### 3. Replace REPLACEME 39 | 40 | In some of the files, notably `elm.json`, `README.md` and the rule files that were created for you, you will find a few `REPLACEME`. You will need to replace all of these and by things that make sense in their individual context. 41 | 42 | Again, you can do this step at a later time if you prefer, but you will have to do these before publishing. You will be reminded to do this when running the tests. 43 | 44 | Note that you will also have to supply the `summary` field in the `elm.json`, which should be close to the same thing that you will write in the README. 45 | 46 | ### 4. (Can be done later) Create the project on GitHub 47 | 48 | You can do this step at a later time if you prefer. 49 | When you do, consider to 50 | 51 | - Adding the `elm-review` tag, so that your project appears in [this list](https://github.com/topics/elm-review). 52 | - [Adding a code of conduct](https://docs.github.com/en/github/building-a-strong-community/adding-a-code-of-conduct-to-your-project) 53 | - [Adding issue and pull request templates](https://docs.github.com/en/github/building-a-strong-community/using-templates-to-encourage-useful-issues-and-pull-requests) 54 | - [Setting guidelines for repository contributors](https://docs.github.com/en/github/building-a-strong-community/setting-guidelines-for-repository-contributors) 55 | 56 | 57 | ## Writing rules 58 | 59 | You can read how to use `elm-review`'s API to write a rule [here](https://package.elm-lang.org/packages/jfmengels/elm-review/latest/Review-Rule). 60 | 61 | I highly recommend writing rules in a test-driven manner (TDD), because the tests you write will dramatically increase the quality of the rule, and because they will make the development quite easy. You can read more about it [here](https://package.elm-lang.org/packages/jfmengels/elm-review/latest/Review-Test). 62 | 63 | If you want to preview the documentation of the package, you can run `npm run preview-docs`, which will run a local version of [`elm-doc-preview`](https://elm-doc-preview.netlify.app/). If you see `docs.json` in your project, please commit it. This will allow you to share the latest version of the docs with collaborators using the online version of [`elm-doc-preview`](https://elm-doc-preview.netlify.app/). 64 | 65 | ### Adding a new rule 66 | 67 | It is better if rule packages contain multiple rules dealing with the same or similar concerns, rather than a single rule. Therefore, you will likely add a new rule to the package at one point. 68 | 69 | To do so, I recommend using the `elm-review new-rule` command. This will prompt you for the name of the rule, and then create a source file and a test file, and update the `elm.json` and the `README.md`, and insert the rule into the preview configurations. 70 | 71 | 72 | ## Example and preview configurations 73 | 74 | In the rules generated by `new-package` and `new-rule`, and also in the README, you will see a section named "Try it out" which recommends using a command that looks like this: 75 | 76 | ```bash 77 | elm-review --template //example --rules 78 | ``` 79 | 80 | This enables people to run the rule without having to set up `elm-review`, add your package as a dependency, add the rules to their configuration and then run it. This is an easy way to try out the rules and see if they can be useful to them. In order for this to work, you will need to do a little bit of work, but this will be useful to you too! 81 | 82 | There are two folders that will exist in your folder that will help make this work, `preview/` and `example/`. Both are review configurations and they serve similar purposes. 83 | - `example/` is the configuration that works with the last **released** version (as a dependency). It should not change in a way that would not work with the latest released version (no unpublished rules, no new arguments, etc.). Examples in the documentation will use this configuration, which is why it should remain stable. 84 | - `preview/` is the configuration that works with the current version of the source code (as a source directory). It can change however/whenever you see fit. You can use this configuration to let users/testers try out new rules or bug fixes to published rules **before releasing a new version**. 85 | 86 | When the project gets created, you will only have a `preview/` folder. You will create `example/` (automatically) when you are ready to [publish the initial release](#initial-release), using `node maintenance/update-examples-from-preview.js` which will copy the contents of the `preview/` configuration and adapt it to use the package as a dependency instead of source directories. Before every release, you will have to run this same command, otherwise tests in the CI will fail when attempting to publish the package. You should ideally not change `example` yourself, and instead consider `preview/` as the source of truth. 87 | 88 | In practice, you are not limited to a single example and a single preview configuration. You can add new configurations and name them however you want. The pre-made scripts will look for any project (containing an `elm.json` at its root) in directories and sub-directories whose name start with `preview`. 89 | You can therefore have `preview/with-configuration-A/` and `preview/with-configuration-B/`, or `preview-with-configuration-A/` and `preview-with-configuration-B/`. When creating the example configurations from these, their names will be the same, except that "preview" in their names will be changed to "example". 90 | 91 | 92 | ## Publishing 93 | 94 | ### Initial release 95 | 96 | The initial release has to be done manually. I recommend running the following script: 97 | 98 | ```bash 99 | # Make sure your tests pass. Fix them if necessary 100 | npm test 101 | 102 | # Generate the example configurations 103 | node maintenance/update-examples-from-preview.js 104 | git add --all 105 | git commit --message '1.0.0' 106 | 107 | # Commit 108 | git tag 1.0.0 109 | git push --tags origin $(git_main_branch) 110 | elm publish 111 | ``` 112 | 113 | ### Successive releases 114 | 115 | Contrary to the initial release, the CI will automatically try to publish a new version of the package when the version in the `elm.json` is bumped. There is **no need** to add the Git tag or to run `elm publish` yourself! More details [here](https://github.com/dillonkearns/elm-publish-action). 116 | 117 | Here is a script that you can run to publish your package, which will help you avoid errors showing up at the CI stage. 118 | 119 | ```bash 120 | npm run elm-bump 121 | 122 | # Commit it all 123 | git add --all 124 | git commit # You'll need to specify a message 125 | git push origin HEAD 126 | 127 | # Now wait for CI to finish and check that it succeeded 128 | ``` 129 | -------------------------------------------------------------------------------- /tests/NoUnused/ModulesTest.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.ModulesTest exposing (all) 2 | 3 | import NoUnused.Modules exposing (rule) 4 | import Review.Test 5 | import Test exposing (Test, describe, test) 6 | import TestProject exposing (application, lamderaApplication, package) 7 | 8 | 9 | details : List String 10 | details = 11 | [ "This module is never used. You may want to remove it to keep your project clean, and maybe detect some unused code in your project." 12 | ] 13 | 14 | 15 | all : Test 16 | all = 17 | describe "NoUnusedModules" 18 | [ test "should not report a module when all modules are used" <| 19 | \() -> 20 | [ """ 21 | module NotReported exposing (..) 22 | import OtherModule 23 | main = text "" 24 | """ 25 | , """ 26 | module OtherModule exposing (..) 27 | a = 1 28 | """ 29 | ] 30 | |> Review.Test.runOnModulesWithProjectData application rule 31 | |> Review.Test.expectNoErrors 32 | , test "should report a module when it is never used" <| 33 | \() -> 34 | [ """ 35 | module NotReported exposing (..) 36 | main = text "" 37 | """ 38 | , """ 39 | module Reported exposing (..) 40 | a = 1 41 | """ 42 | , """ 43 | module Other.Reported exposing (..) 44 | a = 1 45 | """ 46 | ] 47 | |> Review.Test.runOnModulesWithProjectData application rule 48 | |> Review.Test.expectErrorsForModules 49 | [ ( "Reported" 50 | , [ Review.Test.error 51 | { message = "Module `Reported` is never used." 52 | , details = details 53 | , under = "Reported" 54 | } 55 | ] 56 | ) 57 | , ( "Other.Reported" 58 | , [ Review.Test.error 59 | { message = "Module `Other.Reported` is never used." 60 | , details = details 61 | , under = "Other.Reported" 62 | } 63 | ] 64 | ) 65 | ] 66 | , test "should report a module even if it is the only module in the project" <| 67 | \() -> 68 | """ 69 | module Reported exposing (..) 70 | import Something 71 | a = 1 72 | """ 73 | |> Review.Test.runWithProjectData application rule 74 | |> Review.Test.expectErrors 75 | [ Review.Test.error 76 | { message = "Module `Reported` is never used." 77 | , details = details 78 | , under = "Reported" 79 | } 80 | ] 81 | , test "should not report an application module if it exposes a main function" <| 82 | \() -> 83 | """ 84 | module NotReported exposing (..) 85 | main = text "" 86 | """ 87 | |> Review.Test.runWithProjectData application rule 88 | |> Review.Test.expectNoErrors 89 | , test "should not report an application module if it contains a main function even if it is not exposed" <| 90 | \() -> 91 | """ 92 | module NotReported exposing (a) 93 | main = text "" 94 | a = 1 95 | """ 96 | |> Review.Test.runWithProjectData application rule 97 | |> Review.Test.expectNoErrors 98 | , test "should not report a module with main function if we don't know the project type" <| 99 | \() -> 100 | """ 101 | module NotReported exposing (a) 102 | main = text "" 103 | a = 1 104 | """ 105 | |> Review.Test.runWithProjectData application rule 106 | |> Review.Test.expectNoErrors 107 | , test "should not report a module if it imports `Test`" <| 108 | \() -> 109 | """ 110 | module NotReported exposing (..) 111 | import Test 112 | a = 1 113 | """ 114 | |> Review.Test.runWithProjectData application rule 115 | |> Review.Test.expectNoErrors 116 | , test "should not report the `ReviewConfig` module" <| 117 | \() -> 118 | """ 119 | module ReviewConfig exposing (config) 120 | config = [] 121 | """ 122 | |> Review.Test.runWithProjectData application rule 123 | |> Review.Test.expectNoErrors 124 | , test "should not report modules exposed in a package" <| 125 | \() -> 126 | """ 127 | module Exposed exposing (..) 128 | a = 1 129 | """ 130 | |> Review.Test.runWithProjectData package rule 131 | |> Review.Test.expectNoErrors 132 | , test "should report non-exposed and non-used modules from a package" <| 133 | \() -> 134 | """ 135 | module NotExposed exposing (..) 136 | a = 1 137 | """ 138 | |> Review.Test.runWithProjectData package rule 139 | |> Review.Test.expectErrors 140 | [ Review.Test.error 141 | { message = "Module `NotExposed` is never used." 142 | , details = details 143 | , under = "NotExposed" 144 | } 145 | ] 146 | , test "should report non-exposed and non-used package modules that expose a `main` function" <| 147 | \() -> 148 | """ 149 | module Reported exposing (main) 150 | main = text "" 151 | """ 152 | |> Review.Test.runWithProjectData package rule 153 | |> Review.Test.expectErrors 154 | [ Review.Test.error 155 | { message = "Module `Reported` is never used." 156 | , details = details 157 | , under = "Reported" 158 | } 159 | ] 160 | , test "should report non-exposed and non-used package modules that define a `main` function" <| 161 | \() -> 162 | """ 163 | module Reported exposing (a) 164 | main = text "" 165 | a = 1 166 | """ 167 | |> Review.Test.runWithProjectData package rule 168 | |> Review.Test.expectErrors 169 | [ Review.Test.error 170 | { message = "Module `Reported` is never used." 171 | , details = details 172 | , under = "Reported" 173 | } 174 | ] 175 | , test "should report modules that contain a top-level `app` function in packages" <| 176 | \() -> 177 | """ 178 | module Reported exposing (app) 179 | app = text "" 180 | """ 181 | |> Review.Test.runWithProjectData package rule 182 | |> Review.Test.expectErrors 183 | [ Review.Test.error 184 | { message = "Module `Reported` is never used." 185 | , details = details 186 | , under = "Reported" 187 | } 188 | ] 189 | , test "should report modules that contain a top-level `app` function in Elm applications" <| 190 | \() -> 191 | """ 192 | module Reported exposing (app) 193 | app = text "" 194 | """ 195 | |> Review.Test.runWithProjectData application rule 196 | |> Review.Test.expectErrors 197 | [ Review.Test.error 198 | { message = "Module `Reported` is never used." 199 | , details = details 200 | , under = "Reported" 201 | } 202 | ] 203 | , test "should not report modules that contain a top-level `app` function in Lamdera applications" <| 204 | \() -> 205 | """ 206 | module Reported exposing (app) 207 | app = text "" 208 | """ 209 | |> Review.Test.runWithProjectData lamderaApplication rule 210 | |> Review.Test.expectNoErrors 211 | , test "should not report modules that contain a top-level `main` function in Lamdera applications" <| 212 | \() -> 213 | """ 214 | module Reported exposing (main) 215 | main = text "" 216 | """ 217 | |> Review.Test.runWithProjectData lamderaApplication rule 218 | |> Review.Test.expectNoErrors 219 | ] 220 | -------------------------------------------------------------------------------- /src/NoUnused/Modules.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.Modules exposing (rule) 2 | 3 | {-| Forbid the use of modules that are never used in your project. 4 | 5 | **@deprecated** This rule has been deprecated, as it has now been integrated into [`NoUnused.Exports`](NoUnused-Exports). 6 | You should use that rule instead. 7 | 8 | @docs rule 9 | 10 | -} 11 | 12 | import Dict exposing (Dict) 13 | import Elm.Module 14 | import Elm.Project exposing (Project) 15 | import Elm.Syntax.Declaration as Declaration exposing (Declaration) 16 | import Elm.Syntax.Import exposing (Import) 17 | import Elm.Syntax.ModuleName exposing (ModuleName) 18 | import Elm.Syntax.Node as Node exposing (Node(..)) 19 | import Elm.Syntax.Range exposing (Range) 20 | import NoUnused.LamderaSupport as LamderaSupport 21 | import Review.Rule as Rule exposing (Error, Rule) 22 | import Set exposing (Set) 23 | 24 | 25 | {-| Forbid the use of modules that are never used in your project. 26 | 27 | A module is considered used if 28 | 29 | - it contains a `main` function (be it exposed or not) 30 | - it imports the `Test` module 31 | - it is imported in any other modules, even if it is not used. 32 | - the project is a package and the module is part of the `elm.json`'s `exposed-modules` 33 | - it is named `ReviewConfig` 34 | 35 | ```elm 36 | config = 37 | [ NoUnused.Modules.rule 38 | ] 39 | ``` 40 | 41 | 42 | ## Try it out 43 | 44 | You can try this rule out by running the following command: 45 | 46 | ```bash 47 | elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Modules 48 | ``` 49 | 50 | -} 51 | rule : Rule 52 | rule = 53 | Rule.newProjectRuleSchema "NoUnused.Modules" initialProjectContext 54 | |> Rule.withModuleVisitor moduleVisitor 55 | |> Rule.withModuleContextUsingContextCreator 56 | { fromProjectToModule = fromProjectToModule 57 | , fromModuleToProject = fromModuleToProject 58 | , foldProjectContexts = foldProjectContexts 59 | } 60 | |> Rule.withElmJsonProjectVisitor elmJsonVisitor 61 | |> Rule.withFinalProjectEvaluation finalEvaluationForProject 62 | |> Rule.fromProjectRuleSchema 63 | 64 | 65 | moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext 66 | moduleVisitor schema = 67 | schema 68 | |> Rule.withImportVisitor importVisitor 69 | |> Rule.withDeclarationListVisitor declarationListVisitor 70 | 71 | 72 | 73 | -- CONTEXT 74 | 75 | 76 | type alias ProjectContext = 77 | { modules : 78 | Dict 79 | ModuleName 80 | { moduleKey : Rule.ModuleKey 81 | , moduleNameLocation : Range 82 | } 83 | , usedModules : Set ModuleName 84 | , projectType : ProjectType 85 | } 86 | 87 | 88 | type alias ModuleContext = 89 | { importedModules : Set ModuleName 90 | , containsMainFunction : Bool 91 | , projectType : ProjectType 92 | } 93 | 94 | 95 | type ProjectType 96 | = Package 97 | | Application ElmApplicationType 98 | 99 | 100 | type ElmApplicationType 101 | = ElmApplication 102 | | LamderaApplication 103 | 104 | 105 | initialProjectContext : ProjectContext 106 | initialProjectContext = 107 | { modules = Dict.empty 108 | , usedModules = Set.singleton [ "ReviewConfig" ] 109 | , projectType = Application ElmApplication 110 | } 111 | 112 | 113 | fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext 114 | fromProjectToModule = 115 | Rule.initContextCreator 116 | (\projectContext -> 117 | { importedModules = Set.empty 118 | , containsMainFunction = False 119 | , projectType = projectContext.projectType 120 | } 121 | ) 122 | 123 | 124 | fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext 125 | fromModuleToProject = 126 | Rule.initContextCreator 127 | (\(Node moduleNameRange moduleName) moduleKey moduleContext -> 128 | { modules = 129 | Dict.singleton 130 | moduleName 131 | { moduleKey = moduleKey, moduleNameLocation = moduleNameRange } 132 | , usedModules = 133 | if Set.member [ "Test" ] moduleContext.importedModules || moduleContext.containsMainFunction then 134 | Set.insert moduleName moduleContext.importedModules 135 | 136 | else 137 | moduleContext.importedModules 138 | , projectType = moduleContext.projectType 139 | } 140 | ) 141 | |> Rule.withModuleNameNode 142 | |> Rule.withModuleKey 143 | 144 | 145 | foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext 146 | foldProjectContexts newContext previousContext = 147 | { modules = Dict.union newContext.modules previousContext.modules 148 | , usedModules = Set.union newContext.usedModules previousContext.usedModules 149 | , projectType = previousContext.projectType 150 | } 151 | 152 | 153 | 154 | -- PROJECT VISITORS 155 | 156 | 157 | elmJsonVisitor : Maybe { a | project : Project } -> ProjectContext -> ( List nothing, ProjectContext ) 158 | elmJsonVisitor maybeProject projectContext = 159 | let 160 | ( exposedModules, projectType ) = 161 | case maybeProject |> Maybe.map .project of 162 | Just (Elm.Project.Package { exposed }) -> 163 | case exposed of 164 | Elm.Project.ExposedList names -> 165 | ( names, Package ) 166 | 167 | Elm.Project.ExposedDict fakeDict -> 168 | ( List.concatMap Tuple.second fakeDict, Package ) 169 | 170 | Just (Elm.Project.Application { depsDirect }) -> 171 | let 172 | elmApplicationType : ElmApplicationType 173 | elmApplicationType = 174 | if LamderaSupport.isLamderaApplication depsDirect then 175 | LamderaApplication 176 | 177 | else 178 | ElmApplication 179 | in 180 | ( [], Application elmApplicationType ) 181 | 182 | Nothing -> 183 | ( [], Application ElmApplication ) 184 | in 185 | ( [] 186 | , { projectContext 187 | | usedModules = 188 | exposedModules 189 | |> List.map (Elm.Module.toString >> String.split ".") 190 | |> Set.fromList 191 | |> Set.union projectContext.usedModules 192 | , projectType = projectType 193 | } 194 | ) 195 | 196 | 197 | finalEvaluationForProject : ProjectContext -> List (Error scope) 198 | finalEvaluationForProject { modules, usedModules } = 199 | modules 200 | |> Dict.filter (\moduleName _ -> not <| Set.member moduleName usedModules) 201 | |> Dict.toList 202 | |> List.map error 203 | 204 | 205 | error : ( ModuleName, { moduleKey : Rule.ModuleKey, moduleNameLocation : Range } ) -> Error scope 206 | error ( moduleName, { moduleKey, moduleNameLocation } ) = 207 | Rule.errorForModule moduleKey 208 | { message = "Module `" ++ String.join "." moduleName ++ "` is never used." 209 | , details = [ "This module is never used. You may want to remove it to keep your project clean, and maybe detect some unused code in your project." ] 210 | } 211 | moduleNameLocation 212 | 213 | 214 | 215 | -- IMPORT VISITOR 216 | 217 | 218 | importVisitor : Node Import -> ModuleContext -> ( List nothing, ModuleContext ) 219 | importVisitor node context = 220 | ( [] 221 | , { context | importedModules = Set.insert (moduleNameForImport node) context.importedModules } 222 | ) 223 | 224 | 225 | moduleNameForImport : Node Import -> ModuleName 226 | moduleNameForImport node = 227 | node 228 | |> Node.value 229 | |> .moduleName 230 | |> Node.value 231 | 232 | 233 | 234 | -- DECLARATION LIST VISITOR 235 | 236 | 237 | declarationListVisitor : List (Node Declaration) -> ModuleContext -> ( List nothing, ModuleContext ) 238 | declarationListVisitor list context = 239 | case context.projectType of 240 | Package -> 241 | ( [], context ) 242 | 243 | Application elmApplicationType -> 244 | let 245 | isMain : String -> Bool 246 | isMain = 247 | isMainFunction elmApplicationType 248 | 249 | containsMainFunction : Bool 250 | containsMainFunction = 251 | List.any 252 | (\declaration -> 253 | case Node.value declaration of 254 | Declaration.FunctionDeclaration function -> 255 | isMain (function.declaration |> Node.value |> .name |> Node.value) 256 | 257 | _ -> 258 | False 259 | ) 260 | list 261 | in 262 | ( [] 263 | , { context | containsMainFunction = containsMainFunction } 264 | ) 265 | 266 | 267 | isMainFunction : ElmApplicationType -> String -> Bool 268 | isMainFunction elmApplicationType = 269 | case elmApplicationType of 270 | ElmApplication -> 271 | \name -> name == "main" 272 | 273 | LamderaApplication -> 274 | \name -> name == "main" || name == "app" 275 | -------------------------------------------------------------------------------- /tests/Dependencies/ElmJson.elm: -------------------------------------------------------------------------------- 1 | module Dependencies.ElmJson exposing (dependency) 2 | 3 | import Elm.Constraint 4 | import Elm.Docs 5 | import Elm.License 6 | import Elm.Module 7 | import Elm.Package 8 | import Elm.Project 9 | import Elm.Type exposing (Type(..)) 10 | import Elm.Version 11 | import Review.Project.Dependency as Dependency exposing (Dependency) 12 | 13 | 14 | dependency : Dependency 15 | dependency = 16 | Dependency.create "elm/json" 17 | elmJson 18 | dependencyModules 19 | 20 | 21 | elmJson : Elm.Project.Project 22 | elmJson = 23 | Elm.Project.Package 24 | { elm = unsafeConstraint "0.19.0 <= v < 0.20.0" 25 | , exposed = Elm.Project.ExposedList [ unsafeModuleName "Json.Decode", unsafeModuleName "Json.Encode" ] 26 | , license = Elm.License.fromString "BSD-3-Clause" |> Maybe.withDefault Elm.License.bsd3 27 | , name = unsafePackageName "elm/json" 28 | , summary = "Encode and decode JSON values" 29 | , deps = [ ( unsafePackageName "elm/core", unsafeConstraint "1.0.0 <= v < 2.0.0" ) ] 30 | , testDeps = [] 31 | , version = Elm.Version.fromString "1.1.4" |> Maybe.withDefault Elm.Version.one 32 | } 33 | 34 | 35 | dependencyModules : List Elm.Docs.Module 36 | dependencyModules = 37 | [ { name = "Json.Decode" 38 | , comment = "" 39 | , aliases = 40 | [ { name = "Value" 41 | , args = [] 42 | , comment = "" 43 | , tipe = Type "Json.Encode.Value" [] 44 | } 45 | ] 46 | , unions = 47 | [ { name = "Decoder" 48 | , args = [ "a" ] 49 | , comment = "" 50 | , tags = [] 51 | } 52 | , { name = "Error" 53 | , args = [] 54 | , comment = "" 55 | , tags = 56 | [ ( "Field", [ Type "String.String" [], Type "Json.Decode.Error" [] ] ) 57 | , ( "Index", [ Type "Basics.Int" [], Type "Json.Decode.Error" [] ] ) 58 | , ( "OneOf", [ Type "List.List" [ Type "Json.Decode.Error" [] ] ] ) 59 | , ( "Failure", [ Type "String.String" [], Type "Json.Decode.Value" [] ] ) 60 | ] 61 | } 62 | ] 63 | , binops = [] 64 | , values = 65 | [ { name = "andThen" 66 | , comment = "" 67 | , tipe = Lambda (Lambda (Var "a") (Type "Json.Decode.Decoder" [ Var "b" ])) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Var "b" ])) 68 | } 69 | , { name = "array" 70 | , comment = "" 71 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Type "Array.Array" [ Var "a" ] ]) 72 | } 73 | , { name = "at" 74 | , comment = "" 75 | , tipe = Lambda (Type "List.List" [ Type "String.String" [] ]) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Var "a" ])) 76 | } 77 | , { name = "bool" 78 | , comment = "" 79 | , tipe = Type "Json.Decode.Decoder" [ Type "Basics.Bool" [] ] 80 | } 81 | , { name = "decodeString" 82 | , comment = "" 83 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "String.String" []) (Type "Result.Result" [ Type "Json.Decode.Error" [], Var "a" ])) 84 | } 85 | , { name = "decodeValue" 86 | , comment = "" 87 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Value" []) (Type "Result.Result" [ Type "Json.Decode.Error" [], Var "a" ])) 88 | } 89 | , { name = "dict" 90 | , comment = "" 91 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Type "Dict.Dict" [ Type "String.String" [], Var "a" ] ]) 92 | } 93 | , { name = "errorToString" 94 | , comment = "" 95 | , tipe = Lambda (Type "Json.Decode.Error" []) (Type "String.String" []) 96 | } 97 | , { name = "fail" 98 | , comment = "" 99 | , tipe = Lambda (Type "String.String" []) (Type "Json.Decode.Decoder" [ Var "a" ]) 100 | } 101 | , { name = "field" 102 | , comment = "" 103 | , tipe = Lambda (Type "String.String" []) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Var "a" ])) 104 | } 105 | , { name = "float" 106 | , comment = "" 107 | , tipe = Type "Json.Decode.Decoder" [ Type "Basics.Float" [] ] 108 | } 109 | , { name = "index" 110 | , comment = "" 111 | , tipe = Lambda (Type "Basics.Int" []) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Var "a" ])) 112 | } 113 | , { name = "int" 114 | , comment = "" 115 | , tipe = Type "Json.Decode.Decoder" [ Type "Basics.Int" [] ] 116 | } 117 | , { name = "keyValuePairs" 118 | , comment = "" 119 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Type "List.List" [ Tuple [ Type "String.String" [], Var "a" ] ] ]) 120 | } 121 | , { name = "lazy" 122 | , comment = "" 123 | , tipe = Lambda (Lambda (Tuple []) (Type "Json.Decode.Decoder" [ Var "a" ])) (Type "Json.Decode.Decoder" [ Var "a" ]) 124 | } 125 | , { name = "list" 126 | , comment = "" 127 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Type "List.List" [ Var "a" ] ]) 128 | } 129 | , { name = "map" 130 | , comment = "" 131 | , tipe = Lambda (Lambda (Var "a") (Var "value")) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Var "value" ])) 132 | } 133 | , { name = "map2" 134 | , comment = "" 135 | , tipe = Lambda (Lambda (Var "a") (Lambda (Var "b") (Var "value"))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "b" ]) (Type "Json.Decode.Decoder" [ Var "value" ]))) 136 | } 137 | , { name = "map3" 138 | , comment = "" 139 | , tipe = Lambda (Lambda (Var "a") (Lambda (Var "b") (Lambda (Var "c") (Var "value")))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "b" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "c" ]) (Type "Json.Decode.Decoder" [ Var "value" ])))) 140 | } 141 | , { name = "map4" 142 | , comment = "" 143 | , tipe = Lambda (Lambda (Var "a") (Lambda (Var "b") (Lambda (Var "c") (Lambda (Var "d") (Var "value"))))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "b" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "c" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "d" ]) (Type "Json.Decode.Decoder" [ Var "value" ]))))) 144 | } 145 | , { name = "map5" 146 | , comment = "" 147 | , tipe = Lambda (Lambda (Var "a") (Lambda (Var "b") (Lambda (Var "c") (Lambda (Var "d") (Lambda (Var "e") (Var "value")))))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "b" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "c" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "d" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "e" ]) (Type "Json.Decode.Decoder" [ Var "value" ])))))) 148 | } 149 | , { name = "map6" 150 | , comment = "" 151 | , tipe = Lambda (Lambda (Var "a") (Lambda (Var "b") (Lambda (Var "c") (Lambda (Var "d") (Lambda (Var "e") (Lambda (Var "f") (Var "value"))))))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "b" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "c" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "d" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "e" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "f" ]) (Type "Json.Decode.Decoder" [ Var "value" ]))))))) 152 | } 153 | , { name = "map7" 154 | , comment = "" 155 | , tipe = Lambda (Lambda (Var "a") (Lambda (Var "b") (Lambda (Var "c") (Lambda (Var "d") (Lambda (Var "e") (Lambda (Var "f") (Lambda (Var "g") (Var "value")))))))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "b" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "c" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "d" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "e" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "f" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "g" ]) (Type "Json.Decode.Decoder" [ Var "value" ])))))))) 156 | } 157 | , { name = "map8" 158 | , comment = "" 159 | , tipe = Lambda (Lambda (Var "a") (Lambda (Var "b") (Lambda (Var "c") (Lambda (Var "d") (Lambda (Var "e") (Lambda (Var "f") (Lambda (Var "g") (Lambda (Var "h") (Var "value"))))))))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "b" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "c" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "d" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "e" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "f" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "g" ]) (Lambda (Type "Json.Decode.Decoder" [ Var "h" ]) (Type "Json.Decode.Decoder" [ Var "value" ]))))))))) 160 | } 161 | , { name = "maybe" 162 | , comment = "" 163 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Type "Maybe.Maybe" [ Var "a" ] ]) 164 | } 165 | , { name = "null" 166 | , comment = "" 167 | , tipe = Lambda (Var "a") (Type "Json.Decode.Decoder" [ Var "a" ]) 168 | } 169 | , { name = "nullable" 170 | , comment = "" 171 | , tipe = Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Type "Maybe.Maybe" [ Var "a" ] ]) 172 | } 173 | , { name = "oneOf" 174 | , comment = "" 175 | , tipe = Lambda (Type "List.List" [ Type "Json.Decode.Decoder" [ Var "a" ] ]) (Type "Json.Decode.Decoder" [ Var "a" ]) 176 | } 177 | , { name = "oneOrMore" 178 | , comment = "" 179 | , tipe = Lambda (Lambda (Var "a") (Lambda (Type "List.List" [ Var "a" ]) (Var "value"))) (Lambda (Type "Json.Decode.Decoder" [ Var "a" ]) (Type "Json.Decode.Decoder" [ Var "value" ])) 180 | } 181 | , { name = "string" 182 | , comment = "" 183 | , tipe = Type "Json.Decode.Decoder" [ Type "String.String" [] ] 184 | } 185 | , { name = "succeed" 186 | , comment = "" 187 | , tipe = Lambda (Var "a") (Type "Json.Decode.Decoder" [ Var "a" ]) 188 | } 189 | , { name = "value" 190 | , comment = "" 191 | , tipe = Type "Json.Decode.Decoder" [ Type "Json.Decode.Value" [] ] 192 | } 193 | ] 194 | } 195 | , { name = "Json.Encode" 196 | , comment = "" 197 | , aliases = [] 198 | , unions = 199 | [ { name = "Value" 200 | , args = [] 201 | , comment = "" 202 | , tags = [] 203 | } 204 | ] 205 | , binops = [] 206 | , values = 207 | [ { name = "array" 208 | , comment = "" 209 | , tipe = Lambda (Lambda (Var "a") (Type "Json.Encode.Value" [])) (Lambda (Type "Array.Array" [ Var "a" ]) (Type "Json.Encode.Value" [])) 210 | } 211 | , { name = "bool" 212 | , comment = "" 213 | , tipe = Lambda (Type "Basics.Bool" []) (Type "Json.Encode.Value" []) 214 | } 215 | , { name = "dict" 216 | , comment = "" 217 | , tipe = Lambda (Lambda (Var "k") (Type "String.String" [])) (Lambda (Lambda (Var "v") (Type "Json.Encode.Value" [])) (Lambda (Type "Dict.Dict" [ Var "k", Var "v" ]) (Type "Json.Encode.Value" []))) 218 | } 219 | , { name = "encode" 220 | , comment = "" 221 | , tipe = Lambda (Type "Basics.Int" []) (Lambda (Type "Json.Encode.Value" []) (Type "String.String" [])) 222 | } 223 | , { name = "float" 224 | , comment = "" 225 | , tipe = Lambda (Type "Basics.Float" []) (Type "Json.Encode.Value" []) 226 | } 227 | , { name = "int" 228 | , comment = "" 229 | , tipe = Lambda (Type "Basics.Int" []) (Type "Json.Encode.Value" []) 230 | } 231 | , { name = "list" 232 | , comment = "" 233 | , tipe = Lambda (Lambda (Var "a") (Type "Json.Encode.Value" [])) (Lambda (Type "List.List" [ Var "a" ]) (Type "Json.Encode.Value" [])) 234 | } 235 | , { name = "null" 236 | , comment = "" 237 | , tipe = Type "Json.Encode.Value" [] 238 | } 239 | , { name = "object" 240 | , comment = "" 241 | , tipe = Lambda (Type "List.List" [ Tuple [ Type "String.String" [], Type "Json.Encode.Value" [] ] ]) (Type "Json.Encode.Value" []) 242 | } 243 | , { name = "set" 244 | , comment = "" 245 | , tipe = Lambda (Lambda (Var "a") (Type "Json.Encode.Value" [])) (Lambda (Type "Set.Set" [ Var "a" ]) (Type "Json.Encode.Value" [])) 246 | } 247 | , { name = "string" 248 | , comment = "" 249 | , tipe = Lambda (Type "String.String" []) (Type "Json.Encode.Value" []) 250 | } 251 | ] 252 | } 253 | ] 254 | 255 | 256 | unsafePackageName : String -> Elm.Package.Name 257 | unsafePackageName packageName = 258 | case Elm.Package.fromString packageName of 259 | Just name -> 260 | name 261 | 262 | Nothing -> 263 | Debug.todo ("Could not parse " ++ packageName ++ " as a package name") 264 | 265 | 266 | unsafeModuleName : String -> Elm.Module.Name 267 | unsafeModuleName moduleName = 268 | case Elm.Module.fromString moduleName of 269 | Just name -> 270 | name 271 | 272 | Nothing -> 273 | Debug.todo ("Could not parse " ++ moduleName ++ " as a module name") 274 | 275 | 276 | unsafeConstraint : String -> Elm.Constraint.Constraint 277 | unsafeConstraint constraint = 278 | case Elm.Constraint.fromString constraint of 279 | Just constr -> 280 | constr 281 | 282 | Nothing -> 283 | Debug.todo ("Could not parse " ++ constraint ++ " as a package constraint") 284 | -------------------------------------------------------------------------------- /src/NoUnused/Patterns/NameVisitor.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.Patterns.NameVisitor exposing (withValueVisitor) 2 | 3 | {-| Visit each name in the module. 4 | 5 | A "name" is a `Node ( ModuleName, String )` and represents a value or type reference. Here are some examples: 6 | 7 | - `Json.Encode.Value` -> `( [ "Json", "Encode" ], "Value" )` 8 | - `Html.Attributes.class` -> `( [ "Html", "Attributes" ], "class" )` 9 | - `Page` -> `( [], "Page" )` 10 | - `view` -> `( [], "view" )` 11 | 12 | These can appear in many places throughout declarations and expressions, and picking them out each time is a lot of work. Instead of writing 1000 lines of code and tests each time, you can write one `nameVisitor` and plug it straight into your module schema, or separate `valueVisitor` and `typeVisitor`s. 13 | 14 | @docs withNameVisitor, withValueVisitor, withTypeVisitor, withValueAndTypeVisitors 15 | 16 | 17 | ## Scope 18 | 19 | This makes no attempt to resolve module names from imports, it just returns what's written in the code. It would be trivial to connect [elm-review-scope] with the name visitor if you want to do this. 20 | 21 | [elm-review-scope]: http://github.com/jfmengels/elm-review-scope/ 22 | 23 | 24 | ## Version 25 | 26 | Version: 0.2.0 27 | 28 | -} 29 | 30 | import Elm.Syntax.Declaration as Declaration exposing (Declaration) 31 | import Elm.Syntax.Expression as Expression exposing (Expression) 32 | import Elm.Syntax.ModuleName exposing (ModuleName) 33 | import Elm.Syntax.Node as Node exposing (Node(..)) 34 | import Elm.Syntax.Pattern as Pattern exposing (Pattern) 35 | import Elm.Syntax.Signature exposing (Signature) 36 | import Elm.Syntax.Type as Type 37 | import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation) 38 | import Review.Rule as Rule exposing (Error) 39 | 40 | 41 | type Visitor context 42 | = NameVisitor (VisitorFunction context) 43 | | ValueVisitor (VisitorFunction context) 44 | | TypeVisitor (VisitorFunction context) 45 | | ValueAndTypeVisitor (VisitorFunction context) (VisitorFunction context) 46 | 47 | 48 | type alias VisitorFunction context = 49 | Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 50 | 51 | 52 | type Name 53 | = Value (Node ( ModuleName, String )) 54 | | Type (Node ( ModuleName, String )) 55 | 56 | 57 | {-| This will apply the `nameVisitor` to every value and type in the module, you will get no information about whether the name is a value or type. 58 | 59 | rule : Rule 60 | rule = 61 | Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext 62 | |> NameVisitor.withNameVisitor nameVisitor 63 | |> Rule.fromModuleRuleSchema 64 | 65 | nameVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 66 | nameVisitor node context = 67 | -- Do what you want with the name 68 | ( [], context ) 69 | 70 | -} 71 | withNameVisitor : 72 | (Node ( ModuleName, String ) -> context -> ( List (Error {}), context )) 73 | -> Rule.ModuleRuleSchema state context 74 | -> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context 75 | withNameVisitor nameVisitor rule = 76 | let 77 | visitor = 78 | NameVisitor nameVisitor 79 | in 80 | rule 81 | |> Rule.withDeclarationListVisitor (declarationListVisitor visitor) 82 | |> Rule.withExpressionEnterVisitor (expressionVisitor visitor) 83 | 84 | 85 | {-| This will apply the `valueVisitor` to every value in the module, and ignore any types. 86 | 87 | rule : Rule 88 | rule = 89 | Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext 90 | |> NameVisitor.withValueVisitor valueVisitor 91 | |> Rule.fromModuleRuleSchema 92 | 93 | valueVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 94 | valueVisitor node context = 95 | -- Do what you want with the value 96 | ( [], context ) 97 | 98 | -} 99 | withValueVisitor : 100 | (Node ( ModuleName, String ) -> context -> ( List (Error {}), context )) 101 | -> Rule.ModuleRuleSchema state context 102 | -> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context 103 | withValueVisitor valueVisitor rule = 104 | let 105 | visitor = 106 | ValueVisitor valueVisitor 107 | in 108 | rule 109 | |> Rule.withDeclarationListVisitor (declarationListVisitor visitor) 110 | |> Rule.withExpressionEnterVisitor (expressionVisitor visitor) 111 | 112 | 113 | {-| This will apply the `typeVisitor` to every type in the module, and ignore any values. 114 | 115 | rule : Rule 116 | rule = 117 | Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext 118 | |> NameVisitor.withTypeVisitor typeVisitor 119 | |> Rule.fromModuleRuleSchema 120 | 121 | typeVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 122 | typeVisitor node context = 123 | -- Do what you want with the type 124 | ( [], context ) 125 | 126 | -} 127 | withTypeVisitor : 128 | (Node ( ModuleName, String ) -> context -> ( List (Error {}), context )) 129 | -> Rule.ModuleRuleSchema state context 130 | -> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context 131 | withTypeVisitor typeVisitor rule = 132 | let 133 | visitor = 134 | TypeVisitor typeVisitor 135 | in 136 | rule 137 | |> Rule.withDeclarationListVisitor (declarationListVisitor visitor) 138 | |> Rule.withExpressionEnterVisitor (expressionVisitor visitor) 139 | 140 | 141 | {-| This will apply the `valueVisitor` to every value and the `typeVisitor` to every type in the module. 142 | 143 | rule : Rule 144 | rule = 145 | Rule.newModuleRuleSchema "NoInconsistentAliases" initialContext 146 | |> NameVisitor.withValueAndTypeVisitors 147 | { valueVisitor = valueVisitor 148 | , typeVisitor = typeVisitor 149 | } 150 | |> Rule.fromModuleRuleSchema 151 | 152 | valueVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 153 | valueVisitor node context = 154 | -- Do what you want with the value 155 | ( [], context ) 156 | 157 | typeVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 158 | typeVisitor node context = 159 | -- Do what you want with the type 160 | ( [], context ) 161 | 162 | -} 163 | withValueAndTypeVisitors : 164 | { valueVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 165 | , typeVisitor : Node ( ModuleName, String ) -> context -> ( List (Error {}), context ) 166 | } 167 | -> Rule.ModuleRuleSchema state context 168 | -> Rule.ModuleRuleSchema { state | hasAtLeastOneVisitor : () } context 169 | withValueAndTypeVisitors { valueVisitor, typeVisitor } rule = 170 | let 171 | visitor = 172 | ValueAndTypeVisitor valueVisitor typeVisitor 173 | in 174 | rule 175 | |> Rule.withDeclarationListVisitor (declarationListVisitor visitor) 176 | |> Rule.withExpressionEnterVisitor (expressionVisitor visitor) 177 | 178 | 179 | 180 | --- VISITORS 181 | 182 | 183 | declarationListVisitor : 184 | Visitor context 185 | -> (List (Node Declaration) -> context -> ( List (Error {}), context )) 186 | declarationListVisitor visitor list context = 187 | visitDeclarationList list 188 | |> folder visitor context 189 | 190 | 191 | expressionVisitor : Visitor context -> (Node Expression -> context -> ( List (Error {}), context )) 192 | expressionVisitor visitor node context = 193 | visitExpression node 194 | |> folder visitor context 195 | 196 | 197 | 198 | --- FOLDER 199 | 200 | 201 | folder : 202 | Visitor context 203 | -> context 204 | -> List Name 205 | -> ( List (Error {}), context ) 206 | folder visitor context list = 207 | List.foldl (folderHelper visitor) ( [], context ) list 208 | 209 | 210 | folderHelper : 211 | Visitor context 212 | -> Name 213 | -> ( List (Error {}), context ) 214 | -> ( List (Error {}), context ) 215 | folderHelper visitor name ( errors, context ) = 216 | let 217 | ( newErrors, newContext ) = 218 | applyVisitor visitor name context 219 | in 220 | ( newErrors ++ errors, newContext ) 221 | 222 | 223 | applyVisitor : Visitor context -> Name -> context -> ( List (Error {}), context ) 224 | applyVisitor visitor name context = 225 | case name of 226 | Value node -> 227 | applyValueVisitor visitor node context 228 | 229 | Type node -> 230 | applyTypeVisitor visitor node context 231 | 232 | 233 | applyValueVisitor : Visitor context -> VisitorFunction context 234 | applyValueVisitor visitor = 235 | case visitor of 236 | NameVisitor function -> 237 | function 238 | 239 | ValueVisitor function -> 240 | function 241 | 242 | TypeVisitor _ -> 243 | noopVisitor 244 | 245 | ValueAndTypeVisitor function _ -> 246 | function 247 | 248 | 249 | applyTypeVisitor : Visitor context -> VisitorFunction context 250 | applyTypeVisitor visitor = 251 | case visitor of 252 | NameVisitor function -> 253 | function 254 | 255 | ValueVisitor _ -> 256 | noopVisitor 257 | 258 | TypeVisitor function -> 259 | function 260 | 261 | ValueAndTypeVisitor _ function -> 262 | function 263 | 264 | 265 | noopVisitor : VisitorFunction context 266 | noopVisitor _ context = 267 | ( [], context ) 268 | 269 | 270 | 271 | --- PRIVATE 272 | 273 | 274 | visitDeclarationList : List (Node Declaration) -> List Name 275 | visitDeclarationList nodes = 276 | fastConcatMap visitDeclaration nodes 277 | 278 | 279 | visitDeclaration : Node Declaration -> List Name 280 | visitDeclaration node = 281 | case Node.value node of 282 | Declaration.FunctionDeclaration { signature, declaration } -> 283 | visitMaybeSignature signature 284 | ++ visitFunctionImplementation declaration 285 | 286 | Declaration.AliasDeclaration { typeAnnotation } -> 287 | visitTypeAnnotation typeAnnotation 288 | 289 | Declaration.CustomTypeDeclaration { constructors } -> 290 | visitValueConstructorList constructors 291 | 292 | Declaration.PortDeclaration { typeAnnotation } -> 293 | visitTypeAnnotation typeAnnotation 294 | 295 | _ -> 296 | [] 297 | 298 | 299 | visitMaybeSignature : Maybe (Node Signature) -> List Name 300 | visitMaybeSignature maybeNode = 301 | case maybeNode of 302 | Just node -> 303 | visitSignature node 304 | 305 | Nothing -> 306 | [] 307 | 308 | 309 | visitSignature : Node Signature -> List Name 310 | visitSignature node = 311 | visitTypeAnnotation (node |> Node.value |> .typeAnnotation) 312 | 313 | 314 | visitFunctionImplementation : Node Expression.FunctionImplementation -> List Name 315 | visitFunctionImplementation node = 316 | visitPatternList (node |> Node.value |> .arguments) 317 | 318 | 319 | visitValueConstructorList : List (Node Type.ValueConstructor) -> List Name 320 | visitValueConstructorList list = 321 | fastConcatMap visitValueConstructor list 322 | 323 | 324 | visitValueConstructor : Node Type.ValueConstructor -> List Name 325 | visitValueConstructor node = 326 | visitTypeAnnotationList (node |> Node.value |> .arguments) 327 | 328 | 329 | visitTypeAnnotationList : List (Node TypeAnnotation) -> List Name 330 | visitTypeAnnotationList list = 331 | fastConcatMap visitTypeAnnotation list 332 | 333 | 334 | visitTypeAnnotation : Node TypeAnnotation -> List Name 335 | visitTypeAnnotation node = 336 | case Node.value node of 337 | TypeAnnotation.GenericType _ -> 338 | [] 339 | 340 | TypeAnnotation.Typed call types -> 341 | visitType call 342 | ++ visitTypeAnnotationList types 343 | 344 | TypeAnnotation.Unit -> 345 | [] 346 | 347 | TypeAnnotation.Tupled list -> 348 | visitTypeAnnotationList list 349 | 350 | TypeAnnotation.Record list -> 351 | visitRecordFieldList list 352 | 353 | TypeAnnotation.GenericRecord _ list -> 354 | visitRecordFieldList (Node.value list) 355 | 356 | TypeAnnotation.FunctionTypeAnnotation argument return -> 357 | visitTypeAnnotation argument 358 | ++ visitTypeAnnotation return 359 | 360 | 361 | visitRecordFieldList : List (Node TypeAnnotation.RecordField) -> List Name 362 | visitRecordFieldList list = 363 | fastConcatMap visitRecordField list 364 | 365 | 366 | visitRecordField : Node TypeAnnotation.RecordField -> List Name 367 | visitRecordField node = 368 | visitTypeAnnotation (node |> Node.value |> Tuple.second) 369 | 370 | 371 | visitExpression : Node Expression -> List Name 372 | visitExpression (Node range expression) = 373 | case expression of 374 | Expression.FunctionOrValue moduleName function -> 375 | visitValue (Node range ( moduleName, function )) 376 | 377 | Expression.LetExpression { declarations } -> 378 | visitLetDeclarationList declarations 379 | 380 | Expression.CaseExpression { cases } -> 381 | visitCaseList cases 382 | 383 | Expression.LambdaExpression { args } -> 384 | visitPatternList args 385 | 386 | Expression.RecordUpdateExpression name _ -> 387 | visitValue (Node.map (\function -> ( [], function )) name) 388 | 389 | _ -> 390 | [] 391 | 392 | 393 | visitLetDeclarationList : List (Node Expression.LetDeclaration) -> List Name 394 | visitLetDeclarationList list = 395 | fastConcatMap visitLetDeclaration list 396 | 397 | 398 | visitLetDeclaration : Node Expression.LetDeclaration -> List Name 399 | visitLetDeclaration node = 400 | case Node.value node of 401 | Expression.LetFunction { signature, declaration } -> 402 | visitMaybeSignature signature 403 | ++ visitFunctionImplementation declaration 404 | 405 | Expression.LetDestructuring pattern _ -> 406 | visitPattern pattern 407 | 408 | 409 | visitCaseList : List Expression.Case -> List Name 410 | visitCaseList list = 411 | fastConcatMap visitCase list 412 | 413 | 414 | visitCase : Expression.Case -> List Name 415 | visitCase ( pattern, _ ) = 416 | visitPattern pattern 417 | 418 | 419 | visitPatternList : List (Node Pattern) -> List Name 420 | visitPatternList list = 421 | fastConcatMap visitPattern list 422 | 423 | 424 | visitPattern : Node Pattern -> List Name 425 | visitPattern node = 426 | case Node.value node of 427 | Pattern.TuplePattern patterns -> 428 | visitPatternList patterns 429 | 430 | Pattern.UnConsPattern head rest -> 431 | visitPattern head ++ visitPattern rest 432 | 433 | Pattern.ListPattern list -> 434 | visitPatternList list 435 | 436 | Pattern.NamedPattern { moduleName, name } _ -> 437 | let 438 | { start } = 439 | Node.range node 440 | 441 | newEnd = 442 | { start | column = start.column + (name :: moduleName |> String.join "." |> String.length) } 443 | 444 | range = 445 | { start = start, end = newEnd } 446 | in 447 | visitValue (Node range ( moduleName, name )) 448 | 449 | Pattern.AsPattern pattern _ -> 450 | visitPattern pattern 451 | 452 | Pattern.ParenthesizedPattern pattern -> 453 | visitPattern pattern 454 | 455 | _ -> 456 | [] 457 | 458 | 459 | visitValue : Node ( ModuleName, String ) -> List Name 460 | visitValue node = 461 | [ Value node ] 462 | 463 | 464 | visitType : Node ( ModuleName, String ) -> List Name 465 | visitType node = 466 | [ Type node ] 467 | 468 | 469 | 470 | --- High Performance List 471 | 472 | 473 | fastConcatMap : (a -> List b) -> List a -> List b 474 | fastConcatMap fn = 475 | List.foldr (fn >> (++)) [] 476 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | [{"name":"NoUnused.CustomTypeConstructorArgs","comment":"\n\n@docs rule\n\n","unions":[],"aliases":[],"values":[{"name":"rule","comment":" Reports arguments of custom type constructors that are never used.\n\n config =\n [ NoUnused.CustomTypeConstructorArgs.rule\n ]\n\nCustom type constructors can contain data that is never extracted out of the constructor.\nThis rule will warn arguments that are always pattern matched using a wildcard (`_`).\n\nFor package projects, custom types whose constructors are exposed as part of the package API are not reported.\n\nNote that this rule **may report false positives** if you compare custom types with the `==` or `/=` operators\n(and never destructure the custom type), like when you do `value == Just 0`, or store them in lists for instance with\n[`assoc-list`](https://package.elm-lang.org/packages/pzp1997/assoc-list/latest).\nThis rule attempts to detect when the custom type is used in comparisons, but it may still result in false positives.\n\n\n## Fail\n\n type CustomType\n = CustomType Used Unused\n\n case customType of\n CustomType value _ -> value\n\n\n## Success\n\n type CustomType\n = CustomType Used Unused\n\n case customType of\n CustomType value maybeUsed -> value\n\n\n## When not to enable this rule?\n\nIf you like giving names to all arguments when pattern matching, then this rule will not find many problems.\nThis rule will work well when enabled along with [`NoUnused.Patterns`](./NoUnused-Patterns).\n\nAlso, if you like comparing custom types in the way described above, you might pass on this rule, or want to be very careful when enabling it.\n\n\n## Try it out\n\nYou can try this rule out by running the following command:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.CustomTypeConstructorArgs\n```\n\n","type":"Review.Rule.Rule"}],"binops":[]},{"name":"NoUnused.CustomTypeConstructors","comment":" Forbid having unused custom type constructors inside the project.\n\n@docs rule\n\n","unions":[],"aliases":[],"values":[{"name":"rule","comment":" Forbid having unused custom type constructors.\n\n🔧 Running with `--fix` will automatically remove most of the reported errors.\n\n config =\n [ NoUnused.CustomTypeConstructors.rule []\n ]\n\nNote that this rule reports any custom type constructor that isn't used\nanywhere _in the project_.\n\nIf the project is a package and the module that declared the type is exposed and\nthe type's constructors are exposed, then the constructors will not be reported.\n\n\n### Phantom types\n\nThis does not prevent you from using phantom types.\nI highly suggest changing your phantom types to the following shape:\n\n type TypeName\n = TypeName Never\n\nThis shape makes it obvious to tooling and readers that the type can't be created, so if it is used it must be as a phantom type.\n\n**Deprecated configuration for phantom types**\n\n_I recommend changing your types like mentioned right above, and to configure the rule like `NoUnused.CustomTypeConstructors.rule []`.\nI'll keep this section and configuration option around until the next major version comes out._\n\n**Note**: At the time of writing, there may be cases where phantom types are not well detected.\nWhen an opaque type is defined in a dependency, we don't know whether a type variable should be considered as a phantom type.\n\nTherefore, sometimes this rule will need some help, by having you tell it what type variables of which type is a phantom type variable.\nThat's what the argument to the rule is for.\n\nTo explain that the `a` in `type Id a = Id String` from the `IdModule` module\ncorresponds to a phantom type variable, you would configure the rule like this:\n\n config =\n [ NoUnused.CustomTypeConstructors.rule\n [ { moduleName = \"IdModule\"\n , typeName = \"Id\"\n , index = 0 -- Position of the phantom variable in the type's arguments\n }\n ]\n ]\n\nThis rule could do a much better job than it currently does at figuring this out,\nby following the definitions of custom types and type aliases, until it finds out that the type\nvariable is not used, or that it hits the limit related to dependencies described above.\nIn the meantime, you can configure the rule with all the phantom type exceptions.\n\n**End of deprecated section**\n\n\n## Fail\n\n module A exposing (a)\n\n type MyType\n = UsedType\n | UnusedType -- Will get reported\n\n a =\n UsedType\n\n\n## Success\n\n module A exposing (ExposedType(..))\n\n type MyType\n = UsedType\n\n a =\n UsedType\n\n type ExposedType\n = A\n | B\n | C\n\n -----------------------\n module A exposing (..)\n\n type ExposedType\n = A\n | B\n | C\n\n\n## Try it out\n\nYou can try this rule out by running the following command:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.CustomTypeConstructors\n```\n\n","type":"List.List { moduleName : String.String, typeName : String.String, index : Basics.Int } -> Review.Rule.Rule"}],"binops":[]},{"name":"NoUnused.Dependencies","comment":" Forbid the use of dependencies that are never used in your project.\n\n@docs rule\n\n","unions":[],"aliases":[],"values":[{"name":"rule","comment":" Forbid the use of dependencies that are never used in your project.\n\n🔧 Running with `--fix` will automatically remove all the reported errors.\n\nA dependency is considered unused if none of its modules are imported in the project.\n\n config =\n [ NoUnused.Dependencies.rule\n ]\n\n\n## Try it out\n\nYou can try this rule out by running the following command:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Dependencies\n```\n\n","type":"Review.Rule.Rule"}],"binops":[]},{"name":"NoUnused.Exports","comment":" Forbid the use of exposed elements (functions, values or types) that are never used in your project.\n\n🔧 Running with `--fix` will automatically remove all the reported errors,\nexcept for the ones reported when using [`reportUnusedProductionExports`](#reportUnusedProductionExports).\nIt won't automatically remove unused modules though.\n\nIf the project is a package and the module that declared the element is exposed,\nthen nothing will be reported.\n\nThe behavior of the rule also depends on whether the analyzed module is exposing elements explicitly (`module X exposing (A, b)`)\nor exposing everything (`module X exposing (..)`).\n\nWhen exposing elements explicitly, the rule will report and remove elements from the\nexposing clause (`exposing (used, unused)` to `exposing (used)`) when they're never used in other Elm files of the project.\n\nWhen exposing all, the rule will report and remove the declaration of elements\nif they're used neither in the file they're declared in nor in any other files,\nmaking it act somewhat like [`NoUnused.Variables`](./NoUnused-Variables),\ncomplementing it because [`NoUnused.Variables`](./NoUnused-Variables) doesn't report\ntop-level declarations when the module is exposing everything.\n\n@docs rule\n\n\n## Going one step further\n\nThis rule can be configured to report more unused elements than the default configuration.\n\n@docs Configuration, defaults, toRule\n\nBy default, this rule only reports exposed elements that are never imported in other modules.\nIt is however pretty common to have elements imported and used in non-production parts of the codebase,\nsuch as in tests or in a styleguide.\n\nFor instance, let's say there is a module `A` that exposes a function `someFunction`:\n\n module A exposing (someFunction)\n\n someFunction input =\n doSomethingComplexWith input\n\nAnd there is this test module to test `A.someFunction`:\n\n module ATest exposing (tests)\n\n import A\n import Test exposing (Test, describe, test)\n\n tests : Test\n tests =\n describe \"A.someFunction\"\n [ test \"does something complex\" <|\n \\() ->\n A.someFunction someInput\n |> Expect.equal someExpectedOutput\n ]\n\nLet's say this is the only use of `A.someFunction` in the entire project.\nBecause `A.someFunction` is technically used in the project, this rule won't report it.\n\nBut since the function is not used in production code, it is a good practice to remove it, as that will remove the\namount of code that needs to be maintained unnecessarily. We can detect that using [`reportUnusedProductionExports`](#reportUnusedProductionExports).\n\n@docs reportUnusedProductionExports\n\n@docs Exception, annotatedBy, suffixedBy, prefixedBy, definedInModule\n\n\n## Try it out\n\nYou can try this rule out by running the following commands:\n\nUsing the default configuration:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Exports\n```\n\nUsing `reportUnusedProductionExports` with the following configuration:\n\n NoUnused.Exports.defaults\n |> NoUnused.Exports.reportUnusedProductionExports\n { isProductionFile = \\{ moduleName, filePath, isInSourceDirectories } -> isInSourceDirectories\n , exceptionsAre = [ annotatedBy \"@test-helper\", suffixedBy \"_FOR_TESTS\" ]\n }\n |> NoUnused.Exports.toRule\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example-ignore-tests --rules NoUnused.Exports\n```\n\n","unions":[{"name":"Configuration","comment":" Configuration for the rule. Use [`defaults`](#defaults) to get a default configuration and use [`toRule`](#toRule) to turn it into a rule.\nYou can change the configuration using [`reportUnusedProductionExports`](#reportUnusedProductionExports).\n","args":[],"cases":[]},{"name":"Exception","comment":" Predicate to identify exceptions (that shouldn't be reported) for elements defined in production code that are only used in non-production code.\n\nA problem with reporting these elements is that it's going to produce false positives, as there are legitimate use-cases\nfor exporting these elements, hence the need for the rule to be able to identify them.\n\nFor instance, while it's generally discouraged, you might want to test the internals of an API (to make sure some properties hold\ngiven very specific situations). In this case, your module then needs to expose a way to gain insight to the internals.\n\nAnother example is giving the means for tests to create opaque types that are impossible or very hard to create in a test\nenvironment. This can be the case for types that can only be created through the decoding of an HTTP request.\n\nNote that another common way to handle these use-cases is to move the internals to another module that exposes everything\nwhile making sure only specific production modules import it.\n\n","args":[],"cases":[]}],"aliases":[],"values":[{"name":"annotatedBy","comment":" Prevents reporting usages of elements that contain a specific tag in their documentation.\n\nGiven the following configuration\n\n NoUnused.Exports.defaults\n |> NoUnused.Exports.reportUnusedProductionExports\n { isProductionFile = isProductionFile\n , exceptionsAre = [ annotatedBy \"@test-helper\" ]\n }\n |> NoUnused.Exports.toRule\n\nany element that has `@test-helper` in its documentation will not be reported as unused (as long as its used at least once in the project):\n\n {-| @test-helper\n -}\n someFunction input =\n doSomethingComplexWith input\n\nA recommended practice is to have annotations start with `@`.\n\nYou can use this function several times to define multiple annotations.\n\n","type":"String.String -> NoUnused.Exports.Exception"},{"name":"defaults","comment":" Default configuration. This will only report exported elements that are never used in other modules.\n","type":"NoUnused.Exports.Configuration"},{"name":"definedInModule","comment":" Prevents reporting usages of elements in some modules.\n\nGiven the following configuration\n\n NoUnused.Exports.defaults\n |> NoUnused.Exports.reportUnusedProductionExports\n { isProductionFile = isProductionFile\n , exceptionsAre =\n [ definedInModule\n (\\{ moduleName, filePath } ->\n List.member \"Util\" moduleName\n || String.startsWith \"src/test-helpers/\" filePath\n )\n ]\n }\n |> NoUnused.Exports.toRule\n\nno elements from modules named `*.Util.*` or modules inside `src/test-helpers/` will be reported.\n\nThe provided `filePath` is relative to the project's `elm.json` and is in a UNIX style (`/`, no `\\`).\n\n","type":"({ moduleName : Elm.Syntax.ModuleName.ModuleName, filePath : String.String } -> Basics.Bool) -> NoUnused.Exports.Exception"},{"name":"prefixedBy","comment":" Prevents reporting usages of elements whose name start with a specific string.\n\nGiven the following configuration\n\n NoUnused.Exports.defaults\n |> NoUnused.Exports.reportUnusedProductionExports\n { isProductionFile = isProductionFile\n , exceptionsAre = [ prefixedBy \"test_\" ]\n }\n |> NoUnused.Exports.toRule\n\nany element that starts with `\"test_\"` will not be reported as unused (as long as its used at least once in the project):\n\n test_someFunction input =\n doSomethingComplexWith input\n\nYou can use this function several times to define multiple prefixes.\n\n","type":"String.String -> NoUnused.Exports.Exception"},{"name":"reportUnusedProductionExports","comment":" Configures the rule to report elements defined in production code but only used in non-production files.\n\n import NoUnused.Exports exposing (annotatedBy)\n\n config =\n [ NoUnused.Exports.defaults\n |> NoUnused.Exports.reportUnusedProductionExports\n { isProductionFile =\n \\{ moduleName, filePath, isInSourceDirectories } ->\n isInSourceDirectories\n && not (String.endsWith \"/Example.elm\" filePath)\n , exceptionsAre = [ annotatedBy \"@test-helper\" ]\n }\n |> NoUnused.Exports.toRule\n ]\n\nElements reported using this configuration won't be automatically fixed as they require removing the code\nthat uses the element.\n\nThis function needs to know two things:\n\n1. Which files are considered to be production files, which is determined by a function that you provide.\n Generally, production files are in the `\"source-directories\"`, which is indicated by\n `isInSourceDirectories` (given as an argument to the function) being `True`. If you want to exclude\n more files, you can use the `filePath` or `moduleName` of the Elm module, whichever is more practical for you to use.\n `filePath` is relative to the folder containing the `elm.json` file and is written in a UNIX format (`/`, no `\\`).\n\n2. How to identify exceptions. See [`Exception`](#Exception) for more information.\n\n","type":"{ isProductionFile : { moduleName : Elm.Syntax.ModuleName.ModuleName, filePath : String.String, isInSourceDirectories : Basics.Bool } -> Basics.Bool, exceptionsAre : List.List NoUnused.Exports.Exception } -> NoUnused.Exports.Configuration -> NoUnused.Exports.Configuration"},{"name":"rule","comment":" Report functions and types that are exposed from a module but that are never\nused in other modules. Also reports when a module is entirely unused.\n\n config =\n [ NoUnused.Exports.rule\n ]\n\nThis is equivalent to `NoUnused.Exports.toRule NoUnused.Exports.defaults`.\n\n","type":"Review.Rule.Rule"},{"name":"suffixedBy","comment":" Prevents reporting usages of elements whose name end with a specific string.\n\nGiven the following configuration\n\n NoUnused.Exports.defaults\n |> NoUnused.Exports.reportUnusedProductionExports\n { isProductionFile = isProductionFile\n , exceptionsAre = [ suffixedBy \"_FOR_TESTS\" ]\n }\n |> NoUnused.Exports.toRule\n\nany element that ends with `\"_FOR_TESTS\"` will not be reported as unused (as long as its used at least once in the project):\n\n someFunction_FOR_TESTS input =\n doSomethingComplexWith input\n\nYou can use this function several times to define multiple suffixes.\n\n","type":"String.String -> NoUnused.Exports.Exception"},{"name":"toRule","comment":" Creates a rule that reports unused exports using a [`Configuration`](#Configuration).\n","type":"NoUnused.Exports.Configuration -> Review.Rule.Rule"}],"binops":[]},{"name":"NoUnused.Modules","comment":" Forbid the use of modules that are never used in your project.\n\n**@deprecated** This rule has been deprecated, as it has now been integrated into [`NoUnused.Exports`](NoUnused-Exports).\nYou should use that rule instead.\n\n@docs rule\n\n","unions":[],"aliases":[],"values":[{"name":"rule","comment":" Forbid the use of modules that are never used in your project.\n\nA module is considered used if\n\n - it contains a `main` function (be it exposed or not)\n - it imports the `Test` module\n - it is imported in any other modules, even if it is not used.\n - the project is a package and the module is part of the `elm.json`'s `exposed-modules`\n - it is named `ReviewConfig`\n\n```elm\nconfig =\n [ NoUnused.Modules.rule\n ]\n```\n\n\n## Try it out\n\nYou can try this rule out by running the following command:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Modules\n```\n\n","type":"Review.Rule.Rule"}],"binops":[]},{"name":"NoUnused.Parameters","comment":" Report parameters that are not used.\n\n@docs rule\n\n","unions":[],"aliases":[],"values":[{"name":"rule","comment":" Report parameters that are not used.\n\n🔧 Running with `--fix` will automatically remove some of the reported errors.\n\n config =\n [ NoUnused.Parameters.rule\n ]\n\nThis rule looks within function arguments, let functions and lambdas to find any values that are unused. It will report any parameters that are not used.\n\n\n## Fixes for lambdas\n\nWe're only offering fixes for lambdas here because we believe unused parameters in functions are a code smell that should be refactored.\n\n\n## Fail\n\nValue `number` is not used:\n\n add1 number =\n 1\n\nThe rule will also report parameters that are only used to be passed again to the containing recursive function:\n\n last list unused =\n case list of\n [] ->\n Nothing\n\n [ a ] ->\n Just a\n\n _ :: rest ->\n last rest unused\n\n\n## Success\n\n add1 number =\n number + 1\n\n\n## Try it out\n\nYou can try this rule out by running the following command:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Parameters\n```\n\n","type":"Review.Rule.Rule"}],"binops":[]},{"name":"NoUnused.Patterns","comment":" Report useless patterns and pattern values that are not used.\n\n@docs rule\n\n","unions":[],"aliases":[],"values":[{"name":"rule","comment":" Report useless patterns and pattern values that are not used.\n\n🔧 Running with `--fix` will automatically remove all the reported errors.\n\n config =\n [ NoUnused.Patterns.rule\n ]\n\nThis rule looks within let..in blocks and case branches to find any patterns that are unused. It will report any useless patterns as well as any pattern values that are not used.\n\n\n## Fail\n\nValue `something` is not used:\n\n case maybe of\n Just something ->\n True\n\n Nothing ->\n False\n\n\n## Success\n\n case maybe of\n Just _ ->\n True\n\n Nothing ->\n False\n\n\n## Try it out\n\nYou can try this rule out by running the following command:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Patterns\n```\n\n","type":"Review.Rule.Rule"}],"binops":[]},{"name":"NoUnused.Variables","comment":" Report variables or types that are declared or imported but never used inside of a module.\n\n@docs rule\n\n","unions":[],"aliases":[],"values":[{"name":"rule","comment":" Report variables or types that are declared or imported but never used.\n\n🔧 Running with `--fix` will automatically remove all the reported errors.\n\n config =\n [ NoUnused.Variables.rule\n ]\n\n\n## Fail\n\n module A exposing (a, b)\n\n import UnusedImport\n\n a n =\n n + 1\n\n b =\n let\n unused =\n some thing\n\n _ =\n someOther thing\n in\n 2\n\n c =\n a 2\n\n\n## Success\n\n module A exposing (a, b)\n\n a n =\n n + 1\n\n b =\n 2\n\n\n## Exception\n\nTo avoid resorting to weird workarounds that are sometimes used in internal interactive examples, the rule won't report\nvalues assigned to `_` if a direct call to `Debug.log` is assigned to it.\n\n a value =\n let\n _ =\n Debug.log \"value\" value\n in\n value + 1\n\nIf you enable the [`NoDebug.Log`](https://package.elm-lang.org/packages/jfmengels/elm-review-debug/latest/NoDebug-Log) rule\nfrom the [`jfmengels/elm-review-debug`](https://package.elm-lang.org/packages/jfmengels/elm-review-debug/latest/) package,\nand configure it to ignore the locations where it's acceptable, then the combination of both rules will make sure to\nclean up code like the above in all the other locations.\n\n\n## Try it out\n\nYou can try this rule out by running the following command:\n\n```bash\nelm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Variables\n```\n\n","type":"Review.Rule.Rule"}],"binops":[]}] -------------------------------------------------------------------------------- /tests/NoUnused/CustomTypeConstructorArgsTest.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.CustomTypeConstructorArgsTest exposing (all) 2 | 3 | import Elm.Project 4 | import Json.Decode as Decode 5 | import NoUnused.CustomTypeConstructorArgs exposing (rule) 6 | import Review.Project as Project exposing (Project) 7 | import Review.Test 8 | import Review.Test.Dependencies 9 | import Test exposing (Test, describe, test) 10 | 11 | 12 | message : String 13 | message = 14 | "Argument is never extracted and therefore never used." 15 | 16 | 17 | details : List String 18 | details = 19 | [ "This argument is never used. You should either use it somewhere, or remove it at the location I pointed at." 20 | ] 21 | 22 | 23 | all : Test 24 | all = 25 | describe "NoUnused.CustomTypeConstructorArgs" 26 | [ test "should report an error when custom type constructor argument is never used" <| 27 | \() -> 28 | """module A exposing (..) 29 | type CustomType 30 | = B B_Data 31 | 32 | b = B () 33 | 34 | something = 35 | case foo of 36 | B _ -> () 37 | """ 38 | |> Review.Test.run rule 39 | |> Review.Test.expectErrors 40 | [ Review.Test.error 41 | { message = message 42 | , details = details 43 | , under = "B_Data" 44 | } 45 | ] 46 | , test "should report an error when custom type constructor argument is never used, even in parens" <| 47 | \() -> 48 | """module A exposing (..) 49 | type CustomType 50 | = B B_Data 51 | 52 | b = B () 53 | 54 | something = 55 | case foo of 56 | B (_) -> () 57 | """ 58 | |> Review.Test.run rule 59 | |> Review.Test.expectErrors 60 | [ Review.Test.error 61 | { message = message 62 | , details = details 63 | , under = "B_Data" 64 | } 65 | ] 66 | , test "should not report an error if custom type constructor argument is used" <| 67 | \() -> 68 | """module A exposing (..) 69 | type CustomType 70 | = B B_Data 71 | 72 | b = B () 73 | 74 | something = 75 | case foo of 76 | B value -> value 77 | """ 78 | |> Review.Test.run rule 79 | |> Review.Test.expectNoErrors 80 | , test "should report an error only for the unused arguments (multiple arguments)" <| 81 | \() -> 82 | """module A exposing (..) 83 | type CustomType 84 | = Constructor SomeData SomeOtherData 85 | 86 | b = Constructor () 87 | 88 | something = 89 | case foo of 90 | Constructor _ value -> value 91 | """ 92 | |> Review.Test.run rule 93 | |> Review.Test.expectErrors 94 | [ Review.Test.error 95 | { message = message 96 | , details = details 97 | , under = "SomeData" 98 | } 99 | ] 100 | , test "should not report an error for used arguments in nested patterns (tuple)" <| 101 | \() -> 102 | """module A exposing (..) 103 | type CustomType 104 | = Constructor SomeData 105 | 106 | b = Constructor () 107 | 108 | something = 109 | case foo of 110 | (_, Constructor value) -> value 111 | """ 112 | |> Review.Test.run rule 113 | |> Review.Test.expectNoErrors 114 | , test "should not report an error for used arguments in nested patterns (list)" <| 115 | \() -> 116 | """module A exposing (..) 117 | type CustomType 118 | = Constructor SomeData 119 | 120 | b = Constructor () 121 | 122 | something = 123 | case foo of 124 | [Constructor value] -> value 125 | """ 126 | |> Review.Test.run rule 127 | |> Review.Test.expectNoErrors 128 | , test "should not report an error for used arguments in nested patterns (uncons)" <| 129 | \() -> 130 | """module A exposing (..) 131 | type CustomType 132 | = Constructor A B 133 | 134 | b = Constructor () 135 | 136 | something = 137 | case foo of 138 | Constructor a _ :: [Constructor _ b] -> value 139 | """ 140 | |> Review.Test.run rule 141 | |> Review.Test.expectNoErrors 142 | , test "should not report an error for used arguments in nested patterns (parens)" <| 143 | \() -> 144 | """module A exposing (..) 145 | type CustomType 146 | = Constructor A B 147 | 148 | b = Constructor () 149 | 150 | something = 151 | case foo of 152 | ( Constructor a b ) -> value 153 | """ 154 | |> Review.Test.run rule 155 | |> Review.Test.expectNoErrors 156 | , test "should not report an error for used arguments in nested patterns (nested case)" <| 157 | \() -> 158 | """module A exposing (..) 159 | type CustomType 160 | = Constructor A B 161 | 162 | b = Constructor () 163 | 164 | something = 165 | case foo of 166 | Constructor _ (Constructor a _ ) -> value 167 | """ 168 | |> Review.Test.run rule 169 | |> Review.Test.expectNoErrors 170 | , test "should not report an error for used arguments in nested patterns (as pattern)" <| 171 | \() -> 172 | """module A exposing (..) 173 | type CustomType 174 | = Constructor A 175 | 176 | b = Constructor () 177 | 178 | something = 179 | case foo of 180 | (Constructor a ) as thing -> value 181 | """ 182 | |> Review.Test.run rule 183 | |> Review.Test.expectNoErrors 184 | , test "should not report an error for used arguments in top-level function argument destructuring" <| 185 | \() -> 186 | """module A exposing (..) 187 | type CustomType 188 | = Constructor A 189 | 190 | b = Constructor () 191 | 192 | something (Constructor a) = 193 | a 194 | """ 195 | |> Review.Test.run rule 196 | |> Review.Test.expectNoErrors 197 | , test "should not report an error for used arguments in let in function argument destructuring" <| 198 | \() -> 199 | """module A exposing (..) 200 | type CustomType 201 | = Constructor A 202 | 203 | b = Constructor () 204 | 205 | something = 206 | let 207 | foo (Constructor a) = 1 208 | in 209 | a 210 | """ 211 | |> Review.Test.run rule 212 | |> Review.Test.expectNoErrors 213 | , test "should not report an error for used arguments in lambda argument destructuring" <| 214 | \() -> 215 | """module A exposing (..) 216 | type CustomType 217 | = Constructor A 218 | 219 | b = Constructor () 220 | 221 | something = 222 | \\(Constructor a) -> 1 223 | """ 224 | |> Review.Test.run rule 225 | |> Review.Test.expectNoErrors 226 | , test "should not report an error for used arguments in let declaration destructuring" <| 227 | \() -> 228 | """module A exposing (..) 229 | type CustomType 230 | = Constructor A 231 | 232 | b = Constructor () 233 | 234 | something = 235 | let 236 | (Constructor a) = b 237 | in 238 | a 239 | """ 240 | |> Review.Test.run rule 241 | |> Review.Test.expectNoErrors 242 | , test "should not report an error for used arguments used in a different file" <| 243 | \() -> 244 | [ """module A exposing (..) 245 | type CustomType 246 | = Constructor A 247 | """, """module B exposing (..) 248 | import A 249 | 250 | something = 251 | case foo of 252 | A.Constructor value -> value 253 | """ ] 254 | |> Review.Test.runOnModules rule 255 | |> Review.Test.expectNoErrors 256 | , test "should report errors for non-exposed modules in a package (exposing everything)" <| 257 | \() -> 258 | """module NotExposed exposing (..) 259 | type CustomType 260 | = Constructor SomeData 261 | 262 | b = Constructor () 263 | 264 | something = 265 | case foo of 266 | Constructor _ -> 1 267 | """ 268 | |> Review.Test.runWithProjectData packageProject rule 269 | |> Review.Test.expectErrors 270 | [ Review.Test.error 271 | { message = message 272 | , details = details 273 | , under = "SomeData" 274 | } 275 | ] 276 | , test "should report errors for non-exposed modules in a package (exposing explicitly)" <| 277 | \() -> 278 | """module NotExposed exposing (CustomType(..)) 279 | type CustomType 280 | = Constructor SomeData 281 | 282 | b = Constructor () 283 | 284 | something = 285 | case foo of 286 | Constructor _ -> 1 287 | """ 288 | |> Review.Test.runWithProjectData packageProject rule 289 | |> Review.Test.expectErrors 290 | [ Review.Test.error 291 | { message = message 292 | , details = details 293 | , under = "SomeData" 294 | } 295 | ] 296 | , test "should not report errors for exposed modules that expose everything" <| 297 | \() -> 298 | """module Exposed exposing (..) 299 | type CustomType 300 | = Constructor SomeData 301 | 302 | b = Constructor () 303 | 304 | something = 305 | case foo of 306 | Constructor _ -> 1 307 | """ 308 | |> Review.Test.runWithProjectData packageProject rule 309 | |> Review.Test.expectNoErrors 310 | , test "should not report errors for exposed modules in a package (exposing explicitly)" <| 311 | \() -> 312 | """module Exposed exposing (CustomType(..)) 313 | type CustomType 314 | = Constructor SomeData 315 | 316 | b = Constructor () 317 | 318 | something = 319 | case foo of 320 | Constructor _ -> 1 321 | """ 322 | |> Review.Test.runWithProjectData packageProject rule 323 | |> Review.Test.expectNoErrors 324 | , test "should report errors if the type is not exposed outside the module" <| 325 | \() -> 326 | """module Exposed exposing (b) 327 | type CustomType 328 | = Constructor SomeData 329 | 330 | b = Constructor () 331 | 332 | something = 333 | case foo of 334 | Constructor _ -> 1 335 | """ 336 | |> Review.Test.runWithProjectData packageProject rule 337 | |> Review.Test.expectErrors 338 | [ Review.Test.error 339 | { message = message 340 | , details = details 341 | , under = "SomeData" 342 | } 343 | ] 344 | , test "should report errors if the type is exposed but not its constructors" <| 345 | \() -> 346 | """module Exposed exposing (CustomType) 347 | type CustomType 348 | = Constructor SomeData 349 | 350 | b = Constructor () 351 | 352 | something = 353 | case foo of 354 | Constructor _ -> 1 355 | """ 356 | |> Review.Test.runWithProjectData packageProject rule 357 | |> Review.Test.expectErrors 358 | [ Review.Test.error 359 | { message = message 360 | , details = details 361 | , under = "SomeData" 362 | } 363 | ] 364 | , test "should not report args if they are used in a different module" <| 365 | \() -> 366 | [ """ 367 | module Main exposing (Model, main) 368 | import Messages exposing (Msg(..)) 369 | 370 | update : Msg -> Model -> Model 371 | update msg model = 372 | case msg of 373 | Content s -> 374 | { model | content = "content " ++ s } 375 | 376 | Search string -> 377 | { model | content = "search " ++ string } 378 | """ 379 | , """ 380 | module Messages exposing (Msg(..)) 381 | type Msg 382 | = Content String 383 | | Search String 384 | """ 385 | ] 386 | |> Review.Test.runOnModules rule 387 | |> Review.Test.expectNoErrors 388 | , test "should not report Never arguments" <| 389 | \() -> 390 | """ 391 | module Main exposing (a) 392 | a = 1 393 | type CustomType 394 | = B Never 395 | """ 396 | |> Review.Test.runWithProjectData packageProject rule 397 | |> Review.Test.expectNoErrors 398 | , test "should not report Never arguments even when aliased" <| 399 | \() -> 400 | """ 401 | module Main exposing (a) 402 | import Basics as B 403 | a = 1 404 | type CustomType 405 | = B B.Never 406 | """ 407 | |> Review.Test.runWithProjectData packageProject rule 408 | |> Review.Test.expectNoErrors 409 | , test "should report arguments next to Never" <| 410 | -- Honestly I'm unsure about doing this, but I currently don't see 411 | -- the point of having other args next to a Never arg. 412 | \() -> 413 | """ 414 | module Main exposing (a) 415 | a = 1 416 | type CustomType 417 | = B SomeData Never 418 | """ 419 | |> Review.Test.runWithProjectData packageProject rule 420 | |> Review.Test.expectErrors 421 | [ Review.Test.error 422 | { message = message 423 | , details = details 424 | , under = "SomeData" 425 | } 426 | ] 427 | , test "should not report args for type constructors used in an equality expression (==)" <| 428 | \() -> 429 | """ 430 | module MyModule exposing (a, b) 431 | type Foo = Unused Int | B 432 | a = Unused 0 == b 433 | b = B 434 | """ 435 | |> Review.Test.runWithProjectData packageProject rule 436 | |> Review.Test.expectNoErrors 437 | , test "should not report args for type constructors starting with a non-ASCII letter used in an equality expression (==)" <| 438 | \() -> 439 | """ 440 | module MyModule exposing (a, b) 441 | type Foo = Ö_Unused Int | Ö_B 442 | a = Ö_Unused 0 == b 443 | b = Ö_B 444 | """ 445 | |> Review.Test.runWithProjectData packageProject rule 446 | |> Review.Test.expectNoErrors 447 | , test "should not report args for type constructors used in an inequality expression (/=)" <| 448 | \() -> 449 | """ 450 | module MyModule exposing (a, b) 451 | type Foo = Unused Int | B 452 | a = Unused 0 /= b 453 | b = B 454 | """ 455 | |> Review.Test.runWithProjectData packageProject rule 456 | |> Review.Test.expectNoErrors 457 | , test "should report args for type constructors used in non-equality operator expressions" <| 458 | \() -> 459 | """ 460 | module MyModule exposing (a, b) 461 | type Foo = Unused Int | B 462 | a = Unused <| b 463 | b = B 464 | """ 465 | |> Review.Test.runWithProjectData packageProject rule 466 | |> Review.Test.expectErrors 467 | [ Review.Test.error 468 | { message = message 469 | , details = details 470 | , under = "Int" 471 | } 472 | ] 473 | , test "should report args for type constructors starting with a non-ASCII letter used in non-equality operator expressions" <| 474 | \() -> 475 | """ 476 | module MyModule exposing (a, b) 477 | type Foo = Ö_Unused Int | Ö_B 478 | a = Ö_Unused <| b 479 | b = Ö_B 480 | """ 481 | |> Review.Test.runWithProjectData packageProject rule 482 | |> Review.Test.expectErrors 483 | [ Review.Test.error 484 | { message = message 485 | , details = details 486 | , under = "Int" 487 | } 488 | ] 489 | , test "should not report args for type constructors used as arguments to a prefixed equality operator (==)" <| 490 | \() -> 491 | """ 492 | module MyModule exposing (a, b) 493 | type Foo = Unused Int | B 494 | a = (==) b (Unused 0) 495 | b = B 496 | """ 497 | |> Review.Test.runWithProjectData packageProject rule 498 | |> Review.Test.expectNoErrors 499 | , test "should not report args for type constructors used as arguments to a prefixed inequality operator (/=)" <| 500 | \() -> 501 | """ 502 | module MyModule exposing (a, b) 503 | type Foo = Unused Int | B 504 | a = (/=) Unused 0 b 505 | b = B 506 | """ 507 | |> Review.Test.runWithProjectData packageProject rule 508 | |> Review.Test.expectNoErrors 509 | , test "should not report args for type constructors used in an equality expression with parenthesized expressions" <| 510 | \() -> 511 | """ 512 | module MyModule exposing (a, b) 513 | type Foo = Unused Int | B 514 | a = ( Unused 0 ) == b 515 | b = ( B ) 516 | """ 517 | |> Review.Test.runWithProjectData packageProject rule 518 | |> Review.Test.expectNoErrors 519 | , test "should not report args for type constructors used in an equality expression with tuples" <| 520 | \() -> 521 | """ 522 | module MyModule exposing (a, b) 523 | type Foo = Unused Int | B 524 | a = ( Unused 0, Unused 1 ) == b 525 | b = ( B, B ) 526 | """ 527 | |> Review.Test.runWithProjectData packageProject rule 528 | |> Review.Test.expectNoErrors 529 | , test "should not report args for type constructors used in an equality expression with lists" <| 530 | \() -> 531 | """ 532 | module MyModule exposing (a, b) 533 | type Foo = Unused Int | B 534 | a = [ Unused 0 ] == b 535 | b = [ B ] 536 | """ 537 | |> Review.Test.runWithProjectData packageProject rule 538 | |> Review.Test.expectNoErrors 539 | , test "should not report args for type constructors used in an equality expression (==) in a different module" <| 540 | \() -> 541 | [ """ 542 | module MyModule exposing (a, b) 543 | import Foo as F 544 | a = F.Unused 0 == b 545 | b = F.B 546 | """, """ 547 | module Foo exposing (Foo(..)) 548 | type Foo = Unused Int | B 549 | """ ] 550 | |> Review.Test.runOnModulesWithProjectData packageProject rule 551 | |> Review.Test.expectNoErrors 552 | , test "should report args for type constructors used in an equality expression when value is passed to a function" <| 553 | \() -> 554 | """ 555 | module MyModule exposing (a, b) 556 | type Foo = Unused Int | B 557 | a = foo (Unused 0) == b 558 | b = B 559 | """ 560 | |> Review.Test.runWithProjectData packageProject rule 561 | |> Review.Test.expectErrors 562 | [ Review.Test.error 563 | { message = message 564 | , details = details 565 | , under = "Int" 566 | } 567 | ] 568 | ] 569 | 570 | 571 | packageProject : Project 572 | packageProject = 573 | Review.Test.Dependencies.projectWithElmCore 574 | |> Project.addElmJson (createElmJson packageElmJson) 575 | 576 | 577 | packageElmJson : String 578 | packageElmJson = 579 | """ 580 | { 581 | "type": "package", 582 | "name": "author/package", 583 | "summary": "Summary", 584 | "license": "BSD-3-Clause", 585 | "version": "1.0.0", 586 | "exposed-modules": [ 587 | "Exposed" 588 | ], 589 | "elm-version": "0.19.0 <= v < 0.20.0", 590 | "dependencies": { 591 | "elm/core": "1.0.0 <= v < 2.0.0" 592 | }, 593 | "test-dependencies": {} 594 | }""" 595 | 596 | 597 | createElmJson : String -> { path : String, raw : String, project : Elm.Project.Project } 598 | createElmJson rawElmJson = 599 | case Decode.decodeString Elm.Project.decoder rawElmJson of 600 | Ok elmJson -> 601 | { path = "elm.json" 602 | , raw = rawElmJson 603 | , project = elmJson 604 | } 605 | 606 | Err err -> 607 | Debug.todo ("Invalid elm.json supplied to test: " ++ Debug.toString err) 608 | -------------------------------------------------------------------------------- /src/NoUnused/Parameters.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.Parameters exposing (rule) 2 | 3 | {-| Report parameters that are not used. 4 | 5 | @docs rule 6 | 7 | -} 8 | 9 | import Dict exposing (Dict) 10 | import Elm.Syntax.Declaration as Declaration exposing (Declaration) 11 | import Elm.Syntax.Expression as Expression exposing (Expression) 12 | import Elm.Syntax.Node as Node exposing (Node(..)) 13 | import Elm.Syntax.Pattern as Pattern exposing (Pattern) 14 | import Elm.Syntax.Range as Range exposing (Range) 15 | import Review.Fix as Fix exposing (Fix) 16 | import Review.Rule as Rule exposing (Rule) 17 | import Set exposing (Set) 18 | 19 | 20 | {-| Report parameters that are not used. 21 | 22 | 🔧 Running with `--fix` will automatically remove some of the reported errors. 23 | 24 | config = 25 | [ NoUnused.Parameters.rule 26 | ] 27 | 28 | This rule looks within function arguments, let functions and lambdas to find any values that are unused. It will report any parameters that are not used. 29 | 30 | 31 | ## Fixes for lambdas 32 | 33 | We're only offering fixes for lambdas here because we believe unused parameters in functions are a code smell that should be refactored. 34 | 35 | 36 | ## Fail 37 | 38 | Value `number` is not used: 39 | 40 | add1 number = 41 | 1 42 | 43 | The rule will also report parameters that are only used to be passed again to the containing recursive function: 44 | 45 | last list unused = 46 | case list of 47 | [] -> 48 | Nothing 49 | 50 | [ a ] -> 51 | Just a 52 | 53 | _ :: rest -> 54 | last rest unused 55 | 56 | 57 | ## Success 58 | 59 | add1 number = 60 | number + 1 61 | 62 | 63 | ## Try it out 64 | 65 | You can try this rule out by running the following command: 66 | 67 | ```bash 68 | elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Parameters 69 | ``` 70 | 71 | -} 72 | rule : Rule 73 | rule = 74 | Rule.newModuleRuleSchema "NoUnused.Parameters" initialContext 75 | |> Rule.withDeclarationEnterVisitor declarationEnterVisitor 76 | |> Rule.withDeclarationExitVisitor declarationExitVisitor 77 | |> Rule.withExpressionEnterVisitor expressionEnterVisitor 78 | |> Rule.withExpressionExitVisitor expressionExitVisitor 79 | |> Rule.withLetDeclarationEnterVisitor letDeclarationEnterVisitor 80 | |> Rule.withLetDeclarationExitVisitor letDeclarationExitVisitor 81 | |> Rule.providesFixesForModuleRule 82 | |> Rule.fromModuleRuleSchema 83 | 84 | 85 | 86 | --- CONTEXT 87 | 88 | 89 | type alias Context = 90 | { scopes : List Scope 91 | , knownFunctions : Dict String FunctionArgs 92 | , locationsToIgnoreForUsed : LocationsToIgnore 93 | } 94 | 95 | 96 | type alias Scope = 97 | { functionName : String 98 | , declared : List Declared 99 | , used : Set String 100 | , usedRecursively : Set String 101 | } 102 | 103 | 104 | type alias Declared = 105 | { name : String 106 | , range : Range 107 | , kind : Kind 108 | , source : Source 109 | , fix : List Fix 110 | } 111 | 112 | 113 | type alias LocationsToIgnore = 114 | Dict String (List Range) 115 | 116 | 117 | type alias FunctionArgs = 118 | Dict Int String 119 | 120 | 121 | type Kind 122 | = Parameter 123 | | Alias 124 | | TupleWithoutVariables 125 | 126 | 127 | type Source 128 | = NamedFunction 129 | | Lambda 130 | 131 | 132 | initialContext : Context 133 | initialContext = 134 | { scopes = [] 135 | , knownFunctions = Dict.empty 136 | , locationsToIgnoreForUsed = Dict.empty 137 | } 138 | 139 | 140 | 141 | -- DECLARATION VISITOR 142 | 143 | 144 | declarationEnterVisitor : Node Declaration -> Context -> ( List nothing, Context ) 145 | declarationEnterVisitor node context = 146 | case Node.value node of 147 | Declaration.FunctionDeclaration { declaration } -> 148 | let 149 | arguments : List (Node Pattern) 150 | arguments = 151 | (Node.value declaration).arguments 152 | 153 | declared : List (List Declared) 154 | declared = 155 | List.map (getParametersFromPatterns NamedFunction) arguments 156 | 157 | functionName : String 158 | functionName = 159 | Node.value declaration |> .name |> Node.value 160 | in 161 | ( [] 162 | , { scopes = 163 | [ { functionName = functionName 164 | , declared = List.concat declared 165 | , used = Set.empty 166 | , usedRecursively = Set.empty 167 | } 168 | ] 169 | , knownFunctions = Dict.singleton functionName (getArgNames declared) 170 | , locationsToIgnoreForUsed = Dict.empty 171 | } 172 | ) 173 | 174 | _ -> 175 | ( [], context ) 176 | 177 | 178 | declarationExitVisitor : Node Declaration -> Context -> ( List (Rule.Error {}), Context ) 179 | declarationExitVisitor node context = 180 | case Node.value node of 181 | Declaration.FunctionDeclaration _ -> 182 | report context 183 | 184 | _ -> 185 | ( [], context ) 186 | 187 | 188 | getArgNames : List (List Declared) -> FunctionArgs 189 | getArgNames declared = 190 | getArgNamesHelp declared 0 Dict.empty 191 | 192 | 193 | getArgNamesHelp : List (List Declared) -> Int -> FunctionArgs -> FunctionArgs 194 | getArgNamesHelp declared index acc = 195 | case declared of 196 | [] -> 197 | acc 198 | 199 | args :: restOfDeclared -> 200 | let 201 | newAcc : Dict Int String 202 | newAcc = 203 | case args of 204 | [ arg ] -> 205 | Dict.insert index arg.name acc 206 | 207 | _ -> 208 | acc 209 | in 210 | getArgNamesHelp restOfDeclared (index + 1) newAcc 211 | 212 | 213 | getParametersFromPatterns : Source -> Node Pattern -> List Declared 214 | getParametersFromPatterns source node = 215 | case Node.value node of 216 | Pattern.ParenthesizedPattern pattern -> 217 | getParametersFromPatterns source pattern 218 | 219 | Pattern.VarPattern name -> 220 | [ { name = name 221 | , range = Node.range node 222 | , kind = Parameter 223 | , fix = [ Fix.replaceRangeBy (Node.range node) "_" ] 224 | , source = source 225 | } 226 | ] 227 | 228 | Pattern.AsPattern pattern asName -> 229 | getParametersFromAsPattern source pattern asName 230 | 231 | Pattern.RecordPattern fields -> 232 | case fields of 233 | [ field ] -> 234 | [ { name = Node.value field 235 | , range = Node.range field 236 | , kind = Parameter 237 | , fix = [ Fix.replaceRangeBy (Node.range node) "_" ] 238 | , source = source 239 | } 240 | ] 241 | 242 | _ -> 243 | let 244 | fieldNames : List String 245 | fieldNames = 246 | List.map Node.value fields 247 | in 248 | List.map 249 | (\field -> 250 | { name = Node.value field 251 | , range = Node.range field 252 | , kind = Parameter 253 | , fix = 254 | [ Fix.replaceRangeBy 255 | (Node.range node) 256 | (fieldNames |> List.filter (\f -> f /= Node.value field) |> formatRecord) 257 | ] 258 | , source = source 259 | } 260 | ) 261 | fields 262 | 263 | Pattern.TuplePattern patterns -> 264 | let 265 | parametersFromPatterns : List Declared 266 | parametersFromPatterns = 267 | List.concatMap (getParametersFromPatterns source) patterns 268 | in 269 | if List.isEmpty parametersFromPatterns && List.all isPatternWildCard patterns then 270 | [ { name = "" 271 | , range = Node.range node 272 | , kind = TupleWithoutVariables 273 | , fix = [ Fix.replaceRangeBy (Node.range node) "_" ] 274 | , source = source 275 | } 276 | ] 277 | 278 | else 279 | parametersFromPatterns 280 | 281 | Pattern.NamedPattern _ patterns -> 282 | List.concatMap (getParametersFromPatterns source) patterns 283 | 284 | _ -> 285 | [] 286 | 287 | 288 | getParametersFromAsPattern : Source -> Node Pattern -> Node String -> List Declared 289 | getParametersFromAsPattern source pattern asName = 290 | let 291 | parametersFromPatterns : List Declared 292 | parametersFromPatterns = 293 | getParametersFromPatterns source pattern 294 | 295 | asParameter : Declared 296 | asParameter = 297 | { name = Node.value asName 298 | , range = Node.range asName 299 | , kind = Alias 300 | , fix = [ Fix.removeRange { start = (Node.range pattern).end, end = (Node.range asName).end } ] 301 | , source = source 302 | } 303 | in 304 | asParameter :: parametersFromPatterns 305 | 306 | 307 | isPatternWildCard : Node Pattern -> Bool 308 | isPatternWildCard node = 309 | case Node.value node of 310 | Pattern.ParenthesizedPattern pattern -> 311 | isPatternWildCard pattern 312 | 313 | Pattern.AllPattern -> 314 | True 315 | 316 | _ -> 317 | False 318 | 319 | 320 | formatRecord : List String -> String 321 | formatRecord fields = 322 | "{ " ++ String.join ", " fields ++ " }" 323 | 324 | 325 | 326 | -- EXPRESSION ENTER VISITOR 327 | 328 | 329 | expressionEnterVisitor : Node Expression -> Context -> ( List nothing, Context ) 330 | expressionEnterVisitor node context = 331 | ( [], expressionEnterVisitorHelp node context ) 332 | 333 | 334 | expressionEnterVisitorHelp : Node Expression -> Context -> Context 335 | expressionEnterVisitorHelp node context = 336 | case Node.value node of 337 | Expression.FunctionOrValue [] name -> 338 | markValueAsUsed (Node.range node) name context 339 | 340 | Expression.RecordUpdateExpression name _ -> 341 | markValueAsUsed (Node.range name) (Node.value name) context 342 | 343 | Expression.LambdaExpression { args } -> 344 | { context 345 | | scopes = 346 | { functionName = "dummy lambda" 347 | , declared = List.concatMap (getParametersFromPatterns Lambda) args 348 | , used = Set.empty 349 | , usedRecursively = Set.empty 350 | } 351 | :: context.scopes 352 | } 353 | 354 | Expression.Application ((Node _ (Expression.FunctionOrValue [] fnName)) :: arguments) -> 355 | registerFunctionCall fnName 0 arguments context 356 | 357 | Expression.OperatorApplication "|>" _ lastArgument (Node _ (Expression.Application ((Node _ (Expression.FunctionOrValue [] fnName)) :: arguments))) -> 358 | -- Ignoring "arguments" because they will be visited when the Application node will be visited anyway. 359 | registerFunctionCall fnName (List.length arguments) [ lastArgument ] context 360 | 361 | Expression.OperatorApplication "<|" _ (Node _ (Expression.Application ((Node _ (Expression.FunctionOrValue [] fnName)) :: arguments))) lastArgument -> 362 | -- Ignoring "arguments" because they will be visited when the Application node will be visited anyway. 363 | registerFunctionCall fnName (List.length arguments) [ lastArgument ] context 364 | 365 | _ -> 366 | context 367 | 368 | 369 | 370 | -- EXPRESSION EXIT VISITOR 371 | 372 | 373 | expressionExitVisitor : Node Expression -> Context -> ( List (Rule.Error {}), Context ) 374 | expressionExitVisitor (Node _ node) context = 375 | case node of 376 | Expression.LambdaExpression _ -> 377 | report context 378 | 379 | _ -> 380 | ( [], context ) 381 | 382 | 383 | letDeclarationEnterVisitor : a -> Node Expression.LetDeclaration -> Context -> ( List nothing, Context ) 384 | letDeclarationEnterVisitor _ letDeclaration context = 385 | case Node.value letDeclaration of 386 | Expression.LetFunction function -> 387 | let 388 | declaration : Expression.FunctionImplementation 389 | declaration = 390 | Node.value function.declaration 391 | in 392 | if List.isEmpty declaration.arguments then 393 | ( [], context ) 394 | 395 | else 396 | let 397 | functionName : String 398 | functionName = 399 | Node.value declaration.name 400 | 401 | declared : List (List Declared) 402 | declared = 403 | List.map (getParametersFromPatterns NamedFunction) declaration.arguments 404 | 405 | newScope : Scope 406 | newScope = 407 | { functionName = functionName 408 | , declared = List.concat declared 409 | , used = Set.empty 410 | , usedRecursively = Set.empty 411 | } 412 | in 413 | ( [] 414 | , { context 415 | | scopes = newScope :: context.scopes 416 | , knownFunctions = Dict.insert functionName (getArgNames declared) context.knownFunctions 417 | } 418 | ) 419 | 420 | Expression.LetDestructuring _ _ -> 421 | ( [], context ) 422 | 423 | 424 | letDeclarationExitVisitor : a -> Node Expression.LetDeclaration -> Context -> ( List (Rule.Error {}), Context ) 425 | letDeclarationExitVisitor _ letDeclaration context = 426 | case Node.value letDeclaration of 427 | Expression.LetFunction function -> 428 | let 429 | declaration : Expression.FunctionImplementation 430 | declaration = 431 | Node.value function.declaration 432 | in 433 | if List.isEmpty declaration.arguments then 434 | ( [], context ) 435 | 436 | else 437 | report context 438 | 439 | Expression.LetDestructuring _ _ -> 440 | ( [], context ) 441 | 442 | 443 | registerFunctionCall : String -> Int -> List (Node a) -> Context -> Context 444 | registerFunctionCall fnName numberOfIgnoredArguments arguments context = 445 | case Dict.get fnName context.knownFunctions of 446 | Just fnArgs -> 447 | let 448 | locationsToIgnore : LocationsToIgnore 449 | locationsToIgnore = 450 | ignoreLocations fnArgs numberOfIgnoredArguments arguments 0 context.locationsToIgnoreForUsed 451 | in 452 | { context | locationsToIgnoreForUsed = locationsToIgnore } 453 | 454 | Nothing -> 455 | context 456 | 457 | 458 | ignoreLocations : FunctionArgs -> Int -> List (Node a) -> Int -> LocationsToIgnore -> LocationsToIgnore 459 | ignoreLocations fnArgs numberOfIgnoredArguments nodes index acc = 460 | case nodes of 461 | [] -> 462 | acc 463 | 464 | (Node range _) :: rest -> 465 | let 466 | newAcc : LocationsToIgnore 467 | newAcc = 468 | case Dict.get (numberOfIgnoredArguments + index) fnArgs of 469 | Just argName -> 470 | case Dict.get argName acc of 471 | Just existingLocations -> 472 | Dict.insert argName (range :: existingLocations) acc 473 | 474 | Nothing -> 475 | Dict.insert argName [ range ] acc 476 | 477 | Nothing -> 478 | acc 479 | in 480 | ignoreLocations fnArgs numberOfIgnoredArguments rest (index + 1) newAcc 481 | 482 | 483 | markValueAsUsed : Range -> String -> Context -> Context 484 | markValueAsUsed range name context = 485 | case context.scopes of 486 | [] -> 487 | context 488 | 489 | headScope :: restOfScopes -> 490 | let 491 | newHeadScope : Scope 492 | newHeadScope = 493 | if shouldBeIgnored range name context then 494 | { headScope | usedRecursively = Set.insert name headScope.usedRecursively } 495 | 496 | else 497 | { headScope | used = Set.insert name headScope.used } 498 | in 499 | { context | scopes = newHeadScope :: restOfScopes } 500 | 501 | 502 | shouldBeIgnored : Range -> String -> Context -> Bool 503 | shouldBeIgnored range name context = 504 | case Dict.get name context.locationsToIgnoreForUsed of 505 | Just ranges -> 506 | List.any (isRangeIncluded range) ranges 507 | 508 | Nothing -> 509 | False 510 | 511 | 512 | isRangeIncluded : Range -> Range -> Bool 513 | isRangeIncluded inner outer = 514 | (Range.compareLocations inner.start outer.start /= LT) 515 | && (Range.compareLocations inner.end outer.end /= GT) 516 | 517 | 518 | markAllAsUsed : Set String -> List Scope -> List Scope 519 | markAllAsUsed names scopes = 520 | case scopes of 521 | [] -> 522 | scopes 523 | 524 | headScope :: restOfScopes -> 525 | { headScope | used = Set.union names headScope.used } :: restOfScopes 526 | 527 | 528 | report : Context -> ( List (Rule.Error {}), Context ) 529 | report context = 530 | case context.scopes of 531 | headScope :: restOfScopes -> 532 | let 533 | ( errors, remainingUsed ) = 534 | List.foldl 535 | (findErrorsAndVariablesNotPartOfScope headScope) 536 | ( [], headScope.used ) 537 | headScope.declared 538 | in 539 | ( errors 540 | , { context 541 | | scopes = markAllAsUsed remainingUsed restOfScopes 542 | , knownFunctions = Dict.remove headScope.functionName context.knownFunctions 543 | } 544 | ) 545 | 546 | [] -> 547 | ( [], context ) 548 | 549 | 550 | findErrorsAndVariablesNotPartOfScope : Scope -> Declared -> ( List (Rule.Error {}), Set String ) -> ( List (Rule.Error {}), Set String ) 551 | findErrorsAndVariablesNotPartOfScope scope declared ( errors_, remainingUsed_ ) = 552 | if Set.member declared.name scope.usedRecursively then 553 | -- If variable was used as a recursive argument 554 | if Set.member declared.name remainingUsed_ then 555 | -- If variable was used somewhere else as well 556 | ( errors_, Set.remove declared.name remainingUsed_ ) 557 | 558 | else 559 | -- If variable was used ONLY as a recursive argument 560 | ( recursiveParameterError scope.functionName declared :: errors_, Set.remove declared.name remainingUsed_ ) 561 | 562 | else if Set.member declared.name remainingUsed_ then 563 | ( errors_, Set.remove declared.name remainingUsed_ ) 564 | 565 | else 566 | ( errorsForValue declared :: errors_, remainingUsed_ ) 567 | 568 | 569 | errorsForValue : Declared -> Rule.Error {} 570 | errorsForValue { name, kind, range, source, fix } = 571 | Rule.errorWithFix 572 | (errorMessage kind name) 573 | range 574 | (applyFix source fix) 575 | 576 | 577 | errorMessage : Kind -> String -> { message : String, details : List String } 578 | errorMessage kind name = 579 | case kind of 580 | Parameter -> 581 | { message = "Parameter `" ++ name ++ "` is not used" 582 | , details = [ "You should either use this parameter somewhere, or remove it at the location I pointed at." ] 583 | } 584 | 585 | Alias -> 586 | { message = "Pattern alias `" ++ name ++ "` is not used" 587 | , details = [ "You should either use this parameter somewhere, or remove it at the location I pointed at." ] 588 | } 589 | 590 | TupleWithoutVariables -> 591 | { message = "Tuple pattern is not needed" 592 | , details = [ "You should remove this pattern." ] 593 | } 594 | 595 | 596 | recursiveParameterError : String -> Declared -> Rule.Error {} 597 | recursiveParameterError functionName { name, range } = 598 | Rule.error 599 | { message = "Parameter `" ++ name ++ "` is only used in recursion" 600 | , details = 601 | [ "This parameter is only used to be passed as an argument to '" ++ functionName ++ "', but its value is never read or used." 602 | , "You should either use this parameter somewhere, or remove it at the location I pointed at." 603 | ] 604 | } 605 | range 606 | 607 | 608 | applyFix : Source -> List Fix -> List Fix 609 | applyFix source fix = 610 | case source of 611 | NamedFunction -> 612 | [] 613 | 614 | Lambda -> 615 | fix 616 | -------------------------------------------------------------------------------- /src/NoUnused/Dependencies.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.Dependencies exposing (rule) 2 | 3 | {-| Forbid the use of dependencies that are never used in your project. 4 | 5 | @docs rule 6 | 7 | -} 8 | 9 | import Dict exposing (Dict) 10 | import Elm.Constraint 11 | import Elm.Package 12 | import Elm.Project exposing (Project) 13 | import Elm.Syntax.Import exposing (Import) 14 | import Elm.Syntax.Node as Node exposing (Node(..)) 15 | import Elm.Syntax.Range exposing (Range) 16 | import Elm.Version 17 | import List.Extra 18 | import Review.Project.Dependency as Dependency exposing (Dependency) 19 | import Review.Rule as Rule exposing (Error, Rule) 20 | import Set exposing (Set) 21 | 22 | 23 | {-| Forbid the use of dependencies that are never used in your project. 24 | 25 | 🔧 Running with `--fix` will automatically remove all the reported errors. 26 | 27 | A dependency is considered unused if none of its modules are imported in the project. 28 | 29 | config = 30 | [ NoUnused.Dependencies.rule 31 | ] 32 | 33 | 34 | ## Try it out 35 | 36 | You can try this rule out by running the following command: 37 | 38 | ```bash 39 | elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.Dependencies 40 | ``` 41 | 42 | -} 43 | rule : Rule 44 | rule = 45 | Rule.newProjectRuleSchema "NoUnused.Dependencies" initialProjectContext 46 | |> Rule.withElmJsonProjectVisitor elmJsonVisitor 47 | |> Rule.withDirectDependenciesProjectVisitor dependenciesVisitor 48 | |> Rule.withModuleVisitor moduleVisitor 49 | |> Rule.withModuleContextUsingContextCreator 50 | { fromProjectToModule = fromProjectToModule 51 | , fromModuleToProject = fromModuleToProject 52 | , foldProjectContexts = foldProjectContexts 53 | } 54 | |> Rule.withFinalProjectEvaluation finalEvaluationForProject 55 | |> Rule.providesFixesForProjectRule 56 | |> Rule.fromProjectRuleSchema 57 | 58 | 59 | moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext 60 | moduleVisitor schema = 61 | schema 62 | |> Rule.withImportVisitor importVisitor 63 | 64 | 65 | dependenciesVisitor : Dict String Dependency -> ProjectContext -> ( List nothing, ProjectContext ) 66 | dependenciesVisitor dependencies projectContext = 67 | let 68 | moduleNameToDependency : Dict String String 69 | moduleNameToDependency = 70 | Dict.foldl 71 | (\packageName dependency acc -> 72 | List.foldl 73 | (\{ name } subAcc -> 74 | Dict.insert name packageName subAcc 75 | ) 76 | acc 77 | (Dependency.modules dependency) 78 | ) 79 | Dict.empty 80 | dependencies 81 | in 82 | ( [] 83 | , { projectContext 84 | | dependencies = Dict.map (\_ dep -> getDependencyList dep) dependencies 85 | , moduleNameToDependency = moduleNameToDependency 86 | } 87 | ) 88 | 89 | 90 | 91 | -- CONTEXT 92 | 93 | 94 | type alias ProjectContext = 95 | { moduleNameToDependency : Dict String String 96 | , dependencies : Dict String DependencyList 97 | , directProjectDependencies : Set String 98 | , directTestDependencies : Set String 99 | , usedDependencies : Set String 100 | , usedDependenciesFromTest : Set String 101 | , elmJsonKey : Maybe Rule.ElmJsonKey 102 | } 103 | 104 | 105 | type alias DependencyList = 106 | List Elm.Package.Name 107 | 108 | 109 | type alias ModuleContext = 110 | { moduleNameToDependency : Dict String String 111 | , usedDependencies : Set String 112 | } 113 | 114 | 115 | initialProjectContext : ProjectContext 116 | initialProjectContext = 117 | { moduleNameToDependency = Dict.empty 118 | , dependencies = Dict.empty 119 | , directProjectDependencies = Set.empty 120 | , directTestDependencies = Set.empty 121 | , usedDependencies = Set.fromList [ "elm/core", "lamdera/core", "lamdera/codecs" ] 122 | , usedDependenciesFromTest = Set.empty 123 | , elmJsonKey = Nothing 124 | } 125 | 126 | 127 | fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext 128 | fromProjectToModule = 129 | Rule.initContextCreator 130 | (\projectContext -> 131 | { moduleNameToDependency = projectContext.moduleNameToDependency 132 | , usedDependencies = Set.empty 133 | } 134 | ) 135 | 136 | 137 | fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext 138 | fromModuleToProject = 139 | Rule.initContextCreator 140 | (\isInSourceDirectories { usedDependencies } -> 141 | { moduleNameToDependency = Dict.empty 142 | , dependencies = Dict.empty 143 | , directProjectDependencies = Set.empty 144 | , directTestDependencies = Set.empty 145 | , usedDependencies = 146 | if isInSourceDirectories then 147 | usedDependencies 148 | 149 | else 150 | Set.empty 151 | , usedDependenciesFromTest = 152 | if isInSourceDirectories then 153 | Set.empty 154 | 155 | else 156 | usedDependencies 157 | , elmJsonKey = Nothing 158 | } 159 | ) 160 | |> Rule.withIsInSourceDirectories 161 | 162 | 163 | foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext 164 | foldProjectContexts newContext previousContext = 165 | { moduleNameToDependency = previousContext.moduleNameToDependency 166 | , dependencies = previousContext.dependencies 167 | , directProjectDependencies = previousContext.directProjectDependencies 168 | , directTestDependencies = previousContext.directTestDependencies 169 | , usedDependencies = Set.union newContext.usedDependencies previousContext.usedDependencies 170 | , usedDependenciesFromTest = Set.union newContext.usedDependenciesFromTest previousContext.usedDependenciesFromTest 171 | , elmJsonKey = previousContext.elmJsonKey 172 | } 173 | 174 | 175 | 176 | -- PROJECT VISITORS 177 | 178 | 179 | elmJsonVisitor : Maybe { elmJsonKey : Rule.ElmJsonKey, project : Project } -> ProjectContext -> ( List nothing, ProjectContext ) 180 | elmJsonVisitor maybeProject projectContext = 181 | case maybeProject of 182 | Just { elmJsonKey, project } -> 183 | let 184 | ( directProjectDependencies, directTestDependencies ) = 185 | case project of 186 | Elm.Project.Package { deps, testDeps } -> 187 | ( listDependencies deps 188 | , listDependencies testDeps 189 | ) 190 | 191 | Elm.Project.Application { depsDirect, testDepsDirect } -> 192 | ( listDependencies depsDirect 193 | , listDependencies testDepsDirect 194 | ) 195 | in 196 | ( [] 197 | , { projectContext 198 | | elmJsonKey = Just elmJsonKey 199 | , directProjectDependencies = directProjectDependencies 200 | , directTestDependencies = directTestDependencies 201 | , usedDependencies = 202 | case project of 203 | Elm.Project.Application _ -> 204 | Set.insert "elm/json" projectContext.usedDependencies 205 | 206 | Elm.Project.Package _ -> 207 | projectContext.usedDependencies 208 | } 209 | ) 210 | 211 | Nothing -> 212 | ( [], projectContext ) 213 | 214 | 215 | getDependencyList : Dependency -> DependencyList 216 | getDependencyList dependency = 217 | case Dependency.elmJson dependency of 218 | Elm.Project.Application _ -> 219 | [] 220 | 221 | Elm.Project.Package package -> 222 | List.map (\( depName, _ ) -> depName) package.deps 223 | 224 | 225 | listDependencies : List ( Elm.Package.Name, a ) -> Set String 226 | listDependencies deps = 227 | List.foldl 228 | (\( name, _ ) acc -> Set.insert (Elm.Package.toString name) acc) 229 | Set.empty 230 | deps 231 | 232 | 233 | 234 | -- IMPORT VISITOR 235 | 236 | 237 | importVisitor : Node Import -> ModuleContext -> ( List nothing, ModuleContext ) 238 | importVisitor node context = 239 | ( [] 240 | , case Dict.get (moduleNameForImport node) context.moduleNameToDependency of 241 | Just dependency -> 242 | { context | usedDependencies = Set.insert dependency context.usedDependencies } 243 | 244 | Nothing -> 245 | context 246 | ) 247 | 248 | 249 | moduleNameForImport : Node Import -> String 250 | moduleNameForImport (Node _ { moduleName }) = 251 | moduleName 252 | |> Node.value 253 | |> String.join "." 254 | 255 | 256 | 257 | -- FINAL EVALUATION 258 | 259 | 260 | finalEvaluationForProject : ProjectContext -> List (Error { useErrorForModule : () }) 261 | finalEvaluationForProject projectContext = 262 | case projectContext.elmJsonKey of 263 | Just elmJsonKey -> 264 | let 265 | depsNotUsedInSrc : Set String 266 | depsNotUsedInSrc = 267 | Set.diff projectContext.directProjectDependencies projectContext.usedDependencies 268 | 269 | depsNotUsedInSrcButUsedInTests : Set String 270 | depsNotUsedInSrcButUsedInTests = 271 | Set.intersect depsNotUsedInSrc projectContext.usedDependenciesFromTest 272 | 273 | depsNotUsedInSrcErrors : Set String 274 | depsNotUsedInSrcErrors = 275 | Set.diff 276 | depsNotUsedInSrc 277 | depsNotUsedInSrcButUsedInTests 278 | 279 | testDepsNotUsed : Set String 280 | testDepsNotUsed = 281 | Set.diff 282 | projectContext.directTestDependencies 283 | (Set.union projectContext.usedDependenciesFromTest projectContext.usedDependencies) 284 | in 285 | [] 286 | |> add (\packageName -> unusedProjectDependencyError elmJsonKey projectContext.dependencies packageName) depsNotUsedInSrcErrors 287 | |> add (\packageName -> unusedTestDependencyError elmJsonKey projectContext.dependencies packageName) testDepsNotUsed 288 | |> add (\packageName -> moveDependencyToTestError elmJsonKey projectContext.dependencies packageName) depsNotUsedInSrcButUsedInTests 289 | 290 | Nothing -> 291 | [] 292 | 293 | 294 | add : (a -> b) -> Set a -> List b -> List b 295 | add f set initial = 296 | Set.foldl (\item acc -> f item :: acc) initial set 297 | 298 | 299 | 300 | -- ERROR FUNCTIONS 301 | 302 | 303 | unusedProjectDependencyError : Rule.ElmJsonKey -> Dict String DependencyList -> String -> Error scope 304 | unusedProjectDependencyError elmJsonKey dependencies packageName = 305 | Rule.errorForElmJsonWithFix elmJsonKey 306 | (\elmJson -> 307 | { message = "Unused dependency `" ++ packageName ++ "`" 308 | , details = 309 | [ "To remove it, I recommend running the following command:" 310 | , " elm-json uninstall " ++ packageName 311 | ] 312 | , range = findPackageNameInElmJson packageName elmJson 313 | } 314 | ) 315 | (fromProject dependencies InProjectDeps packageName >> Maybe.map (removeProjectDependency >> toProject)) 316 | 317 | 318 | moveDependencyToTestError : Rule.ElmJsonKey -> Dict String DependencyList -> String -> Error scope 319 | moveDependencyToTestError elmJsonKey dependencies packageName = 320 | Rule.errorForElmJsonWithFix elmJsonKey 321 | (\elmJson -> 322 | { message = "`" ++ packageName ++ "` should be moved to test-dependencies" 323 | , details = 324 | [ "This package is not used in the source code, but it is used in tests, and should therefore be moved to the test dependencies. To do so, I recommend running the following commands:" 325 | , " elm-json uninstall " ++ packageName ++ "\n" ++ " elm-json install --test " ++ packageName 326 | ] 327 | , range = findPackageNameInElmJson packageName elmJson 328 | } 329 | ) 330 | (fromProject dependencies InProjectDeps packageName >> Maybe.map (removeProjectDependency >> addTestDependency >> toProject)) 331 | 332 | 333 | unusedTestDependencyError : Rule.ElmJsonKey -> Dict String DependencyList -> String -> Error scope 334 | unusedTestDependencyError elmJsonKey dependencies packageName = 335 | Rule.errorForElmJsonWithFix elmJsonKey 336 | (\elmJson -> 337 | { message = "Unused test dependency `" ++ packageName ++ "`" 338 | , details = 339 | [ "To remove it, I recommend running the following command:" 340 | , " elm-json uninstall " ++ packageName 341 | ] 342 | , range = findPackageNameInElmJson packageName elmJson 343 | } 344 | ) 345 | (\project -> 346 | case fromProject dependencies InTestDeps packageName project of 347 | Just projectAndDependencyIdentifier -> 348 | Just (toProject (removeTestDependency projectAndDependencyIdentifier)) 349 | 350 | Nothing -> 351 | Nothing 352 | ) 353 | 354 | 355 | findPackageNameInElmJson : String -> String -> Range 356 | findPackageNameInElmJson packageName elmJson = 357 | elmJson 358 | |> String.lines 359 | |> List.Extra.findMapWithIndex 360 | (\row line -> 361 | case String.indexes ("\"" ++ packageName ++ "\"") line of 362 | [] -> 363 | Nothing 364 | 365 | column :: _ -> 366 | Just 367 | { start = 368 | { row = row + 1 369 | , column = column + 2 370 | } 371 | , end = 372 | { row = row + 1 373 | , column = column + String.length packageName + 2 374 | } 375 | } 376 | ) 377 | |> Maybe.withDefault defaultPackageNamePosition 378 | 379 | 380 | defaultPackageNamePosition : Range 381 | defaultPackageNamePosition = 382 | { start = { row = 1, column = 1 }, end = { row = 10000, column = 1 } } 383 | 384 | 385 | 386 | -- FIX 387 | 388 | 389 | type ProjectAndDependencyIdentifier 390 | = ApplicationProject 391 | { application : Elm.Project.ApplicationInfo 392 | , name : Elm.Package.Name 393 | , version : Elm.Version.Version 394 | , getDependenciesAndVersion : Elm.Package.Name -> Elm.Project.Deps Elm.Version.Version 395 | } 396 | | PackageProject 397 | { package : Elm.Project.PackageInfo 398 | , name : Elm.Package.Name 399 | , constraint : Elm.Constraint.Constraint 400 | } 401 | 402 | 403 | type DependencyLocation 404 | = InProjectDeps 405 | | InTestDeps 406 | 407 | 408 | fromProject : Dict String DependencyList -> DependencyLocation -> String -> Project -> Maybe ProjectAndDependencyIdentifier 409 | fromProject dependenciesDict dependencyLocation packageNameStr project = 410 | case project of 411 | Elm.Project.Application application -> 412 | fromApplication dependenciesDict dependencyLocation packageNameStr application 413 | 414 | Elm.Project.Package packageInfo -> 415 | let 416 | dependencies : Elm.Project.Deps Elm.Constraint.Constraint 417 | dependencies = 418 | case dependencyLocation of 419 | InProjectDeps -> 420 | packageInfo.deps 421 | 422 | InTestDeps -> 423 | packageInfo.testDeps 424 | in 425 | case List.Extra.find (isPackageWithName packageNameStr) dependencies of 426 | Just ( packageName, constraint ) -> 427 | Just (PackageProject { package = packageInfo, name = packageName, constraint = constraint }) 428 | 429 | Nothing -> 430 | Nothing 431 | 432 | 433 | fromApplication : Dict String DependencyList -> DependencyLocation -> String -> Elm.Project.ApplicationInfo -> Maybe ProjectAndDependencyIdentifier 434 | fromApplication dependenciesDict dependencyLocation packageNameStr application = 435 | let 436 | dependencies : Elm.Project.Deps Elm.Version.Version 437 | dependencies = 438 | case dependencyLocation of 439 | InProjectDeps -> 440 | application.depsDirect 441 | 442 | InTestDeps -> 443 | application.testDepsDirect 444 | 445 | addDeps : List ( Elm.Package.Name, Elm.Version.Version ) -> Dict String Elm.Version.Version -> Dict String Elm.Version.Version 446 | addDeps dep initial = 447 | List.foldl 448 | (\( name, version ) dict -> 449 | Dict.insert (Elm.Package.toString name) version dict 450 | ) 451 | initial 452 | dep 453 | 454 | dependencyVersionDict : Dict String Elm.Version.Version 455 | dependencyVersionDict = 456 | Dict.empty 457 | |> addDeps application.depsDirect 458 | |> addDeps application.depsIndirect 459 | |> addDeps application.testDepsDirect 460 | |> addDeps application.testDepsIndirect 461 | 462 | getDependenciesAndVersion : Elm.Package.Name -> Elm.Project.Deps Elm.Version.Version 463 | getDependenciesAndVersion name = 464 | case Dict.get (Elm.Package.toString name) dependenciesDict of 465 | Just deps -> 466 | packageDependencies dependencyVersionDict deps 467 | 468 | Nothing -> 469 | [] 470 | in 471 | case List.Extra.find (\pkg -> isPackageWithName packageNameStr pkg) dependencies of 472 | Just ( packageName, version ) -> 473 | Just 474 | (ApplicationProject 475 | { application = application 476 | , name = packageName 477 | , version = version 478 | , getDependenciesAndVersion = getDependenciesAndVersion 479 | } 480 | ) 481 | 482 | Nothing -> 483 | Nothing 484 | 485 | 486 | toProject : ProjectAndDependencyIdentifier -> Elm.Project.Project 487 | toProject projectAndDependencyIdentifier = 488 | case projectAndDependencyIdentifier of 489 | ApplicationProject { application } -> 490 | Elm.Project.Application application 491 | 492 | PackageProject { package } -> 493 | Elm.Project.Package package 494 | 495 | 496 | removeProjectDependency : ProjectAndDependencyIdentifier -> ProjectAndDependencyIdentifier 497 | removeProjectDependency projectAndDependencyIdentifier = 498 | case projectAndDependencyIdentifier of 499 | ApplicationProject ({ application } as project) -> 500 | let 501 | depsDirect : List ( Elm.Package.Name, Elm.Version.Version ) 502 | depsDirect = 503 | List.filter (\pkg -> pkg |> isPackageWithName (Elm.Package.toString project.name) |> not) application.depsDirect 504 | 505 | depsIndirect : Elm.Project.Deps Elm.Version.Version 506 | depsIndirect = 507 | listIndirectDependencies project.getDependenciesAndVersion depsDirect 508 | in 509 | ApplicationProject 510 | { project 511 | | application = 512 | { application 513 | | depsDirect = depsDirect 514 | , depsIndirect = depsIndirect 515 | , testDepsIndirect = 516 | listIndirectDependencies project.getDependenciesAndVersion application.testDepsDirect 517 | |> List.filter (\dep -> not (List.member dep depsDirect || List.member dep depsIndirect)) 518 | } 519 | } 520 | 521 | PackageProject ({ package } as project) -> 522 | PackageProject 523 | { project 524 | | package = 525 | { package 526 | | deps = List.filter (\pkg -> pkg |> isPackageWithName (Elm.Package.toString project.name) |> not) package.deps 527 | } 528 | } 529 | 530 | 531 | listIndirectDependencies : (Elm.Package.Name -> Elm.Project.Deps Elm.Version.Version) -> Elm.Project.Deps Elm.Version.Version -> Elm.Project.Deps Elm.Version.Version 532 | listIndirectDependencies getDependenciesAndVersion baseDependencies = 533 | listIndirectDependenciesHelp getDependenciesAndVersion baseDependencies [] [] 534 | |> List.filter (\dep -> not (List.member dep baseDependencies)) 535 | 536 | 537 | listIndirectDependenciesHelp : (Elm.Package.Name -> Elm.Project.Deps Elm.Version.Version) -> Elm.Project.Deps Elm.Version.Version -> List Elm.Package.Name -> Elm.Project.Deps Elm.Version.Version -> Elm.Project.Deps Elm.Version.Version 538 | listIndirectDependenciesHelp getDependenciesAndVersion dependenciesToLookAt visited indirectDependencies = 539 | case List.filter (\( name, _ ) -> not (List.member name visited)) dependenciesToLookAt of 540 | [] -> 541 | indirectDependencies 542 | 543 | ( name, version ) :: restOfDependenciesToLookAt -> 544 | listIndirectDependenciesHelp 545 | getDependenciesAndVersion 546 | (getDependenciesAndVersion name ++ restOfDependenciesToLookAt) 547 | (name :: visited) 548 | (( name, version ) :: indirectDependencies) 549 | 550 | 551 | packageDependencies : Dict String Elm.Version.Version -> DependencyList -> List ( Elm.Package.Name, Elm.Version.Version ) 552 | packageDependencies dependencyVersionDict dependencies = 553 | dependencies 554 | |> List.filterMap 555 | (\name -> 556 | Dict.get (Elm.Package.toString name) dependencyVersionDict 557 | |> Maybe.map (Tuple.pair name) 558 | ) 559 | 560 | 561 | addTestDependency : ProjectAndDependencyIdentifier -> ProjectAndDependencyIdentifier 562 | addTestDependency projectAndDependencyIdentifier = 563 | case projectAndDependencyIdentifier of 564 | ApplicationProject ({ application } as project) -> 565 | let 566 | testDepsDirect : List ( Elm.Package.Name, Elm.Version.Version ) 567 | testDepsDirect = 568 | ( project.name, project.version ) :: application.testDepsDirect 569 | in 570 | ApplicationProject 571 | { project 572 | | application = 573 | { application 574 | | testDepsDirect = testDepsDirect 575 | , testDepsIndirect = 576 | listIndirectDependencies 577 | project.getDependenciesAndVersion 578 | testDepsDirect 579 | |> List.filter (\dep -> not (List.member dep application.depsDirect || List.member dep application.depsIndirect)) 580 | } 581 | } 582 | 583 | PackageProject ({ package } as project) -> 584 | PackageProject 585 | { project 586 | | package = 587 | { package 588 | | testDeps = ( project.name, project.constraint ) :: package.testDeps 589 | } 590 | } 591 | 592 | 593 | removeTestDependency : ProjectAndDependencyIdentifier -> ProjectAndDependencyIdentifier 594 | removeTestDependency projectAndDependencyIdentifier = 595 | case projectAndDependencyIdentifier of 596 | ApplicationProject ({ application } as project) -> 597 | let 598 | testDepsDirect : List ( Elm.Package.Name, Elm.Version.Version ) 599 | testDepsDirect = 600 | List.filter (\pkg -> pkg |> isPackageWithName (Elm.Package.toString project.name) |> not) application.testDepsDirect 601 | in 602 | ApplicationProject 603 | { project 604 | | application = 605 | { application 606 | | testDepsDirect = testDepsDirect 607 | , testDepsIndirect = 608 | listIndirectDependencies 609 | project.getDependenciesAndVersion 610 | testDepsDirect 611 | |> List.filter (\dep -> not (List.member dep application.depsDirect || List.member dep application.depsIndirect)) 612 | } 613 | } 614 | 615 | PackageProject ({ package } as project) -> 616 | PackageProject 617 | { project 618 | | package = 619 | { package 620 | | testDeps = List.filter (\pkg -> pkg |> isPackageWithName (Elm.Package.toString project.name) |> not) package.testDeps 621 | } 622 | } 623 | 624 | 625 | isPackageWithName : String -> ( Elm.Package.Name, a ) -> Bool 626 | isPackageWithName packageName ( packageName_, _ ) = 627 | packageName == Elm.Package.toString packageName_ 628 | -------------------------------------------------------------------------------- /src/NoUnused/CustomTypeConstructorArgs.elm: -------------------------------------------------------------------------------- 1 | module NoUnused.CustomTypeConstructorArgs exposing (rule) 2 | 3 | {-| 4 | 5 | @docs rule 6 | 7 | -} 8 | 9 | import Dict exposing (Dict) 10 | import Elm.Module 11 | import Elm.Project 12 | import Elm.Syntax.Declaration as Declaration exposing (Declaration) 13 | import Elm.Syntax.Exposing as Exposing exposing (Exposing) 14 | import Elm.Syntax.Expression as Expression exposing (Expression) 15 | import Elm.Syntax.Module as Module exposing (Module) 16 | import Elm.Syntax.ModuleName exposing (ModuleName) 17 | import Elm.Syntax.Node as Node exposing (Node(..)) 18 | import Elm.Syntax.Pattern as Pattern exposing (Pattern) 19 | import Elm.Syntax.Range exposing (Range) 20 | import Elm.Syntax.TypeAnnotation as TypeAnnotation exposing (TypeAnnotation) 21 | import List.Extra 22 | import Review.ModuleNameLookupTable as ModuleNameLookupTable exposing (ModuleNameLookupTable) 23 | import Review.Rule as Rule exposing (Error, Rule) 24 | import Set exposing (Set) 25 | import String.Extra 26 | 27 | 28 | {-| Reports arguments of custom type constructors that are never used. 29 | 30 | config = 31 | [ NoUnused.CustomTypeConstructorArgs.rule 32 | ] 33 | 34 | Custom type constructors can contain data that is never extracted out of the constructor. 35 | This rule will warn arguments that are always pattern matched using a wildcard (`_`). 36 | 37 | For package projects, custom types whose constructors are exposed as part of the package API are not reported. 38 | 39 | Note that this rule **may report false positives** if you compare custom types with the `==` or `/=` operators 40 | (and never destructure the custom type), like when you do `value == Just 0`, or store them in lists for instance with 41 | [`assoc-list`](https://package.elm-lang.org/packages/pzp1997/assoc-list/latest). 42 | This rule attempts to detect when the custom type is used in comparisons, but it may still result in false positives. 43 | 44 | 45 | ## Fail 46 | 47 | type CustomType 48 | = CustomType Used Unused 49 | 50 | case customType of 51 | CustomType value _ -> value 52 | 53 | 54 | ## Success 55 | 56 | type CustomType 57 | = CustomType Used Unused 58 | 59 | case customType of 60 | CustomType value maybeUsed -> value 61 | 62 | 63 | ## When not to enable this rule? 64 | 65 | If you like giving names to all arguments when pattern matching, then this rule will not find many problems. 66 | This rule will work well when enabled along with [`NoUnused.Patterns`](./NoUnused-Patterns). 67 | 68 | Also, if you like comparing custom types in the way described above, you might pass on this rule, or want to be very careful when enabling it. 69 | 70 | 71 | ## Try it out 72 | 73 | You can try this rule out by running the following command: 74 | 75 | ```bash 76 | elm-review --template jfmengels/elm-review-unused/example --rules NoUnused.CustomTypeConstructorArgs 77 | ``` 78 | 79 | -} 80 | rule : Rule 81 | rule = 82 | Rule.newProjectRuleSchema "NoUnused.CustomTypeConstructorArgs" initialProjectContext 83 | |> Rule.withElmJsonProjectVisitor elmJsonVisitor 84 | |> Rule.withModuleVisitor moduleVisitor 85 | |> Rule.withModuleContextUsingContextCreator 86 | { fromProjectToModule = fromProjectToModule 87 | , fromModuleToProject = fromModuleToProject 88 | , foldProjectContexts = foldProjectContexts 89 | } 90 | |> Rule.withFinalProjectEvaluation finalEvaluation 91 | |> Rule.fromProjectRuleSchema 92 | 93 | 94 | type alias ProjectContext = 95 | { exposedModules : Set ModuleName 96 | , customTypeArgs : 97 | Dict 98 | ModuleName 99 | { moduleKey : Rule.ModuleKey 100 | , args : Dict String (List Range) 101 | } 102 | , usedArguments : Dict ( ModuleName, String ) (Set Int) 103 | , customTypesNotToReport : Set ( ModuleName, String ) 104 | } 105 | 106 | 107 | type alias ModuleContext = 108 | { lookupTable : ModuleNameLookupTable 109 | , isModuleExposed : Bool 110 | , exposed : Exposing 111 | , customTypeArgs : List ( String, Dict String (List Range) ) 112 | , usedArguments : Dict ( ModuleName, String ) (Set Int) 113 | , customTypesNotToReport : Set ( ModuleName, String ) 114 | } 115 | 116 | 117 | moduleVisitor : Rule.ModuleRuleSchema {} ModuleContext -> Rule.ModuleRuleSchema { hasAtLeastOneVisitor : () } ModuleContext 118 | moduleVisitor schema = 119 | schema 120 | |> Rule.withModuleDefinitionVisitor (\node context -> ( [], moduleDefinitionVisitor node context )) 121 | |> Rule.withDeclarationEnterVisitor (\node context -> ( [], declarationVisitor node context )) 122 | |> Rule.withExpressionEnterVisitor (\node context -> ( [], expressionVisitor node context )) 123 | 124 | 125 | elmJsonVisitor : Maybe { a | project : Elm.Project.Project } -> ProjectContext -> ( List nothing, ProjectContext ) 126 | elmJsonVisitor maybeEProject projectContext = 127 | case Maybe.map .project maybeEProject of 128 | Just (Elm.Project.Package package) -> 129 | let 130 | exposedModules : List Elm.Module.Name 131 | exposedModules = 132 | case package.exposed of 133 | Elm.Project.ExposedList list -> 134 | list 135 | 136 | Elm.Project.ExposedDict list -> 137 | List.concatMap Tuple.second list 138 | 139 | exposedNames : Set ModuleName 140 | exposedNames = 141 | exposedModules 142 | |> List.map (Elm.Module.toString >> String.split ".") 143 | |> Set.fromList 144 | in 145 | ( [], { projectContext | exposedModules = exposedNames } ) 146 | 147 | _ -> 148 | ( [], projectContext ) 149 | 150 | 151 | initialProjectContext : ProjectContext 152 | initialProjectContext = 153 | { exposedModules = Set.empty 154 | , customTypeArgs = Dict.empty 155 | , usedArguments = Dict.empty 156 | , customTypesNotToReport = Set.empty 157 | } 158 | 159 | 160 | fromProjectToModule : Rule.ContextCreator ProjectContext ModuleContext 161 | fromProjectToModule = 162 | Rule.initContextCreator 163 | (\lookupTable moduleName projectContext -> 164 | { lookupTable = lookupTable 165 | , isModuleExposed = Set.member moduleName projectContext.exposedModules 166 | , exposed = Exposing.Explicit [] 167 | , customTypeArgs = [] 168 | , usedArguments = Dict.empty 169 | , customTypesNotToReport = Set.empty 170 | } 171 | ) 172 | |> Rule.withModuleNameLookupTable 173 | |> Rule.withModuleName 174 | 175 | 176 | fromModuleToProject : Rule.ContextCreator ModuleContext ProjectContext 177 | fromModuleToProject = 178 | Rule.initContextCreator 179 | (\moduleKey moduleName moduleContext -> 180 | { exposedModules = Set.empty 181 | , customTypeArgs = 182 | Dict.singleton 183 | moduleName 184 | { moduleKey = moduleKey 185 | , args = getNonExposedCustomTypes moduleContext 186 | } 187 | , usedArguments = replaceLocalModuleNameForDict moduleName moduleContext.usedArguments 188 | , customTypesNotToReport = replaceLocalModuleNameForSet moduleName moduleContext.customTypesNotToReport 189 | } 190 | ) 191 | |> Rule.withModuleKey 192 | |> Rule.withModuleName 193 | 194 | 195 | replaceLocalModuleNameForSet : ModuleName -> Set ( ModuleName, comparable ) -> Set ( ModuleName, comparable ) 196 | replaceLocalModuleNameForSet moduleName set = 197 | Set.map 198 | (\(( moduleNameForType, name ) as untouched) -> 199 | case moduleNameForType of 200 | [] -> 201 | ( moduleName, name ) 202 | 203 | _ -> 204 | untouched 205 | ) 206 | set 207 | 208 | 209 | replaceLocalModuleNameForDict : ModuleName -> Dict ( ModuleName, comparable ) b -> Dict ( ModuleName, comparable ) b 210 | replaceLocalModuleNameForDict moduleName dict = 211 | Dict.foldl 212 | (\(( moduleNameForType, name ) as key) value acc -> 213 | let 214 | newKey : ( ModuleName, comparable ) 215 | newKey = 216 | case moduleNameForType of 217 | [] -> 218 | ( moduleName, name ) 219 | 220 | _ -> 221 | key 222 | in 223 | Dict.insert newKey value acc 224 | ) 225 | Dict.empty 226 | dict 227 | 228 | 229 | getNonExposedCustomTypes : ModuleContext -> Dict String (List Range) 230 | getNonExposedCustomTypes moduleContext = 231 | if moduleContext.isModuleExposed then 232 | case moduleContext.exposed of 233 | Exposing.All _ -> 234 | Dict.empty 235 | 236 | Exposing.Explicit list -> 237 | let 238 | exposedCustomTypes : Set String 239 | exposedCustomTypes = 240 | List.foldl 241 | (\exposed acc -> 242 | case Node.value exposed of 243 | Exposing.TypeExpose { name, open } -> 244 | case open of 245 | Just _ -> 246 | Set.insert name acc 247 | 248 | Nothing -> 249 | acc 250 | 251 | _ -> 252 | acc 253 | ) 254 | Set.empty 255 | list 256 | in 257 | List.foldl 258 | (\( typeName, args ) acc -> 259 | if Set.member typeName exposedCustomTypes then 260 | acc 261 | 262 | else 263 | Dict.union args acc 264 | ) 265 | Dict.empty 266 | moduleContext.customTypeArgs 267 | 268 | else 269 | List.foldl 270 | (\( _, args ) acc -> Dict.union args acc) 271 | Dict.empty 272 | moduleContext.customTypeArgs 273 | 274 | 275 | foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext 276 | foldProjectContexts newContext previousContext = 277 | { exposedModules = previousContext.exposedModules 278 | , customTypeArgs = 279 | Dict.union 280 | newContext.customTypeArgs 281 | previousContext.customTypeArgs 282 | , usedArguments = 283 | Dict.foldl 284 | (\key newSet acc -> 285 | case Dict.get key acc of 286 | Just existingSet -> 287 | Dict.insert key (Set.union newSet existingSet) acc 288 | 289 | Nothing -> 290 | Dict.insert key newSet acc 291 | ) 292 | previousContext.usedArguments 293 | newContext.usedArguments 294 | , customTypesNotToReport = Set.union newContext.customTypesNotToReport previousContext.customTypesNotToReport 295 | } 296 | 297 | 298 | 299 | -- MODULE DEFINITION VISITOR 300 | 301 | 302 | moduleDefinitionVisitor : Node Module -> ModuleContext -> ModuleContext 303 | moduleDefinitionVisitor node moduleContext = 304 | { moduleContext | exposed = Module.exposingList (Node.value node) } 305 | 306 | 307 | isNotNever : ModuleNameLookupTable -> Node TypeAnnotation -> Bool 308 | isNotNever lookupTable node = 309 | case Node.value node of 310 | TypeAnnotation.Typed (Node neverRange ( _, "Never" )) [] -> 311 | ModuleNameLookupTable.moduleNameAt lookupTable neverRange /= Just [ "Basics" ] 312 | 313 | _ -> 314 | True 315 | 316 | 317 | 318 | -- DECLARATION VISITOR 319 | 320 | 321 | declarationVisitor : Node Declaration -> ModuleContext -> ModuleContext 322 | declarationVisitor node context = 323 | case Node.value node of 324 | Declaration.FunctionDeclaration function -> 325 | { context 326 | | usedArguments = 327 | registerUsedPatterns 328 | (collectUsedPatternsFromFunctionDeclaration context function) 329 | context.usedArguments 330 | } 331 | 332 | Declaration.CustomTypeDeclaration typeDeclaration -> 333 | if List.isEmpty typeDeclaration.constructors then 334 | context 335 | 336 | else 337 | let 338 | customTypeConstructors : Dict String (List Range) 339 | customTypeConstructors = 340 | List.foldl 341 | (\(Node _ { name, arguments }) acc -> 342 | Dict.insert 343 | (Node.value name) 344 | (createArguments context.lookupTable arguments) 345 | acc 346 | ) 347 | Dict.empty 348 | typeDeclaration.constructors 349 | in 350 | { context 351 | | customTypeArgs = ( Node.value typeDeclaration.name, customTypeConstructors ) :: context.customTypeArgs 352 | } 353 | 354 | _ -> 355 | context 356 | 357 | 358 | createArguments : ModuleNameLookupTable -> List (Node TypeAnnotation) -> List Range 359 | createArguments lookupTable arguments = 360 | List.foldr 361 | (\argument acc -> 362 | if isNotNever lookupTable argument then 363 | Node.range argument :: acc 364 | 365 | else 366 | acc 367 | ) 368 | [] 369 | arguments 370 | 371 | 372 | collectUsedPatternsFromFunctionDeclaration : ModuleContext -> Expression.Function -> List ( ( ModuleName, String ), Set Int ) 373 | collectUsedPatternsFromFunctionDeclaration context { declaration } = 374 | collectUsedCustomTypeArgs context.lookupTable (Node.value declaration).arguments 375 | 376 | 377 | 378 | -- EXPRESSION VISITOR 379 | 380 | 381 | expressionVisitor : Node Expression -> ModuleContext -> ModuleContext 382 | expressionVisitor node context = 383 | case Node.value node of 384 | Expression.CaseExpression { cases } -> 385 | let 386 | usedArguments : List ( ( ModuleName, String ), Set Int ) 387 | usedArguments = 388 | collectUsedCustomTypeArgs context.lookupTable (List.map Tuple.first cases) 389 | in 390 | { context | usedArguments = registerUsedPatterns usedArguments context.usedArguments } 391 | 392 | Expression.LetExpression { declarations } -> 393 | let 394 | usedArguments : List ( ( ModuleName, String ), Set Int ) 395 | usedArguments = 396 | List.concatMap 397 | (\declaration -> 398 | case Node.value declaration of 399 | Expression.LetDestructuring pattern _ -> 400 | collectUsedCustomTypeArgs context.lookupTable [ pattern ] 401 | 402 | Expression.LetFunction function -> 403 | collectUsedPatternsFromFunctionDeclaration context function 404 | ) 405 | declarations 406 | in 407 | { context | usedArguments = registerUsedPatterns usedArguments context.usedArguments } 408 | 409 | Expression.LambdaExpression { args } -> 410 | { context 411 | | usedArguments = 412 | registerUsedPatterns 413 | (collectUsedCustomTypeArgs context.lookupTable args) 414 | context.usedArguments 415 | } 416 | 417 | Expression.OperatorApplication operator _ left right -> 418 | if operator == "==" || operator == "/=" then 419 | let 420 | customTypesNotToReport : Set ( ModuleName, String ) 421 | customTypesNotToReport = 422 | findCustomTypes context.lookupTable [ left, right ] 423 | in 424 | { context | customTypesNotToReport = Set.union customTypesNotToReport context.customTypesNotToReport } 425 | 426 | else 427 | context 428 | 429 | Expression.Application ((Node _ (Expression.PrefixOperator operator)) :: restOfArgs) -> 430 | if operator == "==" || operator == "/=" then 431 | let 432 | customTypesNotToReport : Set ( ModuleName, String ) 433 | customTypesNotToReport = 434 | findCustomTypes context.lookupTable restOfArgs 435 | in 436 | { context | customTypesNotToReport = Set.union customTypesNotToReport context.customTypesNotToReport } 437 | 438 | else 439 | context 440 | 441 | _ -> 442 | context 443 | 444 | 445 | findCustomTypes : ModuleNameLookupTable -> List (Node Expression) -> Set ( ModuleName, String ) 446 | findCustomTypes lookupTable nodes = 447 | findCustomTypesHelp lookupTable nodes [] 448 | |> Set.fromList 449 | 450 | 451 | findCustomTypesHelp : ModuleNameLookupTable -> List (Node Expression) -> List ( ModuleName, String ) -> List ( ModuleName, String ) 452 | findCustomTypesHelp lookupTable nodes acc = 453 | case nodes of 454 | [] -> 455 | acc 456 | 457 | node :: restOfNodes -> 458 | case Node.value node of 459 | Expression.FunctionOrValue rawModuleName functionName -> 460 | if String.Extra.isCapitalized functionName then 461 | case ModuleNameLookupTable.moduleNameFor lookupTable node of 462 | Just moduleName -> 463 | findCustomTypesHelp lookupTable restOfNodes (( moduleName, functionName ) :: acc) 464 | 465 | Nothing -> 466 | findCustomTypesHelp lookupTable restOfNodes (( rawModuleName, functionName ) :: acc) 467 | 468 | else 469 | findCustomTypesHelp lookupTable restOfNodes acc 470 | 471 | Expression.TupledExpression expressions -> 472 | findCustomTypesHelp lookupTable (expressions ++ restOfNodes) acc 473 | 474 | Expression.ParenthesizedExpression expression -> 475 | findCustomTypesHelp lookupTable (expression :: restOfNodes) acc 476 | 477 | Expression.Application (((Node _ (Expression.FunctionOrValue _ functionName)) as first) :: expressions) -> 478 | if String.Extra.isCapitalized functionName then 479 | findCustomTypesHelp lookupTable (first :: (expressions ++ restOfNodes)) acc 480 | 481 | else 482 | findCustomTypesHelp lookupTable restOfNodes acc 483 | 484 | Expression.OperatorApplication _ _ left right -> 485 | findCustomTypesHelp lookupTable (left :: right :: restOfNodes) acc 486 | 487 | Expression.Negation expression -> 488 | findCustomTypesHelp lookupTable (expression :: restOfNodes) acc 489 | 490 | Expression.ListExpr expressions -> 491 | findCustomTypesHelp lookupTable (expressions ++ restOfNodes) acc 492 | 493 | _ -> 494 | findCustomTypesHelp lookupTable restOfNodes acc 495 | 496 | 497 | registerUsedPatterns : List ( ( ModuleName, String ), Set Int ) -> Dict ( ModuleName, String ) (Set Int) -> Dict ( ModuleName, String ) (Set Int) 498 | registerUsedPatterns newUsedArguments previouslyUsedArguments = 499 | List.foldl 500 | (\( key, usedPositions ) acc -> 501 | let 502 | previouslyUsedPositions : Set Int 503 | previouslyUsedPositions = 504 | Dict.get key acc 505 | |> Maybe.withDefault Set.empty 506 | in 507 | Dict.insert key (Set.union previouslyUsedPositions usedPositions) acc 508 | ) 509 | previouslyUsedArguments 510 | newUsedArguments 511 | 512 | 513 | collectUsedCustomTypeArgs : ModuleNameLookupTable -> List (Node Pattern) -> List ( ( ModuleName, String ), Set Int ) 514 | collectUsedCustomTypeArgs lookupTable nodes = 515 | collectUsedCustomTypeArgsHelp lookupTable nodes [] 516 | 517 | 518 | collectUsedCustomTypeArgsHelp : ModuleNameLookupTable -> List (Node Pattern) -> List ( ( ModuleName, String ), Set Int ) -> List ( ( ModuleName, String ), Set Int ) 519 | collectUsedCustomTypeArgsHelp lookupTable nodes acc = 520 | case nodes of 521 | [] -> 522 | acc 523 | 524 | (Node range pattern) :: restOfNodes -> 525 | case pattern of 526 | Pattern.NamedPattern { name } args -> 527 | let 528 | newAcc : List ( ( ModuleName, String ), Set Int ) 529 | newAcc = 530 | case ModuleNameLookupTable.moduleNameAt lookupTable range of 531 | Just moduleName -> 532 | ( ( moduleName, name ), computeUsedPositions 0 args Set.empty ) :: acc 533 | 534 | Nothing -> 535 | acc 536 | in 537 | collectUsedCustomTypeArgsHelp lookupTable (args ++ restOfNodes) newAcc 538 | 539 | Pattern.TuplePattern patterns -> 540 | collectUsedCustomTypeArgsHelp lookupTable (patterns ++ restOfNodes) acc 541 | 542 | Pattern.ListPattern patterns -> 543 | collectUsedCustomTypeArgsHelp lookupTable (patterns ++ restOfNodes) acc 544 | 545 | Pattern.UnConsPattern left right -> 546 | collectUsedCustomTypeArgsHelp lookupTable (left :: right :: restOfNodes) acc 547 | 548 | Pattern.ParenthesizedPattern subPattern -> 549 | collectUsedCustomTypeArgsHelp lookupTable (subPattern :: restOfNodes) acc 550 | 551 | Pattern.AsPattern subPattern _ -> 552 | collectUsedCustomTypeArgsHelp lookupTable (subPattern :: restOfNodes) acc 553 | 554 | _ -> 555 | collectUsedCustomTypeArgsHelp lookupTable restOfNodes acc 556 | 557 | 558 | computeUsedPositions : Int -> List (Node Pattern) -> Set Int -> Set Int 559 | computeUsedPositions index arguments acc = 560 | case arguments of 561 | [] -> 562 | acc 563 | 564 | arg :: restOfArgs -> 565 | let 566 | newAcc : Set Int 567 | newAcc = 568 | if isWildcard arg then 569 | acc 570 | 571 | else 572 | Set.insert index acc 573 | in 574 | computeUsedPositions (index + 1) restOfArgs newAcc 575 | 576 | 577 | isWildcard : Node Pattern -> Bool 578 | isWildcard node = 579 | case Node.value node of 580 | Pattern.AllPattern -> 581 | True 582 | 583 | Pattern.ParenthesizedPattern pattern -> 584 | isWildcard pattern 585 | 586 | _ -> 587 | False 588 | 589 | 590 | 591 | -- FINAL EVALUATION 592 | 593 | 594 | finalEvaluation : ProjectContext -> List (Error { useErrorForModule : () }) 595 | finalEvaluation context = 596 | Dict.foldl (finalEvaluationForSingleModule context) [] context.customTypeArgs 597 | 598 | 599 | finalEvaluationForSingleModule : ProjectContext -> ModuleName -> { moduleKey : Rule.ModuleKey, args : Dict String (List Range) } -> List (Error { useErrorForModule : () }) -> List (Error { useErrorForModule : () }) 600 | finalEvaluationForSingleModule context moduleName { moduleKey, args } previousErrors = 601 | Dict.foldl 602 | (\name ranges acc -> 603 | let 604 | constructor : ( ModuleName, String ) 605 | constructor = 606 | ( moduleName, name ) 607 | in 608 | if Set.member constructor context.customTypesNotToReport then 609 | acc 610 | 611 | else 612 | errorsForUnusedArguments context.usedArguments moduleKey constructor ranges acc 613 | ) 614 | previousErrors 615 | args 616 | 617 | 618 | errorsForUnusedArguments : Dict ( ModuleName, String ) (Set Int) -> Rule.ModuleKey -> ( ModuleName, String ) -> List Range -> List (Error anywhere) -> List (Error anywhere) 619 | errorsForUnusedArguments usedArguments moduleKey constructor ranges acc = 620 | case Dict.get constructor usedArguments of 621 | Just usedArgumentPositions -> 622 | List.Extra.indexedFilterMap 623 | (\index range -> 624 | if Set.member index usedArgumentPositions then 625 | Nothing 626 | 627 | else 628 | Just (error moduleKey range) 629 | ) 630 | 0 631 | ranges 632 | acc 633 | 634 | Nothing -> 635 | List.map (error moduleKey) ranges ++ acc 636 | 637 | 638 | error : Rule.ModuleKey -> Range -> Error anywhere 639 | error moduleKey range = 640 | Rule.errorForModule moduleKey 641 | { message = "Argument is never extracted and therefore never used." 642 | , details = 643 | [ "This argument is never used. You should either use it somewhere, or remove it at the location I pointed at." 644 | ] 645 | } 646 | range 647 | --------------------------------------------------------------------------------