├── .dialyzer_ignore.exs ├── .formatter.exs ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── NOTICE ├── README.md ├── docs ├── circleci.md ├── github_actions.md └── gitlab_ci.md ├── lib ├── dialyxir.ex ├── dialyxir │ ├── dialyzer.ex │ ├── filter_map.ex │ ├── formatter.ex │ ├── formatter │ │ ├── dialyxir.ex │ │ ├── dialyzer.ex │ │ ├── github.ex │ │ ├── ignore_file.ex │ │ ├── ignore_file_strict.ex │ │ ├── raw.ex │ │ ├── short.ex │ │ └── utils.ex │ ├── output.ex │ ├── plt.ex │ ├── project.ex │ ├── warning.ex │ ├── warning_helpers.ex │ ├── warnings.ex │ └── warnings │ │ ├── app_call.ex │ │ ├── apply.ex │ │ ├── binary_construction.ex │ │ ├── call.ex │ │ ├── call_to_missing_function.ex │ │ ├── call_with_opaque.ex │ │ ├── call_without_opaque.ex │ │ ├── callback_argument_type_mismatch.ex │ │ ├── callback_info_missing.ex │ │ ├── callback_missing.ex │ │ ├── callback_not_exported.ex │ │ ├── callback_spec_argument_type_mismatch.ex │ │ ├── callback_spec_type_mismatch.ex │ │ ├── callback_type_mismatch.ex │ │ ├── contract_diff.ex │ │ ├── contract_range.ex │ │ ├── contract_subtype.ex │ │ ├── contract_supertype.ex │ │ ├── contract_with_opaque.ex │ │ ├── exact_equality.ex │ │ ├── extra_range.ex │ │ ├── function_application_arguments.ex │ │ ├── function_application_no_function.ex │ │ ├── guard_fail.ex │ │ ├── guard_fail_pattern.ex │ │ ├── improper_list_construction.ex │ │ ├── invalid_contract.ex │ │ ├── map_update.ex │ │ ├── missing_range.ex │ │ ├── negative_guard_fail.ex │ │ ├── no_return.ex │ │ ├── opaque_equality.ex │ │ ├── opaque_guard.ex │ │ ├── opaque_match.ex │ │ ├── opaque_nonequality.ex │ │ ├── opaque_type_test.ex │ │ ├── overlapping_contract.ex │ │ ├── pattern_match.ex │ │ ├── pattern_match_covered.ex │ │ ├── record_construction.ex │ │ ├── record_match.ex │ │ ├── record_matching.ex │ │ ├── unknown_behaviour.ex │ │ ├── unknown_function.ex │ │ ├── unknown_type.ex │ │ ├── unmatched_return.ex │ │ └── unused_function.ex └── mix │ └── tasks │ ├── dialyzer.ex │ └── dialyzer │ └── explain.ex ├── mix.exs ├── mix.lock └── test ├── dialyxir ├── dialyzer_test.exs ├── formatter_test.exs ├── output_test.exs ├── plt_test.exs └── project_test.exs ├── examples ├── apply.ex ├── call.ex ├── callback_argument.ex ├── callback_spec_argument.ex ├── contract_supertype.ex ├── contract_with_opaque.ex ├── exact_equality.ex ├── extra-range.ex ├── function_app_args.ex ├── function_app_no_fun.ex ├── guard_fail.ex ├── guard_fail_pattern.ex ├── opaque_equality.ex ├── opaque_match.ex ├── pattern_match.ex ├── pattern_match_covered.ex └── unknown_type.ex ├── fixtures ├── alt_core_path │ └── mix.exs ├── alt_local_path │ └── mix.exs ├── default_apps │ └── mix.exs ├── direct_apps │ └── mix.exs ├── ignore │ ├── ignore_test.exs │ └── mix.exs ├── ignore_apps │ └── mix.exs ├── ignore_custom_empty │ ├── ignore_test.exs │ └── mix.exs ├── ignore_custom_missing │ └── mix.exs ├── ignore_strict │ ├── ignore_strict_test.exs │ └── mix.exs ├── ignore_string │ ├── dialyzer.ignore-warnings │ └── mix.exs ├── local_plt │ ├── mix.exs │ └── mix.lock ├── local_plt_no_warn │ └── mix.exs ├── no_lockfile │ └── mix.exs ├── no_umbrella │ └── mix.exs ├── nonexistent_deps │ └── mix.exs ├── plt_add_deps_deprecations │ └── mix.exs ├── umbrella │ ├── .gitignore │ ├── README.md │ ├── apps │ │ ├── first_one │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── config │ │ │ │ └── config.exs │ │ │ ├── lib │ │ │ │ └── first_one.ex │ │ │ └── mix.exs │ │ └── second_one │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── config │ │ │ └── config.exs │ │ │ ├── lib │ │ │ └── second_one.ex │ │ │ └── mix.exs │ ├── config │ │ └── config.exs │ └── mix.exs └── umbrella_ignore_apps │ ├── .gitignore │ ├── apps │ ├── first_one │ │ └── mix.exs │ └── second_one │ │ └── mix.exs │ └── mix.exs ├── mix └── tasks │ └── dialyzer_test.exs ├── test_helper.exs └── warning_test.exs /.dialyzer_ignore.exs: -------------------------------------------------------------------------------- 1 | [ 2 | {":0:unknown_function Function :erl_types.t_is_opaque/1 does not exist."}, 3 | {":0:unknown_function Function :erl_types.t_to_string/1 does not exist."} 4 | ] 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["*.exs", "lib/**/*.{ex,exs}", "test/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [jeremyjh] 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | check_duplicate_runs: 11 | name: Check for duplicate runs 12 | continue-on-error: true 13 | runs-on: ubuntu-20.04 14 | outputs: 15 | should_skip: ${{ steps.skip_check.outputs.should_skip }} 16 | steps: 17 | - id: skip_check 18 | uses: fkirc/skip-duplicate-actions@master 19 | with: 20 | github_token: ${{ github.token }} 21 | concurrent_skipping: always 22 | cancel_others: true 23 | skip_after_successful_duplicate: true 24 | paths_ignore: '["**/README.md", "**/CHANGELOG.md", "**/LICENSE"]' 25 | do_not_skip: '["pull_request"]' 26 | 27 | test: 28 | name: Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}} 29 | runs-on: ubuntu-20.04 30 | needs: check_duplicate_runs 31 | if: ${{ needs.check_duplicate_runs.outputs.should_skip != 'true' }} 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | elixir: 37 | - 1.12.3 38 | - 1.13.4 39 | - 1.14.3 40 | - 1.15.7 41 | - 1.16.3 42 | - 1.17.2 43 | otp: 44 | - 23.3 45 | - 24.3 46 | - 25.3 47 | - 26.1 48 | - 27.0 49 | 50 | exclude: 51 | - elixir: 1.12.3 52 | otp: 25.3 53 | 54 | - elixir: 1.12.3 55 | otp: 26.1 56 | 57 | - elixir: 1.12.3 58 | otp: 27.0 59 | 60 | - elixir: 1.13.4 61 | otp: 26.1 62 | 63 | - elixir: 1.13.4 64 | otp: 27.0 65 | 66 | - elixir: 1.14.3 67 | otp: 26.1 68 | 69 | - elixir: 1.14.3 70 | otp: 27.0 71 | 72 | - elixir: 1.15.7 73 | otp: 23.3 74 | 75 | - elixir: 1.15.7 76 | otp: 27.0 77 | 78 | - elixir: 1.16.3 79 | otp: 23.3 80 | 81 | - elixir: 1.16.3 82 | otp: 27.0 83 | 84 | - elixir: 1.17.2 85 | otp: 23.3 86 | 87 | - elixir: 1.17.2 88 | otp: 24.3 89 | 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@v4 93 | 94 | - name: Set up Elixir 95 | uses: erlef/setup-beam@v1 96 | with: 97 | elixir-version: ${{ matrix.elixir }} 98 | otp-version: ${{ matrix.otp }} 99 | 100 | - name: Restore deps cache 101 | uses: actions/cache@v4 102 | with: 103 | path: | 104 | deps 105 | _build 106 | key: ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}-git-${{ github.sha }} 107 | restore-keys: | 108 | ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} 109 | ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }} 110 | 111 | - name: Install package dependencies 112 | run: mix deps.get 113 | 114 | - name: Remove compiled application files 115 | run: mix clean 116 | 117 | - name: Compile dependencies 118 | run: mix compile 119 | env: 120 | MIX_ENV: test 121 | 122 | - name: Run unit tests 123 | run: mix test --trace 124 | 125 | - name: Check compilation warnings 126 | run: mix do compile --warnings-as-errors, archive.build, archive.install --force 127 | env: 128 | MIX_ENV: prod 129 | 130 | - name: Check source code formatting 131 | if: ${{ matrix.elixir == '1.17.2' && matrix.otp == '27.0' }} 132 | run: mix format --check-formatted 133 | 134 | - name: Get results in short format 135 | run: mix dialyzer --format short 136 | env: 137 | MIX_ENV: prod 138 | 139 | - name: Get results in raw format 140 | run: mix dialyzer --format raw 141 | env: 142 | MIX_ENV: prod 143 | 144 | - name: Get results in dialyzer format 145 | run: mix dialyzer --format dialyzer 146 | env: 147 | MIX_ENV: prod 148 | 149 | - name: Get results in ignore_file format 150 | run: mix dialyzer --format ignore_file 151 | env: 152 | MIX_ENV: prod 153 | 154 | - name: Run output tests 155 | run: mix test 156 | env: 157 | OUTPUT_TESTS: true 158 | 159 | - name: Check examples 160 | run: mix compile 2> /dev/null && mix dialyzer --format short --ignore-exit-status 161 | env: 162 | MIX_ENV: examples 163 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | doc 3 | /deps 4 | .depsolver.plt 5 | erl_crash.dump 6 | *.ez 7 | .elixir_ls 8 | src/dialyzer_parser.erl 9 | src/dialyzer_lexer.erl 10 | lib/example.ex 11 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.15.7-otp-26 2 | erlang 26.1.2 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | 7 | Versions follow [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html) 8 | 9 | ## Unreleased changes post [1.4.5] 10 | 11 | ## [1.4.5] - 2024-11-17 12 | 13 | ### Fixed 14 | - Crash when default ignore file missing and custom file specified 15 | - Revert format option ignore_file_string to ignore_file_strict 16 | 17 | ## [1.4.4] - 2024-09-28 18 | 19 | ### Fixed 20 | - Invalid contract formatting failed on OTP 26 & later. 21 | - Empty ignore files are ignored. 22 | - Several improvements and corrections to documentation. 23 | 24 | ### Changed 25 | - Updated Erlex minimum version to 0.27, bringing in several fixes and improvements. 26 | 27 | ### Added 28 | - Multiple formatters can be used in the same invocation. 29 | 30 | ## [1.4.3] - 2023-12-28 31 | ### Fixed 32 | - Warnings with line & column. 33 | - Formatting of `:record_match` warning. 34 | 35 | ## [1.4.2] - 2023-10-21 36 | 37 | ### Changed 38 | - Revert minimum required Elixir version back to 1.6. 39 | - Improved performance in calculating Umbrella dependencies. 40 | 41 | ## [1.4.1] - 2023-08-30 42 | 43 | ### Changed 44 | - Bump minimum required Elixir version to 1.12. 45 | 46 | ## [1.4.0] - 2023-08-27 47 | 48 | ### Added 49 | - --quiet-with-result flag. 50 | 51 | ### Changed 52 | - (docs) Improved caching behaviour in example templates. 53 | 54 | ### Fixed 55 | - Erroneous "DEPENDENCY MISSING" message in Elixir 1.15. 56 | - Handle transitive optional dependencies in Elixir 1.15. 57 | 58 | ## [1.3.0] - 2023-04-08 59 | 60 | ### Added 61 | - Elixir 1.15 support. 62 | - Support for warning `:callback_not_exported`. 63 | 64 | ### Changed 65 | - Several improvements to documentation, particularly Github CI documentation. 66 | 67 | ### Removed 68 | - Support for `:race_conditions` flag which was [removed from Erlang](https://github.com/erlang/otp/pull/5502). 69 | 70 | ### Fixed 71 | - Crash when `mix.lock` is missing. 72 | 73 | ## [1.2.0] - 2022-07-20 74 | ### Added 75 | - "github" formatter. 76 | 77 | ## [1.1.0] - 2021-02-18 78 | 79 | ### Added 80 | - Configuration option to set the project's PLT path: `:plt_local_path`. 81 | - Project configuration setting to exclude files based on a regex: `:exclude_files`. 82 | - `explain` text for `:missing_range` warning. 83 | 84 | ### Fixed 85 | 86 | - Fixes and improvements to README and documentation. 87 | - Fixed `mix.lock` hash stability. Will cause a recheck of PLTs on first usage in each project. 88 | 89 | ### Changed 90 | - Improved wording of argument mismatch warnings. 91 | 92 | ## [1.0.0] - 2020-03-16 93 | 94 | ### Changed 95 | - Deprecated `plt_add_deps` modes: `transitive`, `project`. Use `app_tree` and `apps_direct` instead. 96 | - Moved Explain text to `@moduledoc`. 97 | 98 | ### Fixed 99 | - Warning pretty printing and message fixes/improvements. 100 | - Prevent crash when short_format fails. 101 | - Ensure path to PLT target directory exists. 102 | - Bumped required `erlex` for formatting fix. 103 | 104 | ## [1.0.0-rc.7] - 2019-09-21 105 | 106 | ### Changed 107 | - Halt with a non-zero exit status by default; swap `--halt-exit-status` for `--ignore-exit-status`. 108 | 109 | ### Added 110 | - OTP 22 compatibility in `:fun_app_args` warning. 111 | - Support for `:map_update` warning. 112 | - Report elapsed time in building/updating PLT. 113 | 114 | ### Fixed 115 | - Warnings for protocols not implemented for built-in types. 116 | - Fix ANSI disabling - its now actually possible to disable ANSI. 117 | - Improve wording and fix grammar/punctuation in many warnings. 118 | 119 | ## [1.0.0-rc.6] - 2019-04-02 120 | 121 | ### Fixed 122 | - Improved warning formatting for unknown types/functions 123 | 124 | ## [1.0.0-rc.5] - 2019-03-26 125 | 126 | ### Added 127 | - `plt_ignore_apps` option to ignore specific dependencies 128 | 129 | ### Removed 130 | - Removed instructions for global (mix archive) installation. Installing as a per-project 131 | mix dependency is the only supported method. 132 | 133 | ### Changed 134 | - Updated many short warning formats to be shorter and more consistent 135 | 136 | ### Fixed 137 | - Pretty print for a few warnings 138 | - Improved wording in explanations 139 | - Fix raw format and add all formats to CI 140 | 141 | ## [1.0.0-rc.4] - 2018-10-31 142 | 143 | ### Added 144 | - Regex support in Elixir Term Format ignore entries. 145 | 146 | ### Changed 147 | - Extracted parsing / pretty printing to separate library: erlex. 148 | 149 | ### Fixed 150 | - Parsing, formatting fixes. 151 | 152 | ## [1.0.0-rc.3] - 2018-06-30 153 | 154 | ### Fixed 155 | - Parsing, formatting fixes. 156 | - OptionParser fixes - remove unimplemented options. 157 | 158 | ## [1.0.0-rc.1-2] - 2018-06-14 159 | 160 | ### Fixed 161 | - Exception handling around formatter. 162 | - hex package file list. 163 | 164 | ## [1.0.0-rc.0] - 2018-06-13 165 | 166 | ### Added 167 | - Parsing Erlang terms from `dialyzer` warnings and pretty-printing as Elixir terms. 168 | - Format options: short, raw, dialxyir dialyzer. 169 | - Ignore rules can be supplied in Elixir term format. 170 | 171 | ## [0.5.1] - 2017-07-29 172 | 173 | ### Added 174 | - Elixir 1.5 support. 175 | 176 | ## [0.5.0] - 2017-02-21 177 | 178 | ### Changed 179 | 180 | - Use `:dialyzer` API to run analysis rather than shelling the dialyzer CLI 181 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Precheck 2 | 3 | * Take a look at the open issues and be sure that your issue is not already covered. 4 | * Be sure your versions of Dialyxir and Erlex are up to date. 5 | 6 | ### Environment 7 | 8 | * Elixir & Erlang/OTP versions (elixir --version): 9 | 10 | * Which version of Dialyxir are you using? (cat mix.lock | grep dialyxir): 11 | 12 | ### Current behavior 13 | * Describe current behavior. Include errors, stack traces, and any additional information that might be important here. 14 | 15 | ### Expected behavior 16 | * A short description of the expected behavior. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2013-2023 Jeremy Huffman and contributors. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /docs/circleci.md: -------------------------------------------------------------------------------- 1 | # CircleCI 2 | 3 | ```yaml 4 | --- 5 | version: 2 6 | 7 | jobs: 8 | build: 9 | docker: 10 | - image: cimg/elixir:1.14 11 | 12 | steps: 13 | - checkout 14 | 15 | # Compile steps omitted for simplicity 16 | 17 | # Cache key based on Erlang/Elixir version and the mix.lock hash 18 | - run: 19 | name: "Save Elixir and Erlang version for PLT caching" 20 | command: echo "$ELIXIR_VERSION $ERLANG_VERSION" > .elixir_otp_version 21 | 22 | - restore_cache: 23 | name: "Restore PLT cache" 24 | keys: 25 | - plt-{{ arch }}-{{ checksum ".elixir_otp_version" }}-{{ checksum "mix.lock" }} 26 | 27 | - run: 28 | name: "Create PLTs" 29 | command: mix dialyzer --plt 30 | 31 | - save_cache: 32 | name: "Save PLT cache" 33 | key: plt-{{ arch }}-{{ checksum ".elixir_otp_version" }}-{{ checksum "mix.lock" }} 34 | paths: "priv/plts" 35 | 36 | - run: 37 | name: "Run dialyzer" 38 | command: mix dialyzer 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/github_actions.md: -------------------------------------------------------------------------------- 1 | # Github Actions 2 | 3 | ```yaml 4 | steps: 5 | - name: Check out source 6 | uses: actions/checkout@v2 7 | 8 | - name: Set up Elixir 9 | id: beam 10 | uses: erlef/setup-beam@v1 11 | with: 12 | otp-version: "24.1" # Define the OTP version 13 | elixir-version: "1.12.3" # Define the Elixir version 14 | 15 | # Cache key based on Erlang/Elixir version and the mix.lock hash 16 | - name: Restore PLT cache 17 | id: plt_cache 18 | uses: actions/cache/restore@v3 19 | with: 20 | key: | 21 | plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} 22 | restore-keys: | 23 | plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}- 24 | path: | 25 | priv/plts 26 | 27 | # Create PLTs if no cache was found 28 | - name: Create PLTs 29 | if: steps.plt_cache.outputs.cache-hit != 'true' 30 | run: mix dialyzer --plt 31 | 32 | # By default, the GitHub Cache action will only save the cache if all steps in the job succeed, 33 | # so we separate the cache restore and save steps in case running dialyzer fails. 34 | - name: Save PLT cache 35 | id: plt_cache_save 36 | uses: actions/cache/save@v3 37 | if: steps.plt_cache.outputs.cache-hit != 'true' 38 | with: 39 | key: | 40 | plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} 41 | path: | 42 | priv/plts 43 | 44 | - name: Run dialyzer 45 | # Two formats are included for ease of debugging and it is lightly recommended to use both, see https://github.com/jeremyjh/dialyxir/issues/530 for reasoning 46 | # --format github is helpful to print the warnings in a way that GitHub understands and can place on the /files page of a PR 47 | # --format dialyxir allows the raw GitHub actions logs to be useful because they have the full warning printed 48 | run: mix dialyzer --format github --format dialyxir 49 | 50 | # ... 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/gitlab_ci.md: -------------------------------------------------------------------------------- 1 | # GitLab CI 2 | 3 | ```yaml 4 | # Some of the duplication can be reduced with YAML anchors: 5 | # https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html 6 | 7 | image: elixir:1.14 8 | 9 | stages: 10 | - compile 11 | - check-elixir-types 12 | 13 | # You'll want to cache based on your Erlang/Elixir version. 14 | 15 | # The example jobs below uses asdf's config file as the cache key: 16 | # https://asdf-vm.com/manage/configuration.html 17 | 18 | # An example build job with cache, to prevent dialyzer from needing to compile your project first 19 | build-dev: 20 | stage: compile 21 | cache: 22 | - key: 23 | files: 24 | - .tool-versions 25 | - mix.lock 26 | paths: 27 | - deps/ 28 | - _build/dev 29 | policy: pull-push 30 | script: 31 | - mix do deps.get, compile 32 | 33 | # The main difference between the following jobs is their cache policy: 34 | # https://docs.gitlab.com/ee/ci/yaml/index.html#cachepolicy 35 | 36 | dialyzer-plt: 37 | stage: check-elixir-types 38 | needs: 39 | - build-dev 40 | cache: 41 | - key: 42 | files: 43 | - .tool-versions 44 | - mix.lock 45 | paths: 46 | - priv/plts 47 | # Pull cache at start, push updated cache after completion 48 | policy: pull-push 49 | script: 50 | - mix dialyzer --plt 51 | 52 | dialyzer-check: 53 | stage: check-elixir-types 54 | needs: 55 | - dialyzer-plt 56 | cache: 57 | - key: 58 | files: 59 | - .tool-versions 60 | - mix.lock 61 | paths: 62 | - priv/plts 63 | # Pull cache at start, don't push cache after completion 64 | policy: pull 65 | script: 66 | - mix dialyzer --format short 67 | 68 | # ... 69 | ``` 70 | -------------------------------------------------------------------------------- /lib/dialyxir.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir do 2 | @moduledoc false 3 | use Application 4 | alias Dialyxir.Output 5 | 6 | def start(_, _) do 7 | Output.info(""" 8 | Warning: the `dialyxir` application's start function was called, which likely means you 9 | did not add the dependency with the `runtime: false` flag. This is not recommended because 10 | it will mean that unnecessary applications are started, and unnecessary applications are most 11 | likely being added to your PLT file, increasing build time. 12 | Please add `runtime: false` in your `mix.exs` dependency section e.g.: 13 | {:dialyxir, "~> 0.5", only: [:dev], runtime: false} 14 | """) 15 | 16 | Supervisor.start_link([], strategy: :one_for_one) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/dialyxir/dialyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Dialyzer do 2 | import Dialyxir.Output 3 | alias String.Chars 4 | alias Dialyxir.Formatter 5 | alias Dialyxir.Project 6 | alias Dialyxir.FilterMap 7 | 8 | defmodule Runner do 9 | @dialyxir_args [ 10 | :raw, 11 | :format, 12 | :list_unused_filters, 13 | :ignore_exit_status, 14 | :quiet_with_result 15 | ] 16 | 17 | @default_formatter Dialyxir.Formatter.Dialyxir 18 | 19 | def run(args, filterer) do 20 | try do 21 | {split, args} = Keyword.split(args, @dialyxir_args) 22 | 23 | quiet_with_result? = split[:quiet_with_result] 24 | 25 | raw_formatters = 26 | if split[:raw] do 27 | Enum.uniq(split[:format] ++ ["raw"]) 28 | else 29 | split[:format] 30 | end 31 | 32 | formatters = 33 | case raw_formatters do 34 | [] -> [@default_formatter] 35 | raw_formatters -> Enum.map(raw_formatters, &parse_formatter/1) 36 | end 37 | 38 | info("Starting Dialyzer") 39 | 40 | args 41 | |> inspect(label: "dialyzer args", pretty: true, limit: 8) 42 | |> info 43 | 44 | {duration_us, result} = :timer.tc(&:dialyzer.run/1, [args]) 45 | 46 | formatted_time_elapsed = Formatter.formatted_time(duration_us) 47 | 48 | filter_map_args = FilterMap.to_args(split) 49 | 50 | case Formatter.format_and_filter( 51 | result, 52 | filterer, 53 | filter_map_args, 54 | formatters, 55 | quiet_with_result? 56 | ) do 57 | {:ok, formatted_warnings, :no_unused_filters} -> 58 | {:ok, {formatted_time_elapsed, formatted_warnings, ""}} 59 | 60 | {:warn, formatted_warnings, {:unused_filters_present, formatted_unnecessary_skips}} -> 61 | {:ok, {formatted_time_elapsed, formatted_warnings, formatted_unnecessary_skips}} 62 | 63 | {:error, _formatted_warnings, {:unused_filters_present, formatted_unnecessary_skips}} -> 64 | {:error, {"unused filters present", formatted_unnecessary_skips}} 65 | end 66 | catch 67 | {:dialyzer_error, msg} -> 68 | {:error, ":dialyzer.run error: " <> Chars.to_string(msg)} 69 | end 70 | end 71 | 72 | defp parse_formatter("dialyzer"), do: Dialyxir.Formatter.Dialyzer 73 | defp parse_formatter("dialyxir"), do: Dialyxir.Formatter.Dialyxir 74 | defp parse_formatter("github"), do: Dialyxir.Formatter.Github 75 | defp parse_formatter("ignore_file"), do: Dialyxir.Formatter.IgnoreFile 76 | defp parse_formatter("ignore_file_strict"), do: Dialyxir.Formatter.IgnoreFileStrict 77 | defp parse_formatter("raw"), do: Dialyxir.Formatter.Raw 78 | defp parse_formatter("short"), do: Dialyxir.Formatter.Short 79 | 80 | defp parse_formatter(unknown) do 81 | warning(""" 82 | Unrecognized formatter #{unknown} received. \ 83 | Known formatters are dialyzer, dialyxir, github, ignore_file, ignore_file_strict, raw, and short. \ 84 | Falling back to dialyxir. 85 | """) 86 | 87 | @default_formatter 88 | end 89 | end 90 | 91 | @success_return_code 0 92 | @warning_return_code 2 93 | @error_return_code 1 94 | 95 | def dialyze(args, runner \\ Runner, filterer \\ Project) do 96 | case runner.run(args, filterer) do 97 | {:ok, {time, [], formatted_unnecessary_skips}} -> 98 | {:ok, @success_return_code, [time, formatted_unnecessary_skips, success_msg()]} 99 | 100 | {:ok, {time, result, formatted_unnecessary_skips}} -> 101 | warnings = Enum.map(result, &color(&1, :red)) 102 | 103 | {:warn, @warning_return_code, 104 | [time] ++ warnings ++ [formatted_unnecessary_skips, warnings_msg()]} 105 | 106 | {:warn, {time, result, formatted_unnecessary_skips}} -> 107 | warnings = Enum.map(result, &color(&1, :red)) 108 | 109 | {:warn, @warning_return_code, 110 | [time] ++ warnings ++ [formatted_unnecessary_skips, warnings_msg()]} 111 | 112 | {:error, {msg, formatted_unnecessary_skips}} -> 113 | {:error, @error_return_code, [color(formatted_unnecessary_skips, :red), color(msg, :red)]} 114 | 115 | {:error, msg} -> 116 | {:error, @error_return_code, [color(msg, :red)]} 117 | end 118 | end 119 | 120 | defp success_msg, do: color("done (passed successfully)", :green) 121 | 122 | defp warnings_msg, do: color("done (warnings were emitted)", :yellow) 123 | end 124 | -------------------------------------------------------------------------------- /lib/dialyxir/filter_map.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.FilterMap do 2 | @moduledoc """ 3 | A counters holding warnings to be skipped. 4 | 5 | `:counters` points to a `Map` where the keys are warnings to be skipped and the value indicates 6 | how often the warning was skipped. 7 | """ 8 | defstruct list_unused_filters?: false, unused_filters_as_errors?: false, counters: %{} 9 | 10 | @doc """ 11 | Fill a `FilterMap` from an ignore file. 12 | """ 13 | def from_file(ignore_file, list_unused_filters?, ignore_exit_status?) do 14 | filter_map = %__MODULE__{ 15 | list_unused_filters?: list_unused_filters?, 16 | unused_filters_as_errors?: list_unused_filters? && !ignore_exit_status? 17 | } 18 | 19 | cond do 20 | File.exists?(ignore_file) && 21 | match?(%{size: size} when size > 0, File.stat!(ignore_file)) -> 22 | {ignore, _} = 23 | ignore_file 24 | |> File.read!() 25 | |> Code.eval_string() 26 | 27 | Enum.reduce(ignore, filter_map, fn skip, filter_map -> 28 | put_in(filter_map.counters[skip], 0) 29 | end) 30 | 31 | true -> 32 | filter_map 33 | end 34 | end 35 | 36 | @doc """ 37 | Remove all non-allowed arguments from `args`. 38 | """ 39 | def to_args(args) do 40 | Keyword.take(args, [:list_unused_filters, :ignore_exit_status]) 41 | end 42 | 43 | @doc """ 44 | Retrieve the filters from a `FilterMap`. 45 | """ 46 | def filters(filter_map) do 47 | Map.keys(filter_map.counters) 48 | end 49 | 50 | @doc """ 51 | Increase usage count of a filter in `FilterMap`. 52 | """ 53 | def inc(filter_map, filter) do 54 | update_in(filter_map.counters[filter], &(&1 + 1)) 55 | end 56 | 57 | @doc """ 58 | List unused filters. 59 | """ 60 | def unused_filters(filter_map) do 61 | filter_map.counters 62 | |> Enum.filter(&unused?/1) 63 | |> Enum.unzip() 64 | |> elem(0) 65 | end 66 | 67 | @doc """ 68 | Determine if any filters were not used. 69 | """ 70 | def unused_filters?(filter_map) do 71 | Enum.any?(filter_map.counters, &unused?/1) 72 | end 73 | 74 | @doc """ 75 | Check if a `FilterMap` entry is unused. 76 | """ 77 | def unused?({_filter, count}), do: count < 1 78 | end 79 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter do 2 | @moduledoc """ 3 | Elixir-friendly dialyzer formatter. 4 | 5 | Wrapper around normal Dialyzer warning messages that provides 6 | example output for error messages. 7 | """ 8 | import Dialyxir.Output 9 | 10 | alias Dialyxir.FilterMap 11 | 12 | @type warning() :: 13 | {tag :: term(), {file :: Path.t(), location :: :erl_anno.location()}, {atom(), list()}} 14 | 15 | @type t() :: module() 16 | 17 | @callback format(warning()) :: String.t() 18 | 19 | def formatted_time(duration_us) do 20 | minutes = div(duration_us, 60_000_000) 21 | seconds = (rem(duration_us, 60_000_000) / 1_000_000) |> Float.round(2) 22 | "done in #{minutes}m#{seconds}s" 23 | end 24 | 25 | @spec format_and_filter([tuple], module, Keyword.t(), [t()], boolean()) :: tuple 26 | def format_and_filter( 27 | warnings, 28 | filterer, 29 | filter_map_args, 30 | formatter, 31 | quiet_with_result? \\ false 32 | ) 33 | 34 | def format_and_filter(warnings, filterer, filter_map_args, formatters, quiet_with_result?) do 35 | filter_map = filterer.filter_map(filter_map_args) 36 | 37 | {filtered_warnings, filter_map} = filter_warnings(warnings, filterer, filter_map) 38 | 39 | formatted_warnings = 40 | filtered_warnings 41 | |> filter_legacy_warnings(filterer) 42 | |> Enum.flat_map(fn legacy_warning -> 43 | Enum.map(formatters, & &1.format(legacy_warning)) 44 | end) 45 | |> Enum.uniq() 46 | 47 | show_count_skipped(warnings, formatted_warnings, filter_map, quiet_with_result?) 48 | formatted_unnecessary_skips = format_unnecessary_skips(filter_map) 49 | 50 | result(formatted_warnings, filter_map, formatted_unnecessary_skips) 51 | end 52 | 53 | defp result(formatted_warnings, filter_map, formatted_unnecessary_skips) do 54 | cond do 55 | FilterMap.unused_filters?(filter_map) && filter_map.unused_filters_as_errors? -> 56 | {:error, formatted_warnings, {:unused_filters_present, formatted_unnecessary_skips}} 57 | 58 | FilterMap.unused_filters?(filter_map) -> 59 | {:warn, formatted_warnings, {:unused_filters_present, formatted_unnecessary_skips}} 60 | 61 | true -> 62 | {:ok, formatted_warnings, :no_unused_filters} 63 | end 64 | end 65 | 66 | defp show_count_skipped(warnings, filtered_warnings, filter_map, quiet_with_result?) do 67 | warnings_count = Enum.count(warnings) 68 | filtered_warnings_count = Enum.count(filtered_warnings) 69 | skipped_count = warnings_count - filtered_warnings_count 70 | unnecessary_skips_count = count_unnecessary_skips(filter_map) 71 | 72 | message = 73 | "Total errors: #{warnings_count}, Skipped: #{skipped_count}, Unnecessary Skips: #{unnecessary_skips_count}" 74 | 75 | if quiet_with_result? do 76 | Mix.shell(Mix.Shell.IO) 77 | info(message) 78 | Mix.shell(Mix.Shell.Quiet) 79 | else 80 | info(message) 81 | end 82 | 83 | :ok 84 | end 85 | 86 | defp format_unnecessary_skips(filter_map = %FilterMap{list_unused_filters?: true}) do 87 | unused_filters = FilterMap.unused_filters(filter_map) 88 | 89 | if Enum.empty?(unused_filters) do 90 | "" 91 | else 92 | unused_filters = Enum.map_join(unused_filters, "\n", &inspect/1) 93 | "Unused filters:\n#{unused_filters}" 94 | end 95 | end 96 | 97 | defp format_unnecessary_skips(_) do 98 | "" 99 | end 100 | 101 | defp count_unnecessary_skips(filter_map) do 102 | filter_map.counters 103 | |> Enum.filter(&FilterMap.unused?/1) 104 | |> Enum.count() 105 | end 106 | 107 | defp filter_warnings(warnings, filterer, filter_map) do 108 | {warnings, filter_map} = 109 | Enum.map_reduce(warnings, filter_map, &filter_warning(filterer, &1, &2)) 110 | 111 | warnings = Enum.reject(warnings, &is_nil/1) 112 | {warnings, filter_map} 113 | end 114 | 115 | defp filter_warning(filterer, {_, {_file, _line}, {warning_type, _args}} = warning, filter_map) do 116 | if Map.has_key?(Dialyxir.Warnings.warnings(), warning_type) do 117 | {skip?, matching_filters} = 118 | try do 119 | filterer.filter_warning?(warning, filter_map) 120 | rescue 121 | _ -> 122 | {false, []} 123 | catch 124 | _ -> 125 | {false, []} 126 | end 127 | 128 | filter_map = 129 | Enum.reduce(matching_filters, filter_map, fn filter, filter_map -> 130 | FilterMap.inc(filter_map, filter) 131 | end) 132 | 133 | if skip? do 134 | {nil, filter_map} 135 | else 136 | {warning, filter_map} 137 | end 138 | else 139 | {warning, filter_map} 140 | end 141 | end 142 | 143 | defp filter_legacy_warnings(warnings, filterer) do 144 | Enum.reject(warnings, fn warning -> 145 | formatted_warnings = 146 | warning 147 | |> Dialyxir.Formatter.Dialyzer.format() 148 | |> List.wrap() 149 | 150 | Enum.empty?(filterer.filter_legacy_warnings(formatted_warnings)) 151 | end) 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/dialyxir.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.Dialyxir do 2 | @moduledoc false 3 | 4 | alias Dialyxir.Formatter.Utils 5 | 6 | @behaviour Dialyxir.Formatter 7 | 8 | @impl Dialyxir.Formatter 9 | def format(dialyzer_warning = {_tag, {file, location}, message}) do 10 | {warning_name, arguments} = message 11 | base_name = Path.relative_to_cwd(file) 12 | 13 | formatted = 14 | try do 15 | warning = Utils.warning(warning_name) 16 | string = warning.format_long(arguments) 17 | 18 | """ 19 | #{base_name}:#{Utils.format_location(location)}:#{warning_name} 20 | #{string} 21 | """ 22 | rescue 23 | e -> 24 | message = """ 25 | Unknown error occurred: #{inspect(e)} 26 | """ 27 | 28 | wrap_error_message(message, dialyzer_warning) 29 | catch 30 | {:error, :unknown_warning, warning_name} -> 31 | message = """ 32 | Unknown warning: 33 | #{inspect(warning_name)} 34 | """ 35 | 36 | wrap_error_message(message, dialyzer_warning) 37 | 38 | {:error, :lexing, warning} -> 39 | message = """ 40 | Failed to lex warning: 41 | #{inspect(warning)} 42 | """ 43 | 44 | wrap_error_message(message, dialyzer_warning) 45 | 46 | {:error, :parsing, failing_string} -> 47 | message = """ 48 | Failed to parse warning: 49 | #{inspect(failing_string)} 50 | """ 51 | 52 | wrap_error_message(message, dialyzer_warning) 53 | 54 | {:error, :pretty_printing, failing_string} -> 55 | message = """ 56 | Failed to pretty print warning: 57 | #{inspect(failing_string)} 58 | """ 59 | 60 | wrap_error_message(message, dialyzer_warning) 61 | 62 | {:error, :formatting, code} -> 63 | message = """ 64 | Failed to format warning: 65 | #{inspect(code)} 66 | """ 67 | 68 | wrap_error_message(message, dialyzer_warning) 69 | end 70 | 71 | formatted <> String.duplicate("_", 80) 72 | end 73 | 74 | defp wrap_error_message(message, warning) do 75 | """ 76 | Please file a bug in https://github.com/jeremyjh/dialyxir/issues with this message. 77 | 78 | #{message} 79 | 80 | Legacy warning: 81 | #{Dialyxir.Formatter.Dialyzer.format(warning)} 82 | """ 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/dialyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.Dialyzer do 2 | @moduledoc false 3 | 4 | @behaviour Dialyxir.Formatter 5 | 6 | @impl Dialyxir.Formatter 7 | def format(warning) do 8 | # OTP 22 uses indented output, but that's incompatible with dialyzer.ignore-warnings format. 9 | # Can be disabled, but OTP 21 and older only accept an atom, so only disable on OTP 22+. 10 | opts = 11 | if String.to_integer(System.otp_release()) < 22, 12 | do: :fullpath, 13 | else: [{:filename_opt, :fullpath}, {:indent_opt, false}] 14 | 15 | warning 16 | |> :dialyzer.format_warning(opts) 17 | |> String.Chars.to_string() 18 | |> String.replace_trailing("\n", "") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/github.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.Github do 2 | @moduledoc false 3 | 4 | alias Dialyxir.Formatter.Utils 5 | 6 | @behaviour Dialyxir.Formatter 7 | 8 | @impl Dialyxir.Formatter 9 | def format({_tag, {file, location}, {warning_name, arguments}}) do 10 | base_name = Path.relative_to_cwd(file) 11 | 12 | warning = Utils.warning(warning_name) 13 | string = warning.format_short(arguments) 14 | 15 | case location do 16 | {line, col} -> 17 | "::warning file=#{base_name},line=#{line},col=#{col},title=#{warning_name}::#{string}" 18 | 19 | line -> 20 | "::warning file=#{base_name},line=#{line},title=#{warning_name}::#{string}" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/ignore_file.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.IgnoreFile do 2 | @moduledoc false 3 | 4 | @behaviour Dialyxir.Formatter 5 | 6 | @impl Dialyxir.Formatter 7 | def format({_tag, {file, _location}, {warning_name, _arguments}}) do 8 | ~s({"#{file}", :#{warning_name}},) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/ignore_file_strict.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.IgnoreFileStrict do 2 | @moduledoc false 3 | 4 | alias Dialyxir.Formatter.Utils 5 | 6 | @behaviour Dialyxir.Formatter 7 | 8 | @impl Dialyxir.Formatter 9 | def format({_tag, {file, _location}, {warning_name, arguments}}) do 10 | warning = Utils.warning(warning_name) 11 | string = warning.format_short(arguments) 12 | 13 | ~s({"#{file}", "#{string}"},) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/raw.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.Raw do 2 | @moduledoc false 3 | 4 | @behaviour Dialyxir.Formatter 5 | 6 | @impl Dialyxir.Formatter 7 | def format(warning) do 8 | inspect(warning, limit: :infinity) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/short.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.Short do 2 | @moduledoc false 3 | 4 | alias Dialyxir.Formatter.Utils 5 | 6 | @behaviour Dialyxir.Formatter 7 | 8 | @impl Dialyxir.Formatter 9 | def format({_tag, {file, location}, {warning_name, arguments}}) do 10 | base_name = Path.relative_to_cwd(file) 11 | 12 | warning = Utils.warning(warning_name) 13 | string = warning.format_short(arguments) 14 | 15 | "#{base_name}:#{Utils.format_location(location)}:#{warning_name} #{string}" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/dialyxir/formatter/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Formatter.Utils do 2 | def warning(warning_name) do 3 | warnings = Dialyxir.Warnings.warnings() 4 | 5 | if Map.has_key?(warnings, warning_name) do 6 | Map.get(warnings, warning_name) 7 | else 8 | throw({:error, :unknown_warning, warning_name}) 9 | end 10 | end 11 | 12 | @doc false 13 | @spec format_location(:erl_anno.location()) :: String.t() 14 | def format_location(location) 15 | def format_location({line, column}), do: "#{line}:#{column}" 16 | def format_location(line), do: "#{line}" 17 | end 18 | -------------------------------------------------------------------------------- /lib/dialyxir/output.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Output do 2 | alias IO.ANSI 3 | 4 | def color(text, color) when is_binary(text) do 5 | ANSI.format([color, text]) 6 | end 7 | 8 | def info(""), do: :ok 9 | def info(text), do: Mix.shell().info(text) 10 | 11 | def warning(""), do: :ok 12 | def warning(text), do: Mix.shell().info(color(text, :yellow)) 13 | 14 | def error(""), do: :ok 15 | def error(text), do: Mix.shell().error(text) 16 | end 17 | -------------------------------------------------------------------------------- /lib/dialyxir/plt.ex: -------------------------------------------------------------------------------- 1 | # Credits: this code was originally part of the `dialyze` task 2 | # Copyright by James Fish 3 | # https://github.com/fishcakez/dialyze 4 | 5 | defmodule Dialyxir.Plt do 6 | @moduledoc false 7 | 8 | import Dialyxir.Output 9 | alias Dialyxir.Formatter 10 | 11 | def check(plts, fun \\ &check_plt/4) do 12 | find_plts(plts, [], fun) 13 | end 14 | 15 | defp find_plts([{plt, apps} | plts], acc, fun) do 16 | case plt_files(plt) do 17 | nil -> 18 | find_plts(plts, [{plt, apps, nil} | acc], fun) 19 | 20 | beams -> 21 | apps_rest = Enum.flat_map(plts, fn {_plt2, apps2} -> apps2 end) 22 | apps = Enum.uniq(apps ++ apps_rest) 23 | check_plts([{plt, apps, beams} | acc], fun) 24 | end 25 | end 26 | 27 | defp find_plts([], acc, fun) do 28 | check_plts(acc, fun) 29 | end 30 | 31 | defp check_plts(plts, fun) do 32 | _ = 33 | Enum.reduce(plts, {nil, MapSet.new(), %{}}, fn {plt, apps, beams}, acc -> 34 | fun.(plt, apps, beams, acc) 35 | end) 36 | end 37 | 38 | defp check_plt(plt, apps, old_beams, {prev_plt, prev_beams, prev_cache}) do 39 | info("Finding applications for #{Path.basename(plt)}") 40 | cache = resolve_apps(apps, prev_cache) 41 | mods = cache_mod_diff(cache, prev_cache) 42 | info("Finding modules for #{Path.basename(plt)}") 43 | beams = resolve_modules(mods, prev_beams) 44 | check_beams(plt, beams, old_beams, prev_plt) 45 | {plt, beams, cache} 46 | end 47 | 48 | defp cache_mod_diff(new, old) do 49 | Enum.flat_map(new, fn {app, {mods, _deps}} -> 50 | case Map.has_key?(old, app) do 51 | true -> [] 52 | false -> mods 53 | end 54 | end) 55 | end 56 | 57 | defp resolve_apps(apps, cache) do 58 | apps 59 | |> Enum.uniq() 60 | |> Enum.filter(&(not Map.has_key?(cache, &1))) 61 | |> Enum.map(&app_info/1) 62 | |> Enum.into(cache) 63 | end 64 | 65 | defp app_info(app) do 66 | app_file = Atom.to_charlist(app) ++ ~c".app" 67 | 68 | case :code.where_is_file(app_file) do 69 | :non_existing -> 70 | error("Unknown application #{inspect(app)}") 71 | {app, {[], []}} 72 | 73 | app_file -> 74 | Path.expand(app_file) 75 | |> read_app_info(app) 76 | end 77 | end 78 | 79 | defp read_app_info(app_file, app) do 80 | case :file.consult(app_file) do 81 | {:ok, [{:application, ^app, info}]} -> 82 | parse_app_info(info, app) 83 | 84 | {:error, reason} -> 85 | Mix.raise("Could not read #{app_file}: #{:file.format_error(reason)}") 86 | end 87 | end 88 | 89 | defp parse_app_info(info, app) do 90 | mods = Keyword.get(info, :modules, []) 91 | apps = Keyword.get(info, :applications, []) 92 | inc_apps = Keyword.get(info, :included_applications, []) 93 | runtime_deps = get_runtime_deps(info) 94 | {app, {mods, runtime_deps ++ inc_apps ++ apps}} 95 | end 96 | 97 | defp get_runtime_deps(info) do 98 | Keyword.get(info, :runtime_dependencies, []) 99 | |> Enum.map(&parse_runtime_dep/1) 100 | end 101 | 102 | defp parse_runtime_dep(runtime_dep) do 103 | runtime_dep = IO.chardata_to_string(runtime_dep) 104 | regex = ~r/^(.+)\-\d+(?|\.\d+)*$/ 105 | [app] = Regex.run(regex, runtime_dep, capture: :all_but_first) 106 | String.to_atom(app) 107 | end 108 | 109 | defp resolve_modules(modules, beams) do 110 | Enum.reduce(modules, beams, &resolve_module/2) 111 | end 112 | 113 | defp resolve_module(module, beams) do 114 | beam = Atom.to_charlist(module) ++ ~c".beam" 115 | 116 | case :code.where_is_file(beam) do 117 | path when is_list(path) -> 118 | path = Path.expand(path) 119 | MapSet.put(beams, path) 120 | 121 | :non_existing -> 122 | error("Unknown module #{inspect(module)}") 123 | beams 124 | end 125 | end 126 | 127 | defp check_beams(plt, beams, nil, prev_plt) do 128 | plt_ensure(plt, prev_plt) 129 | 130 | case plt_files(plt) do 131 | nil -> 132 | Mix.raise("Could not open #{plt}: #{:file.format_error(:enoent)}") 133 | 134 | old_beams -> 135 | check_beams(plt, beams, old_beams) 136 | end 137 | end 138 | 139 | defp check_beams(plt, beams, old_beams, _prev_plt) do 140 | check_beams(plt, beams, old_beams) 141 | end 142 | 143 | defp check_beams(plt, beams, old_beams) do 144 | remove = MapSet.difference(old_beams, beams) 145 | plt_remove(plt, remove) 146 | check = MapSet.intersection(beams, old_beams) 147 | plt_check(plt, check) 148 | add = MapSet.difference(beams, old_beams) 149 | plt_add(plt, add) 150 | end 151 | 152 | defp plt_ensure(plt, nil), do: plt_new(plt) 153 | defp plt_ensure(plt, prev_plt), do: plt_copy(prev_plt, plt) 154 | 155 | defp plt_new(plt) do 156 | info("Creating #{Path.basename(plt)}") 157 | plt = erl_path(plt) 158 | _ = plt_run(analysis_type: :plt_build, output_plt: plt, apps: [:erts]) 159 | :ok 160 | end 161 | 162 | defp plt_copy(plt, new_plt) do 163 | info("Copying #{Path.basename(plt)} to #{Path.basename(new_plt)}") 164 | 165 | new_plt 166 | |> Path.dirname() 167 | |> File.mkdir_p!() 168 | 169 | File.cp!(plt, new_plt) 170 | end 171 | 172 | defp plt_add(plt, files) do 173 | case MapSet.size(files) do 174 | 0 -> 175 | :ok 176 | 177 | n -> 178 | Mix.shell().info("Adding #{n} modules to #{Path.basename(plt)}") 179 | plt = erl_path(plt) 180 | files = erl_files(files) 181 | 182 | {duration_us, _} = 183 | :timer.tc(fn -> plt_run(analysis_type: :plt_add, init_plt: plt, files: files) end) 184 | 185 | Mix.shell().info(Formatter.formatted_time(duration_us)) 186 | :ok 187 | end 188 | end 189 | 190 | defp plt_remove(plt, files) do 191 | case MapSet.size(files) do 192 | 0 -> 193 | :ok 194 | 195 | n -> 196 | info("Removing #{n} modules from #{Path.basename(plt)}") 197 | plt = erl_path(plt) 198 | files = erl_files(files) 199 | _ = plt_run(analysis_type: :plt_remove, init_plt: plt, files: files) 200 | :ok 201 | end 202 | end 203 | 204 | defp plt_check(plt, files) do 205 | case MapSet.size(files) do 206 | 0 -> 207 | :ok 208 | 209 | n -> 210 | Mix.shell().info("Checking #{n} modules in #{Path.basename(plt)}") 211 | plt = erl_path(plt) 212 | _ = plt_run(analysis_type: :plt_check, init_plt: plt) 213 | :ok 214 | end 215 | end 216 | 217 | defp plt_run(opts) do 218 | try do 219 | :dialyzer.run([check_plt: false] ++ opts) 220 | catch 221 | {:dialyzer_error, msg} -> 222 | error(color(":dialyzer.run error: #{msg}", :red)) 223 | end 224 | end 225 | 226 | defp plt_info(plt) do 227 | erl_path(plt) 228 | |> :dialyzer.plt_info() 229 | end 230 | 231 | defp erl_files(files) do 232 | Enum.reduce(files, [], &[erl_path(&1) | &2]) 233 | end 234 | 235 | defp erl_path(path) do 236 | encoding = :file.native_name_encoding() 237 | :unicode.characters_to_list(path, encoding) 238 | end 239 | 240 | defp plt_files(plt) do 241 | info("Looking up modules in #{Path.basename(plt)}") 242 | 243 | case plt_info(plt) do 244 | {:ok, info} -> 245 | Keyword.fetch!(info, :files) 246 | |> Enum.reduce(MapSet.new(), &MapSet.put(&2, Path.expand(&1))) 247 | 248 | {:error, :no_such_file} -> 249 | nil 250 | 251 | {:error, reason} -> 252 | Mix.raise("Could not open #{plt}: #{:file.format_error(reason)}") 253 | end 254 | end 255 | end 256 | -------------------------------------------------------------------------------- /lib/dialyxir/warning.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warning do 2 | @moduledoc """ 3 | Behaviour for defining warning semantics. 4 | 5 | Contains callbacks for various warnings 6 | """ 7 | 8 | @doc """ 9 | By expressing the warning that is to be matched on, error handling 10 | and dispatching can be avoided in format functions. 11 | """ 12 | @callback warning() :: atom 13 | 14 | @doc """ 15 | The default documentation when seeing an error without the user 16 | otherwise overriding the format. 17 | """ 18 | @callback format_long([String.t()] | {String.t(), String.t(), String.t()} | String.t()) :: 19 | String.t() 20 | 21 | @doc """ 22 | A short message, often missing things like success types and expected types for space. 23 | """ 24 | @callback format_short([String.t()] | {String.t(), String.t(), String.t()} | String.t()) :: 25 | String.t() 26 | 27 | @doc """ 28 | Explanation for a warning of this type. Should include a simple example of how to trigger it. 29 | """ 30 | @callback explain() :: String.t() 31 | 32 | @spec default_explain() :: String.t() 33 | def default_explain() do 34 | """ 35 | This warning type does not have an explanation yet. If you have 36 | code that causes it, please file an issue or pull request in 37 | https://github.com/jeremyjh/dialyxir/issues 38 | """ 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/dialyxir/warning_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.WarningHelpers do 2 | @spec ordinal(non_neg_integer) :: String.t() 3 | def ordinal(1), do: "1st" 4 | def ordinal(2), do: "2nd" 5 | def ordinal(3), do: "3rd" 6 | def ordinal(n) when is_integer(n), do: "#{n}th" 7 | 8 | def call_or_apply_to_string( 9 | arg_positions, 10 | :only_sig, 11 | signature_args, 12 | _signature_return, 13 | {_overloaded?, _contract} 14 | ) do 15 | pretty_signature_args = Erlex.pretty_print_args(signature_args) 16 | 17 | if Enum.empty?(arg_positions) do 18 | # We do not know which argument(s) caused the failure 19 | """ 20 | will never return since the success typing arguments are 21 | #{pretty_signature_args} 22 | """ 23 | else 24 | positions = form_position_string(arg_positions) 25 | 26 | """ 27 | will never return since the #{positions} arguments differ 28 | from the success typing arguments: 29 | 30 | #{pretty_signature_args} 31 | """ 32 | end 33 | end 34 | 35 | def call_or_apply_to_string( 36 | arg_positions, 37 | :only_contract, 38 | _signature_args, 39 | _signature_return, 40 | {overloaded?, contract} 41 | ) do 42 | pretty_contract = Erlex.pretty_print_contract(contract) 43 | 44 | if Enum.empty?(arg_positions) || overloaded? do 45 | # We do not know which arguments caused the failure 46 | """ 47 | breaks the contract 48 | #{pretty_contract} 49 | """ 50 | else 51 | position_string = form_position_string(arg_positions) 52 | 53 | """ 54 | breaks the contract 55 | #{pretty_contract} 56 | 57 | in #{position_string} argument 58 | """ 59 | end 60 | end 61 | 62 | def call_or_apply_to_string( 63 | _arg_positions, 64 | :both, 65 | signature_args, 66 | signature_return, 67 | {_overloaded?, contract} 68 | ) do 69 | pretty_contract = Erlex.pretty_print_contract(contract) 70 | 71 | pretty_print_signature = 72 | Erlex.pretty_print_contract("#{signature_args} -> #{signature_return}") 73 | 74 | """ 75 | will never return since the success typing is: 76 | #{pretty_print_signature} 77 | 78 | and the contract is 79 | #{pretty_contract} 80 | """ 81 | end 82 | 83 | def form_position_string(arg_positions) do 84 | Enum.map_join(arg_positions, " and ", &ordinal/1) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings do 2 | @warnings Enum.into( 3 | [ 4 | Dialyxir.Warnings.AppCall, 5 | Dialyxir.Warnings.Apply, 6 | Dialyxir.Warnings.BinaryConstruction, 7 | Dialyxir.Warnings.Call, 8 | Dialyxir.Warnings.CallToMissingFunction, 9 | Dialyxir.Warnings.CallWithOpaque, 10 | Dialyxir.Warnings.CallWithoutOpaque, 11 | Dialyxir.Warnings.CallbackArgumentTypeMismatch, 12 | Dialyxir.Warnings.CallbackInfoMissing, 13 | Dialyxir.Warnings.CallbackMissing, 14 | Dialyxir.Warnings.CallbackNotExported, 15 | Dialyxir.Warnings.CallbackSpecArgumentTypeMismatch, 16 | Dialyxir.Warnings.CallbackSpecTypeMismatch, 17 | Dialyxir.Warnings.CallbackTypeMismatch, 18 | Dialyxir.Warnings.ContractDiff, 19 | Dialyxir.Warnings.ContractRange, 20 | Dialyxir.Warnings.ContractSubtype, 21 | Dialyxir.Warnings.ContractSupertype, 22 | Dialyxir.Warnings.ContractWithOpaque, 23 | Dialyxir.Warnings.ExactEquality, 24 | Dialyxir.Warnings.ExtraRange, 25 | Dialyxir.Warnings.FunctionApplicationArguments, 26 | Dialyxir.Warnings.FunctionApplicationNoFunction, 27 | Dialyxir.Warnings.GuardFail, 28 | Dialyxir.Warnings.GuardFailPattern, 29 | Dialyxir.Warnings.ImproperListConstruction, 30 | Dialyxir.Warnings.InvalidContract, 31 | Dialyxir.Warnings.MapUpdate, 32 | Dialyxir.Warnings.MissingRange, 33 | Dialyxir.Warnings.NegativeGuardFail, 34 | Dialyxir.Warnings.NoReturn, 35 | Dialyxir.Warnings.OpaqueGuard, 36 | Dialyxir.Warnings.OpaqueEquality, 37 | Dialyxir.Warnings.OpaqueMatch, 38 | Dialyxir.Warnings.OpaqueNonequality, 39 | Dialyxir.Warnings.OpaqueTypeTest, 40 | Dialyxir.Warnings.OverlappingContract, 41 | Dialyxir.Warnings.PatternMatch, 42 | Dialyxir.Warnings.PatternMatchCovered, 43 | Dialyxir.Warnings.RecordConstruction, 44 | Dialyxir.Warnings.RecordMatch, 45 | Dialyxir.Warnings.RecordMatching, 46 | Dialyxir.Warnings.UnknownBehaviour, 47 | Dialyxir.Warnings.UnknownFunction, 48 | Dialyxir.Warnings.UnknownType, 49 | Dialyxir.Warnings.UnmatchedReturn, 50 | Dialyxir.Warnings.UnusedFunction 51 | ], 52 | %{}, 53 | fn warning -> {warning.warning(), warning} end 54 | ) 55 | 56 | @doc """ 57 | Returns a mapping of the warning to the warning module. 58 | """ 59 | def warnings(), do: @warnings 60 | end 61 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/app_call.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.AppCall do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :app_call 6 | def warning(), do: :app_call 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([_module, function | _]) do 11 | "Module or function to apply is not an atom in #{function}." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([module, function, arity, culprit, expected_type, actual_type]) do 17 | pretty_module = Erlex.pretty_print(module) 18 | pretty_expected_type = Erlex.pretty_print_type(expected_type) 19 | pretty_actual_type = Erlex.pretty_print_type(actual_type) 20 | 21 | "The call #{pretty_module}.#{function}/#{arity} requires that " <> 22 | "#{culprit} is of type #{pretty_expected_type}, not #{pretty_actual_type}." 23 | end 24 | 25 | @impl Dialyxir.Warning 26 | @spec explain() :: String.t() 27 | def explain() do 28 | Dialyxir.Warning.default_explain() 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/apply.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.Apply do 2 | @moduledoc """ 3 | The function being invoked exists, and has the correct arity, but 4 | will not succeed. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | def ok() do 10 | fun = fn :ok -> :ok end 11 | fun.(:error) 12 | end 13 | end 14 | """ 15 | 16 | @behaviour Dialyxir.Warning 17 | 18 | @impl Dialyxir.Warning 19 | @spec warning() :: :apply 20 | def warning(), do: :apply 21 | 22 | @impl Dialyxir.Warning 23 | @spec format_short([String.t()]) :: String.t() 24 | def format_short([args | _]) do 25 | pretty_args = Erlex.pretty_print_args(args) 26 | "Function application with args #{pretty_args} will not succeed." 27 | end 28 | 29 | @impl Dialyxir.Warning 30 | @spec format_long([String.t()]) :: String.t() 31 | def format_long([args, arg_positions, fail_reason, signature_args, signature_return, contract]) do 32 | pretty_args = Erlex.pretty_print_args(args) 33 | 34 | call_string = 35 | Dialyxir.WarningHelpers.call_or_apply_to_string( 36 | arg_positions, 37 | fail_reason, 38 | signature_args, 39 | signature_return, 40 | contract 41 | ) 42 | 43 | "Function application with arguments #{pretty_args} #{call_string}" 44 | end 45 | 46 | @impl Dialyxir.Warning 47 | @spec explain() :: String.t() 48 | def explain() do 49 | @moduledoc 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/binary_construction.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.BinaryConstruction do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :bin_construction 6 | def warning(), do: :bin_construction 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([culprit | _]) do 11 | "Binary construction with #{culprit} will fail." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([culprit, size, segment, type]) do 17 | pretty_type = Erlex.pretty_print_type(type) 18 | 19 | "Binary construction will fail since the #{culprit} field #{size} in " <> 20 | "segment #{segment} has type #{pretty_type}." 21 | end 22 | 23 | @impl Dialyxir.Warning 24 | @spec explain() :: String.t() 25 | def explain() do 26 | Dialyxir.Warning.default_explain() 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/call.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.Call do 2 | @moduledoc """ 3 | The function call will not succeed. 4 | 5 | ## Example 6 | 7 | defmodule Example do 8 | def ok() do 9 | ok(:error) 10 | end 11 | 12 | def ok(:ok) do 13 | :ok 14 | end 15 | end 16 | """ 17 | 18 | @behaviour Dialyxir.Warning 19 | 20 | @impl Dialyxir.Warning 21 | @spec warning() :: :call 22 | def warning(), do: :call 23 | 24 | @impl Dialyxir.Warning 25 | @spec format_short([String.t()]) :: String.t() 26 | def format_short([_module, function | _]) do 27 | "The function call #{function} will not succeed." 28 | end 29 | 30 | @impl Dialyxir.Warning 31 | @spec format_long([String.t()]) :: String.t() 32 | def format_long([ 33 | module, 34 | function, 35 | args, 36 | arg_positions, 37 | fail_reason, 38 | signature_args, 39 | signature_return, 40 | contract 41 | ]) do 42 | pretty_args = Erlex.pretty_print_args(args) 43 | pretty_module = Erlex.pretty_print(module) 44 | 45 | call_string = 46 | Dialyxir.WarningHelpers.call_or_apply_to_string( 47 | arg_positions, 48 | fail_reason, 49 | signature_args, 50 | signature_return, 51 | contract 52 | ) 53 | 54 | """ 55 | The function call will not succeed. 56 | 57 | #{pretty_module}.#{function}#{pretty_args} 58 | 59 | #{String.trim_trailing(call_string)} 60 | """ 61 | end 62 | 63 | @impl Dialyxir.Warning 64 | @spec explain() :: String.t() 65 | def explain() do 66 | @moduledoc 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/call_to_missing_function.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallToMissingFunction do 2 | @moduledoc """ 3 | Function calls a missing or private function. This may be caused by 4 | a typo or incorrect arity. This is also a compiler warning. 5 | 6 | ## Example 7 | 8 | defmodule Missing do 9 | def missing(:ok) do 10 | :ok 11 | end 12 | 13 | defp missing() do 14 | :ok 15 | end 16 | end 17 | 18 | defmodule Example do 19 | def error() do 20 | Missing.missing() 21 | end 22 | end 23 | """ 24 | 25 | @behaviour Dialyxir.Warning 26 | 27 | @impl Dialyxir.Warning 28 | @spec warning() :: :call_to_missing 29 | def warning(), do: :call_to_missing 30 | 31 | @impl Dialyxir.Warning 32 | @spec format_short([String.t()]) :: String.t() 33 | def format_short(args), do: format_long(args) 34 | 35 | @impl Dialyxir.Warning 36 | @spec format_long([String.t()]) :: String.t() 37 | def format_long([module, function, arity]) do 38 | pretty_module = Erlex.pretty_print(module) 39 | "Call to missing or private function #{pretty_module}.#{function}/#{arity}." 40 | end 41 | 42 | @impl Dialyxir.Warning 43 | @spec explain() :: String.t() 44 | def explain() do 45 | @moduledoc 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/call_with_opaque.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallWithOpaque do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :call_with_opaque 6 | def warning(), do: :call_with_opaque 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([_module, function | _]) do 11 | "Type mismatch in call with opaque term in #{function}." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([module, function, args, arg_positions, expected_args]) do 17 | pretty_module = Erlex.pretty_print(module) 18 | 19 | "The call #{pretty_module}.#{function}#{args} contains #{form_positions(arg_positions)} " <> 20 | "when #{form_expected(expected_args)}}." 21 | end 22 | 23 | defp form_positions(arg_positions = [_]) do 24 | form_position_string = Dialyxir.WarningHelpers.form_position_string(arg_positions) 25 | "an opaque term in #{form_position_string} argument" 26 | end 27 | 28 | defp form_positions(arg_positions) do 29 | form_position_string = Dialyxir.WarningHelpers.form_position_string(arg_positions) 30 | "opaque terms in #{form_position_string} arguments" 31 | end 32 | 33 | defp form_expected([type]) do 34 | type_string = :erl_types.t_to_string(type) 35 | 36 | if :erl_types.t_is_opaque(type) do 37 | "an opaque term of type #{type_string} is expected" 38 | else 39 | "a structured term of type #{type_string} is expected" 40 | end 41 | end 42 | 43 | defp form_expected(_expected_args) do 44 | "terms of different types are expected in these positions" 45 | end 46 | 47 | @impl Dialyxir.Warning 48 | @spec explain() :: String.t() 49 | def explain() do 50 | Dialyxir.Warning.default_explain() 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/call_without_opaque.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallWithoutOpaque do 2 | @moduledoc """ 3 | Function call without opaqueness type mismatch. 4 | 5 | ## Example 6 | 7 | defmodule OpaqueStruct do 8 | defstruct [:opaque] 9 | 10 | @opaque t :: %OpaqueStruct{} 11 | end 12 | 13 | defmodule Example do 14 | @spec error(OpaqueStruct.t()) :: :error 15 | def error(struct = %OpaqueStruct{}) do 16 | do_error(struct) 17 | end 18 | 19 | @spec do_error(OpaqueStruct.t()) :: :error 20 | defp do_error(_) do 21 | :error 22 | end 23 | end 24 | """ 25 | 26 | @behaviour Dialyxir.Warning 27 | 28 | @impl Dialyxir.Warning 29 | @spec warning() :: :call_without_opaque 30 | def warning(), do: :call_without_opaque 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_short([String.t()]) :: String.t() 34 | def format_short([_module, function | _]) do 35 | "Type mismatch in call without opaque term in #{function}." 36 | end 37 | 38 | @impl Dialyxir.Warning 39 | @spec format_long([String.t()]) :: String.t() 40 | def format_long([module, function, args, expected_triples]) do 41 | expected = form_expected_without_opaque(expected_triples) 42 | pretty_module = Erlex.pretty_print(module) 43 | pretty_args = Erlex.pretty_print_args(args) 44 | 45 | """ 46 | Function call without opaqueness type mismatch. 47 | 48 | Call does not have expected #{expected}. 49 | 50 | #{pretty_module}.#{function}#{pretty_args} 51 | """ 52 | end 53 | 54 | # We know which positions N are to blame; 55 | # the list of triples will never be empty. 56 | defp form_expected_without_opaque([{position, type, type_string}]) do 57 | pretty_type = Erlex.pretty_print_type(type_string) 58 | form_position_string = Dialyxir.WarningHelpers.form_position_string([position]) 59 | 60 | if :erl_types.t_is_opaque(type) do 61 | "opaque term of type #{pretty_type} in the #{form_position_string} position" 62 | else 63 | "term of type #{pretty_type} (with opaque subterms) in the #{form_position_string} position" 64 | end 65 | end 66 | 67 | # TODO: can do much better here 68 | defp form_expected_without_opaque(expected_triples) do 69 | {arg_positions, _typess, _type_strings} = :lists.unzip3(expected_triples) 70 | form_position_string = Dialyxir.WarningHelpers.form_position_string(arg_positions) 71 | "opaque terms in the #{form_position_string} position" 72 | end 73 | 74 | @impl Dialyxir.Warning 75 | @spec explain() :: String.t() 76 | def explain() do 77 | @moduledoc 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/callback_argument_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallbackArgumentTypeMismatch do 2 | @moduledoc """ 3 | Type of argument does not match the callback's expected type. 4 | 5 | ## Example 6 | 7 | defmodule ExampleBehaviour do 8 | @callback ok(:ok) :: :ok 9 | end 10 | 11 | defmodule Example do 12 | 13 | @behaviour ExampleBehaviour 14 | 15 | def ok(:error) do 16 | :ok 17 | end 18 | end 19 | """ 20 | @behaviour Dialyxir.Warning 21 | 22 | @impl Dialyxir.Warning 23 | @spec warning() :: :callback_arg_type_mismatch 24 | def warning(), do: :callback_arg_type_mismatch 25 | 26 | @impl Dialyxir.Warning 27 | @spec format_short([String.t()]) :: String.t() 28 | def format_short([_behaviour, function, arity | _]) do 29 | "Type mismatch with behaviour callback to #{function}/#{arity}." 30 | end 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_long([String.t()]) :: String.t() 34 | def format_long([behaviour, function, arity, position, success_type, callback_type]) do 35 | pretty_behaviour = Erlex.pretty_print(behaviour) 36 | pretty_success_type = Erlex.pretty_print_type(success_type) 37 | pretty_callback_type = Erlex.pretty_print_type(callback_type) 38 | ordinal_position = Dialyxir.WarningHelpers.ordinal(position) 39 | 40 | """ 41 | The inferred type for the #{ordinal_position} argument is not a 42 | supertype of the expected type for the #{function}/#{arity} callback 43 | in the #{pretty_behaviour} behaviour. 44 | 45 | Success type: 46 | #{pretty_success_type} 47 | 48 | Behaviour callback type: 49 | #{pretty_callback_type} 50 | """ 51 | end 52 | 53 | @impl Dialyxir.Warning 54 | @spec explain() :: String.t() 55 | def explain() do 56 | @moduledoc 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/callback_info_missing.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallbackInfoMissing do 2 | @moduledoc """ 3 | The module is using a behaviour that does not exist or is not a 4 | behaviour. This is also a compiler warning. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | @behaviour BehaviourThatDoesNotExist 10 | 11 | def ok() do 12 | :ok 13 | end 14 | end 15 | """ 16 | 17 | @behaviour Dialyxir.Warning 18 | 19 | @impl Dialyxir.Warning 20 | @spec warning() :: :callback_info_missing 21 | def warning(), do: :callback_info_missing 22 | 23 | @impl Dialyxir.Warning 24 | @spec format_short([String.t()]) :: String.t() 25 | def format_short(args), do: format_long(args) 26 | 27 | @impl Dialyxir.Warning 28 | @spec format_long([String.t()]) :: String.t() 29 | def format_long([behaviour]) do 30 | pretty_behaviour = Erlex.pretty_print(behaviour) 31 | 32 | "Callback info about the #{pretty_behaviour} behaviour is not available." 33 | end 34 | 35 | @impl Dialyxir.Warning 36 | @spec explain() :: String.t() 37 | def explain() do 38 | @moduledoc 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/callback_missing.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallbackMissing do 2 | @moduledoc """ 3 | Module implements a behaviour, but does not have all of its 4 | callbacks. This is also a compiler warning. 5 | 6 | ## Example 7 | 8 | defmodule ExampleBehaviour do 9 | @callback ok() :: :ok 10 | @callback missing() :: :ok 11 | end 12 | 13 | defmodule Example do 14 | @behaviour ExampleBehaviour 15 | 16 | def ok() do 17 | :ok 18 | end 19 | end 20 | """ 21 | 22 | @behaviour Dialyxir.Warning 23 | 24 | @impl Dialyxir.Warning 25 | @spec warning() :: :callback_missing 26 | def warning(), do: :callback_missing 27 | 28 | @impl Dialyxir.Warning 29 | @spec format_short([String.t()]) :: String.t() 30 | def format_short(args), do: format_long(args) 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_long([String.t()]) :: String.t() 34 | def format_long([behaviour, function, arity]) do 35 | pretty_behaviour = Erlex.pretty_print(behaviour) 36 | 37 | "Undefined callback function #{function}/#{arity} (behaviour #{pretty_behaviour})." 38 | end 39 | 40 | @impl Dialyxir.Warning 41 | @spec explain() :: String.t() 42 | def explain() do 43 | @moduledoc 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/callback_not_exported.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallbackNotExported do 2 | @moduledoc """ 3 | Module implements a behaviour, but does not export some of its 4 | callbacks. 5 | 6 | ## Example 7 | defmodule Example do 8 | @behaviour GenServer 9 | 10 | def init(_) do 11 | :ok 12 | end 13 | 14 | # OK. No warning. 15 | def handle_all(_request, _from, state) do 16 | {:noreply, state} 17 | end 18 | 19 | # Not exported. Should be a warning. 20 | @spec handle_cast(any(), any()) :: binary() 21 | defp handle_cast(_request, _state) do 22 | "abc" 23 | end 24 | 25 | # Not exported and conflicting arguments and return value. No warning 26 | # since format_status/1 is an optional callback. 27 | @spec format_status(binary()) :: binary() 28 | def format_status(bin) when is_binary(bin) do 29 | bin 30 | end 31 | end 32 | """ 33 | 34 | @behaviour Dialyxir.Warning 35 | 36 | @impl Dialyxir.Warning 37 | @spec warning() :: :callback_not_exported 38 | def warning(), do: :callback_not_exported 39 | 40 | @impl Dialyxir.Warning 41 | @spec format_short([String.t()]) :: String.t() 42 | def format_short(args), do: format_long(args) 43 | 44 | @impl Dialyxir.Warning 45 | @spec format_long([String.t()]) :: String.t() 46 | def format_long([behaviour, function, arity]) do 47 | pretty_behaviour = Erlex.pretty_print(behaviour) 48 | 49 | "Callback function #{function}/#{arity} exists but is not exported (behaviour #{pretty_behaviour})." 50 | end 51 | 52 | @impl Dialyxir.Warning 53 | @spec explain() :: String.t() 54 | def explain() do 55 | @moduledoc 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/callback_spec_argument_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallbackSpecArgumentTypeMismatch do 2 | @moduledoc """ 3 | Spec type of argument does not match the callback's expected type. 4 | 5 | ## Example 6 | 7 | defmodule ExampleBehaviour do 8 | @callback ok(:ok) :: :ok 9 | end 10 | 11 | defmodule Example do 12 | @behaviour ExampleBehaviour 13 | 14 | @spec ok(:error) :: :ok 15 | def ok(:ok) do 16 | :ok 17 | end 18 | end 19 | """ 20 | 21 | @behaviour Dialyxir.Warning 22 | 23 | @impl Dialyxir.Warning 24 | @spec warning() :: :callback_spec_arg_type_mismatch 25 | def warning(), do: :callback_spec_arg_type_mismatch 26 | 27 | @impl Dialyxir.Warning 28 | @spec format_short([String.t()]) :: String.t() 29 | def format_short([_behaviour, function | _]) do 30 | "Spec type mismatch in argument to callback #{function}." 31 | end 32 | 33 | @impl Dialyxir.Warning 34 | @spec format_long([String.t()]) :: String.t() 35 | def format_long([behaviour, function, arity, position, success_type, callback_type]) do 36 | pretty_behaviour = Erlex.pretty_print(behaviour) 37 | pretty_success_type = Erlex.pretty_print_type(success_type) 38 | pretty_callback_type = Erlex.pretty_print_type(callback_type) 39 | ordinal_position = Dialyxir.WarningHelpers.ordinal(position) 40 | 41 | """ 42 | The @spec type for the #{ordinal_position} argument is not a 43 | supertype of the expected type for the #{function}/#{arity} callback 44 | in the #{pretty_behaviour} behaviour. 45 | 46 | Success type: 47 | #{pretty_success_type} 48 | 49 | Behaviour callback type: 50 | #{pretty_callback_type} 51 | """ 52 | end 53 | 54 | @impl Dialyxir.Warning 55 | @spec explain() :: String.t() 56 | def explain() do 57 | @moduledoc 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/callback_spec_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallbackSpecTypeMismatch do 2 | @moduledoc """ 3 | The return type in the @spec does not match the 4 | expected return type of the behaviour. 5 | 6 | ## Example 7 | 8 | defmodule ExampleBehaviour do 9 | @callback ok(:ok) :: :ok 10 | end 11 | 12 | defmodule Example do 13 | @behaviour ExampleBehaviour 14 | 15 | @spec ok(:ok) :: :error 16 | def ok(:ok) do 17 | :error 18 | end 19 | end 20 | """ 21 | 22 | @behaviour Dialyxir.Warning 23 | 24 | @impl Dialyxir.Warning 25 | @spec warning() :: :callback_spec_type_mismatch 26 | def warning(), do: :callback_spec_type_mismatch 27 | 28 | @impl Dialyxir.Warning 29 | @spec format_short([String.t()]) :: String.t() 30 | def format_short([_behaviour, function | _]) do 31 | "The @spec return type does not match the expected return type for #{function}." 32 | end 33 | 34 | @impl Dialyxir.Warning 35 | @spec format_long([String.t()]) :: String.t() 36 | def format_long([behaviour, function, arity, success_type, callback_type]) do 37 | pretty_behaviour = Erlex.pretty_print(behaviour) 38 | pretty_success_type = Erlex.pretty_print_type(success_type) 39 | pretty_callback_type = Erlex.pretty_print_type(callback_type) 40 | 41 | """ 42 | The @spec return type does not match the expected return type 43 | for #{function}/#{arity} callback in #{pretty_behaviour} behaviour. 44 | 45 | Actual: 46 | @spec #{function}(...) :: #{pretty_success_type} 47 | 48 | Expected: 49 | @spec #{function}(...) :: #{pretty_callback_type} 50 | """ 51 | end 52 | 53 | @impl Dialyxir.Warning 54 | @spec explain() :: String.t() 55 | def explain() do 56 | @moduledoc 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/callback_type_mismatch.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.CallbackTypeMismatch do 2 | @moduledoc """ 3 | The success type of the function does not match the callback type in 4 | the behaviour. 5 | 6 | ## Example 7 | 8 | defmodule ExampleBehaviour do 9 | @callback ok() :: :ok 10 | end 11 | 12 | defmodule Example do 13 | @behaviour ExampleBehaviour 14 | 15 | def ok() do 16 | :error 17 | end 18 | end 19 | """ 20 | 21 | @behaviour Dialyxir.Warning 22 | 23 | @impl Dialyxir.Warning 24 | @spec warning() :: :callback_type_mismatch 25 | def warning(), do: :callback_type_mismatch 26 | 27 | @impl Dialyxir.Warning 28 | @spec format_short([String.t()]) :: String.t() 29 | def format_short([_behaviour, function | _]) do 30 | "Type mismatch for @callback #{function}." 31 | end 32 | 33 | @impl Dialyxir.Warning 34 | @spec format_long([String.t() | non_neg_integer]) :: String.t() 35 | def format_long([behaviour, function, arity, fail_type, success_type]) do 36 | pretty_behaviour = Erlex.pretty_print(behaviour) 37 | pretty_fail_type = Erlex.pretty_print_type(fail_type) 38 | pretty_success_type = Erlex.pretty_print_type(success_type) 39 | 40 | """ 41 | Type mismatch for @callback #{function}/#{arity} in #{pretty_behaviour} behaviour. 42 | 43 | Expected type: 44 | #{pretty_success_type} 45 | 46 | Actual type: 47 | #{pretty_fail_type} 48 | """ 49 | end 50 | 51 | @impl Dialyxir.Warning 52 | @spec explain() :: String.t() 53 | def explain() do 54 | @moduledoc 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/contract_diff.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ContractDiff do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :contract_diff 6 | def warning(), do: :contract_diff 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([_module, function | _]) do 11 | "Type specification is not equal to the success typing for #{function}." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([module, function, arity, contract, signature]) do 17 | pretty_module = Erlex.pretty_print(module) 18 | pretty_contract = Erlex.pretty_print_contract(contract) 19 | pretty_signature = Erlex.pretty_print_contract(signature) 20 | 21 | """ 22 | Type specification is not equal to the success typing. 23 | 24 | Function: 25 | #{pretty_module}.#{function}/#{arity} 26 | 27 | Type specification: 28 | #{pretty_contract} 29 | 30 | Success typing: 31 | #{pretty_signature} 32 | """ 33 | end 34 | 35 | @impl Dialyxir.Warning 36 | @spec explain() :: String.t() 37 | def explain() do 38 | Dialyxir.Warning.default_explain() 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/contract_range.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ContractRange do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :contract_range 6 | def warning(), do: :contract_range 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([_, _, function | _]) do 11 | "Contract cannot be correct because return type for #{function} is mismatched." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([contract, module, function, arg_strings, line, contract_return]) do 17 | pretty_contract = Erlex.pretty_print_contract(contract) 18 | pretty_module = Erlex.pretty_print(module) 19 | pretty_contract_return = Erlex.pretty_print_type(contract_return) 20 | pretty_args = Erlex.pretty_print_args(arg_strings) 21 | 22 | """ 23 | Contract cannot be correct because return type on line number #{line} is mismatched. 24 | 25 | Function: 26 | #{pretty_module}.#{function}#{pretty_args} 27 | 28 | Type specification: 29 | #{pretty_contract} 30 | 31 | Success typing (line #{line}): 32 | #{pretty_contract_return} 33 | """ 34 | end 35 | 36 | @impl Dialyxir.Warning 37 | @spec explain() :: String.t() 38 | def explain() do 39 | Dialyxir.Warning.default_explain() 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/contract_subtype.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ContractSubtype do 2 | # TODO: could not create warning with this example (and --overspecs) 3 | @moduledoc """ 4 | The type in the @spec does not completely cover the types returned 5 | by function. 6 | 7 | ## Example 8 | 9 | defmodule Example do 10 | @spec ok(:ok | :error) :: :ok 11 | def ok(:ok) do 12 | :ok 13 | end 14 | 15 | def ok(:error) do 16 | :error 17 | end 18 | end 19 | """ 20 | 21 | @behaviour Dialyxir.Warning 22 | 23 | @impl Dialyxir.Warning 24 | @spec warning() :: :contract_subtype 25 | def warning(), do: :contract_subtype 26 | 27 | @impl Dialyxir.Warning 28 | @spec format_short([String.t()]) :: String.t() 29 | def format_short([_module, function | _]) do 30 | "Type specification for #{function} is a subtype of the success typing." 31 | end 32 | 33 | @impl Dialyxir.Warning 34 | @spec format_long([String.t()]) :: String.t() 35 | def format_long([module, function, arity, contract, signature]) do 36 | pretty_module = Erlex.pretty_print(module) 37 | pretty_signature = Erlex.pretty_print_contract(signature) 38 | pretty_contract = Erlex.pretty_print_contract(contract, module, function) 39 | 40 | """ 41 | Type specification is a subtype of the success typing. 42 | 43 | Function: 44 | #{pretty_module}.#{function}/#{arity} 45 | 46 | Type specification: 47 | @spec #{function}#{pretty_contract} 48 | 49 | Success typing: 50 | @spec #{function}#{pretty_signature} 51 | """ 52 | end 53 | 54 | @impl Dialyxir.Warning 55 | @spec explain() :: String.t() 56 | def explain() do 57 | @moduledoc 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/contract_supertype.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ContractSupertype do 2 | @moduledoc """ 3 | The @spec, while not incorrect, is more general than the type 4 | returned by the function. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | @spec ok() :: any 10 | def ok() do 11 | :ok 12 | end 13 | end 14 | """ 15 | 16 | @behaviour Dialyxir.Warning 17 | 18 | @impl Dialyxir.Warning 19 | @spec warning() :: :contract_supertype 20 | def warning(), do: :contract_supertype 21 | 22 | @impl Dialyxir.Warning 23 | @spec format_short([String.t()]) :: String.t() 24 | def format_short([_module, function | _]) do 25 | "Type specification for #{function} is a supertype of the success typing." 26 | end 27 | 28 | @impl Dialyxir.Warning 29 | @spec format_long([String.t()]) :: String.t() 30 | def format_long([module, function, arity, contract, signature]) do 31 | pretty_module = Erlex.pretty_print(module) 32 | pretty_contract = Erlex.pretty_print_contract(contract) 33 | pretty_signature = Erlex.pretty_print_contract(signature) 34 | 35 | """ 36 | Type specification is a supertype of the success typing. 37 | 38 | Function: 39 | #{pretty_module}.#{function}/#{arity} 40 | 41 | Type specification: 42 | @spec #{function}#{pretty_contract} 43 | 44 | Success typing: 45 | @spec #{function}#{pretty_signature} 46 | """ 47 | end 48 | 49 | @impl Dialyxir.Warning 50 | @spec explain() :: String.t() 51 | def explain() do 52 | @moduledoc 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/contract_with_opaque.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ContractWithOpaque do 2 | @moduledoc """ 3 | The @spec says the function is returning an opaque type, but it is 4 | returning a different type. 5 | 6 | ## Example 7 | 8 | defmodule Types do 9 | @opaque type :: :ok 10 | end 11 | 12 | defmodule Example do 13 | @spec ok() :: Types.type() 14 | def ok() do 15 | :ok 16 | end 17 | end 18 | """ 19 | 20 | @behaviour Dialyxir.Warning 21 | 22 | @impl Dialyxir.Warning 23 | @spec warning() :: :contract_with_opaque 24 | def warning(), do: :contract_with_opaque 25 | 26 | @impl Dialyxir.Warning 27 | @spec format_short([String.t()]) :: String.t() 28 | def format_short([_module, function | _]) do 29 | "The @spec for #{function} has an opaque subtype which is violated by the success typing." 30 | end 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_long([String.t()]) :: String.t() 34 | def format_long([module, function, arity, type, signature_type]) do 35 | pretty_module = Erlex.pretty_print(module) 36 | pretty_type = Erlex.pretty_print_type(type) 37 | pretty_success_type = Erlex.pretty_print_contract(signature_type) 38 | 39 | """ 40 | The @spec for #{pretty_module}.#{function}/#{arity} has an opaque 41 | subtype #{pretty_type} which is violated by the success typing. 42 | 43 | Success typing: 44 | #{pretty_success_type} 45 | """ 46 | end 47 | 48 | @impl Dialyxir.Warning 49 | @spec explain() :: String.t() 50 | def explain() do 51 | @moduledoc 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/exact_equality.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ExactEquality do 2 | @moduledoc """ 3 | The expression can never evaluate to true. 4 | 5 | ## Example 6 | 7 | defmodule Example do 8 | def ok() do 9 | :ok == :error 10 | end 11 | end 12 | """ 13 | 14 | @behaviour Dialyxir.Warning 15 | 16 | @impl Dialyxir.Warning 17 | @spec warning() :: :exact_eq 18 | def warning(), do: :exact_eq 19 | 20 | @impl Dialyxir.Warning 21 | @spec format_short([String.t()]) :: String.t() 22 | def format_short(args), do: format_long(args) 23 | 24 | @impl Dialyxir.Warning 25 | @spec format_long([String.t()]) :: String.t() 26 | def format_long([type1, op, type2]) do 27 | pretty_type1 = Erlex.pretty_print_type(type1) 28 | pretty_type2 = Erlex.pretty_print_type(type2) 29 | 30 | "The test #{pretty_type1} #{op} #{pretty_type2} can never evaluate to 'true'." 31 | end 32 | 33 | @impl Dialyxir.Warning 34 | @spec explain() :: String.t() 35 | def explain() do 36 | @moduledoc 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/extra_range.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ExtraRange do 2 | @moduledoc """ 3 | The @spec says the function returns more types than the function 4 | actually returns. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | @spec ok() :: :ok | :error 10 | def ok() do 11 | :ok 12 | end 13 | end 14 | """ 15 | 16 | @behaviour Dialyxir.Warning 17 | 18 | @impl Dialyxir.Warning 19 | @spec warning() :: :extra_range 20 | def warning(), do: :extra_range 21 | 22 | @impl Dialyxir.Warning 23 | @spec format_short([String.t()]) :: String.t() 24 | def format_short([_module, function | _]) do 25 | "@spec for #{function} has more types than are returned by the function." 26 | end 27 | 28 | @impl Dialyxir.Warning 29 | @spec format_long([String.t()]) :: String.t() 30 | def format_long([module, function, arity, extra_ranges, signature_range]) do 31 | pretty_module = Erlex.pretty_print(module) 32 | pretty_extra = Erlex.pretty_print_type(extra_ranges) 33 | pretty_signature = Erlex.pretty_print_type(signature_range) 34 | 35 | """ 36 | The type specification has too many types for the function. 37 | 38 | Function: 39 | #{pretty_module}.#{function}/#{arity} 40 | 41 | Extra type: 42 | #{pretty_extra} 43 | 44 | Success typing: 45 | #{pretty_signature} 46 | """ 47 | end 48 | 49 | @impl Dialyxir.Warning 50 | @spec explain() :: String.t() 51 | def explain() do 52 | @moduledoc 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/function_application_arguments.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.FunctionApplicationArguments do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :fun_app_args 6 | def warning(), do: :fun_app_args 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([args, _type]) do 11 | pretty_args = Erlex.pretty_print_args(args) 12 | "Function application with #{pretty_args} will fail." 13 | end 14 | 15 | # OTP 22+ format 16 | def format_short([_arg_positions, args, type]) do 17 | format_short([args, type]) 18 | end 19 | 20 | @impl Dialyxir.Warning 21 | @spec format_long([String.t()]) :: String.t() 22 | def format_long([args, type]) do 23 | pretty_args = Erlex.pretty_print_args(args) 24 | pretty_type = Erlex.pretty_print(type) 25 | 26 | "Function application with arguments #{pretty_args} will fail, " <> 27 | "because the function has type #{pretty_type}." 28 | end 29 | 30 | # OTP 22+ format 31 | def format_long([arg_positions, args, type]) do 32 | pretty_arg_positions = form_positions(arg_positions) 33 | pretty_args = Erlex.pretty_print_args(args) 34 | pretty_type = Erlex.pretty_print(type) 35 | 36 | "Function application with arguments #{pretty_args} will fail, " <> 37 | "because the function has type #{pretty_type}, " <> 38 | "which differs in #{pretty_arg_positions}." 39 | end 40 | 41 | defp form_positions(arg_positions = [_]) do 42 | form_position_string = Dialyxir.WarningHelpers.form_position_string(arg_positions) 43 | "the #{form_position_string} argument" 44 | end 45 | 46 | defp form_positions(arg_positions) do 47 | form_position_string = Dialyxir.WarningHelpers.form_position_string(arg_positions) 48 | "the #{form_position_string} arguments" 49 | end 50 | 51 | @impl Dialyxir.Warning 52 | @spec explain() :: String.t() 53 | def explain() do 54 | Dialyxir.Warning.default_explain() 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/function_application_no_function.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.FunctionApplicationNoFunction do 2 | @moduledoc """ 3 | The function being invoked exists, but has an arity mismatch. 4 | 5 | ## Example 6 | 7 | defmodule Example do 8 | def ok() do 9 | fun = fn _ -> :ok end 10 | fun.() 11 | end 12 | end 13 | """ 14 | 15 | @behaviour Dialyxir.Warning 16 | 17 | @impl Dialyxir.Warning 18 | @spec warning() :: :fun_app_no_fun 19 | def warning(), do: :fun_app_no_fun 20 | 21 | @impl Dialyxir.Warning 22 | @spec format_short([String.t()]) :: String.t() 23 | def format_short([_, _, arity]) do 24 | "Function application will fail, because anonymous function has arity of #{arity}." 25 | end 26 | 27 | @impl Dialyxir.Warning 28 | @spec format_long([String.t()]) :: String.t() 29 | def format_long([op, type, arity]) do 30 | pretty_op = Erlex.pretty_print(op) 31 | pretty_type = Erlex.pretty_print_type(type) 32 | 33 | "Function application will fail, because #{pretty_op} :: #{pretty_type} is not a function of arity #{arity}." 34 | end 35 | 36 | @impl Dialyxir.Warning 37 | @spec explain() :: String.t() 38 | def explain() do 39 | @moduledoc 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/guard_fail.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.GuardFail do 2 | @moduledoc """ 3 | The function guard either presents an impossible guard or the only 4 | calls will never succeed against the guards. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | def ok() do 10 | ok(0) 11 | end 12 | 13 | defp ok(n) when n > 1 do 14 | :ok 15 | end 16 | end 17 | 18 | or 19 | 20 | defmodule Example do 21 | def ok() when 0 > 1 do 22 | :ok 23 | end 24 | end 25 | """ 26 | 27 | @behaviour Dialyxir.Warning 28 | 29 | @impl Dialyxir.Warning 30 | @spec warning() :: :guard_fail 31 | def warning(), do: :guard_fail 32 | 33 | @impl Dialyxir.Warning 34 | @spec format_short([String.t()]) :: String.t() 35 | def format_short(_) do 36 | "The guard clause can never succeed." 37 | end 38 | 39 | @impl Dialyxir.Warning 40 | @spec format_long([String.t()]) :: String.t() 41 | def format_long([]) do 42 | "The guard clause can never succeed." 43 | end 44 | 45 | def format_long([guard, args]) do 46 | pretty_args = Erlex.pretty_print_args(args) 47 | 48 | """ 49 | The guard test: 50 | 51 | #{guard}#{pretty_args} 52 | 53 | can never succeed. 54 | """ 55 | end 56 | 57 | def format_long([arg, infix, guard]) do 58 | pretty_arg = Erlex.pretty_print_args(arg) 59 | pretty_infix = Erlex.pretty_print_infix(infix) 60 | pretty_guard = Erlex.pretty_print(guard) 61 | 62 | """ 63 | The guard clause: 64 | 65 | when #{pretty_arg} #{pretty_infix} #{pretty_guard} 66 | 67 | can never succeed. 68 | """ 69 | end 70 | 71 | @impl Dialyxir.Warning 72 | @spec explain() :: String.t() 73 | def explain() do 74 | @moduledoc 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/guard_fail_pattern.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.GuardFailPattern do 2 | @moduledoc """ 3 | The clause guard describes a condition of literals that fails the pattern 4 | given in the function head. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | def ok(n = 0) when not n < 1 do 10 | :ok 11 | end 12 | end 13 | """ 14 | 15 | @behaviour Dialyxir.Warning 16 | 17 | @impl Dialyxir.Warning 18 | @spec warning() :: :guard_fail_pat 19 | def warning(), do: :guard_fail_pat 20 | 21 | @impl Dialyxir.Warning 22 | @spec format_short([String.t()]) :: String.t() 23 | def format_short([pattern, _]) do 24 | "The clause guard #{pattern} cannot succeed." 25 | end 26 | 27 | @impl Dialyxir.Warning 28 | @spec format_long([String.t()]) :: String.t() 29 | def format_long([pattern, type]) do 30 | pretty_type = Erlex.pretty_print_type(type) 31 | pretty_pattern = Erlex.pretty_print_pattern(pattern) 32 | 33 | "The clause guard cannot succeed. The pattern #{pretty_pattern} " <> 34 | "was matched against the type #{pretty_type}." 35 | end 36 | 37 | @impl Dialyxir.Warning 38 | @spec explain() :: String.t() 39 | def explain() do 40 | @moduledoc 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/improper_list_construction.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.ImproperListConstruction do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :improper_list_constr 6 | def warning(), do: :improper_list_constr 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short(args), do: format_long(args) 11 | 12 | @impl Dialyxir.Warning 13 | @spec format_long([String.t()]) :: String.t() 14 | def format_long([tl_type]) do 15 | pretty_type = Erlex.pretty_print_type(tl_type) 16 | 17 | "List construction (cons) will produce an improper list, " <> 18 | "because its second argument is #{pretty_type}." 19 | end 20 | 21 | @impl Dialyxir.Warning 22 | @spec explain() :: String.t() 23 | def explain() do 24 | Dialyxir.Warning.default_explain() 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/invalid_contract.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.InvalidContract do 2 | @moduledoc """ 3 | The @spec for the function does not match the success typing of the 4 | function. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | @spec process(:error) :: :ok 10 | def process(:ok) do 11 | :ok 12 | end 13 | end 14 | 15 | The @spec in this case claims that the function accepts a parameter 16 | :error but the function head only accepts :ok, resulting in the 17 | mismatch. 18 | """ 19 | 20 | @behaviour Dialyxir.Warning 21 | 22 | @impl Dialyxir.Warning 23 | @spec warning() :: :invalid_contract 24 | def warning(), do: :invalid_contract 25 | 26 | @impl Dialyxir.Warning 27 | @spec format_short([String.t()]) :: String.t() 28 | def format_short([_module, function | _]) do 29 | "Invalid type specification for function #{function}." 30 | end 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_long([String.t()]) :: String.t() 34 | def format_long([module, function, arity, signature]) do 35 | format_long([module, function, arity, nil, signature]) 36 | end 37 | 38 | def format_long([module, function, arity, _args, spec, success_typing]) do 39 | pretty_module = Erlex.pretty_print(module) 40 | pretty_success_typing = Erlex.pretty_print_contract(success_typing) 41 | pretty_spec = Erlex.pretty_print_contract(spec) 42 | 43 | """ 44 | The @spec for the function does not match the success typing of the function. 45 | 46 | Function: 47 | #{pretty_module}.#{function}/#{arity} 48 | 49 | Success typing: 50 | #{pretty_success_typing} 51 | 52 | But the spec is: 53 | #{pretty_spec} 54 | """ 55 | end 56 | 57 | @impl Dialyxir.Warning 58 | @spec explain() :: String.t() 59 | def explain() do 60 | @moduledoc 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/map_update.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.MapUpdate do 2 | @moduledoc """ 3 | Elixir can only use the map update syntax to update a key that is in 4 | the map. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | @spec error() :: map 10 | def error() do 11 | map = %{exists: :exists} 12 | %{map | does_not_exist: :fail} 13 | end 14 | end 15 | """ 16 | 17 | @behaviour Dialyxir.Warning 18 | 19 | @impl Dialyxir.Warning 20 | @spec warning() :: :map_update 21 | def warning(), do: :map_update 22 | 23 | @impl Dialyxir.Warning 24 | @spec format_short([String.t()]) :: String.t() 25 | def format_short([_map, key]) do 26 | pretty_key = Erlex.pretty_print(key) 27 | "Attempted to update key #{pretty_key} in a map that does not have that key." 28 | end 29 | 30 | @impl Dialyxir.Warning 31 | @spec format_long([String.t()]) :: String.t() 32 | def format_long([map, key]) do 33 | pretty_key = Erlex.pretty_print(key) 34 | pretty_map = Erlex.pretty_print(map) 35 | 36 | """ 37 | Attempted to update a key in a map that does not have that key. 38 | 39 | Key: 40 | #{pretty_key} 41 | 42 | Map: 43 | #{pretty_map} 44 | """ 45 | end 46 | 47 | @impl Dialyxir.Warning 48 | @spec explain() :: String.t() 49 | def explain() do 50 | @moduledoc 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/missing_range.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.MissingRange do 2 | @moduledoc """ 3 | Function spec declares a list of types, but function returns value 4 | outside stated range. 5 | 6 | This error only appears with the :overspecs flag. 7 | 8 | ## Example 9 | 10 | defmodule Example do 11 | @spec foo(any()) :: :ok 12 | def foo(:ok) do 13 | :ok 14 | end 15 | 16 | def foo(_) do 17 | :error 18 | end 19 | end 20 | """ 21 | 22 | @behaviour Dialyxir.Warning 23 | 24 | @impl Dialyxir.Warning 25 | @spec warning() :: :missing_range 26 | def warning(), do: :missing_range 27 | 28 | @impl Dialyxir.Warning 29 | @spec format_short([String.t()]) :: String.t() 30 | def format_short([module, function, arity | _]) do 31 | pretty_module = Erlex.pretty_print(module) 32 | 33 | "The type specification is missing types returned by #{pretty_module}.#{function}/#{arity}." 34 | end 35 | 36 | @impl Dialyxir.Warning 37 | @spec format_long([String.t()]) :: String.t() 38 | def format_long([module, function, arity, extra_ranges, contract_range]) do 39 | pretty_module = Erlex.pretty_print(module) 40 | pretty_contract_range = Erlex.pretty_print_type(contract_range) 41 | pretty_extra_ranges = Erlex.pretty_print_type(extra_ranges) 42 | 43 | """ 44 | The type specification is missing types returned by function. 45 | 46 | Function: 47 | #{pretty_module}.#{function}/#{arity} 48 | 49 | Type specification return types: 50 | #{pretty_contract_range} 51 | 52 | Missing from spec: 53 | #{pretty_extra_ranges} 54 | """ 55 | end 56 | 57 | @impl Dialyxir.Warning 58 | @spec explain() :: String.t() 59 | def explain() do 60 | @moduledoc 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/negative_guard_fail.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.NegativeGuardFail do 2 | @moduledoc """ 3 | The function guard either presents an impossible guard or the only 4 | calls will never succeed against the guards. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | def ok(ok = "ok") when not is_bitstring(ok) do 10 | :ok 11 | end 12 | end 13 | 14 | or 15 | 16 | defmodule Example do 17 | def ok() do 18 | ok(:ok) 19 | end 20 | 21 | defp ok(ok) when not is_atom(ok) do 22 | :ok 23 | end 24 | end 25 | """ 26 | 27 | @behaviour Dialyxir.Warning 28 | 29 | @impl Dialyxir.Warning 30 | @spec warning() :: :neg_guard_fail 31 | def warning(), do: :neg_guard_fail 32 | 33 | @impl Dialyxir.Warning 34 | @spec format_short([String.t()]) :: String.t() 35 | def format_short(_) do 36 | "The guard test can never succeed." 37 | end 38 | 39 | @impl Dialyxir.Warning 40 | @spec format_long([String.t()]) :: String.t() 41 | def format_long([guard, args]) do 42 | pretty_args = Erlex.pretty_print_args(args) 43 | 44 | """ 45 | Guard test: 46 | not #{guard}#{pretty_args} 47 | 48 | can never succeed. 49 | """ 50 | end 51 | 52 | def format_long([arg1, infix, arg2]) do 53 | pretty_infix = Erlex.pretty_print_infix(infix) 54 | 55 | """ 56 | Guard test: 57 | not #{arg1} #{pretty_infix} #{arg2} 58 | 59 | can never succeed. 60 | """ 61 | end 62 | 63 | @impl Dialyxir.Warning 64 | @spec explain() :: String.t() 65 | def explain() do 66 | @moduledoc 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/no_return.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.NoReturn do 2 | @moduledoc """ 3 | The function has no return. This is usually due to an issue later on 4 | in the call stack causing it to not be recognized as returning for 5 | some reason. It is often helpful to cross reference the complete 6 | list of warnings with the call stack in the function and fix the 7 | deepest part of the call stack, which will usually fix many of the 8 | other no_return errors. 9 | 10 | ## Example 11 | 12 | defmodule Example do 13 | def ok() do 14 | Enum.each([1, 2, 3], fn _ -> raise "error" end) 15 | end 16 | end 17 | 18 | or 19 | 20 | defmodule Example do 21 | def ok() do 22 | raise "error" 23 | 24 | :ok 25 | end 26 | 27 | def ok(:ok) do 28 | ok() 29 | end 30 | end 31 | """ 32 | 33 | @behaviour Dialyxir.Warning 34 | 35 | @impl Dialyxir.Warning 36 | @spec warning() :: :no_return 37 | def warning(), do: :no_return 38 | 39 | @impl Dialyxir.Warning 40 | @spec format_short([String.t()]) :: String.t() 41 | def format_short(args), do: format_long(args) 42 | 43 | @impl Dialyxir.Warning 44 | @spec format_long([String.t() | atom]) :: String.t() 45 | def format_long([type | name]) do 46 | name_string = 47 | case name do 48 | [] -> 49 | "The created anonymous function" 50 | 51 | [function, arity] -> 52 | "Function #{function}/#{arity}" 53 | end 54 | 55 | type_string = 56 | case type do 57 | :no_match -> 58 | "has no clauses that will ever match." 59 | 60 | :only_explicit -> 61 | "only terminates with explicit exception." 62 | 63 | :only_normal -> 64 | "has no local return." 65 | 66 | :both -> 67 | "has no local return." 68 | end 69 | 70 | "#{name_string} #{type_string}" 71 | end 72 | 73 | @impl Dialyxir.Warning 74 | @spec explain() :: String.t() 75 | def explain() do 76 | @moduledoc 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/opaque_equality.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.OpaqueEquality do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :opaque_eq 6 | def warning(), do: :opaque_eq 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([_type, _op, opaque_type]) do 11 | pretty_opaque_type = opaque_type |> Erlex.pretty_print() |> unqualify_module() 12 | "Attempt to test for equality with an opaque type #{pretty_opaque_type}." 13 | end 14 | 15 | defp unqualify_module(name) when is_binary(name) do 16 | case String.split(name, ".") do 17 | [only] -> 18 | only 19 | 20 | multiple -> 21 | multiple 22 | |> Enum.take(-2) 23 | |> Enum.join(".") 24 | end 25 | end 26 | 27 | @impl Dialyxir.Warning 28 | @spec format_long([String.t()]) :: String.t() 29 | def format_long([type, _op, opaque_type]) do 30 | pretty_opaque_type = Erlex.pretty_print_type(opaque_type) 31 | 32 | "Attempt to test for equality between a term of type #{type} " <> 33 | "and a term of opaque type #{pretty_opaque_type}." 34 | end 35 | 36 | @impl Dialyxir.Warning 37 | @spec explain() :: String.t() 38 | def explain() do 39 | Dialyxir.Warning.default_explain() 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/opaque_guard.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.OpaqueGuard do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :opaque_guard 6 | def warning(), do: :opaque_guard 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([guard | _]) do 11 | "The guard test #{guard} breaks the opaqueness of its argument." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([guard, args]) do 17 | "The guard test #{guard}#{args} breaks the opaqueness of its argument." 18 | end 19 | 20 | @impl Dialyxir.Warning 21 | @spec explain() :: String.t() 22 | def explain() do 23 | Dialyxir.Warning.default_explain() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/opaque_match.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.OpaqueMatch do 2 | @moduledoc """ 3 | Attempted to pattern match against the internal structure of an 4 | opaque term. 5 | 6 | ## Example 7 | 8 | defmodule OpaqueStruct do 9 | defstruct [:opaque] 10 | 11 | @opaque t :: %__MODULE__{} 12 | 13 | @spec opaque() :: t 14 | def opaque() do 15 | %__MODULE__{} 16 | end 17 | end 18 | 19 | defmodule Example do 20 | @spec error() :: :error 21 | def error() do 22 | %{opaque: _} = OpaqueStruct.opaque() 23 | :error 24 | end 25 | end 26 | """ 27 | 28 | @behaviour Dialyxir.Warning 29 | 30 | @impl Dialyxir.Warning 31 | @spec warning() :: :opaque_match 32 | def warning(), do: :opaque_match 33 | 34 | @impl Dialyxir.Warning 35 | @spec format_short([String.t()]) :: String.t() 36 | def format_short([_pattern, type | _]) do 37 | pretty_type = Erlex.pretty_print_type(type) 38 | 39 | "Attempted to pattern match against the internal structure of an opaque term of type #{pretty_type}." 40 | end 41 | 42 | @impl Dialyxir.Warning 43 | @spec format_long([String.t()]) :: String.t() 44 | def format_long([pattern, opaque_type, opaque_term]) do 45 | pretty_opaque_term = Erlex.pretty_print(opaque_term) 46 | 47 | term = 48 | if opaque_type == opaque_term do 49 | "the term" 50 | else 51 | pretty_opaque_term 52 | end 53 | 54 | pretty_pattern = Erlex.pretty_print_pattern(pattern) 55 | 56 | """ 57 | Attempted to pattern match against the internal structure of an opaque term. 58 | 59 | Type: 60 | #{pretty_opaque_term} 61 | 62 | Pattern: 63 | #{pretty_pattern} 64 | 65 | This breaks the opaqueness of #{term}. 66 | """ 67 | end 68 | 69 | @impl Dialyxir.Warning 70 | @spec explain() :: String.t() 71 | def explain() do 72 | @moduledoc 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/opaque_nonequality.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.OpaqueNonequality do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :opaque_neq 6 | def warning(), do: :opaque_neq 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([type, _op, opaque_type]) do 11 | "Attempted to test for inequality between #{type} and opaque type #{opaque_type}." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([type, _op, opaque_type]) do 17 | "Attempt to test for inequality between a term of type #{type} " <> 18 | "and a term of opaque type #{opaque_type}." 19 | end 20 | 21 | @impl Dialyxir.Warning 22 | @spec explain() :: String.t() 23 | def explain() do 24 | Dialyxir.Warning.default_explain() 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/opaque_type_test.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.OpaqueTypeTest do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :opaque_type_test 6 | def warning(), do: :opaque_type_test 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short([function, _opaque]) do 11 | "The type test in #{function} breaks the opaqueness of the term." 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_long([String.t()]) :: String.t() 16 | def format_long([function, opaque]) do 17 | "The type test #{function}(#{opaque}) breaks the opaqueness of the term #{opaque}." 18 | end 19 | 20 | @impl Dialyxir.Warning 21 | @spec explain() :: String.t() 22 | def explain() do 23 | Dialyxir.Warning.default_explain() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/overlapping_contract.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.OverlappingContract do 2 | @moduledoc """ 3 | The function has an additional @spec that is already covered more 4 | generally by a higher @spec. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | @spec ok(atom) :: :ok 10 | def ok(:ok) do 11 | :ok 12 | end 13 | 14 | @spec ok(:error) :: :ok 15 | def ok(:error) do 16 | :ok 17 | end 18 | end 19 | """ 20 | @behaviour Dialyxir.Warning 21 | 22 | @impl Dialyxir.Warning 23 | @spec warning() :: :overlapping_contract 24 | def warning(), do: :overlapping_contract 25 | 26 | @impl Dialyxir.Warning 27 | @spec format_short([String.t()]) :: String.t() 28 | def format_short([_module, function, arity]) do 29 | "The contract for #{function}/#{arity} is overloaded." 30 | end 31 | 32 | @impl Dialyxir.Warning 33 | @spec explain() :: String.t() 34 | def explain() do 35 | @moduledoc 36 | end 37 | 38 | @impl Dialyxir.Warning 39 | @spec format_long([String.t()]) :: String.t() 40 | def format_long([module, function, arity]) do 41 | pretty_module = Erlex.pretty_print(module) 42 | 43 | """ 44 | Overloaded contract for #{pretty_module}.#{function}/#{arity} has 45 | overlapping domains; such contracts are currently unsupported and 46 | are simply ignored. 47 | """ 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/pattern_match.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.PatternMatch do 2 | @moduledoc """ 3 | The pattern matching is never given a value that satisfies all of 4 | its clauses. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | def ok() do 10 | unmatched(:ok) 11 | end 12 | 13 | defp unmatched(:ok), do: :ok 14 | 15 | defp unmatched(:error), do: :error 16 | end 17 | """ 18 | 19 | @behaviour Dialyxir.Warning 20 | 21 | @impl Dialyxir.Warning 22 | @spec warning() :: :pattern_match 23 | def warning(), do: :pattern_match 24 | 25 | @impl Dialyxir.Warning 26 | @spec format_short([String.t()]) :: String.t() 27 | def format_short([_pattern, type]) do 28 | pretty_type = Erlex.pretty_print_type(type) 29 | "The pattern can never match the type #{pretty_type}." 30 | end 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_long([String.t()]) :: String.t() 34 | def format_long([pattern, type]) do 35 | pretty_pattern = Erlex.pretty_print_pattern(pattern) 36 | pretty_type = Erlex.pretty_print_type(type) 37 | 38 | """ 39 | The pattern can never match the type. 40 | 41 | Pattern: 42 | #{pretty_pattern} 43 | 44 | Type: 45 | #{pretty_type} 46 | """ 47 | end 48 | 49 | @impl Dialyxir.Warning 50 | @spec explain() :: String.t() 51 | def explain() do 52 | @moduledoc 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/pattern_match_covered.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.PatternMatchCovered do 2 | @moduledoc """ 3 | The pattern match has a later clause that will never be executed, 4 | because a more general clause is higher in the matching order. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | def ok() do 10 | unmatched(:error) 11 | end 12 | 13 | defp unmatched(_), do: :ok 14 | 15 | defp unmatched(:error), do: :error 16 | end 17 | """ 18 | 19 | @behaviour Dialyxir.Warning 20 | 21 | @impl Dialyxir.Warning 22 | @spec warning() :: :pattern_match_cov 23 | def warning(), do: :pattern_match_cov 24 | 25 | @impl Dialyxir.Warning 26 | @spec format_short([String.t()]) :: String.t() 27 | def format_short([pattern, _]) do 28 | "The pattern #{pattern} can never match the type, " <> 29 | "because it is covered by previous clauses." 30 | end 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_long([String.t()]) :: String.t() 34 | def format_long([pattern, type]) do 35 | pretty_pattern = Erlex.pretty_print_pattern(pattern) 36 | pretty_type = Erlex.pretty_print_type(type) 37 | 38 | """ 39 | The pattern 40 | #{pretty_pattern} 41 | 42 | can never match, because previous clauses completely cover the type 43 | #{pretty_type}. 44 | """ 45 | end 46 | 47 | @impl Dialyxir.Warning 48 | @spec explain() :: String.t() 49 | def explain() do 50 | @moduledoc 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/record_construction.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.RecordConstruction do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :record_constr 6 | def warning(), do: :record_constr 7 | 8 | @impl Dialyxir.Warning 9 | @spec explain() :: String.t() 10 | def explain() do 11 | Dialyxir.Warning.default_explain() 12 | end 13 | 14 | @impl Dialyxir.Warning 15 | @spec format_short([String.t()]) :: String.t() 16 | def format_short([_types, name]) do 17 | "Record construction violates the declared type for #{name}." 18 | end 19 | 20 | def format_short([name, _field, _type]) do 21 | "Record construction violates the declared type for #{name}." 22 | end 23 | 24 | @impl Dialyxir.Warning 25 | @spec format_long([String.t()]) :: String.t() 26 | def format_long([types, name]) do 27 | "Record construction #{types} violates the declared type for ##{name}{}." 28 | end 29 | 30 | def format_long([name, field, type]) do 31 | "Record construction violates the declared type for ##{name}{}, " <> 32 | "because #{field} cannot be of type #{type}." 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/record_match.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.RecordMatch do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :record_match 6 | def warning(), do: :record_match 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | defdelegate format_short(args), to: Dialyxir.Warnings.RecordMatching 11 | 12 | @impl Dialyxir.Warning 13 | defdelegate format_long(args), to: Dialyxir.Warnings.RecordMatching 14 | 15 | @impl Dialyxir.Warning 16 | defdelegate explain(), to: Dialyxir.Warnings.RecordMatching 17 | end 18 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/record_matching.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.RecordMatching do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :record_matching 6 | def warning(), do: :record_matching 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short([String.t()]) :: String.t() 10 | def format_short(args), do: format_long(args) 11 | 12 | @impl Dialyxir.Warning 13 | @spec format_long([String.t()]) :: String.t() 14 | def format_long([string, name]) do 15 | "The #{string} violates the declared type for ##{name}{}." 16 | end 17 | 18 | @impl Dialyxir.Warning 19 | @spec explain() :: String.t() 20 | def explain() do 21 | Dialyxir.Warning.default_explain() 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/unknown_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.UnknownBehaviour do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :unknown_behaviour 6 | def warning(), do: :unknown_behaviour 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short(String.t()) :: String.t() 10 | def format_short(args), do: format_long(args) 11 | 12 | @impl Dialyxir.Warning 13 | @spec format_long(String.t()) :: String.t() 14 | def format_long(behaviour) do 15 | pretty_module = Erlex.pretty_print(behaviour) 16 | 17 | "Unknown behaviour: #{pretty_module}." 18 | end 19 | 20 | @impl Dialyxir.Warning 21 | @spec explain() :: String.t() 22 | def explain() do 23 | Dialyxir.Warning.default_explain() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/unknown_function.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.UnknownFunction do 2 | @behaviour Dialyxir.Warning 3 | 4 | @impl Dialyxir.Warning 5 | @spec warning() :: :unknown_function 6 | def warning(), do: :unknown_function 7 | 8 | @impl Dialyxir.Warning 9 | @spec format_short({String.t(), String.t(), String.t()}) :: String.t() 10 | def format_short({module, function, arity}) do 11 | pretty_module = Erlex.pretty_print(module) 12 | "Function #{pretty_module}.#{function}/#{arity} does not exist." 13 | end 14 | 15 | @impl Dialyxir.Warning 16 | @spec format_long({String.t(), String.t(), String.t()}) :: String.t() 17 | def format_long({module, function, arity}) do 18 | pretty_module = Erlex.pretty_print(module) 19 | "Function #{pretty_module}.#{function}/#{arity} does not exist." 20 | end 21 | 22 | @impl Dialyxir.Warning 23 | @spec explain() :: String.t() 24 | def explain() do 25 | Dialyxir.Warning.default_explain() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/unknown_type.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.UnknownType do 2 | @moduledoc """ 3 | Spec references a missing @type. 4 | 5 | ## Example 6 | 7 | defmodule Missing do 8 | end 9 | 10 | defmodule Example do 11 | @spec ok(Missing.t()) :: :ok 12 | def ok(_) do 13 | :ok 14 | end 15 | end 16 | """ 17 | 18 | @behaviour Dialyxir.Warning 19 | 20 | @impl Dialyxir.Warning 21 | @spec warning() :: :unknown_type 22 | def warning(), do: :unknown_type 23 | 24 | @impl Dialyxir.Warning 25 | @spec format_short({String.t(), String.t(), String.t()}) :: String.t() 26 | def format_short({module, function, arity}) do 27 | pretty_module = Erlex.pretty_print(module) 28 | 29 | "Unknown type: #{pretty_module}.#{function}/#{arity}." 30 | end 31 | 32 | @impl Dialyxir.Warning 33 | @spec format_long({String.t(), String.t(), String.t()}) :: String.t() 34 | def format_long({module, function, arity}) do 35 | format_short({module, function, arity}) 36 | end 37 | 38 | @impl Dialyxir.Warning 39 | @spec explain() :: String.t() 40 | def explain() do 41 | @moduledoc 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/unmatched_return.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.UnmatchedReturn do 2 | @moduledoc """ 3 | The invoked expression returns a union of types and the call does 4 | not match on its return types using e.g. a case or wildcard. 5 | 6 | ## Example 7 | 8 | defmodule Example do 9 | require Integer 10 | 11 | def ok() do 12 | n = :rand.uniform(100) 13 | 14 | multiple_returns(n) 15 | 16 | :ok 17 | end 18 | 19 | defp multiple_returns(n) do 20 | if Integer.is_even(n) do 21 | :ok 22 | else 23 | {:error, "error"} 24 | end 25 | end 26 | end 27 | 28 | This would NOT result in a warning: 29 | 30 | defmodule Example do 31 | require Integer 32 | 33 | def ok() do 34 | n = :rand.uniform(100) 35 | 36 | multiple_returns(n) 37 | 38 | :ok 39 | end 40 | 41 | defp multiple_returns(n) do 42 | if Integer.is_even(n) do 43 | :ok 44 | else 45 | :error 46 | end 47 | end 48 | end 49 | """ 50 | @behaviour Dialyxir.Warning 51 | 52 | @impl Dialyxir.Warning 53 | @spec warning() :: :unmatched_return 54 | def warning(), do: :unmatched_return 55 | 56 | @impl Dialyxir.Warning 57 | @spec format_short([String.t()]) :: String.t() 58 | def format_short(_) do 59 | "The expression produces multiple types, but none are matched." 60 | end 61 | 62 | @impl Dialyxir.Warning 63 | @spec format_long([String.t()]) :: String.t() 64 | def format_long([type]) do 65 | pretty_type = Erlex.pretty_print_type(type) 66 | 67 | """ 68 | The expression produces a value of type: 69 | 70 | #{pretty_type} 71 | 72 | but this value is unmatched. 73 | """ 74 | end 75 | 76 | @impl Dialyxir.Warning 77 | @spec explain() :: String.t() 78 | def explain() do 79 | @moduledoc 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/dialyxir/warnings/unused_function.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Warnings.UnusedFunction do 2 | @moduledoc """ 3 | Due to issues higher in the function or call stack, while the 4 | function is recognized as used by the compiler, it will never be 5 | recognized as having been called until the other error is resolved. 6 | 7 | ## Example 8 | 9 | defmodule Example do 10 | def ok() do 11 | raise "error" 12 | 13 | unused() 14 | end 15 | 16 | defp unused(), do: :ok 17 | end 18 | """ 19 | 20 | @behaviour Dialyxir.Warning 21 | 22 | @impl Dialyxir.Warning 23 | @spec warning() :: :unused_fun 24 | def warning(), do: :unused_fun 25 | 26 | @impl Dialyxir.Warning 27 | @spec format_short([String.t()]) :: String.t() 28 | def format_short(args), do: format_long(args) 29 | 30 | @impl Dialyxir.Warning 31 | @spec format_long([String.t()]) :: String.t() 32 | def format_long([function, arity]) do 33 | "Function #{function}/#{arity} will never be called." 34 | end 35 | 36 | @impl Dialyxir.Warning 37 | @spec explain() :: String.t() 38 | def explain() do 39 | @moduledoc 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/mix/tasks/dialyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Dialyzer do 2 | @shortdoc "Runs dialyzer with default or project-defined flags." 3 | 4 | @moduledoc """ 5 | This task compiles the mix project, creates a PLT with dependencies if needed and runs `dialyzer`. Much of its behavior can be managed in configuration as described below. 6 | 7 | If executed outside of a mix project, it will build the core PLT files and exit. 8 | 9 | ## Command line options 10 | 11 | * `--no-compile` - do not compile even if needed 12 | * `--no-check` - do not perform (quick) check to see if PLT needs update 13 | * `--force-check` - force PLT check also if lock file is unchanged useful 14 | when dealing with local deps. 15 | * `--ignore-exit-status` - display warnings but do not halt the VM or 16 | return an exit status code 17 | * `--list-unused-filters` - list unused ignore filters useful for CI. do 18 | not use with `mix do`. 19 | * `--plt` - only build the required PLT(s) and exit 20 | * `--format ` - Specify the format for the warnings, can be specified multiple times to print warnings multiple times in different output formats. Defaults to `dialyxir`. 21 | * `--format short` - format the warnings in a compact format, suitable for ignore file using Elixir term format. 22 | * `--format raw` - format the warnings in format returned before Dialyzer formatting 23 | * `--format dialyxir` - format the warnings in a pretty printed format (default) 24 | * `--format dialyzer` - format the warnings in the original Dialyzer format 25 | * `--format github` - format the warnings in the Github Actions message format 26 | * `--format ignore_file` - format the warnings in {file, warning} format for Elixir Format ignore file 27 | * `--format ignore_file_strict` - format the warnings in {file, short_description} format for Elixir Format ignore file. 28 | * `--quiet` - suppress all informational messages 29 | * `--quiet-with-result` - suppress all informational messages except for the final result message 30 | 31 | Warning flags passed to this task are passed on to `:dialyzer` - e.g. 32 | 33 | mix dialyzer --unmatched_returns 34 | 35 | ## Configuration 36 | 37 | All configuration is included under a dialyzer key in the mix project keyword list. 38 | 39 | ### Flags 40 | 41 | You can specify any `dialyzer` command line argument with the :flags keyword. 42 | 43 | Dialyzer supports a number of warning flags used to enable or disable certain kinds of analysis features. Until version 0.4, `dialyxir` used by default the additional warning flags shown in the example below. However some of these create warnings that are often more confusing than helpful, particularly to new users of Dialyzer. As of 0.4, there are no longer any flags used by default. To get the old behavior, specify them in your Mix project file. For compatibility reasons you can use either the `-Wwarning` convention of the dialyzer CLI, or (preferred) the `WarnOpts` atoms supported by the [API](http://erlang.org/doc/man/dialyzer.html#gui-1). e.g. 44 | 45 | ```elixir 46 | def project do 47 | [ 48 | app: :my_app, 49 | version: "0.0.1", 50 | deps: deps, 51 | dialyzer: [flags: ["-Wunmatched_returns", :error_handling, :underspecs]] 52 | ] 53 | end 54 | ``` 55 | 56 | ### PLT Configuration 57 | 58 | The task will build a PLT with default core Erlang applications: `:erts :kernel :stdlib :crypto` and re-use this core file in multiple projects - another core file is created for Elixir. 59 | 60 | OTP application dependencies are (transitively) added to your project's PLT by default. The applications added are the same as you would see displayed with the command `mix app.tree`. There is also a `:plt_add_deps` option you can set to control the dependencies added. The following options are supported: 61 | 62 | * `:apps_direct` - Only Direct OTP runtime application dependencies - not the entire tree 63 | * `:app_tree` - Transitive OTP runtime application dependencies e.g. `mix app.tree` (default) 64 | 65 | ``` 66 | def project do 67 | [ 68 | app: :my_app, 69 | version: "0.0.1", 70 | deps: deps, 71 | dialyzer: [plt_add_deps: :apps_direct, plt_add_apps: [:wx]] 72 | ] 73 | end 74 | ``` 75 | 76 | You can also configure applications to include in the PLT more directly: 77 | 78 | * `dialyzer: :plt_add_apps` - applications to include 79 | *in addition* to the core applications and project dependencies. 80 | 81 | * `dialyzer: :plt_ignore_apps` - applications to ignore from the list of core 82 | applications and dependencies. 83 | 84 | * `dialyzer: :plt_apps` - a list of applications to include that will replace the default, 85 | include all the apps you need e.g. 86 | 87 | ### Other Configuration 88 | 89 | * `dialyzer: :plt_file` - Deprecated - specify the PLT file name to create and use - default is to create one in the project's current build environment (e.g. _build/dev/) specific to the Erlang/Elixir version used. Note that use of this key in version 0.4 or later will produce a deprecation warning - you can silence the warning by providing a pair with key :no_warn e.g. `plt_file: {:no_warn,"filename"}`. 90 | 91 | * `dialyzer: :plt_local_path` - specify the PLT directory name to create and use - default is the project's current build environment (e.g. `_build/dev/`). 92 | 93 | * `dialyzer: :plt_core_path` - specify an alternative to `MIX_HOME` to use to store the Erlang and Elixir core files. 94 | 95 | * `dialyzer: :ignore_warnings` - specify file path to filter well-known warnings. 96 | """ 97 | 98 | use Mix.Task 99 | import System, only: [user_home!: 0] 100 | import Dialyxir.Output 101 | alias Dialyxir.Project 102 | alias Dialyxir.Plt 103 | alias Dialyxir.Dialyzer 104 | 105 | defmodule Build do 106 | @shortdoc "Build the required PLT(s) and exit." 107 | 108 | @moduledoc """ 109 | This task compiles the mix project and creates a PLT with dependencies if needed. 110 | It is equivalent to running `mix dialyzer --plt` 111 | 112 | ## Command line options 113 | 114 | * `--no-compile` - do not compile even if needed. 115 | """ 116 | use Mix.Task 117 | 118 | def run(args) do 119 | Mix.Tasks.Dialyzer.run(["--plt" | args]) 120 | end 121 | end 122 | 123 | defmodule Clean do 124 | @shortdoc "Delete PLT(s) and exit." 125 | 126 | @moduledoc """ 127 | This task deletes PLT files and hash files. 128 | 129 | ## Command line options 130 | 131 | * `--all` - delete also core PLTs. 132 | """ 133 | use Mix.Task 134 | 135 | @command_options [all: :boolean] 136 | def run(args) do 137 | {opts, _, _dargs} = OptionParser.parse(args, strict: @command_options) 138 | Mix.Tasks.Dialyzer.clean(opts) 139 | end 140 | end 141 | 142 | @default_warnings [:unknown] 143 | 144 | @old_options [ 145 | halt_exit_status: :boolean 146 | ] 147 | 148 | @command_options Keyword.merge(@old_options, 149 | force_check: :boolean, 150 | ignore_exit_status: :boolean, 151 | list_unused_filters: :boolean, 152 | no_check: :boolean, 153 | no_compile: :boolean, 154 | plt: :boolean, 155 | quiet: :boolean, 156 | quiet_with_result: :boolean, 157 | raw: :boolean, 158 | format: [:string, :keep] 159 | ) 160 | 161 | def run(args) do 162 | {opts, _, dargs} = OptionParser.parse(args, strict: @command_options) 163 | original_shell = Mix.shell() 164 | if opts[:quiet] || opts[:quiet_with_result], do: Mix.shell(Mix.Shell.Quiet) 165 | opts = Keyword.delete(opts, :quiet) 166 | check_dialyzer() 167 | compatibility_notice() 168 | 169 | if Mix.Project.get() do 170 | Project.check_config() 171 | 172 | unless opts[:no_compile], do: Mix.Task.run("compile") 173 | 174 | _ = 175 | unless no_check?(opts) do 176 | info("Finding suitable PLTs") 177 | force_check? = Keyword.get(opts, :force_check, false) 178 | check_plt(force_check?) 179 | end 180 | 181 | default = Dialyxir.Project.default_ignore_warnings() 182 | ignore_warnings = Dialyxir.Project.dialyzer_ignore_warnings() 183 | 184 | cond do 185 | !ignore_warnings && File.exists?(default) -> 186 | info(""" 187 | No :ignore_warnings opt specified in mix.exs. Using default: #{default}. 188 | """) 189 | 190 | ignore_warnings && File.exists?(ignore_warnings) && 191 | match?(%{size: size} when size == 0, File.stat!(ignore_warnings)) -> 192 | info(""" 193 | :ignore_warnings opt specified in mix.exs: #{ignore_warnings}, but file is empty. 194 | """) 195 | 196 | ignore_warnings && File.exists?(ignore_warnings) -> 197 | info(""" 198 | ignore_warnings: #{ignore_warnings} 199 | """) 200 | 201 | ignore_warnings -> 202 | info(""" 203 | :ignore_warnings opt specified in mix.exs: #{ignore_warnings}, but file does not exist. 204 | """) 205 | 206 | true -> 207 | info(""" 208 | No :ignore_warnings opt specified in mix.exs and default does not exist. 209 | """) 210 | end 211 | 212 | warn_old_options(opts) 213 | unless opts[:plt], do: run_dialyzer(opts, dargs) 214 | else 215 | info("No mix project found - checking core PLTs...") 216 | Project.plts_list([], false) |> Plt.check() 217 | end 218 | 219 | Mix.shell(original_shell) 220 | end 221 | 222 | def clean(opts, fun \\ &delete_plt/4) do 223 | check_dialyzer() 224 | compatibility_notice() 225 | if opts[:all], do: Project.plts_list([], false) |> Plt.check(fun) 226 | 227 | if Mix.Project.get() do 228 | {apps, _hash} = dependency_hash() 229 | info("Deleting PLTs") 230 | Project.plts_list(apps, true, true) |> Plt.check(fun) 231 | info("About to delete PLT hash file: #{plt_hash_file()}") 232 | File.rm(plt_hash_file()) 233 | end 234 | end 235 | 236 | def delete_plt(plt, _, _, _) do 237 | info("About to delete PLT file: #{plt}") 238 | File.rm(plt) 239 | end 240 | 241 | defp no_check?(opts) do 242 | case {in_child?(), no_plt?()} do 243 | {true, true} -> 244 | info("In an Umbrella child and no PLT found - building that first.") 245 | build_parent_plt() 246 | true 247 | 248 | {true, false} -> 249 | info("In an Umbrella child, not checking PLT...") 250 | true 251 | 252 | _ -> 253 | opts[:no_check] 254 | end 255 | end 256 | 257 | defp check_plt(force_check?) do 258 | info("Checking PLT...") 259 | {apps, hash} = dependency_hash() 260 | 261 | if not force_check? and check_hash?(hash) do 262 | info("PLT is up to date!") 263 | else 264 | Project.plts_list(apps) |> Plt.check() 265 | File.write(plt_hash_file(), hash) 266 | end 267 | end 268 | 269 | defp run_dialyzer(opts, dargs) do 270 | args = [ 271 | {:check_plt, opts[:force_check] || false}, 272 | {:init_plt, String.to_charlist(Project.plt_file())}, 273 | {:files, Project.dialyzer_files()}, 274 | {:warnings, dialyzer_warnings(dargs)}, 275 | {:format, Keyword.get_values(opts, :format)}, 276 | {:raw, opts[:raw]}, 277 | {:list_unused_filters, opts[:list_unused_filters]}, 278 | {:ignore_exit_status, opts[:ignore_exit_status]}, 279 | {:quiet_with_result, opts[:quiet_with_result]} 280 | ] 281 | 282 | {status, exit_status, [time | result]} = Dialyzer.dialyze(args) 283 | info(time) 284 | 285 | quiet_with_result? = opts[:quiet_with_result] 286 | 287 | report = 288 | cond do 289 | status == :ok && quiet_with_result? -> 290 | fn text -> 291 | Mix.shell(Mix.Shell.IO) 292 | info(text) 293 | Mix.shell(Mix.Shell.Quiet) 294 | end 295 | 296 | status == :ok -> 297 | &info/1 298 | 299 | true -> 300 | &error/1 301 | end 302 | 303 | Enum.each(result, report) 304 | 305 | unless exit_status == 0 || opts[:ignore_exit_status] do 306 | error("Halting VM with exit status #{exit_status}") 307 | System.halt(exit_status) 308 | end 309 | end 310 | 311 | defp dialyzer_warnings(dargs) do 312 | raw_opts = Project.dialyzer_flags() ++ Enum.map(dargs, &elem(&1, 0)) 313 | transform(raw_opts) ++ (@default_warnings -- Project.dialyzer_removed_defaults()) 314 | end 315 | 316 | defp transform(options) when is_list(options), do: Enum.map(options, &transform/1) 317 | defp transform(option) when is_atom(option), do: option 318 | 319 | defp transform(option) when is_binary(option) do 320 | option 321 | |> String.replace_leading("-W", "") 322 | |> String.replace("--", "") 323 | |> String.to_atom() 324 | end 325 | 326 | defp in_child? do 327 | case Project.no_umbrella?() do 328 | true -> false 329 | false -> String.contains?(Mix.Project.config()[:lockfile], "..") 330 | end 331 | end 332 | 333 | defp no_plt? do 334 | not File.exists?(Project.deps_plt()) 335 | end 336 | 337 | defp build_parent_plt() do 338 | parent = Mix.Project.config()[:lockfile] |> Path.expand() |> Path.dirname() 339 | opts = [into: IO.stream(:stdio, :line), stderr_to_stdout: true, cd: parent] 340 | # It would seem more natural to use Mix.in_project here to start in our parent project. 341 | # However part of the app.tree resolution includes loading all sub apps, and we will 342 | # hit an exception when we try to do that for *this* child, which is already loaded. 343 | {out, rc} = System.cmd("mix", ["dialyzer", "--plt"], opts) 344 | 345 | unless rc == 0 do 346 | info("Error building parent PLT, process returned code: #{rc}\n#{out}") 347 | end 348 | end 349 | 350 | if Version.match?(System.version(), ">= 1.15.0") do 351 | defp check_dialyzer do 352 | Mix.ensure_application!(:dialyzer) 353 | end 354 | else 355 | defp check_dialyzer do 356 | if not Code.ensure_loaded?(:dialyzer) do 357 | error(""" 358 | DEPENDENCY MISSING 359 | ------------------------ 360 | If you are reading this message, then Elixir and Erlang are installed but the 361 | Erlang Dialyzer is not available. Probably this is because you installed Erlang 362 | with your OS package manager and the Dialyzer package is separate. 363 | 364 | On Debian/Ubuntu: 365 | 366 | `apt-get install erlang-dialyzer` 367 | 368 | Fedora: 369 | 370 | `yum install erlang-dialyzer` 371 | 372 | Arch and Homebrew include Dialyzer in their base erlang packages. Please report a Github 373 | issue to add or correct distribution-specific information. 374 | """) 375 | 376 | :erlang.halt(3) 377 | end 378 | end 379 | end 380 | 381 | defp warn_old_options(opts) do 382 | for {opt, _} <- opts, @old_options[opt] do 383 | error("#{opt} is no longer a valid CLI argument.") 384 | end 385 | 386 | nil 387 | end 388 | 389 | defp compatibility_notice do 390 | old_plt = "#{user_home!()}/.dialyxir_core_*.plt" 391 | 392 | if File.exists?(old_plt) && 393 | (!File.exists?(Project.erlang_plt()) || !File.exists?(Project.elixir_plt())) do 394 | info(""" 395 | COMPATIBILITY NOTICE 396 | ------------------------ 397 | Previous usage of a pre-0.4 version of Dialyxir detected. Please be aware that the 0.4 release 398 | makes a number of changes to previous defaults. Among other things, the PLT task is automatically 399 | run when dialyzer is run, PLT paths have changed, 400 | transitive dependencies are included by default in the PLT, and no additional warning flags 401 | beyond the dialyzer defaults are included. All these properties can be changed in configuration. 402 | (see `mix help dialyzer`). 403 | 404 | If you no longer use the older Dialyxir in any projects and do not want to see this notice each time you upgrade your Erlang/Elixir distribution, you can delete your old pre-0.4 PLT files. (`rm ~/.dialyxir_core_*.plt`) 405 | """) 406 | end 407 | end 408 | 409 | @spec check_hash?(binary()) :: boolean() 410 | defp check_hash?(hash) do 411 | case File.read(plt_hash_file()) do 412 | {:ok, stored_hash} -> hash == stored_hash 413 | _ -> false 414 | end 415 | end 416 | 417 | defp plt_hash_file, do: Project.plt_file() <> ".hash" 418 | 419 | @spec dependency_hash :: {[atom()], binary()} 420 | def dependency_hash do 421 | apps = Project.cons_apps() 422 | apps |> inspect() |> info() 423 | hash = :crypto.hash(:sha, lock_file() <> :erlang.term_to_binary(apps)) 424 | {apps, hash} 425 | end 426 | 427 | defp lock_file() do 428 | lockfile = Mix.Project.config()[:lockfile] 429 | read_res = File.read(lockfile) 430 | 431 | case read_res do 432 | {:ok, data} -> 433 | data 434 | 435 | {:error, :enoent} -> 436 | # If there is no lock file, an empty bitstring will do to indicate there is none there 437 | <<>> 438 | 439 | {:error, reason} -> 440 | raise File.Error, 441 | reason: reason, 442 | action: "read file", 443 | path: lockfile 444 | end 445 | end 446 | end 447 | -------------------------------------------------------------------------------- /lib/mix/tasks/dialyzer/explain.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Dialyzer.Explain do 2 | @shortdoc "Display information about Dialyzer warnings." 3 | 4 | @moduledoc """ 5 | This task provides background information about Dialyzer warnings. 6 | If invoked without any arguments it will list all warning atoms. 7 | When invoked with the name of a particular warning, it will display 8 | information regarding it. 9 | 10 | ## Command line options 11 | 12 | * `[warning]` - display information regarding warning 13 | 14 | ``` 15 | mix dialyzer.explain pattern_match 16 | ``` 17 | """ 18 | use Mix.Task 19 | alias Dialyxir.Output 20 | 21 | def run(args) do 22 | case OptionParser.parse(args, strict: []) do 23 | {_, [warning], _} -> 24 | warning |> explanation_text() |> Output.info() 25 | 26 | {_, [], _} -> 27 | list_warnings() |> Output.info() 28 | 29 | _ -> 30 | Mix.Task.run("help", ["dialyzer.explain"]) 31 | end 32 | end 33 | 34 | defp explanation_text(warning_name) do 35 | warning = String.to_atom(warning_name) 36 | 37 | case Map.get(Dialyxir.Warnings.warnings(), warning) do 38 | nil -> 39 | "Unknown warning named: #{warning_name}" 40 | 41 | module -> 42 | module.explain() 43 | end 44 | end 45 | 46 | defp list_warnings do 47 | warnings = 48 | Dialyxir.Warnings.warnings() 49 | |> Map.keys() 50 | |> Enum.sort() 51 | |> Enum.map_join("\n", &Atom.to_string/1) 52 | 53 | [ 54 | """ 55 | Explain warning with mix dialyzer.explain 56 | 57 | #{warnings} 58 | """ 59 | ] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/jeremyjh/dialyxir" 5 | @version "1.4.5" 6 | 7 | def project do 8 | [ 9 | app: :dialyxir, 10 | version: @version, 11 | elixir: ">= 1.6.0", 12 | elixirc_paths: elixirc_paths(Mix.env()), 13 | description: description(), 14 | package: package(), 15 | deps: deps(), 16 | aliases: [test: "test --no-start"], 17 | dialyzer: [ 18 | plt_apps: [:dialyzer, :elixir, :kernel, :mix, :stdlib, :erlex, :logger], 19 | ignore_warnings: ".dialyzer_ignore.exs", 20 | flags: [:unmatched_returns, :error_handling, :underspecs] 21 | ], 22 | # Docs 23 | name: "Dialyxir", 24 | homepage_url: @source_url, 25 | # The main page in the docs 26 | docs: [ 27 | main: "readme", 28 | source_url: @source_url, 29 | source_ref: @version, 30 | extras: ["CHANGELOG.md", "README.md"] 31 | ] 32 | ] 33 | end 34 | 35 | def application do 36 | [ 37 | mod: {Dialyxir, []}, 38 | extra_applications: [:dialyzer, :crypto, :mix, :erts, :syntax_tools, :logger] 39 | ] 40 | end 41 | 42 | defp description do 43 | """ 44 | Mix tasks to simplify use of Dialyzer in Elixir projects. 45 | """ 46 | end 47 | 48 | defp elixirc_paths(:examples), do: ["lib", "test/examples"] 49 | defp elixirc_paths(_), do: ["lib"] 50 | 51 | defp deps do 52 | [ 53 | {:erlex, ">= 0.2.7"}, 54 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 55 | ] 56 | end 57 | 58 | defp package do 59 | [ 60 | files: [ 61 | "lib", 62 | "mix.exs", 63 | "README.md", 64 | "LICENSE" 65 | ], 66 | maintainers: ["Jeremy Huffman"], 67 | licenses: ["Apache-2.0"], 68 | links: %{ 69 | "Changelog" => "https://hexdocs.pm/dialyxir/changelog.html", 70 | "GitHub" => @source_url 71 | } 72 | ] 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, 4 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 5 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 6 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 7 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.0", "74bb8348c9b3a51d5c589bf5aebb0466a84b33274150e3b6ece1da45584afc82", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49159b7d7d999e836bedaf09dcf35ca18b312230cf901b725a64f3f42e407983"}, 8 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, 9 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 10 | } 11 | -------------------------------------------------------------------------------- /test/dialyxir/dialyzer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.DialyzerTest do 2 | use ExUnit.Case 3 | alias Dialyxir.Dialyzer 4 | 5 | defmodule StubSuccess do 6 | def run(_, _), do: {:ok, {"time", [], ""}} 7 | end 8 | 9 | defmodule StubWarn do 10 | def run(_, _), do: {:ok, {"time", ["warning 1", "warning 2"], ""}} 11 | end 12 | 13 | defmodule StubError do 14 | def run(_, _), do: {:error, "dialyzer failed"} 15 | end 16 | 17 | setup do 18 | ansi_enabled = IO.ANSI.enabled?() 19 | 20 | on_exit(fn -> 21 | Application.put_env(:elixir, :ansi_enabled, ansi_enabled) 22 | end) 23 | end 24 | 25 | describe "dialyze/3" do 26 | import Dialyzer, only: [dialyze: 3] 27 | 28 | test "formatting with no warnings, colors enabled" do 29 | Application.put_env(:elixir, :ansi_enabled, true) 30 | 31 | {expected_result_code, expected_exit_code, expected_messages} = 32 | dialyze(nil, StubSuccess, nil) 33 | 34 | assert expected_result_code == :ok 35 | assert expected_exit_code == 0 36 | 37 | assert expected_messages == [ 38 | "time", 39 | "", 40 | [[[[] | "\e[32m"], "done (passed successfully)"] | "\e[0m"] 41 | ] 42 | end 43 | 44 | test "formatting with no warnings, colors disabled" do 45 | Application.put_env(:elixir, :ansi_enabled, false) 46 | 47 | {expected_result_code, expected_exit_code, expected_messages} = 48 | dialyze(nil, StubSuccess, nil) 49 | 50 | assert expected_result_code == :ok 51 | assert expected_exit_code == 0 52 | assert expected_messages == ["time", "", [[], "done (passed successfully)"]] 53 | end 54 | 55 | test "formatting with warnings, colors enabled" do 56 | Application.put_env(:elixir, :ansi_enabled, true) 57 | 58 | {expected_result_code, expected_exit_code, expected_messages} = dialyze(nil, StubWarn, nil) 59 | 60 | assert expected_result_code == :warn 61 | assert expected_exit_code == 2 62 | 63 | assert expected_messages == [ 64 | "time", 65 | [[[[] | "\e[31m"], "warning 1"] | "\e[0m"], 66 | [[[[] | "\e[31m"], "warning 2"] | "\e[0m"], 67 | "", 68 | [[[[] | "\e[33m"], "done (warnings were emitted)"] | "\e[0m"] 69 | ] 70 | end 71 | 72 | test "formatting with warnings, colors disabled" do 73 | Application.put_env(:elixir, :ansi_enabled, false) 74 | 75 | {expected_result_code, expected_exit_code, expected_messages} = dialyze(nil, StubWarn, nil) 76 | 77 | assert expected_result_code == :warn 78 | assert expected_exit_code == 2 79 | 80 | assert expected_messages == [ 81 | "time", 82 | [[], "warning 1"], 83 | [[], "warning 2"], 84 | "", 85 | [[], "done (warnings were emitted)"] 86 | ] 87 | end 88 | 89 | test "formatting with errors, colors enabled" do 90 | Application.put_env(:elixir, :ansi_enabled, true) 91 | 92 | {expected_result_code, expected_exit_code, expected_messages} = dialyze(nil, StubError, nil) 93 | 94 | assert expected_result_code == :error 95 | assert expected_exit_code == 1 96 | assert expected_messages == [[[[[] | "\e[31m"], "dialyzer failed"] | "\e[0m"]] 97 | end 98 | 99 | test "formatting with errors, colors disabled" do 100 | Application.put_env(:elixir, :ansi_enabled, false) 101 | 102 | {expected_result_code, expected_exit_code, expected_messages} = dialyze(nil, StubError, nil) 103 | 104 | assert expected_result_code == :error 105 | assert expected_exit_code == 1 106 | assert expected_messages == [[[], "dialyzer failed"]] 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /test/dialyxir/formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.FormatterTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureIO, only: [capture_io: 1] 5 | 6 | alias Dialyxir.Formatter 7 | alias Dialyxir.Formatter.Dialyxir, as: DialyxirFormatter 8 | alias Dialyxir.Formatter.Dialyzer, as: DialyzerFormatter 9 | alias Dialyxir.Formatter.Github, as: GithubFormatter 10 | alias Dialyxir.Formatter.Short, as: ShortFormatter 11 | alias Dialyxir.Formatter.IgnoreFileStrict, as: IgnoreFileStrictFormatter 12 | alias Dialyxir.Project 13 | 14 | defp in_project(app, f) when is_atom(app) do 15 | Mix.Project.in_project(app, "test/fixtures/#{Atom.to_string(app)}", fn _ -> f.() end) 16 | end 17 | 18 | describe "formats dialyzer warning" do 19 | if System.otp_release() >= "24" do 20 | for {formatter, message} <- %{ 21 | Formatter.Dialyxir => 22 | "lib/file/warning_type/line.ex:19:4:no_return\nFunction format_long/1 has no local return.", 23 | Formatter.Dialyzer => 24 | "lib/file/warning_type/line.ex:19:4: Function format_long/1 has no local return", 25 | Formatter.Github => 26 | "::warning file=lib/file/warning_type/line.ex,line=19,col=4,title=no_return::Function format_long/1 has no local return.", 27 | Formatter.IgnoreFileStrict => 28 | ~s|{"lib/file/warning_type/line.ex", "Function format_long/1 has no local return."},|, 29 | Formatter.IgnoreFile => ~s|{"lib/file/warning_type/line.ex", :no_return},|, 30 | # TODO: Remove if once only Elixir ~> 1.15 is supported 31 | Formatter.Raw => 32 | if Version.match?(System.version(), "<= 1.15.0") do 33 | ~s|{:warn_return_no_exit, {'lib/file/warning_type/line.ex', {19, 4}}, {:no_return, [:only_normal, :format_long, 1]}}| 34 | else 35 | ~s|{:warn_return_no_exit, {~c"lib/file/warning_type/line.ex", {19, 4}}, {:no_return, [:only_normal, :format_long, 1]}}| 36 | end, 37 | Formatter.Short => 38 | "lib/file/warning_type/line.ex:19:4:no_return Function format_long/1 has no local return." 39 | } do 40 | test "file location including column for #{formatter} formatter" do 41 | assert {:warn, [message], _unused_filters} = 42 | Formatter.format_and_filter( 43 | [ 44 | {:warn_return_no_exit, {~c"lib/file/warning_type/line.ex", {19, 4}}, 45 | {:no_return, [:only_normal, :format_long, 1]}} 46 | ], 47 | Project, 48 | [], 49 | [unquote(formatter)] 50 | ) 51 | 52 | assert message =~ unquote(message) 53 | end 54 | end 55 | end 56 | end 57 | 58 | describe "exs ignore" do 59 | test "evaluates an ignore file and ignores warnings matching the pattern" do 60 | warnings = [ 61 | {:warn_return_no_exit, {~c"lib/short_description.ex", 17}, 62 | {:no_return, [:only_normal, :format_long, 1]}}, 63 | {:warn_return_no_exit, {~c"lib/file/warning_type.ex", 18}, 64 | {:no_return, [:only_normal, :format_long, 1]}}, 65 | {:warn_return_no_exit, {~c"lib/file/warning_type/line.ex", 19}, 66 | {:no_return, [:only_normal, :format_long, 1]}} 67 | ] 68 | 69 | in_project(:ignore, fn -> 70 | {:error, remaining, _unused_filters_present} = 71 | Formatter.format_and_filter(warnings, Project, [], [ShortFormatter]) 72 | 73 | assert remaining == [] 74 | end) 75 | end 76 | 77 | test "evaluates an ignore file of the form {file, short_description} and ignores warnings matching the pattern" do 78 | warnings = [ 79 | {:warn_return_no_exit, {~c"lib/poorly_written_code.ex", 10}, 80 | {:no_return, [:only_normal, :do_a_thing, 1]}}, 81 | {:warn_return_no_exit, {~c"lib/poorly_written_code.ex", 20}, 82 | {:no_return, [:only_normal, :do_something_else, 2]}}, 83 | {:warn_return_no_exit, {~c"lib/poorly_written_code.ex", 30}, 84 | {:no_return, [:only_normal, :do_many_things, 3]}} 85 | ] 86 | 87 | in_project(:ignore_strict, fn -> 88 | {:ok, remaining, :no_unused_filters} = 89 | Formatter.format_and_filter(warnings, Project, [], [IgnoreFileStrictFormatter]) 90 | 91 | assert remaining == [] 92 | end) 93 | end 94 | 95 | test "does not filter lines not matching the pattern" do 96 | warning = 97 | {:warn_return_no_exit, {~c"a/different_file.ex", 17}, 98 | {:no_return, [:only_normal, :format_long, 1]}} 99 | 100 | in_project(:ignore, fn -> 101 | {:error, [remaining], _} = 102 | Formatter.format_and_filter([warning], Project, [], [ShortFormatter]) 103 | 104 | assert remaining =~ ~r/different_file.* no local return/ 105 | end) 106 | end 107 | 108 | test "can filter by regex" do 109 | warning = 110 | {:warn_return_no_exit, {~c"a/regex_file.ex", 17}, 111 | {:no_return, [:only_normal, :format_long, 1]}} 112 | 113 | in_project(:ignore, fn -> 114 | {:error, remaining, _unused_filters_present} = 115 | Formatter.format_and_filter([warning], Project, [], [ShortFormatter]) 116 | 117 | assert remaining == [] 118 | end) 119 | end 120 | 121 | test "lists unnecessary skips as warnings if ignoring exit status" do 122 | warning = 123 | {:warn_return_no_exit, {~c"a/regex_file.ex", 17}, 124 | {:no_return, [:only_normal, :format_long, 1]}} 125 | 126 | filter_args = [{:ignore_exit_status, true}] 127 | 128 | in_project(:ignore, fn -> 129 | assert {:warn, [], {:unused_filters_present, warning}} = 130 | Formatter.format_and_filter([warning], Project, filter_args, [:dialyxir]) 131 | 132 | assert warning =~ "Unused filters:" 133 | end) 134 | end 135 | 136 | test "error on unnecessary skips without ignore_exit_status" do 137 | warning = 138 | {:warn_return_no_exit, {~c"a/regex_file.ex", 17}, 139 | {:no_return, [:only_normal, :format_long, 1]}} 140 | 141 | filter_args = [{:ignore_exit_status, false}] 142 | 143 | in_project(:ignore, fn -> 144 | {:error, [], {:unused_filters_present, error}} = 145 | Formatter.format_and_filter([warning], Project, filter_args, [:dialyxir]) 146 | 147 | assert error =~ "Unused filters:" 148 | end) 149 | end 150 | 151 | test "overwrite ':list_unused_filters_present'" do 152 | warning = 153 | {:warn_return_no_exit, {~c"a/regex_file.ex", 17}, 154 | {:no_return, [:only_normal, :format_long, 1]}} 155 | 156 | filter_args = [{:list_unused_filters, false}] 157 | 158 | in_project(:ignore, fn -> 159 | assert {:warn, [], {:unused_filters_present, warning}} = 160 | Formatter.format_and_filter([warning], Project, filter_args, [:dialyxir]) 161 | 162 | refute warning =~ "Unused filters:" 163 | end) 164 | end 165 | end 166 | 167 | describe "simple string ignore" do 168 | test "evaluates an ignore file and ignores warnings matching the pattern" do 169 | warning = 170 | {:warn_matching, {~c"a/file.ex", 17}, {:pattern_match, [~c"pattern 'ok'", ~c"'error'"]}} 171 | 172 | in_project(:ignore_string, fn -> 173 | assert Formatter.format_and_filter([warning], Project, [], [:dialyzer]) == 174 | {:ok, [], :no_unused_filters} 175 | end) 176 | end 177 | end 178 | 179 | describe "multiple formatters" do 180 | test "short and github" do 181 | warning = 182 | {:warn_return_no_exit, {~c"a/different_file.ex", 17}, 183 | {:no_return, [:only_normal, :format_long, 1]}} 184 | 185 | in_project(:ignore, fn -> 186 | {:error, [short_formatted, github_formatted], _} = 187 | Formatter.format_and_filter([warning], Project, [], [ShortFormatter, GithubFormatter]) 188 | 189 | assert short_formatted =~ ~r/different_file.* no local return/ 190 | assert github_formatted =~ ~r/^::warning file=a\/different_file\.ex.* no local return/ 191 | end) 192 | end 193 | end 194 | 195 | test "listing unused filter behaves the same for different formats" do 196 | warnings = [ 197 | {:warn_return_no_exit, {~c"a/regex_file.ex", 17}, 198 | {:no_return, [:only_normal, :format_long, 1]}}, 199 | {:warn_return_no_exit, {~c"a/another-file.ex", 18}, {:unknown_type, {:M, :F, :A}}} 200 | ] 201 | 202 | expected_warning = "a/another-file.ex:18" 203 | 204 | expected_unused_filter = ~s(Unused filters: 205 | {"lib/short_description.ex:17:no_return Function format_long/1 has no local return."} 206 | {"lib/file/warning_type.ex", :no_return, 18} 207 | {"lib/file/warning_type/line.ex", :no_return, 19}) 208 | 209 | filter_args = [{:list_unused_filters, true}] 210 | 211 | for format <- [ShortFormatter, DialyxirFormatter, DialyzerFormatter] do 212 | in_project(:ignore, fn -> 213 | capture_io(fn -> 214 | result = Formatter.format_and_filter(warnings, Project, filter_args, [format]) 215 | 216 | assert {:error, [warning], {:unused_filters_present, unused}} = result 217 | assert warning =~ expected_warning 218 | assert unused == expected_unused_filter 219 | # A warning for regex_file.ex was explicitly put into format_and_filter. 220 | refute unused =~ "regex_file.ex" 221 | end) 222 | end) 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /test/dialyxir/output_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.OutputTest do 2 | use ExUnit.Case 3 | alias Dialyxir.Output 4 | 5 | setup do 6 | ansi_enabled = IO.ANSI.enabled?() 7 | 8 | on_exit(fn -> 9 | Application.put_env(:elixir, :ansi_enabled, ansi_enabled) 10 | end) 11 | end 12 | 13 | describe "color/2" do 14 | test "inserts ANSI escape codes when they are enabled" do 15 | Application.put_env(:elixir, :ansi_enabled, true) 16 | 17 | assert Output.color("hello", :red) == [[[[] | "\e[31m"], "hello"] | "\e[0m"] 18 | end 19 | 20 | test "does not insert escape codes when they are disabled" do 21 | Application.put_env(:elixir, :ansi_enabled, false) 22 | 23 | assert Output.color("world", :green) == [[], "world"] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/dialyxir/plt_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.PltTest do 2 | alias Dialyxir.Plt 3 | import ExUnit.CaptureIO, only: [capture_io: 1] 4 | 5 | use ExUnit.Case 6 | 7 | def absname_plt(plt, _, _, _), do: IO.puts(Path.absname(plt)) 8 | 9 | test "check plts" do 10 | plts = [ 11 | {"/var/dialyxir_erlang-20.3.plt", [:erts, :kernel, :stdlib, :crypto]}, 12 | {"/var/dialyxir_erlang-20.3_elixir-1.6.2_deps-dev.plt", [:elixir]} 13 | ] 14 | 15 | fun = fn -> 16 | assert Plt.check(plts, &absname_plt/4) == :ok 17 | end 18 | 19 | assert capture_io(fun) =~ 20 | "Looking up modules in dialyxir_erlang-20.3.plt\n" <> 21 | "Looking up modules in dialyxir_erlang-20.3_elixir-1.6.2_deps-dev.plt\n" <> 22 | "/var/dialyxir_erlang-20.3_elixir-1.6.2_deps-dev.plt\n" <> 23 | "/var/dialyxir_erlang-20.3.plt\n" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/dialyxir/project_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.ProjectTest do 2 | alias Dialyxir.Project 3 | 4 | use ExUnit.Case 5 | import ExUnit.CaptureIO, only: [capture_io: 1, capture_io: 2] 6 | 7 | defp in_project(app, f) when is_atom(app) do 8 | in_project(app, "test/fixtures/#{Atom.to_string(app)}", f) 9 | end 10 | 11 | defp in_project(apps, f) when is_list(apps) do 12 | path = Enum.map_join(apps, "/", &Atom.to_string/1) 13 | app = List.last(apps) 14 | in_project(app, "test/fixtures/#{path}", f) 15 | end 16 | 17 | defp in_project(app, path, f) do 18 | Mix.Project.in_project(app, path, fn _ -> f.() end) 19 | end 20 | 21 | test "Default Project PLT File in _build dir" do 22 | in_project(:default_apps, fn -> 23 | assert Regex.match?(~r/_build\/.*plt/, Project.plt_file()) 24 | end) 25 | end 26 | 27 | test "Can specify a different local PLT path" do 28 | in_project(:alt_local_path, fn -> 29 | assert Regex.match?(~r/dialyzer\/.*plt/, Project.plt_file()) 30 | end) 31 | end 32 | 33 | test "Can specify a different PLT file name" do 34 | in_project(:local_plt, fn -> 35 | assert Regex.match?(~r/local\.plt/, Project.plt_file()) 36 | end) 37 | end 38 | 39 | test "Deprecation warning on use of bare :plt_file" do 40 | in_project(:local_plt, fn -> 41 | out = capture_io(&Project.check_config/0) 42 | assert Regex.match?(~r/.*plt_file.*deprecated.*/, out) 43 | end) 44 | end 45 | 46 | test "Can specify a different PLT file name along with :no_warn" do 47 | in_project(:local_plt_no_warn, fn -> 48 | assert Regex.match?(~r/local\.plt/, Project.plt_file()) 49 | end) 50 | end 51 | 52 | test "No deprecation warning on use of plt_file: {:no_warn, myfile}" do 53 | in_project(:local_plt_no_warn, fn -> 54 | out = capture_io(&Project.check_config/0) 55 | refute Regex.match?(~r/.*plt_path.*deprecated.*/, out) 56 | end) 57 | end 58 | 59 | test "App list for default contains direct and 60 | indirect :application dependencies" do 61 | in_project(:default_apps, fn -> 62 | apps = Project.cons_apps() 63 | # direct 64 | assert Enum.member?(apps, :logger) 65 | # direct 66 | assert Enum.member?(apps, :public_key) 67 | # indirect 68 | assert Enum.member?(apps, :asn1) 69 | end) 70 | end 71 | 72 | test "App list for umbrella contains child dependencies 73 | indirect :application dependencies" do 74 | in_project(:umbrella, fn -> 75 | apps = Project.cons_apps() 76 | # direct 77 | assert Enum.member?(apps, :logger) 78 | # direct, child1 79 | assert Enum.member?(apps, :public_key) 80 | # indirect 81 | assert Enum.member?(apps, :asn1) 82 | # direct, child2 83 | assert Enum.member?(apps, :mix) 84 | end) 85 | end 86 | 87 | @tag :skip 88 | test "App list for umbrella contains all child dependencies 89 | when run from child directory" do 90 | in_project([:umbrella, :apps, :second_one], fn -> 91 | apps = Project.cons_apps() 92 | # direct 93 | assert Enum.member?(apps, :logger) 94 | # direct, child1 95 | assert Enum.member?(apps, :public_key) 96 | # indirect 97 | assert Enum.member?(apps, :asn1) 98 | # direct, child2 99 | assert Enum.member?(apps, :mix) 100 | end) 101 | end 102 | 103 | test "App list for :apps_direct contains only direct dependencies" do 104 | in_project(:direct_apps, fn -> 105 | apps = Project.cons_apps() 106 | # direct 107 | assert Enum.member?(apps, :logger) 108 | # direct 109 | assert Enum.member?(apps, :public_key) 110 | # indirect 111 | refute Enum.member?(apps, :asn1) 112 | end) 113 | end 114 | 115 | test "App list for :plt_ignore_apps does not contain the ignored dependency" do 116 | in_project(:ignore_apps, fn -> 117 | apps = Project.cons_apps() 118 | 119 | refute Enum.member?(apps, :logger) 120 | end) 121 | end 122 | 123 | test "Core PLT files located in mix home by default" do 124 | in_project(:default_apps, fn -> 125 | assert String.contains?(Project.erlang_plt(), Mix.Utils.mix_home()) 126 | end) 127 | end 128 | 129 | test "Core PLT file paths can be specified with :plt_core_path" do 130 | in_project(:alt_core_path, fn -> 131 | assert String.contains?(Project.erlang_plt(), "_build") 132 | end) 133 | end 134 | 135 | test "By default core elixir and erlang plts are in mix.home" do 136 | in_project(:default_apps, fn -> 137 | assert String.contains?(Project.erlang_plt(), Mix.Utils.mix_home()) 138 | end) 139 | end 140 | 141 | test "By default a dialyzer ignore file is nil" do 142 | in_project(:default_apps, fn -> 143 | assert Project.dialyzer_ignore_warnings() == nil 144 | end) 145 | end 146 | 147 | test "Filtered dialyzer warnings" do 148 | in_project(:default_apps, fn -> 149 | output_list = 150 | ~S""" 151 | project.ex:9 This should still be here 152 | project.ex:9: Guard test is_atom(_@5::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed 153 | project.ex:9: Guard test is_binary(_@4::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed 154 | """ 155 | |> String.trim_trailing("\n") 156 | |> String.split("\n") 157 | 158 | pattern = ~S""" 159 | Guard test is_atom(_@5::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed 160 | 161 | Guard test is_binary(_@4::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed 162 | """ 163 | 164 | lines = Project.filter_legacy_warnings(output_list, pattern) 165 | assert lines == ["project.ex:9 This should still be here"] 166 | end) 167 | end 168 | 169 | test "Project with non-existent dependency" do 170 | in_project(:nonexistent_deps, fn -> 171 | out = capture_io(:stderr, &Project.cons_apps/0) 172 | assert Regex.match?(~r/Error loading nonexistent, dependency list may be incomplete/, out) 173 | end) 174 | end 175 | 176 | test "igonored apps are removed in umbrella projects" do 177 | in_project(:umbrella_ignore_apps, fn -> 178 | refute Enum.member?(Project.cons_apps(), :logger) 179 | end) 180 | end 181 | 182 | test "Deprecation warning on use of plt_add_deps: true" do 183 | in_project(:plt_add_deps_deprecations, fn -> 184 | out = capture_io(&Project.cons_apps/0) 185 | assert Regex.match?(~r/.*deprecated.*plt_add_deps.*/, out) 186 | end) 187 | end 188 | 189 | test "list_unused_filters? works as intended" do 190 | assert Project.list_unused_filters?(list_unused_filters: true) 191 | refute Project.list_unused_filters?(list_unused_filters: nil) 192 | 193 | # Override in mix.exs 194 | in_project(:ignore, fn -> 195 | assert Project.list_unused_filters?(list_unused_filters: nil) 196 | end) 197 | end 198 | 199 | test "no_umbrella? works as expected" do 200 | in_project(:umbrella, fn -> 201 | refute Project.no_umbrella?() 202 | end) 203 | 204 | in_project(:no_umbrella, fn -> 205 | assert Project.no_umbrella?() 206 | end) 207 | end 208 | end 209 | -------------------------------------------------------------------------------- /test/examples/apply.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.Apply do 2 | def ok() do 3 | fun = fn :ok -> :ok end 4 | fun.(:error) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/examples/call.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.Call do 2 | def ok() do 3 | ok(:error) 4 | end 5 | 6 | def ok(:ok) do 7 | :ok 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/examples/callback_argument.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.CallbackArgument do 2 | defmodule Behaviour do 3 | @callback ok(:ok) :: :ok 4 | end 5 | 6 | @behaviour Behaviour 7 | 8 | def ok(:error) do 9 | :ok 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/examples/callback_spec_argument.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.CallbackSpecArgument do 2 | defmodule Behaviour do 3 | @callback ok(:ok) :: :ok 4 | end 5 | 6 | @behaviour Behaviour 7 | 8 | @spec ok(:error) :: :ok 9 | def ok(:ok) do 10 | :ok 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/examples/contract_supertype.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.ContractSuperType do 2 | @spec ok() :: any 3 | def ok() do 4 | :ok 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/examples/contract_with_opaque.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.ContractWithOpaque do 2 | defmodule Types do 3 | @opaque type :: :ok 4 | end 5 | 6 | @spec ok() :: Types.type() 7 | def ok() do 8 | :ok 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/examples/exact_equality.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.ExactEquality do 2 | def ok() do 3 | :ok == :error 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/examples/extra-range.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.ExtraRange do 2 | @spec ok() :: :ok | :error 3 | def ok() do 4 | :ok 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/examples/function_app_args.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.FunctionApplicationArgs do 2 | def f do 3 | fn :a, [] -> :ok end 4 | end 5 | 6 | def ok() do 7 | fun = f() 8 | fun.(:b, []) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/examples/function_app_no_fun.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.FunctionApplicationNoFun do 2 | def ok() do 3 | fun = fn _ -> :ok end 4 | fun.() 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/examples/guard_fail.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.GuardFail do 2 | def ok() do 3 | ok(0) 4 | end 5 | 6 | defp ok(n) when n > 1 do 7 | :ok 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/examples/guard_fail_pattern.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.GuardFailPattern do 2 | def ok(n = 0) when not n < 1 do 3 | :ok 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/examples/opaque_equality.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.OpaqueEquality do 2 | defmodule Types do 3 | @opaque type :: :ok 4 | 5 | @spec ok() :: type() 6 | def ok() do 7 | :ok 8 | end 9 | end 10 | 11 | def eq_ok() do 12 | Types.ok() == :ok 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/examples/opaque_match.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.OpaqueMatch do 2 | defmodule Struct do 3 | defstruct [:opaque] 4 | 5 | @opaque t :: %__MODULE__{} 6 | 7 | @spec opaque() :: t 8 | def opaque() do 9 | %__MODULE__{} 10 | end 11 | end 12 | 13 | @spec error() :: :error 14 | def error() do 15 | %{opaque: _} = Struct.opaque() 16 | :error 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/examples/pattern_match.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.PatternMatch do 2 | def ok() do 3 | unmatched(:ok) 4 | end 5 | 6 | defp unmatched(:ok), do: :ok 7 | 8 | defp unmatched(:error), do: :error 9 | end 10 | -------------------------------------------------------------------------------- /test/examples/pattern_match_covered.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.PatternMuchCovered do 2 | def ok() do 3 | unmatched(:error) 4 | end 5 | 6 | defp unmatched(_), do: :ok 7 | 8 | defp unmatched(:error), do: :error 9 | end 10 | -------------------------------------------------------------------------------- /test/examples/unknown_type.ex: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Examples.UnknownType do 2 | defmodule Missing do 3 | end 4 | 5 | @spec ok(Missing.t()) :: :ok 6 | def ok(_) do 7 | :ok 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/alt_core_path/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AltCorePath.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :alt_core_path, 7 | prune_code_paths: false, 8 | version: "1.0.0", 9 | dialyzer: [plt_core_path: "_build"] 10 | ] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/alt_local_path/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AltLocalPath.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :alt_local_path, 7 | prune_code_paths: false, 8 | version: "1.0.0", 9 | dialyzer: [plt_local_path: "dialyzer"] 10 | ] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/default_apps/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DefaultApps.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :default_apps, prune_code_paths: false, version: "0.1.0", deps: deps()] 6 | end 7 | 8 | def application do 9 | [applications: [:logger, :public_key]] 10 | end 11 | 12 | defp deps do 13 | [] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/direct_apps/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DirectApps.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :direct_apps, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | deps: deps(), 10 | dialyzer: [plt_add_deps: :apps_direct] 11 | ] 12 | end 13 | 14 | def application do 15 | [applications: [:logger, :public_key]] 16 | end 17 | 18 | defp deps do 19 | [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/ignore/ignore_test.exs: -------------------------------------------------------------------------------- 1 | [ 2 | {"lib/short_description.ex:17:no_return Function format_long/1 has no local return."}, 3 | {"lib/file/warning_type.ex", :no_return, 18}, 4 | {"lib/file/warning_type/line.ex", :no_return, 19}, 5 | ~r/regex_file.ex.*no local return/ 6 | ] 7 | -------------------------------------------------------------------------------- /test/fixtures/ignore/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ignore.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ignore, 7 | version: "0.1.0", 8 | prune_code_paths: false, 9 | dialyzer: [ 10 | ignore_warnings: "ignore_test.exs", 11 | list_unused_filters: true 12 | ] 13 | ] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/ignore_apps/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule IgnoreApps.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ignore_apps, 7 | version: "0.1.0", 8 | prune_code_paths: false, 9 | deps: deps(), 10 | dialyzer: [ 11 | plt_ignore_apps: [:logger] 12 | ] 13 | ] 14 | end 15 | 16 | def application do 17 | [applications: [:logger, :public_key]] 18 | end 19 | 20 | defp deps do 21 | [] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/fixtures/ignore_custom_empty/ignore_test.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremyjh/dialyxir/8441bebfe3ed0fcb25e36c09e86f3d08ad48769a/test/fixtures/ignore_custom_empty/ignore_test.exs -------------------------------------------------------------------------------- /test/fixtures/ignore_custom_empty/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule IgnoreCustomEmpty.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ignore_custom_empty, 7 | version: "0.1.0", 8 | prune_code_paths: false, 9 | dialyzer: [ 10 | # this file is expected to not exist 11 | ignore_warnings: "ignore_test.exs", 12 | list_unused_filters: true 13 | ] 14 | ] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/ignore_custom_missing/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule IgnoreCustomMissing.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ignore_custom_missing, 7 | version: "0.1.0", 8 | prune_code_paths: false, 9 | dialyzer: [ 10 | # this file is expected to not exist 11 | ignore_warnings: "ignore_test.exs", 12 | list_unused_filters: true 13 | ] 14 | ] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/ignore_strict/ignore_strict_test.exs: -------------------------------------------------------------------------------- 1 | [ 2 | {"lib/poorly_written_code.ex", "Function do_a_thing/1 has no local return."}, 3 | {"lib/poorly_written_code.ex", "Function do_something_else/2 has no local return."}, 4 | {"lib/poorly_written_code.ex", "Function do_many_things/3 has no local return."} 5 | ] 6 | -------------------------------------------------------------------------------- /test/fixtures/ignore_strict/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule IgnoreStrict.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ignore_strict, 7 | version: "0.1.0", 8 | prune_code_paths: false, 9 | dialyzer: [ 10 | ignore_warnings: "ignore_strict_test.exs", 11 | list_unused_filters: true 12 | ] 13 | ] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/ignore_string/dialyzer.ignore-warnings: -------------------------------------------------------------------------------- 1 | a/file.ex:17: The pattern 'ok' can never match the type 'error' 2 | -------------------------------------------------------------------------------- /test/fixtures/ignore_string/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule IgnoreString.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ignore_string, 7 | version: "0.1.0", 8 | prune_code_paths: false, 9 | dialyzer: [ 10 | ignore_warnings: "dialyzer.ignore-warnings", 11 | list_unused_filters: true 12 | ] 13 | ] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/local_plt/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LocalPlt.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :local_plt, 7 | prune_code_paths: false, 8 | version: "1.0.0", 9 | dialyzer: [plt_file: "local.plt"] 10 | ] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/local_plt/mix.lock: -------------------------------------------------------------------------------- 1 | %{} 2 | -------------------------------------------------------------------------------- /test/fixtures/local_plt_no_warn/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule LocalPltNoWarn.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :local_plt_no_warn, 7 | prune_code_paths: false, 8 | version: "1.0.0", 9 | dialyzer: [plt_file: {:no_warn, "local.plt"}] 10 | ] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/fixtures/no_lockfile/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NoLockfile.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :no_lockfile, 7 | prune_code_paths: false, 8 | version: "1.0.0" 9 | ] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/no_umbrella/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NoUmbrella.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :no_umbrella, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | lockfile: "../mix.lock", 10 | elixir: "~> 1.3", 11 | build_embedded: Mix.env() == :prod, 12 | start_permanent: Mix.env() == :prod, 13 | deps: [], 14 | dialyzer: [no_umbrella: true] 15 | ] 16 | end 17 | 18 | # Configuration for the OTP application 19 | # 20 | # Type "mix help compile.app" for more information 21 | def application do 22 | [applications: [:logger, :public_key]] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/fixtures/nonexistent_deps/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule NonexistentDeps.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :nonexistent_deps, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | deps: deps() 10 | ] 11 | end 12 | 13 | def application do 14 | # This application has a run-time dependency on a non-existent 15 | # application. 16 | [applications: [:logger, :public_key, :nonexistent]] 17 | end 18 | 19 | defp deps do 20 | [] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/fixtures/plt_add_deps_deprecations/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PltAddDepsDeprecations.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :plt_add_deps_deprecations, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | dialyzer: [ 10 | plt_add_deps: true 11 | ] 12 | ] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/README.md: -------------------------------------------------------------------------------- 1 | # Umbrella 2 | 3 | **TODO: Add description** 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/first_one/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/first_one/README.md: -------------------------------------------------------------------------------- 1 | # FirstOne 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 8 | 9 | 1. Add `first_one` to your list of dependencies in `mix.exs`: 10 | 11 | ```elixir 12 | def deps do 13 | [{:first_one, "~> 0.1.0"}] 14 | end 15 | ``` 16 | 17 | 2. Ensure `first_one` is started before your application: 18 | 19 | ```elixir 20 | def application do 21 | [applications: [:first_one]] 22 | end 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/first_one/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :first_one, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:first_one, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/first_one/lib/first_one.ex: -------------------------------------------------------------------------------- 1 | defmodule FirstOne do 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/first_one/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FirstOne.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :first_one, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | build_path: "../../_build", 10 | config_path: "../../config/config.exs", 11 | deps_path: "../../deps", 12 | lockfile: "../../mix.lock", 13 | elixir: "~> 1.3", 14 | build_embedded: Mix.env() == :prod, 15 | start_permanent: Mix.env() == :prod, 16 | deps: [] 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application 21 | # 22 | # Type "mix help compile.app" for more information 23 | def application do 24 | [applications: [:logger, :public_key]] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/second_one/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/second_one/README.md: -------------------------------------------------------------------------------- 1 | # SecondOne 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 8 | 9 | 1. Add `second_one` to your list of dependencies in `mix.exs`: 10 | 11 | ```elixir 12 | def deps do 13 | [{:second_one, "~> 0.1.0"}] 14 | end 15 | ``` 16 | 17 | 2. Ensure `second_one` is started before your application: 18 | 19 | ```elixir 20 | def application do 21 | [applications: [:second_one]] 22 | end 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/second_one/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :second_one, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:second_one, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/second_one/lib/second_one.ex: -------------------------------------------------------------------------------- 1 | defmodule SecondOne do 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/second_one/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SecondOne.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :second_one, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | build_path: "../../_build", 10 | config_path: "../../config/config.exs", 11 | deps_path: "../../deps", 12 | lockfile: "../../mix.lock", 13 | elixir: "~> 1.3", 14 | build_embedded: Mix.env() == :prod, 15 | start_permanent: Mix.env() == :prod, 16 | deps: [] 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application 21 | # 22 | # Type "mix help compile.app" for more information 23 | def application do 24 | [applications: [:logger, :mix]] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # By default, the umbrella project as well as each child 6 | # application will require this configuration file, ensuring 7 | # they all use the same configuration. While one could 8 | # configure all applications here, we prefer to delegate 9 | # back to each application for organization purposes. 10 | import_config "../apps/*/config/config.exs" 11 | 12 | # Sample configuration (overrides the imported configuration above): 13 | # 14 | # config :logger, :console, 15 | # level: :info, 16 | # format: "$date $time [$level] $metadata$message\n", 17 | # metadata: [:user_id] 18 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Umbrella.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | prune_code_paths: false, 8 | build_embedded: Mix.env() == :prod, 9 | start_permanent: Mix.env() == :prod, 10 | deps: [] 11 | ] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/fixtures/umbrella_ignore_apps/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /test/fixtures/umbrella_ignore_apps/apps/first_one/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FirstOne.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :first_one, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | build_path: "../../_build", 10 | config_path: "../../config/config.exs", 11 | deps_path: "../../deps", 12 | lockfile: "../../mix.lock", 13 | elixir: "~> 1.3", 14 | build_embedded: Mix.env() == :prod, 15 | start_permanent: Mix.env() == :prod, 16 | deps: [] 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application 21 | # 22 | # Type "mix help compile.app" for more information 23 | def application do 24 | [applications: [:logger, :public_key]] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/fixtures/umbrella_ignore_apps/apps/second_one/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SecondOne.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :second_one, 7 | prune_code_paths: false, 8 | version: "0.1.0", 9 | build_path: "../../_build", 10 | config_path: "../../config/config.exs", 11 | deps_path: "../../deps", 12 | lockfile: "../../mix.lock", 13 | elixir: "~> 1.3", 14 | build_embedded: Mix.env() == :prod, 15 | start_permanent: Mix.env() == :prod, 16 | deps: [] 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application 21 | # 22 | # Type "mix help compile.app" for more information 23 | def application do 24 | [applications: [:logger, :mix]] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/fixtures/umbrella_ignore_apps/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule UmbrellaIgnoreApps.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | prune_code_paths: false, 8 | build_embedded: Mix.env() == :prod, 9 | start_permanent: Mix.env() == :prod, 10 | deps: [], 11 | dialyzer: [ 12 | plt_ignore_apps: [:logger] 13 | ] 14 | ] 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/mix/tasks/dialyzer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.DialyzerTest do 2 | use ExUnit.Case 3 | 4 | import ExUnit.CaptureIO, only: [capture_io: 1] 5 | 6 | defp in_project(app, f) when is_atom(app) do 7 | Mix.Project.in_project(app, "test/fixtures/#{Atom.to_string(app)}", fn _ -> f.() end) 8 | end 9 | 10 | defp no_delete_plt(plt, _, _, _), do: IO.puts("About to delete PLT file: #{plt}") 11 | 12 | test "Delete PLT file name" do 13 | in_project(:local_plt, fn -> 14 | fun = fn -> Mix.Tasks.Dialyzer.clean([], &no_delete_plt/4) end 15 | 16 | assert Regex.match?( 17 | ~r/About to delete PLT file: .*\/test\/fixtures\/local_plt\/local.plt/, 18 | capture_io(fun) 19 | ) 20 | end) 21 | end 22 | 23 | test "Core PLTs are not deleted without --all flag" do 24 | in_project(:local_plt, fn -> 25 | fun = fn -> Mix.Tasks.Dialyzer.clean([], &no_delete_plt/4) end 26 | 27 | assert not Regex.match?( 28 | ~r/About to delete PLT file: .*dialyxir_erlang/, 29 | capture_io(fun) 30 | ) 31 | end) 32 | end 33 | 34 | test "Core PLTs are deleted with --all flag" do 35 | in_project(:local_plt, fn -> 36 | fun = fn -> Mix.Tasks.Dialyzer.clean([{:all, true}], &no_delete_plt/4) end 37 | 38 | assert Regex.match?( 39 | ~r/About to delete PLT file: .*dialyxir_erlang/, 40 | capture_io(fun) 41 | ) 42 | end) 43 | end 44 | 45 | test "Does not crash when running on project without mix.lock" do 46 | in_project(:no_lockfile, fn -> 47 | fun = fn -> Mix.Tasks.Dialyzer.clean([], &no_delete_plt/4) end 48 | capture_io(fun) 49 | # does not assert anything, we just need to ensure this doesn't crash. 50 | end) 51 | end 52 | 53 | @tag :output_tests 54 | test "Informational output is suppressed with --quiet" do 55 | args = ["dialyzer", "--quiet"] 56 | env = [{"MIX_ENV", "prod"}] 57 | {result, 0} = System.cmd("mix", args, env: env) 58 | assert result == "" 59 | end 60 | 61 | @tag :output_tests 62 | test "Only final result is printed with --quiet-with-result" do 63 | args = ["dialyzer", "--quiet-with-result"] 64 | env = [{"MIX_ENV", "prod"}] 65 | {result, 0} = System.cmd("mix", args, env: env) 66 | 67 | assert result =~ 68 | ~r/Total errors: ., Skipped: ., Unnecessary Skips: .\ndone \(passed successfully\)\n/ 69 | end 70 | 71 | @tag :output_tests 72 | test "Warning is printed when unknown format is requested" do 73 | args = ["dialyzer", "--format", "foo"] 74 | env = [{"MIX_ENV", "prod"}] 75 | {result, 0} = System.cmd("mix", args, env: env) 76 | 77 | assert result =~ 78 | "Unrecognized formatter foo received. Known formatters are dialyzer, dialyxir, github, ignore_file, ignore_file_strict, raw, and short. Falling back to dialyxir." 79 | end 80 | 81 | test "task runs when custom ignore file provided and exists" do 82 | in_project(:ignore, fn -> 83 | fun = fn -> Mix.Tasks.Dialyzer.run(["--ignore-exit-status"]) end 84 | 85 | assert capture_io(fun) =~ "ignore_warnings: ignore_test.exs" 86 | end) 87 | end 88 | 89 | test "task runs when custom ignore file provided and does not exist" do 90 | in_project(:ignore_custom_missing, fn -> 91 | fun = fn -> Mix.Tasks.Dialyzer.run(["--ignore-exit-status"]) end 92 | 93 | assert capture_io(fun) =~ 94 | ":ignore_warnings opt specified in mix.exs: ignore_test.exs, but file does not exist" 95 | end) 96 | end 97 | 98 | test "task runs when custom ignore file provided and it is empty" do 99 | in_project(:ignore_custom_empty, fn -> 100 | fun = fn -> Mix.Tasks.Dialyzer.run(["--ignore-exit-status"]) end 101 | 102 | assert capture_io(fun) =~ 103 | ":ignore_warnings opt specified in mix.exs: ignore_test.exs, but file is empty" 104 | end) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | case System.get_env("OUTPUT_TESTS") do 2 | "true" -> ExUnit.start(exclude: :test, include: :output_tests) 3 | _ -> ExUnit.start(exclude: :output_tests) 4 | end 5 | -------------------------------------------------------------------------------- /test/warning_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Dialyxir.Test.WarningTest do 2 | use ExUnit.Case 3 | 4 | # Don't test output in here, just that it can succeed. 5 | 6 | test "pattern match warning succeeds on valid input" do 7 | arguments = [~c"pattern {'ok', Vuser@1}", ~c"{'error',<<_:64,_:_*8>>}"] 8 | assert(Dialyxir.Warnings.PatternMatch.format_long(arguments)) 9 | end 10 | end 11 | --------------------------------------------------------------------------------