diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 29e5632b..18cc3ab6 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,5 +1,4 @@ [ {":0:unknown_function Function :erl_types.t_is_opaque/1 does not exist."}, - {":0:unknown_function Function :erl_types.t_to_string/1 does not exist."}, - ~r/format_(long|short).*no local return/ + {":0:unknown_function Function :erl_types.t_to_string/1 does not exist."} ] diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..c643beaf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,10 @@ +# These are supported funding model platforms + +github: [jeremyjh] + + + + + + + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..9a55700e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,121 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + check_duplicate_runs: + name: Check for duplicate runs + continue-on-error: true + runs-on: ubuntu-20.04 + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@master + with: + github_token: ${{ github.token }} + concurrent_skipping: always + cancel_others: true + skip_after_successful_duplicate: true + paths_ignore: '["**/README.md", "**/CHANGELOG.md", "**/LICENSE"]' + do_not_skip: '["pull_request"]' + + test: + name: Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}} + runs-on: ubuntu-20.04 + needs: check_duplicate_runs + if: ${{ needs.check_duplicate_runs.outputs.should_skip != 'true' }} + + strategy: + fail-fast: false + matrix: + elixir: + - '1.12.3' + - '1.13.4' + - '1.14.3' + otp: + - '23.3' + - '24.3' + - '25.3' + + exclude: + - elixir: '1.12.3' + otp: '25.3' + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + + - name: Restore deps cache + uses: actions/cache@v2 + with: + path: | + deps + _build + key: ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}-git-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }} + ${{ runner.os }}-deps-${{ matrix.otp }}-${{ matrix.elixir }} + + - name: Install package dependencies + run: mix deps.get + + - name: Remove compiled application files + run: mix clean + + - name: Compile dependencies + run: mix compile + env: + MIX_ENV: test + + - name: Run unit tests + run: mix test --trace + + - name: Check compilation warnings + run: mix do compile --warnings-as-errors, archive.build, archive.install --force + env: + MIX_ENV: prod + + - name: Check source code formatting + if: ${{ matrix.elixir > '1.12.0' }} + run: mix format --check-formatted + + - name: Get results in short format + run: mix dialyzer --format short + env: + MIX_ENV: prod + + - name: Get results in raw format + run: mix dialyzer --format raw + env: + MIX_ENV: prod + + - name: Get results in dialyzer format + run: mix dialyzer --format dialyzer + env: + MIX_ENV: prod + + - name: Get results in ignore_file format + run: mix dialyzer --format ignore_file + env: + MIX_ENV: prod + + - name: Run output tests + run: mix test + env: + OUTPUT_TESTS: true + + - name: Check examples + run: mix dialyzer --format short --ignore-exit-status + env: + MIX_ENV: examples diff --git a/.tool-versions b/.tool-versions index 89ed3a8b..b245c798 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.7.3-otp-21 -erlang 21.0.4 +elixir 1.14.3-otp-25 +erlang 25.2.2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ff0bcbe9..00000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -language: elixir -elixir: - - 1.7.3 - - 1.8.1 -otp_release: - - 20.3 - - 21.3 - - 22.0 -script: - - mix test --trace - - MIX_ENV=prod mix do compile --warnings-as-errors, archive.build, archive.install --force - - mix format --check-formatted - - MIX_ENV=prod mix dialyzer --list-unused-filters - - OUTPUT_TESTS=true mix test - - MIX_ENV=prod mix dialyzer --format short - - MIX_ENV=prod mix dialyzer --format raw - - MIX_ENV=prod mix dialyzer --format dialyzer - - MIX_ENV=examples mix dialyzer --format short --ignore-exit-status -branches: - except: - - /^[0-9]\.[0-9]/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a5bdabc2..af3b2d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +## Unreleased changes post [1.3.0] + +## [1.3.0] - 2023-04-08 + +### Added + - Elixir 1.15 support. + - Support for warning `:callback_not_exported`. + +### Changed + - Several improvements to documentation, particularly Github CI documentation. + +### Removed + - Support for `:race_conditions` flag which was [removed from Erlang](https://github.com/erlang/otp/pull/5502). + +### Fixed + - Crash when `mix.lock` is missing. + +## [1.2.0] - 2022-07-20 +### Added + - "github" formatter. + +## [1.1.0] - 2021-02-18 + +### Added + - Configuration option to set the project's PLT path: `:plt_local_path`. + - Project configuration setting to exclude files based on a regex: `:exclude_files`. + - `explain` text for `:missing_range` warning. + +### Fixed + + - Fixes and improvements to README and documentation. + - Fixed `mix.lock` hash stability. Will cause a recheck of PLTs on first usage in each project. + +### Changed + - Improved wording of argument mismatch warnings. + ## [1.0.0] - 2020-03-16 ### Changed @@ -14,7 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Warning pretty printing and message fixes/improvements. - Prevent crash when short_format fails. - Ensure path to PLT target directory exists. - - Bumpe required `erlex` for formatting fix. + - Bumped required `erlex` for formatting fix. ## [1.0.0-rc.7] - 2019-09-21 diff --git a/LICENSE b/LICENSE index 10078d64..d9a10c0d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,176 @@ -Copyright 2013-2018 Jeremy Huffman and contributors. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - http://www.apache.org/licenses/LICENSE-2.0 + 1. Definitions. - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..4c83b664 --- /dev/null +++ b/NOTICE @@ -0,0 +1,13 @@ +Copyright 2013-2023 Jeremy Huffman and contributors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 043ca320..150c9922 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,23 @@ # Dialyxir -Mix tasks to simplify use of Dialyzer in Elixir projects. - -[![Build Status](https://travis-ci.org/jeremyjh/dialyxir.svg?branch=master)](https://travis-ci.org/jeremyjh/dialyxir) - -## Changes in 1.0 +[![Module Version](https://img.shields.io/hexpm/v/dialyxir.svg)](https://hex.pm/packages/dialyxir) +[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/dialyxir/) +[![Total Download](https://img.shields.io/hexpm/dt/dialyxir.svg)](https://hex.pm/packages/dialyxir) +[![License](https://img.shields.io/hexpm/l/dialyxir.svg)](https://github.com/jeremyjh/dialyxir/blob/master/LICENSE) +[![Last Updated](https://img.shields.io/github/last-commit/jeremyjh/dialyxir.svg)](https://github.com/jeremyjh/dialyxir/commits/master) -Elixir 1.6 is required, to support the new pretty printing feature. If your -project is not yet on 1.6, continue to specify 0.5 in your mix deps. - -Warning messages have been greatly improved, but are filtered through the legacy formatter to support your existing ignore files. You can optionally use the new Elixir [term format](#elixir-term-format) for ignore files. You may want to use the `--format short` argument in your CI pipelines. There are several formats, also there is a new `explain` feature - for details see CLI [options](#command-line-options). - -## Quickstart -If you are planning to use Dialyzer with an application built with the [Phoenix Framework](http://www.phoenixframework.org/), check out the [Quickstart wiki](https://github.com/jeremyjh/dialyxir/wiki/Phoenix-Dialyxir-Quickstart). +Mix tasks to simplify use of Dialyzer in Elixir projects. ## Installation Dialyxir is available on [hex.pm](https://hex.pm/packages/dialyxir). -You can either add it as a dependency in your mix.exs, or install it globally as an archive task. - To add it to a mix project, just add a line like this in your deps function in mix.exs: ```elixir defp deps do [ - {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, + {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, ] end ``` @@ -44,19 +36,23 @@ mix dialyzer ### Command line options - * `--no-compile` - do not compile even if needed. - * `--no-check` - do not perform (quick) check to see if PLT needs updated. - * `--ignore-exit-status` - display warnings but do not halt the VM or return an exit status code - * `--format short` - format the warnings in a compact format. - * `--format raw` - format the warnings in format returned before Dialyzer formatting - * `--format dialyxir` - format the warnings in a pretty printed format - * `--format dialyzer` - format the warnings in the original Dialyzer format - * `--quiet` - suppress all informational messages + * `--no-compile` - do not compile even if needed. + * `--no-check` - do not perform (quick) check to see if PLT needs to be updated. + * `--ignore-exit-status` - display warnings but do not halt the VM or return an exit status code. + * `--format short` - format the warnings in a compact format, suitable for ignore file using Elixir term format. + * `--format raw` - format the warnings in format returned before Dialyzer formatting. + * `--format dialyxir` - format the warnings in a pretty printed format. (default) + * `--format dialyzer` - format the warnings in the original Dialyzer format, suitable for ignore file using simple string matches. + * `--format github` - format the warnings in the Github Actions message format. + * `--format ignore_file` - format the warnings in {file, warning} format for Elixir Format ignore file. + * `--format ignore_file_strict` - format the warnings in {file, short_description} format for Elixir Format ignore file. + * `--quiet` - suppress all informational messages. -Warning flags passed to this task are passed on to `:dialyzer`. +Warning flags passed to this task are passed on to `:dialyzer` - e.g. - e.g. - `mix dialyzer --unmatched_returns` +```console +mix dialyzer --unmatched_returns +``` There is information available about the warnings via the explain task - e.g. @@ -69,32 +65,13 @@ If invoked without arguments, `mix dialyzer.explain` will list all the known war ## Continuous Integration To use Dialyzer in CI, you must be aware of several things: -1) Building the PLT file may take a while if a project has many dependencies -2) The PLT should be cached using the CI caching system -3) The PLT will need to be rebuilt whenever adding a new Erlang or Elixir version to build matrix - -Using Travis, this would look like: - -`.travis.yml` -```markdown -language: elixir - -elixir: - - 1.8 -otp_release: - - 21.0 +1. Building the PLT file may take a while if a project has many dependencies +2. The PLT should be cached using the CI caching system +3. The PLT will need to be rebuilt whenever adding a new Erlang or Elixir version to your build matrix -script: - - mix dialyzer - -cache: - directories: - - priv/plts -``` - -`mix.exs` ```elixir +# mix.exs def project do [ ... @@ -105,12 +82,18 @@ def project do end ``` -`.gitignore` -``` +```shell +# .gitignore /priv/plts/*.plt /priv/plts/*.plt.hash ``` +### Example CI Configs + +- [CircleCI](./docs/circleci.md) +- [GitHub Actions](./docs/github_actions.md) +- [GitLab CI](./docs/gitlab_ci.md) + ## With Explaining Stuff [Dialyzer](http://www.erlang.org/doc/apps/dialyzer/dialyzer_chapter.html) is a static analysis tool for Erlang and other languages that compile to BEAM bytecode for the Erlang VM. It can analyze the BEAM files and provide warnings about problems in your code including type mismatches and other issues that are commonly detected by static language compilers. The analysis can be improved by inclusion of type hints (called [specs](https://hexdocs.pm/elixir/typespecs.html)) but it can be useful even without those. For more information I highly recommend the [Success Typings](http://user.it.uu.se/~kostis/Papers/succ_types.pdf) paper that describes the theory behind the tool. @@ -121,17 +104,18 @@ Usage is straightforward but you should be aware of the available configuration ### PLT The Persistent Lookup Table (PLT) is basically a cached output of the analysis. This is important because you'd probably stab yourself in the eye with -a fork if you had to wait for Dialyzer to analyze all the standard library and OTP modules you are using everytime you ran it. +a fork if you had to wait for Dialyzer to analyze all the standard library and OTP modules you are using every time you ran it. Running the mix task `dialyzer` by default builds several PLT files: - * A core Erlang file in $MIX_HOME/dialyxir_erlang-[OTP Version].plt - * A core Elixir file in $MIX_HOME/dialyxir_erlang-[OTP Version]_elixir-[Elixir Version].plt - * A project environment specific file in _build/env/dialyze_erlang-[OTP Version]_elixir-[Elixir Version]_deps-dev.plt + + * A core Erlang file in `$MIX_HOME/dialyxir_erlang-[OTP Version].plt` + * A core Elixir file in `$MIX_HOME/dialyxir_erlang-[OTP Version]_elixir-[Elixir Version].plt` + * A project environment specific file in `_build/env/dialyze_erlang-[OTP Version]_elixir-[Elixir Version]_deps-dev.plt` The core files are simply copied to your project folder when you run `dialyxir` for the first time with a given version of Erlang and Elixir. By default, all -the modules in the project PLT are checked against your dependencies to be sure they are up to date. If you do not want to use MIX_HOME to store your core Erlang and Elixir files, you can provide a :plt_core_path key with a file path. You can specify a different location for the project PLT file with the :plt_file keyword - this is deprecated because people were using it with the old `dialyxir` to have project-specific PLTs, which are now the default. To silence the deprecation warning, specify this value as `plt_file: {:no_warn, "/myproject/mypltfile"}`. +the modules in the project PLT are checked against your dependencies to be sure they are up to date. If you do not want to use MIX_HOME to store your core Erlang and Elixir files, you can provide a `:plt_core_path` key with a file path. You can specify a different directory for the project PLT file with the `:plt_local_path keyword`. You can specify a different filename for the project PLT file with the `:plt_file keyword` - this is deprecated because people were using it with the old `dialyxir` to have project-specific PLTs, which are now the default. To silence the deprecation warning, specify this value as `plt_file: {:no_warn, "/myproject/mypltfile"}`. The core PLTs include a basic set of OTP applications, as well as all of the Elixir standard libraries. -The apps included by default are `[ :erts, :kernel, :stdlib, :crypto]`. +The apps included by default are `[:erts, :kernel, :stdlib, :crypto]`. If you don't want to include the default apps you can specify a `:plt_apps` key and list there only the apps you want in the PLT. Using this option will mean dependencies are not added automatically (see below). If you want to just add an application to the list of defaults and dependencies you can use the `:plt_add_apps` key. @@ -140,8 +124,9 @@ If you want to ignore a specific dependency, you can specify it in the `:plt_ign #### Dependencies OTP application dependencies are (transitively) added to your 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: - * :apps_direct - Only Direct OTP runtime application dependencies - not the entire tree - * :app_tree - Transitive OTP runtime application dependencies e.g. `mix app.tree` (default) + + * `:apps_direct` - Only Direct OTP runtime application dependencies - not the entire tree + * `:app_tree` - Transitive OTP runtime application dependencies e.g. `mix app.tree` (default) The example below changes the default to include only direct OTP dependencies, adds another specific dependency, and removes a dependency from the list. This can be helpful if a large dependency tree is creating memory issues and only some of the transitive dependencies are required for analysis. @@ -168,6 +153,7 @@ Explanations are available for classes of warnings by executing `mix dialyzer.ex #### Formats Dialyxir supports formatting the errors in several different ways: + * Short - By passing `--format short`, the structs and other spec/type information will be dropped from the error message, with a minimal message. This is useful for CI environments. Includes `warning_name ` for use in explanations. * Dialyzer - By passing `--format dialyzer`, the messages will be printed in the default Dialyzer format. This format is used in [legacy string matching](#simple-string-matches) ignore files. * Raw - By passing `--format raw`, messages will be printed in their form before being pretty printed by Dialyzer or Dialyxir. @@ -183,7 +169,7 @@ def project do app: :my_app, version: "0.0.1", deps: deps, - dialyzer: [flags: ["-Wunmatched_returns", :error_handling, :race_conditions, :underspecs]] + dialyzer: [flags: ["-Wunmatched_returns", :error_handling, :underspecs]] ] end ``` @@ -200,7 +186,7 @@ def project do deps: deps, dialyzer: [ plt_add_apps: [:mnesia], - flags: [:unmatched_returns, :error_handling, :race_conditions, :no_opaque], + flags: [:unmatched_returns, :error_handling, :no_opaque], paths: ["_build/dev/lib/my_app/ebin", "_build/dev/lib/foo/ebin"] ] ] @@ -242,7 +228,9 @@ def project do end ``` -This file comes in two formats: `--format dialyzer` string matches (compatbile with <= 0.5.1 ignore files), and the [term format](#elixir-term-format). +This file comes in two formats: `--format dialyzer` string matches (compatible with `<= 0.5.1` ignore files), and the [term format](#elixir-term-format). + +Dialyzer will look for an ignore file using the term format with the name `.dialyzer_ignore.exs` by default if you don't specify something otherwise. #### Simple String Matches @@ -254,7 +242,7 @@ For example, in a project where `mix dialyzer --format dialyzer` outputs: ``` Proceeding with analysis... -config.ex:64: The call ets:insert('Elixir.MyApp.Config',{'Elixir.MyApp.Config',_}) might have an unintended effect due to a possible race condition caused by its combination withthe ets:lookup('Elixir.MyApp.Config','Elixir.MyApp.Config') call in config.ex on line 26 +config.ex:64: The call ets:insert('Elixir.MyApp.Config',{'Elixir.MyApp.Config',_}) might have an unintended effect due to a possible race condition caused by its combination with the ets:lookup('Elixir.MyApp.Config','Elixir.MyApp.Config') call in config.ex on line 26 config.ex:79: Guard test is_binary(_@5::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed config.ex:79: Guard test is_atom(_@6::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed done in 0m1.32s @@ -272,15 +260,15 @@ And then run `mix dialyzer` would output: ``` Proceeding with analysis... -config.ex:64: The call ets:insert('Elixir.MyApp.Config',{'Elixir.MyApp.Config',_}) might have an unintended effect due to a possible race condition caused by its combination withthe ets:lookup('Elixir.MyApp.Config','Elixir.MyApp.Config') call in config.ex on line 26 +config.ex:64: The call ets:insert('Elixir.MyApp.Config',{'Elixir.MyApp.Config',_}) might have an unintended effect due to a possible race condition caused by its combination with the ets:lookup('Elixir.MyApp.Config','Elixir.MyApp.Config') call in config.ex on line 26 done in 0m1.32s done (warnings were emitted) ``` #### Elixir Term Format -Dialyxir also recognizes an Elixir format of the ignore file. If your ignore file is an `exs` file, Dialyxir will evaluate it and process its data structure. The file looks like the following, and can match either tuple patterns or an arbitrary Regex -applied to the *short-description* (`mix dialyzer --format short`): +Dialyxir also recognizes an Elixir format of the ignore file. If your ignore file is an `exs` file, Dialyxir will evaluate it and process its data structure. A line may be either a tuple or an arbitrary Regex +applied to the *short-description* format of Dialyzer output (`mix dialyzer --format short`). The file looks like the following: ```elixir # .dialyzer_ignore.exs @@ -293,6 +281,8 @@ applied to the *short-description* (`mix dialyzer --format short`): {":0:unknown_function Function :erl_types.t_to_string/1 does not exist.", :unknown_function, 0}, # {file, warning_type, line} {"lib/dialyxir/pretty_print.ex", :no_return, 100}, + # {file, warning_description} + {"lib/dialyxir/warning_helpers.ex", "Function :erl_types.t_to_string/1 does not exist."}, # {file, warning_type} {"lib/dialyxir/warning_helpers.ex", :no_return}, # {file} @@ -302,6 +292,43 @@ applied to the *short-description* (`mix dialyzer --format short`): ] ``` +_Note that `short_description` contains additional information that `warning_description` does not._ + +Entries for existing warnings can be generated with one of the following: +- `mix dialyzer --format ignore_file` +- `mix dialyzer --format ignore_file_strict` (recommended) + +For example, if `mix dialyzer --format short` gives you a result like: +``` +lib/something.ex:15:no_return Function init/1 has no local return. +lib/something.ex:36:no_return Function refresh/0 has no local return. +lib/something.ex:45:no_return Function create/2 has no local return. +lib/something.ex:26:no_return Function update/2 has no local return. +lib/something.ex:49:no_return Function delete/1 has no local return. +``` + +If you had used `--format ignore_file`, you'd be given a single file ignore line for all five warnings: +```elixir +# .dialyzer_ignore.exs +[ + # {file, warning_type} + {"lib/something.ex", :no_return}, +] +``` + +If you had used `--format ignore_file_strict`, you'd be given more granular ignore lines: +```elixir +# .dialyzer_ignore.exs +[ + # {file, warning_description} + {"lib/something.ex", "Function init/1 has no local return."}, + {"lib/something.ex", "Function refresh/0 has no local return."}, + {"lib/something.ex", "Function create/2 has no local return."}, + {"lib/something.ex", "Function update/2 has no local return."}, + {"lib/something.ex", "Function delete/1 has no local return."}, +] +``` + #### List unused Filters As filters tend to become obsolete (either because a discrepancy was fixed, or because the location @@ -317,3 +344,15 @@ dialyzer: [ This option can also be set on the command line with `--list-unused-filters`. When used without `--ignore-exit-status`, this option will result in an error status code. + +#### `no_umbrella` flag + +Projects with lockfiles at a parent folder are treated as umbrella projects. In some cases however +you may wish to have the lockfile on a parent folder without having an umbrella. By setting the +`no_umbrella` flag to `true` your project will be treated as a non umbrella project: + +```elixir +dialyzer: [ + no_umbrella: true +] +``` diff --git a/docs/circleci.md b/docs/circleci.md new file mode 100644 index 00000000..feb69d57 --- /dev/null +++ b/docs/circleci.md @@ -0,0 +1,40 @@ +# CircleCI + +```yaml +--- +version: 2 + +jobs: + build: + docker: + - image: cimg/elixir:1.14 + + steps: + - checkout + + # Compile steps omitted for simplicity + + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also useful when running in matrix) + - run: + name: "Save Elixir and Erlang version for PLT caching" + command: echo "$ELIXIR_VERSION $ERLANG_VERSION" > .elixir_otp_version + + - restore_cache: + name: "Restore PLT cache" + keys: + - {{ arch }}-{{ checksum ".elixir_otp_version" }}-plt + + - run: + name: "Create PLTs" + command: mix dialyzer --plt + + - save_cache: + name: "Save PLT cache" + key: {{ arch }}-{{ checksum ".elixir_otp_version" }}-plt + paths: "priv/plts" + + - run: + name: "Run dialyzer" + command: mix dialyzer +``` diff --git a/docs/github_actions.md b/docs/github_actions.md new file mode 100644 index 00000000..5a6c62a7 --- /dev/null +++ b/docs/github_actions.md @@ -0,0 +1,49 @@ +# Github Actions + +```yaml +steps: + - name: Check out source + uses: actions/checkout@v2 + + - name: Set up Elixir + id: beam + uses: erlef/setup-beam@v1 + with: + elixir-version: "1.12.3" # Define the Elixir version + otp-version: "24.1" # Define the OTP version + + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also useful when running in matrix) + - name: Restore PLT cache + id: plt_cache + uses: actions/cache/restore@v3 + with: + key: | + ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt + restore-keys: | + ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt + path: | + priv/plts + + # Create PLTs if no cache was found + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' + run: mix dialyzer --plt + + # By default, the GitHub Cache action will only save the cache if all steps in the job succeed, + # so we separate the cache restore and save steps in case running dialyzer fails. + - name: Save PLT cache + id: plt_cache_save + uses: actions/cache/save@v3 + if: steps.plt_cache.outputs.cache-hit != 'true' + with: + key: | + ${{ runner.os }}-${{ steps.beam.outputs.elixir-version }}-${{ steps.beam.outputs.otp-version }}-plt + path: | + priv/plts + + - name: Run dialyzer + run: mix dialyzer --format github + +# ... +``` \ No newline at end of file diff --git a/docs/gitlab_ci.md b/docs/gitlab_ci.md new file mode 100644 index 00000000..d59e793c --- /dev/null +++ b/docs/gitlab_ci.md @@ -0,0 +1,67 @@ +# GitLab CI + +```yaml +# Some of the duplication can be reduced with YAML anchors: +# https://docs.gitlab.com/ee/ci/yaml/yaml_optimization.html + +image: elixir:1.14 + +stages: + - compile + - check-elixir-types + +# You'll want to cache based on your Erlang/Elixir version. + +# The example jobs below uses asdf's config file as the cache key: +# https://asdf-vm.com/manage/configuration.html + +# An example build job with cache, to prevent dialyzer from needing to compile your project first +build-dev: + stage: compile + cache: + - key: + files: + - mix.lock + - .tool-versions + paths: + - deps/ + - _build/dev + policy: pull-push + script: + - mix do deps.get, compile + +# The main difference between the following jobs is their cache policy: +# https://docs.gitlab.com/ee/ci/yaml/index.html#cachepolicy + +dialyzer-plt: + stage: check-elixir-types + needs: + - build-dev + cache: + - key: + files: + - .tool-versions + paths: + - priv/plts + # Pull cache at start, push updated cache after completion + policy: pull-push + script: + - mix dialyzer --plt + +dialyzer-check: + stage: check-elixir-types + needs: + - dialyzer-plt + cache: + - key: + files: + - .tool-versions + paths: + - priv/plts + # Pull cache at start, don't push cache after completion + policy: pull + script: + - mix dialyzer --format short + +# ... +``` diff --git a/lib/dialyxir.ex b/lib/dialyxir.ex index 61bbdf93..4b2a1fdb 100644 --- a/lib/dialyxir.ex +++ b/lib/dialyxir.ex @@ -5,14 +5,14 @@ defmodule Dialyxir do def start(_, _) do Output.info(""" - Warning: the `dialyxir` application's start function was called, which likely means you - did not add the dependency with the `runtime: false` flag. This is not recommended because - it will mean that unnecessary applications are started, and unnecessary applications are most - likely being added to your PLT file, increasing build time. - Please add `runtime: false` in your `mix.exs` dependency section e.g.: - {:dialyxir, "~> 0.5", only: [:dev], runtime: false} + Warning: the `dialyxir` application's start function was called, which likely means you + did not add the dependency with the `runtime: false` flag. This is not recommended because + it will mean that unnecessary applications are started, and unnecessary applications are most + likely being added to your PLT file, increasing build time. + Please add `runtime: false` in your `mix.exs` dependency section e.g.: + {:dialyxir, "~> 0.5", only: [:dev], runtime: false} """) - {:ok, self()} + Supervisor.start_link([], strategy: :one_for_one) end end diff --git a/lib/dialyxir/dialyzer.ex b/lib/dialyxir/dialyzer.ex index 6f9b396a..e6de5e42 100644 --- a/lib/dialyxir/dialyzer.ex +++ b/lib/dialyxir/dialyzer.ex @@ -20,22 +20,31 @@ defmodule Dialyxir.Dialyzer do formatter = cond do split[:format] == "dialyzer" -> - :dialyzer + Dialyxir.Formatter.Dialyzer split[:format] == "dialyxir" -> - :dialyxir + Dialyxir.Formatter.Dialyxir + + split[:format] == "github" -> + Dialyxir.Formatter.Github + + split[:format] == "ignore_file" -> + Dialyxir.Formatter.IgnoreFile + + split[:format] == "ignore_file_strict" -> + Dialyxir.Formatter.IgnoreFileStrict split[:format] == "raw" -> - :raw + Dialyxir.Formatter.Raw split[:format] == "short" -> - :short + Dialyxir.Formatter.Short split[:raw] -> - :raw + Dialyxir.Formatter.Raw true -> - :dialyxir + Dialyxir.Formatter.Dialyxir end info("Starting Dialyzer") diff --git a/lib/dialyxir/formatter.ex b/lib/dialyxir/formatter.ex index 3bdc2edb..0bf62a5e 100644 --- a/lib/dialyxir/formatter.ex +++ b/lib/dialyxir/formatter.ex @@ -9,13 +9,19 @@ defmodule Dialyxir.Formatter do alias Dialyxir.FilterMap + @type warning() :: {tag :: term(), {file :: Path.t(), line :: pos_integer()}, {atom(), list()}} + + @type t() :: module() + + @callback format(warning()) :: String.t() + def formatted_time(duration_us) do minutes = div(duration_us, 60_000_000) seconds = (rem(duration_us, 60_000_000) / 1_000_000) |> Float.round(2) "done in #{minutes}m#{seconds}s" end - @spec format_and_filter([tuple], module, Keyword.t(), atom) :: tuple + @spec format_and_filter([tuple], module, Keyword.t(), t()) :: tuple def format_and_filter(warnings, filterer, filter_map_args, formatter) do filter_map = filterer.filter_map(filter_map_args) @@ -24,7 +30,8 @@ defmodule Dialyxir.Formatter do formatted_warnings = filtered_warnings |> filter_legacy_warnings(filterer) - |> Enum.map(&format_warning(&1, formatter)) + |> Enum.map(&formatter.format/1) + |> Enum.uniq() show_count_skipped(warnings, formatted_warnings, filter_map) formatted_unnecessary_skips = format_unnecessary_skips(filter_map) @@ -45,119 +52,15 @@ defmodule Dialyxir.Formatter do end end - defp format_warning(warning, :raw) do - inspect(warning, limit: :infinity) - end - - defp format_warning(warning, :dialyzer) do - # OTP 22 uses indented output, but that's incompatible with dialyzer.ignore-warnings format. - # Can be disabled, but OTP 21 and older only accept an atom, so only disable on OTP 22+. - opts = - if String.to_integer(System.otp_release()) < 22, - do: :fullpath, - else: [{:filename_opt, :fullpath}, {:indent_opt, false}] - - warning - |> :dialyzer.format_warning(opts) - |> String.Chars.to_string() - |> String.replace_trailing("\n", "") - end - - defp format_warning({_tag, {file, line}, message}, :short) do - {warning_name, arguments} = message - base_name = Path.relative_to_cwd(file) - - warning = warning(warning_name) - string = warning.format_short(arguments) - - "#{base_name}:#{line}:#{warning_name} #{string}" - end - - defp format_warning(dialyzer_warning = {_tag, {file, line}, message}, :dialyxir) do - {warning_name, arguments} = message - base_name = Path.relative_to_cwd(file) - - formatted = - try do - warning = warning(warning_name) - string = warning.format_long(arguments) - - """ - #{base_name}:#{line}:#{warning_name} - #{string} - """ - rescue - e -> - message = """ - Unknown error occurred: #{inspect(e)} - """ - - wrap_error_message(message, dialyzer_warning) - catch - {:error, :unknown_warning, warning_name} -> - message = """ - Unknown warning: - #{inspect(warning_name)} - """ - - wrap_error_message(message, dialyzer_warning) - - {:error, :lexing, warning} -> - message = """ - Failed to lex warning: - #{inspect(warning)} - """ - - wrap_error_message(message, dialyzer_warning) - - {:error, :parsing, failing_string} -> - message = """ - Failed to parse warning: - #{inspect(failing_string)} - """ - - wrap_error_message(message, dialyzer_warning) - - {:error, :pretty_printing, failing_string} -> - message = """ - Failed to pretty print warning: - #{inspect(failing_string)} - """ - - wrap_error_message(message, dialyzer_warning) - - {:error, :formatting, code} -> - message = """ - Failed to format warning: - #{inspect(code)} - """ - - wrap_error_message(message, dialyzer_warning) - end - - formatted <> String.duplicate("_", 80) - end - - defp wrap_error_message(message, warning) do - """ - Please file a bug in https://github.com/jeremyjh/dialyxir/issues with this message. - - #{message} - - Legacy warning: - #{format_warning(warning, :dialyzer)} - """ - end - defp show_count_skipped(warnings, filtered_warnings, filter_map) do warnings_count = Enum.count(warnings) filtered_warnings_count = Enum.count(filtered_warnings) skipped_count = warnings_count - filtered_warnings_count - unnessary_skips_count = count_unnecessary_skips(filter_map) + unnecessary_skips_count = count_unnecessary_skips(filter_map) info( "Total errors: #{warnings_count}, Skipped: #{skipped_count}, " <> - "Unnecessary Skips: #{unnessary_skips_count}" + "Unnecessary Skips: #{unnecessary_skips_count}" ) :ok @@ -184,16 +87,6 @@ defmodule Dialyxir.Formatter do |> Enum.count() end - defp warning(warning_name) do - warnings = Dialyxir.Warnings.warnings() - - if Map.has_key?(warnings, warning_name) do - Map.get(warnings, warning_name) - else - throw({:error, :unknown_warning, warning_name}) - end - end - defp filter_warnings(warnings, filterer, filter_map) do {warnings, filter_map} = Enum.map_reduce(warnings, filter_map, &filter_warning(filterer, &1, &2)) @@ -202,14 +95,11 @@ defmodule Dialyxir.Formatter do {warnings, filter_map} end - defp filter_warning(filterer, warning = {_, {file, line}, {warning_type, _}}, filter_map) do + defp filter_warning(filterer, {_, {_file, _line}, {warning_type, _args}} = warning, filter_map) do if Map.has_key?(Dialyxir.Warnings.warnings(), warning_type) do {skip?, matching_filters} = try do - filterer.filter_warning?( - {to_string(file), warning_type, line, format_warning(warning, :short)}, - filter_map - ) + filterer.filter_warning?(warning, filter_map) rescue _ -> {false, []} @@ -237,7 +127,7 @@ defmodule Dialyxir.Formatter do Enum.reject(warnings, fn warning -> formatted_warnings = warning - |> format_warning(:dialyzer) + |> Dialyxir.Formatter.Dialyzer.format() |> List.wrap() Enum.empty?(filterer.filter_legacy_warnings(formatted_warnings)) diff --git a/lib/dialyxir/formatter/dialyxir.ex b/lib/dialyxir/formatter/dialyxir.ex new file mode 100644 index 00000000..241a9368 --- /dev/null +++ b/lib/dialyxir/formatter/dialyxir.ex @@ -0,0 +1,84 @@ +defmodule Dialyxir.Formatter.Dialyxir do + @moduledoc false + + alias Dialyxir.Formatter.Utils + + @behaviour Dialyxir.Formatter + + @impl Dialyxir.Formatter + def format(dialyzer_warning = {_tag, {file, line}, message}) do + {warning_name, arguments} = message + base_name = Path.relative_to_cwd(file) + + formatted = + try do + warning = Utils.warning(warning_name) + string = warning.format_long(arguments) + + """ + #{base_name}:#{line}:#{warning_name} + #{string} + """ + rescue + e -> + message = """ + Unknown error occurred: #{inspect(e)} + """ + + wrap_error_message(message, dialyzer_warning) + catch + {:error, :unknown_warning, warning_name} -> + message = """ + Unknown warning: + #{inspect(warning_name)} + """ + + wrap_error_message(message, dialyzer_warning) + + {:error, :lexing, warning} -> + message = """ + Failed to lex warning: + #{inspect(warning)} + """ + + wrap_error_message(message, dialyzer_warning) + + {:error, :parsing, failing_string} -> + message = """ + Failed to parse warning: + #{inspect(failing_string)} + """ + + wrap_error_message(message, dialyzer_warning) + + {:error, :pretty_printing, failing_string} -> + message = """ + Failed to pretty print warning: + #{inspect(failing_string)} + """ + + wrap_error_message(message, dialyzer_warning) + + {:error, :formatting, code} -> + message = """ + Failed to format warning: + #{inspect(code)} + """ + + wrap_error_message(message, dialyzer_warning) + end + + formatted <> String.duplicate("_", 80) + end + + defp wrap_error_message(message, warning) do + """ + Please file a bug in https://github.com/jeremyjh/dialyxir/issues with this message. + + #{message} + + Legacy warning: + #{Dialyxir.Formatter.Dialyzer.format(warning)} + """ + end +end diff --git a/lib/dialyxir/formatter/dialyzer.ex b/lib/dialyxir/formatter/dialyzer.ex new file mode 100644 index 00000000..934eabd7 --- /dev/null +++ b/lib/dialyxir/formatter/dialyzer.ex @@ -0,0 +1,20 @@ +defmodule Dialyxir.Formatter.Dialyzer do + @moduledoc false + + @behaviour Dialyxir.Formatter + + @impl Dialyxir.Formatter + def format(warning) do + # OTP 22 uses indented output, but that's incompatible with dialyzer.ignore-warnings format. + # Can be disabled, but OTP 21 and older only accept an atom, so only disable on OTP 22+. + opts = + if String.to_integer(System.otp_release()) < 22, + do: :fullpath, + else: [{:filename_opt, :fullpath}, {:indent_opt, false}] + + warning + |> :dialyzer.format_warning(opts) + |> String.Chars.to_string() + |> String.replace_trailing("\n", "") + end +end diff --git a/lib/dialyxir/formatter/github.ex b/lib/dialyxir/formatter/github.ex new file mode 100644 index 00000000..da306d6d --- /dev/null +++ b/lib/dialyxir/formatter/github.ex @@ -0,0 +1,17 @@ +defmodule Dialyxir.Formatter.Github do + @moduledoc false + + alias Dialyxir.Formatter.Utils + + @behaviour Dialyxir.Formatter + + @impl Dialyxir.Formatter + def format({_tag, {file, line}, {warning_name, arguments}}) do + base_name = Path.relative_to_cwd(file) + + warning = Utils.warning(warning_name) + string = warning.format_short(arguments) + + "::warning file=#{base_name},line=#{line},title=#{warning_name}::#{string}" + end +end diff --git a/lib/dialyxir/formatter/ignore_file.ex b/lib/dialyxir/formatter/ignore_file.ex new file mode 100644 index 00000000..b1bf6cc1 --- /dev/null +++ b/lib/dialyxir/formatter/ignore_file.ex @@ -0,0 +1,10 @@ +defmodule Dialyxir.Formatter.IgnoreFile do + @moduledoc false + + @behaviour Dialyxir.Formatter + + @impl Dialyxir.Formatter + def format({_tag, {file, _line}, {warning_name, _arguments}}) do + ~s({"#{file}", :#{warning_name}},) + end +end diff --git a/lib/dialyxir/formatter/ignore_file_strict.ex b/lib/dialyxir/formatter/ignore_file_strict.ex new file mode 100644 index 00000000..49aeb109 --- /dev/null +++ b/lib/dialyxir/formatter/ignore_file_strict.ex @@ -0,0 +1,15 @@ +defmodule Dialyxir.Formatter.IgnoreFileStrict do + @moduledoc false + + alias Dialyxir.Formatter.Utils + + @behaviour Dialyxir.Formatter + + @impl Dialyxir.Formatter + def format({_tag, {file, _line}, {warning_name, arguments}}) do + warning = Utils.warning(warning_name) + string = warning.format_short(arguments) + + ~s({"#{file}", "#{string}"},) + end +end diff --git a/lib/dialyxir/formatter/raw.ex b/lib/dialyxir/formatter/raw.ex new file mode 100644 index 00000000..c18a221a --- /dev/null +++ b/lib/dialyxir/formatter/raw.ex @@ -0,0 +1,10 @@ +defmodule Dialyxir.Formatter.Raw do + @moduledoc false + + @behaviour Dialyxir.Formatter + + @impl Dialyxir.Formatter + def format(warning) do + inspect(warning, limit: :infinity) + end +end diff --git a/lib/dialyxir/formatter/short.ex b/lib/dialyxir/formatter/short.ex new file mode 100644 index 00000000..5c5691c1 --- /dev/null +++ b/lib/dialyxir/formatter/short.ex @@ -0,0 +1,17 @@ +defmodule Dialyxir.Formatter.Short do + @moduledoc false + + alias Dialyxir.Formatter.Utils + + @behaviour Dialyxir.Formatter + + @impl Dialyxir.Formatter + def format({_tag, {file, line}, {warning_name, arguments}}) do + base_name = Path.relative_to_cwd(file) + + warning = Utils.warning(warning_name) + string = warning.format_short(arguments) + + "#{base_name}:#{line}:#{warning_name} #{string}" + end +end diff --git a/lib/dialyxir/formatter/utils.ex b/lib/dialyxir/formatter/utils.ex new file mode 100644 index 00000000..8f3fd541 --- /dev/null +++ b/lib/dialyxir/formatter/utils.ex @@ -0,0 +1,11 @@ +defmodule Dialyxir.Formatter.Utils do + def warning(warning_name) do + warnings = Dialyxir.Warnings.warnings() + + if Map.has_key?(warnings, warning_name) do + Map.get(warnings, warning_name) + else + throw({:error, :unknown_warning, warning_name}) + end + end +end diff --git a/lib/dialyxir/project.ex b/lib/dialyxir/project.ex index 97b63e6a..d2d4c209 100644 --- a/lib/dialyxir/project.ex +++ b/lib/dialyxir/project.ex @@ -3,6 +3,8 @@ defmodule Dialyxir.Project do import Dialyxir.Output, only: [info: 1, error: 1] alias Dialyxir.FilterMap + alias Dialyxir.Formatter.Short + alias Dialyxir.Formatter.Utils def plts_list(deps, include_project \\ true, exclude_core \\ false) do elixir_apps = [:elixir] @@ -22,9 +24,7 @@ defmodule Dialyxir.Project do end end - def plt_file() do - plt_path(dialyzer_config()[:plt_file]) || deps_plt() - end + def plt_file, do: plt_path(dialyzer_config()[:plt_file]) || deps_plt() defp plt_path(file) when is_binary(file), do: Path.expand(file) defp plt_path({:no_warn, file}) when is_binary(file), do: Path.expand(file) @@ -41,13 +41,9 @@ defmodule Dialyxir.Project do end def cons_apps do - # compile & load all deps paths - Mix.Tasks.Deps.Loadpaths.run([]) - # compile & load current project paths - Mix.Project.compile([]) - apps = plt_apps() || plt_add_apps() ++ include_deps() + Mix.Task.run("compile") - apps + (plt_apps() || plt_add_apps() ++ Enum.flat_map(local_apps(), &direct_children/1)) |> Enum.sort() |> Enum.uniq() |> Kernel.--(plt_ignore_apps()) @@ -67,7 +63,28 @@ defmodule Dialyxir.Project do beam_files |> Map.merge(consolidated_files) - |> Enum.map(fn {_file, path} -> path |> to_charlist() end) + |> Enum.map(fn {_file, path} -> path end) + |> reject_exclude_files() + |> Enum.map(&to_charlist(&1)) + end + + defp reject_exclude_files(files) do + file_exclusions = dialyzer_config()[:exclude_files] || [] + + Enum.reject(files, fn file -> + :lists.any( + fn reject_file_pattern -> + re = <> + result = :re.run(file, re) + + case result do + {:match, _captured} -> true + :nomatch -> false + end + end, + file_exclusions + ) + end) end defp dialyzer_paths do @@ -88,24 +105,47 @@ defmodule Dialyxir.Project do Mix.Project.config()[:dialyzer][:flags] || [] end - defp skip?({file, warning, line}, {file, warning, line, _}), do: true - defp skip?({file, warning}, {file, warning, _, _}), do: true - defp skip?({file}, {file, _, _, _}), do: true - defp skip?({short_description, warning, line}, {_, warning, line, short_description}), do: true - defp skip?({short_description, warning}, {_, warning, _, short_description}), do: true - defp skip?({short_description}, {_, _, _, short_description}), do: true + def no_umbrella? do + case dialyzer_config()[:no_umbrella] do + true -> true + _other -> false + end + end + + defp skip?({file, warning, line}, {file, warning, line, _, _}), do: true + + defp skip?({file, warning_description}, {file, _, _, _, warning_description}) + when is_binary(warning_description), + do: true + + defp skip?({file, warning}, {file, warning, _, _, _}) when is_atom(warning), do: true + defp skip?({file}, {file, _, _, _, _}), do: true - defp skip?(%Regex{} = pattern, {_, _, _, short_description}) do + defp skip?({short_description, warning, line}, {_, warning, line, short_description, _}), + do: true + + defp skip?({short_description, warning}, {_, warning, _, short_description, _}), do: true + defp skip?({short_description}, {_, _, _, short_description, _}), do: true + + defp skip?(%Regex{} = pattern, {_, _, _, short_description, _}) do Regex.match?(pattern, short_description) end defp skip?(_, _), do: false - def filter_warning?({file, warning, line, short_description}, filter_map = %FilterMap{}) do + def filter_warning?( + {_, {file, line}, {warning_type, args}} = warning, + filter_map = %FilterMap{} + ) do + short_description = Short.format(warning) + warning_description = Utils.warning(warning_type).format_short(args) + {matching_filters, _non_matching_filters} = filter_map |> FilterMap.filters() - |> Enum.split_with(&skip?(&1, {file, warning, line, short_description})) + |> Enum.split_with( + &skip?(&1, {to_string(file), warning_type, line, short_description, warning_description}) + ) {not Enum.empty?(matching_filters), matching_filters} end @@ -237,88 +277,40 @@ defmodule Dialyxir.Project do defp core_path(), do: dialyzer_config()[:plt_core_path] || Mix.Utils.mix_home() defp local_plt(name) do - Path.join(Mix.Project.build_path(), "dialyxir_" <> name <> ".plt") - end - - defp default_paths() do - reduce_umbrella_children([], fn paths -> - [Mix.Project.compile_path() | paths] - end) + Path.join(local_path(), "dialyxir_" <> name <> ".plt") end - defp plt_apps, do: dialyzer_config()[:plt_apps] |> load_apps() - defp plt_add_apps, do: dialyzer_config()[:plt_add_apps] || [] |> load_apps() - defp plt_ignore_apps, do: dialyzer_config()[:plt_ignore_apps] || [] - - defp load_apps(nil), do: nil - - defp load_apps(apps) do - Enum.each(apps, &Application.load/1) - apps - end - - defp include_deps do - method = dialyzer_config()[:plt_add_deps] - - reduce_umbrella_children([], fn deps -> - deps ++ - case method do - false -> - [] - - # compatibility - true -> - deps_project() ++ deps_app(false) - - :project -> - info( - "Dialyxir has deprecated plt_add_deps: :project in favor of apps_direct, which includes only runtime dependencies." - ) - - deps_project() ++ deps_app(false) - - :apps_direct -> - deps_app(false) - - :transitive -> - info( - "Dialyxir has deprecated plt_add_deps: :transitive in favor of app_tree, which includes only runtime dependencies." - ) - - deps_transitive() ++ deps_app(true) + defp local_path(), do: dialyzer_config()[:plt_local_path] || Mix.Project.build_path() - _app_tree -> - deps_app(true) - end - end) - end + defp default_paths() do + build_path = Mix.Project.build_path() - defp deps_project do - Mix.Project.config()[:deps] - |> Enum.filter(&env_dep(&1)) - |> Enum.map(&elem(&1, 0)) + for app <- local_apps() do + Path.join([build_path, "lib", Atom.to_string(app), "ebin"]) + end end - defp deps_transitive do - Mix.Project.deps_paths() - |> Map.keys() - end + defp plt_apps, do: dialyzer_config()[:plt_apps] + defp plt_add_apps, do: dialyzer_config()[:plt_add_apps] || [] + defp plt_ignore_apps, do: dialyzer_config()[:plt_ignore_apps] || [] - @spec deps_app(boolean()) :: [atom] - defp deps_app(recursive) do - app = Mix.Project.config()[:app] - deps_app(app, recursive) - end + defp local_apps() do + deps_apps = + for {app, scm} <- Mix.Project.deps_scms(), + not scm.fetchable?(), + do: app - @spec deps_app(atom(), boolean()) :: [atom] - defp deps_app(app, recursive) do - with_each = - if recursive do - &deps_app(&1, true) + project_apps = + if children = Mix.Project.apps_paths() do + Map.keys(children) else - fn _ -> [] end + [Mix.Project.config()[:app]] end + Enum.uniq(project_apps ++ deps_apps) -- plt_ignore_apps() + end + + defp direct_children(app) do case Application.load(app) do :ok -> nil @@ -327,46 +319,10 @@ defmodule Dialyxir.Project do nil {:error, err} -> - nil error("Error loading #{app}, dependency list may be incomplete.\n #{inspect(err)}") end - case Application.spec(app, :applications) do - [] -> - [] - - nil -> - [] - - this_apps -> - Enum.map(this_apps, with_each) - |> List.flatten() - |> Enum.concat(this_apps) - end - end - - defp env_dep(dep) do - only_envs = dep_only(dep) - only_envs == nil || Mix.env() in List.wrap(only_envs) - end - - defp dep_only({_, opts}) when is_list(opts), do: opts[:only] - defp dep_only({_, _, opts}) when is_list(opts), do: opts[:only] - defp dep_only(_), do: nil - - @spec reduce_umbrella_children(list(), (list() -> list())) :: list() - defp reduce_umbrella_children(acc, f) do - if Mix.Project.umbrella?() do - children = Mix.Dep.Umbrella.loaded() - - Enum.reduce(children, acc, fn child, acc -> - Mix.Project.in_project(child.app, child.opts[:path], fn _ -> - reduce_umbrella_children(acc, f) - end) - end) - else - f.(acc) - end + List.wrap(Application.spec(app, :applications)) end defp dialyzer_config(), do: Mix.Project.config()[:dialyzer] diff --git a/lib/dialyxir/warning.ex b/lib/dialyxir/warning.ex index d3d41ecd..b3e7a79f 100644 --- a/lib/dialyxir/warning.ex +++ b/lib/dialyxir/warning.ex @@ -6,13 +6,13 @@ defmodule Dialyxir.Warning do """ @doc """ - By expressing the warning that is to be matched on, error handlong + By expressing the warning that is to be matched on, error handling and dispatching can be avoided in format functions. """ @callback warning() :: atom @doc """ - The default documentation when seeing an error wihout the user + The default documentation when seeing an error without the user otherwise overriding the format. """ @callback format_long([String.t()] | {String.t(), String.t(), String.t()} | String.t()) :: diff --git a/lib/dialyxir/warning_helpers.ex b/lib/dialyxir/warning_helpers.ex index 048b85c7..c40f55a6 100644 --- a/lib/dialyxir/warning_helpers.ex +++ b/lib/dialyxir/warning_helpers.ex @@ -24,8 +24,8 @@ defmodule Dialyxir.WarningHelpers do positions = form_position_string(arg_positions) """ - will never return since it differs in arguments with - positions #{positions} from the success typing arguments: + will never return since the #{positions} arguments differ + from the success typing arguments: #{pretty_signature_args} """ diff --git a/lib/dialyxir/warnings.ex b/lib/dialyxir/warnings.ex index 57db8fe8..9e1b2216 100644 --- a/lib/dialyxir/warnings.ex +++ b/lib/dialyxir/warnings.ex @@ -11,6 +11,7 @@ defmodule Dialyxir.Warnings do Dialyxir.Warnings.CallbackArgumentTypeMismatch, Dialyxir.Warnings.CallbackInfoMissing, Dialyxir.Warnings.CallbackMissing, + Dialyxir.Warnings.CallbackNotExported, Dialyxir.Warnings.CallbackSpecArgumentTypeMismatch, Dialyxir.Warnings.CallbackSpecTypeMismatch, Dialyxir.Warnings.CallbackTypeMismatch, @@ -39,7 +40,6 @@ defmodule Dialyxir.Warnings do Dialyxir.Warnings.OverlappingContract, Dialyxir.Warnings.PatternMatch, Dialyxir.Warnings.PatternMatchCovered, - Dialyxir.Warnings.RaceCondition, Dialyxir.Warnings.RecordConstruction, Dialyxir.Warnings.RecordMatching, Dialyxir.Warnings.UnknownBehaviour, diff --git a/lib/dialyxir/warnings/callback_not_exported.ex b/lib/dialyxir/warnings/callback_not_exported.ex new file mode 100644 index 00000000..c1f8da3f --- /dev/null +++ b/lib/dialyxir/warnings/callback_not_exported.ex @@ -0,0 +1,57 @@ +defmodule Dialyxir.Warnings.CallbackNotExported do + @moduledoc """ + Module implements a behaviour, but does not export some of its + callbacks. + + ## Example + defmodule Example do + @behaviour GenServer + + def init(_) do + :ok + end + + # OK. No warning. + def handle_all(_request, _from, state) do + {:noreply, state} + end + + # Not exported. Should be a warning. + @spec handle_cast(any(), any()) :: binary() + defp handle_cast(_request, _state) do + "abc" + end + + # Not exported and conflicting arguments and return value. No warning + # since format_status/1 is an optional callback. + @spec format_status(binary()) :: binary() + def format_status(bin) when is_binary(bin) do + bin + end + end + """ + + @behaviour Dialyxir.Warning + + @impl Dialyxir.Warning + @spec warning() :: :callback_not_exported + def warning(), do: :callback_not_exported + + @impl Dialyxir.Warning + @spec format_short([String.t()]) :: String.t() + def format_short(args), do: format_long(args) + + @impl Dialyxir.Warning + @spec format_long([String.t()]) :: String.t() + def format_long([behaviour, function, arity]) do + pretty_behaviour = Erlex.pretty_print(behaviour) + + "Callback function #{function}/#{arity} exists but is not exported (behaviour #{pretty_behaviour})." + end + + @impl Dialyxir.Warning + @spec explain() :: String.t() + def explain() do + @moduledoc + end +end diff --git a/lib/dialyxir/warnings/function_application_no_function.ex b/lib/dialyxir/warnings/function_application_no_function.ex index 42582216..f776f5b1 100644 --- a/lib/dialyxir/warnings/function_application_no_function.ex +++ b/lib/dialyxir/warnings/function_application_no_function.ex @@ -30,9 +30,7 @@ defmodule Dialyxir.Warnings.FunctionApplicationNoFunction do pretty_op = Erlex.pretty_print(op) pretty_type = Erlex.pretty_print_type(type) - "Function application will fail, because #{pretty_op} :: #{pretty_type} is not a function of arity #{ - arity - }." + "Function application will fail, because #{pretty_op} :: #{pretty_type} is not a function of arity #{arity}." end @impl Dialyxir.Warning diff --git a/lib/dialyxir/warnings/missing_range.ex b/lib/dialyxir/warnings/missing_range.ex index 6d2b5e37..93fd55cc 100644 --- a/lib/dialyxir/warnings/missing_range.ex +++ b/lib/dialyxir/warnings/missing_range.ex @@ -1,4 +1,24 @@ defmodule Dialyxir.Warnings.MissingRange do + @moduledoc """ + Function spec declares a list of types, but function returns value + outside stated range. + + This error only appears with the :overspecs flag. + + ## Example + + defmodule Example do + @spec foo(any()) :: :ok + def foo(:ok) do + :ok + end + + def foo(_) do + :error + end + end + """ + @behaviour Dialyxir.Warning @impl Dialyxir.Warning @@ -17,8 +37,8 @@ defmodule Dialyxir.Warnings.MissingRange do @spec format_long([String.t()]) :: String.t() def format_long([module, function, arity, extra_ranges, contract_range]) do pretty_module = Erlex.pretty_print(module) - pretty_contract_range = Erlex.pretty_print_args(contract_range) - pretty_extra_ranges = Erlex.pretty_print_contract(extra_ranges) + pretty_contract_range = Erlex.pretty_print_type(contract_range) + pretty_extra_ranges = Erlex.pretty_print_type(extra_ranges) """ The type specification is missing types returned by function. @@ -29,7 +49,7 @@ defmodule Dialyxir.Warnings.MissingRange do Type specification return types: #{pretty_contract_range} - Missing types: + Missing from spec: #{pretty_extra_ranges} """ end @@ -37,6 +57,6 @@ defmodule Dialyxir.Warnings.MissingRange do @impl Dialyxir.Warning @spec explain() :: String.t() def explain() do - Dialyxir.Warning.default_explain() + @moduledoc end end diff --git a/lib/dialyxir/warnings/opaque_match.ex b/lib/dialyxir/warnings/opaque_match.ex index 95659bed..fb6f9a0e 100644 --- a/lib/dialyxir/warnings/opaque_match.ex +++ b/lib/dialyxir/warnings/opaque_match.ex @@ -36,9 +36,7 @@ defmodule Dialyxir.Warnings.OpaqueMatch do def format_short([_pattern, type | _]) do pretty_type = Erlex.pretty_print_type(type) - "Attempted to pattern match against the internal structure of an opaque term of type #{ - pretty_type - }." + "Attempted to pattern match against the internal structure of an opaque term of type #{pretty_type}." end @impl Dialyxir.Warning diff --git a/lib/dialyxir/warnings/race_condition.ex b/lib/dialyxir/warnings/race_condition.ex deleted file mode 100644 index 7e3a3a0a..00000000 --- a/lib/dialyxir/warnings/race_condition.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Dialyxir.Warnings.RaceCondition do - @behaviour Dialyxir.Warning - - @impl Dialyxir.Warning - @spec warning() :: :race_condition - def warning(), do: :race_condition - - @impl Dialyxir.Warning - @spec format_short([String.t()]) :: String.t() - def format_short([_module, function | _]) do - "Possible race condition in #{function}." - end - - @impl Dialyxir.Warning - @spec format_long([String.t()]) :: String.t() - def format_long([module, function, args, reason]) do - pretty_args = Erlex.pretty_print_args(args) - pretty_module = Erlex.pretty_print(module) - - "The call #{pretty_module}, #{function}#{pretty_args} #{reason}." - end - - @impl Dialyxir.Warning - @spec explain() :: String.t() - def explain() do - Dialyxir.Warning.default_explain() - end -end diff --git a/lib/mix/tasks/dialyzer.ex b/lib/mix/tasks/dialyzer.ex index b793268c..07e00a52 100644 --- a/lib/mix/tasks/dialyzer.ex +++ b/lib/mix/tasks/dialyzer.ex @@ -8,24 +8,26 @@ defmodule Mix.Tasks.Dialyzer do ## Command line options - * `--no-compile` - do not compile even if needed. - * `--no-check` - do not perform (quick) check to see if PLT needs update. - * `--force-check` - force PLT check also if lock file is unchanged. - useful when dealing with local deps. - * `--ignore-exit-status` - display warnings but do not halt the VM or return an exit status code - * `--list-unused-filters` - list unused ignore filters - useful for CI. do not use with `mix do`. - * `--plt` - only build the required plt(s) and exit. - * `--format short` - format the warnings in a compact format. - * `--format raw` - format the warnings in format returned before Dialyzer formatting - * `--format dialyxir` - format the warnings in a pretty printed format - * `--format dialyzer` - format the warnings in the original Dialyzer format - * `--quiet` - suppress all informational messages - - Warning flags passed to this task are passed on to `:dialyzer`. - - e.g. - `mix dialyzer --unmatched_returns` + * `--no-compile` - do not compile even if needed + * `--no-check` - do not perform (quick) check to see if PLT needs update + * `--force-check` - force PLT check also if lock file is unchanged useful + when dealing with local deps. + * `--ignore-exit-status` - display warnings but do not halt the VM or + return an exit status code + * `--list-unused-filters` - list unused ignore filters useful for CI. do + not use with `mix do`. + * `--plt` - only build the required PLT(s) and exit + * `--format short` - format the warnings in a compact format + * `--format raw` - format the warnings in format returned before Dialyzer formatting + * `--format dialyxir` - format the warnings in a pretty printed format + * `--format dialyzer` - format the warnings in the original Dialyzer format + * `--format github` - format the warnings in the Github Actions message format + * `--format ignore_file` - format the warnings to be suitable for adding to Elixir Format ignore file + * `--quiet` - suppress all informational messages + + Warning flags passed to this task are passed on to `:dialyzer` - e.g. + + mix dialyzer --unmatched_returns ## Configuration @@ -54,8 +56,8 @@ defmodule Mix.Tasks.Dialyzer do 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: - * :apps_direct - Only Direct OTP runtime application dependencies - not the entire tree - * :app_tree - Transitive OTP runtime application dependencies e.g. `mix app.tree` (default) + * `:apps_direct` - Only Direct OTP runtime application dependencies - not the entire tree + * `:app_tree` - Transitive OTP runtime application dependencies e.g. `mix app.tree` (default) ``` def project do @@ -81,9 +83,11 @@ defmodule Mix.Tasks.Dialyzer do ### Other Configuration - * `dialyzer: :plt_file` - Deprecated - specify the plt file name to create and use - default is to create one in the project's current build environmnet (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"}`. + * `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"}`. - * `dialyzer: :plt_core_path` - specify an alternative to MIX_HOME to use to store the Erlang and Elixir core files. + * `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/`). + + * `dialyzer: :plt_core_path` - specify an alternative to `MIX_HOME` to use to store the Erlang and Elixir core files. * `dialyzer: :ignore_warnings` - specify file path to filter well-known warnings. """ @@ -96,7 +100,7 @@ defmodule Mix.Tasks.Dialyzer do alias Dialyxir.Dialyzer defmodule Build do - @shortdoc "Build the required plt(s) and exit." + @shortdoc "Build the required PLT(s) and exit." @moduledoc """ This task compiles the mix project and creates a PLT with dependencies if needed. @@ -104,7 +108,7 @@ defmodule Mix.Tasks.Dialyzer do ## Command line options - * `--no-compile` - do not compile even if needed. + * `--no-compile` - do not compile even if needed. """ use Mix.Task @@ -114,14 +118,14 @@ defmodule Mix.Tasks.Dialyzer do end defmodule Clean do - @shortdoc "Delete plt(s) and exit." + @shortdoc "Delete PLT(s) and exit." @moduledoc """ This task deletes PLT files and hash files. ## Command line options - * `--all` - delete also core PLTs. + * `--all` - delete also core PLTs. """ use Mix.Task @@ -161,7 +165,7 @@ defmodule Mix.Tasks.Dialyzer do if Mix.Project.get() do Project.check_config() - unless opts[:no_compile], do: Mix.Project.compile([]) + unless opts[:no_compile], do: Mix.Task.run("compile") _ = unless no_check?(opts) do @@ -291,7 +295,10 @@ defmodule Mix.Tasks.Dialyzer do end defp in_child? do - String.contains?(Mix.Project.config()[:lockfile], "..") + case Project.no_umbrella?() do + true -> false + false -> String.contains?(Mix.Project.config()[:lockfile], "..") + end end defp no_plt? do @@ -359,7 +366,7 @@ defmodule Mix.Tasks.Dialyzer do beyond the dialyzer defaults are included. All these properties can be changed in configuration. (see `mix help dialyzer`). - 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 ) + 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`) """) end end @@ -382,7 +389,23 @@ defmodule Mix.Tasks.Dialyzer do {apps, hash} end - def lock_file() do - Mix.Project.config()[:lockfile] |> File.read!() + defp lock_file() do + lockfile = Mix.Project.config()[:lockfile] + read_res = File.read(lockfile) + + case read_res do + {:ok, data} -> + data + + {:error, :enoent} -> + # If there is no lock file, an empty bitstring will do to indicate there is none there + <<>> + + {:error, reason} -> + raise File.Error, + reason: reason, + action: "read file", + path: lockfile + end end end diff --git a/lib/mix/tasks/dialyzer/explain.ex b/lib/mix/tasks/dialyzer/explain.ex index 85534b13..282099cf 100644 --- a/lib/mix/tasks/dialyzer/explain.ex +++ b/lib/mix/tasks/dialyzer/explain.ex @@ -1,8 +1,8 @@ defmodule Mix.Tasks.Dialyzer.Explain do - @shortdoc "Display information about dialyzer warnings." + @shortdoc "Display information about Dialyzer warnings." @moduledoc """ - This task provides background information about dialyzer warnings. + This task provides background information about Dialyzer warnings. If invoked without any arguments it will list all warning atoms. When invoked with the name of a particular warning, it will display information regarding it. diff --git a/mix.exs b/mix.exs index 98467861..b408a242 100644 --- a/mix.exs +++ b/mix.exs @@ -1,15 +1,19 @@ defmodule Dialyxir.Mixfile do use Mix.Project + @source_url "https://github.com/jeremyjh/dialyxir" + @version "1.3.0" + def project do [ app: :dialyxir, - version: "1.0.0", - elixir: ">= 1.6.0", + version: @version, + elixir: ">= 1.10.0", elixirc_paths: elixirc_paths(Mix.env()), description: description(), package: package(), deps: deps(), + aliases: [test: "test --no-start"], dialyzer: [ plt_apps: [:dialyzer, :elixir, :kernel, :mix, :stdlib, :erlex], ignore_warnings: ".dialyzer_ignore.exs", @@ -17,15 +21,19 @@ defmodule Dialyxir.Mixfile do ], # Docs name: "Dialyxir", - source_url: "https://github.com/jeremyjh/dialyxir", - homepage_url: "https://github.com/jeremyjh/dialyxir", + homepage_url: @source_url, # The main page in the docs - docs: [main: "readme", extras: ["README.md"]] + docs: [ + main: "readme", + source_url: @source_url, + source_ref: @version, + extras: ["CHANGELOG.md", "README.md"] + ] ] end def application do - [mod: {Dialyxir, []}, extra_applications: [:dialyzer, :crypto, :mix]] + [mod: {Dialyxir, []}, extra_applications: [:dialyzer, :crypto, :mix, :erts, :syntax_tools]] end defp description do @@ -40,7 +48,7 @@ defmodule Dialyxir.Mixfile do defp deps do [ {:erlex, ">= 0.2.6"}, - {:ex_doc, ">= 0.0.0", only: :dev} + {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} ] end @@ -53,8 +61,11 @@ defmodule Dialyxir.Mixfile do "LICENSE" ], maintainers: ["Jeremy Huffman"], - licenses: ["Apache 2.0"], - links: %{"GitHub" => "https://github.com/jeremyjh/dialyxir"} + licenses: ["Apache-2.0"], + links: %{ + "Changelog" => "https://hexdocs.pm/dialyxir/changelog.html", + "GitHub" => @source_url + } ] end end diff --git a/mix.lock b/mix.lock index 36d2d53a..dd6b20d6 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,9 @@ %{ - "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.23.0", "a069bc9b0bf8efe323ecde8c0d62afc13d308b1fa3d228b65bca5cf8703a529d", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f5e2c4702468b2fd11b10d39416ddadd2fcdd173ba2a0285ebd92c39827a5a16"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, } diff --git a/test/dialyxir/formatter_test.exs b/test/dialyxir/formatter_test.exs index a7690868..a76db820 100644 --- a/test/dialyxir/formatter_test.exs +++ b/test/dialyxir/formatter_test.exs @@ -4,6 +4,10 @@ defmodule Dialyxir.FormatterTest do import ExUnit.CaptureIO, only: [capture_io: 1] alias Dialyxir.Formatter + alias Dialyxir.Formatter.Dialyxir, as: DialyxirFormatter + alias Dialyxir.Formatter.Dialyzer, as: DialyzerFormatter + alias Dialyxir.Formatter.Short, as: ShortFormatter + alias Dialyxir.Formatter.IgnoreFileStrict, as: IgnoreFileStrictFormatter alias Dialyxir.Project defp in_project(app, f) when is_atom(app) do @@ -12,12 +16,36 @@ defmodule Dialyxir.FormatterTest do describe "exs ignore" do test "evaluates an ignore file and ignores warnings matching the pattern" do - warning = - {:warn_return_no_exit, {'a/file.ex', 17}, {:no_return, [:only_normal, :format_long, 1]}} + warnings = [ + {:warn_return_no_exit, {'lib/short_description.ex', 17}, + {:no_return, [:only_normal, :format_long, 1]}}, + {:warn_return_no_exit, {'lib/file/warning_type.ex', 18}, + {:no_return, [:only_normal, :format_long, 1]}}, + {:warn_return_no_exit, {'lib/file/warning_type/line.ex', 19}, + {:no_return, [:only_normal, :format_long, 1]}} + ] in_project(:ignore, fn -> {:error, remaining, _unused_filters_present} = - Formatter.format_and_filter([warning], Project, [], :short) + Formatter.format_and_filter(warnings, Project, [], ShortFormatter) + + assert remaining == [] + end) + end + + test "evaluates an ignore file of the form {file, short_description} and ignores warnings matching the pattern" do + warnings = [ + {:warn_return_no_exit, {'lib/poorly_written_code.ex', 10}, + {:no_return, [:only_normal, :do_a_thing, 1]}}, + {:warn_return_no_exit, {'lib/poorly_written_code.ex', 20}, + {:no_return, [:only_normal, :do_something_else, 2]}}, + {:warn_return_no_exit, {'lib/poorly_written_code.ex', 30}, + {:no_return, [:only_normal, :do_many_things, 3]}} + ] + + in_project(:ignore_strict, fn -> + {:ok, remaining, :no_unused_filters} = + Formatter.format_and_filter(warnings, Project, [], IgnoreFileStrictFormatter) assert remaining == [] end) @@ -29,7 +57,9 @@ defmodule Dialyxir.FormatterTest do {:no_return, [:only_normal, :format_long, 1]}} in_project(:ignore, fn -> - {:error, [remaining], _} = Formatter.format_and_filter([warning], Project, [], :short) + {:error, [remaining], _} = + Formatter.format_and_filter([warning], Project, [], ShortFormatter) + assert remaining =~ ~r/different_file.* no local return/ end) end @@ -41,13 +71,13 @@ defmodule Dialyxir.FormatterTest do in_project(:ignore, fn -> {:error, remaining, _unused_filters_present} = - Formatter.format_and_filter([warning], Project, [], :short) + Formatter.format_and_filter([warning], Project, [], ShortFormatter) assert remaining == [] end) end - test "lists unnecessary skips as warnings if ignoreing exit status " do + test "lists unnecessary skips as warnings if ignoring exit status" do warning = {:warn_return_no_exit, {'a/regex_file.ex', 17}, {:no_return, [:only_normal, :format_long, 1]}} @@ -114,19 +144,21 @@ defmodule Dialyxir.FormatterTest do expected_warning = "a/another-file.ex:18" - expected_unused_filter = - "Unused filters:\n{\"a/file.ex:17:no_return Function format_long/1 has no local return.\"}" + expected_unused_filter = ~s(Unused filters: +{"lib/short_description.ex:17:no_return Function format_long/1 has no local return."} +{"lib/file/warning_type.ex", :no_return, 18} +{"lib/file/warning_type/line.ex", :no_return, 19}) filter_args = [{:list_unused_filters, true}] - for format <- [:short, :dialyxir, :dialyzer] do + for format <- [ShortFormatter, DialyxirFormatter, DialyzerFormatter] do in_project(:ignore, fn -> capture_io(fn -> result = Formatter.format_and_filter(warnings, Project, filter_args, format) assert {:error, [warning], {:unused_filters_present, unused}} = result assert warning =~ expected_warning - assert unused =~ expected_unused_filter + assert unused == expected_unused_filter # A warning for regex_file.ex was explicitly put into format_and_filter. refute unused =~ "regex_file.ex" end) diff --git a/test/dialyxir/project_test.exs b/test/dialyxir/project_test.exs index 7d3d5cc9..4d164bf6 100644 --- a/test/dialyxir/project_test.exs +++ b/test/dialyxir/project_test.exs @@ -2,7 +2,7 @@ defmodule Dialyxir.ProjectTest do alias Dialyxir.Project use ExUnit.Case - import ExUnit.CaptureIO, only: [capture_io: 1, capture_io: 2] + import ExUnit.CaptureIO, only: [capture_io: 1] defp in_project(app, f) when is_atom(app) do Mix.Project.in_project(app, "test/fixtures/#{Atom.to_string(app)}", fn _ -> f.() end) @@ -16,7 +16,13 @@ defmodule Dialyxir.ProjectTest do test "Default Project PLT File in _build dir" do in_project(:default_apps, fn -> - assert Regex.match?(~r/_build.*plt/, Project.plt_file()) + assert Regex.match?(~r/_build\/.*plt/, Project.plt_file()) + end) + end + + test "Can specify a different local PLT path" do + in_project(:alt_local_path, fn -> + assert Regex.match?(~r/dialyzer\/.*plt/, Project.plt_file()) end) end @@ -46,8 +52,7 @@ defmodule Dialyxir.ProjectTest do end) end - test "App list for default contains direct and - indirect :application dependencies" do + test "App list for default contains direct :application dependencies" do in_project(:default_apps, fn -> apps = Project.cons_apps() # direct @@ -55,12 +60,11 @@ defmodule Dialyxir.ProjectTest do # direct assert Enum.member?(apps, :public_key) # indirect - assert Enum.member?(apps, :asn1) + refute Enum.member?(apps, :asn1) end) end - test "App list for umbrella contains child dependencies - indirect :application dependencies" do + test "App list for umbrella contains direct child and :application dependencies" do in_project(:umbrella, fn -> apps = Project.cons_apps() # direct @@ -68,15 +72,14 @@ defmodule Dialyxir.ProjectTest do # direct, child1 assert Enum.member?(apps, :public_key) # indirect - assert Enum.member?(apps, :asn1) + refute Enum.member?(apps, :asn1) # direct, child2 assert Enum.member?(apps, :mix) end) end @tag :skip - test "App list for umbrella contains all child dependencies - when run from child directory" do + test "App list for umbrella contains all child dependencies when run from child directory" do in_project([:umbrella, :apps, :second_one], fn -> apps = Project.cons_apps() # direct @@ -156,13 +159,6 @@ defmodule Dialyxir.ProjectTest do end) end - test "Project with non-existent dependency" do - in_project(:nonexistent_deps, fn -> - out = capture_io(:stderr, &Project.cons_apps/0) - assert Regex.match?(~r/Error loading nonexistent, dependency list may be incomplete/, out) - end) - end - test "igonored apps are removed in umbrella projects" do in_project(:umbrella_ignore_apps, fn -> refute Enum.member?(Project.cons_apps(), :logger) @@ -178,4 +174,14 @@ defmodule Dialyxir.ProjectTest do assert Project.list_unused_filters?(list_unused_filters: nil) end) end + + test "no_umbrella? works as expected" do + in_project(:umbrella, fn -> + refute Project.no_umbrella?() + end) + + in_project(:no_umbrella, fn -> + assert Project.no_umbrella?() + end) + end end diff --git a/test/fixtures/alt_local_path/mix.exs b/test/fixtures/alt_local_path/mix.exs new file mode 100644 index 00000000..ec8fdd42 --- /dev/null +++ b/test/fixtures/alt_local_path/mix.exs @@ -0,0 +1,7 @@ +defmodule AltLocalPath.Mixfile do + use Mix.Project + + def project do + [app: :alt_local_path, version: "1.0.0", dialyzer: [plt_local_path: "dialyzer"]] + end +end diff --git a/test/fixtures/ignore/ignore_test.exs b/test/fixtures/ignore/ignore_test.exs index 3bcda6fe..2a5d0705 100644 --- a/test/fixtures/ignore/ignore_test.exs +++ b/test/fixtures/ignore/ignore_test.exs @@ -1,4 +1,6 @@ [ - {"a/file.ex:17:no_return Function format_long/1 has no local return."}, + {"lib/short_description.ex:17:no_return Function format_long/1 has no local return."}, + {"lib/file/warning_type.ex", :no_return, 18}, + {"lib/file/warning_type/line.ex", :no_return, 19}, ~r/regex_file.ex.*no local return/ ] diff --git a/test/fixtures/ignore_strict/ignore_strict_test.exs b/test/fixtures/ignore_strict/ignore_strict_test.exs new file mode 100644 index 00000000..33503329 --- /dev/null +++ b/test/fixtures/ignore_strict/ignore_strict_test.exs @@ -0,0 +1,5 @@ +[ + {"lib/poorly_written_code.ex", "Function do_a_thing/1 has no local return."}, + {"lib/poorly_written_code.ex", "Function do_something_else/2 has no local return."}, + {"lib/poorly_written_code.ex", "Function do_many_things/3 has no local return."} +] diff --git a/test/fixtures/ignore_strict/mix.exs b/test/fixtures/ignore_strict/mix.exs new file mode 100644 index 00000000..dab0c95c --- /dev/null +++ b/test/fixtures/ignore_strict/mix.exs @@ -0,0 +1,14 @@ +defmodule IgnoreStrict.Mixfile do + use Mix.Project + + def project do + [ + app: :ignore_strict, + version: "0.1.0", + dialyzer: [ + ignore_warnings: "ignore_strict_test.exs", + list_unused_filters: true + ] + ] + end +end diff --git a/test/fixtures/no_lockfile/mix.exs b/test/fixtures/no_lockfile/mix.exs new file mode 100644 index 00000000..641a55a7 --- /dev/null +++ b/test/fixtures/no_lockfile/mix.exs @@ -0,0 +1,10 @@ +defmodule NoLockfile.Mixfile do + use Mix.Project + + def project do + [ + app: :no_lockfile, + version: "1.0.0" + ] + end +end diff --git a/test/fixtures/no_umbrella/mix.exs b/test/fixtures/no_umbrella/mix.exs new file mode 100644 index 00000000..ea9441ab --- /dev/null +++ b/test/fixtures/no_umbrella/mix.exs @@ -0,0 +1,23 @@ +defmodule NoUmbrella.Mixfile do + use Mix.Project + + def project do + [ + app: :no_umbrella, + version: "0.1.0", + lockfile: "../mix.lock", + elixir: "~> 1.3", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: [], + dialyzer: [no_umbrella: true] + ] + end + + # Configuration for the OTP application + # + # Type "mix help compile.app" for more information + def application do + [applications: [:logger, :public_key]] + end +end diff --git a/test/mix/tasks/dialyzer_test.exs b/test/mix/tasks/dialyzer_test.exs index 772d6530..460a712f 100644 --- a/test/mix/tasks/dialyzer_test.exs +++ b/test/mix/tasks/dialyzer_test.exs @@ -42,6 +42,14 @@ defmodule Mix.Tasks.DialyzerTest do end) end + test "Does not crash when running on project without mix.lock" do + in_project(:no_lockfile, fn -> + fun = fn -> Mix.Tasks.Dialyzer.clean([], &no_delete_plt/4) end + capture_io(fun) + # does not assert anything, we just need to ensure this doesn't crash. + end) + end + @tag :output_tests test "Informational output is suppressed with --quiet" do args = ["dialyzer", "--quiet"]